第六十四回:DataTable Widget

Flutter中的DataTableWidget详解
文章介绍了Flutter中的DataTableWidget,一种用于生成表格的布局组件,类似于Column和Row。DataTableWidget提供数据排序和选择功能,常作为ListView的条目使用。文章列举了核心属性如columns和rows,以及示例代码展示如何创建一个简单的三行三列的表格。


我们在上一章回中介绍了Wrap Widget相关的内容,本章回中将介绍如何使用 DataTable Widget.闲话休提,让我们一起Talk Flutter吧。

概念介绍

我们在本文中将要介绍的DataTable Widget是一种布局类组件,类似Column或者Row组件,它主要用来生成存放数据的表格,有看官说ListView也可以存放数据呀,为什么还需要DataTable,因为它把数据进行了表格化,和我们常用的excel表格完全相同,而且它还提供了常用的数据操作功能,比如排序。

还有一点原因:ListView只是提供了滚动列表功能,但是列表中的每个条目如何呈现呢?它并有明确的要求,列表中的条目常用ListTitle组件实现,也可以换成的Text组件。而DataTable中的条目是按照行列进行呈现。在实际项目中经常把DataTable组件当作ListView组件的条目,这样既可以让数据用列表的形式呈现出来,也可以让数据随着列表滚动。

使用方法

和其它组件一样DataTable Widget也提供了相关的属性来操作自己,下面是常用的属性:

  • columns属性:主要用来控制表格的列,类型是List
  • rows属性:主要用来控制表格的行,类型是List
  • columnSpacing属性:主要用来控制列宽,默认值是56;
  • sortColumnIndex属性:主要用来指出使用哪一列的数据进行排序;
  • sortAscending属性:主要用来控制使用升序还是降序进行排序,默认值是true,表示升序;
  • onSelectAll属性:主要用来对表中的数据进行选择操作,这里的选择表示全部选择;注意它是一个方法类型;

上面介绍的这些属性中,前两个属性是必选属性;也是核心属性,它们把数据以行和列的形式进行管理,从它们的类型中也可以看出来,它们的数量是可变的。其它的属性可以不写,不过在实现数据排序和选择功能时需要使用这些属性,我们在后面章回中将会详细介绍它们的用法。

示例代码

DataTable(
//每列的宽度,默认值是56
columnSpacing: 64,
//表格标题,就是每一列的Title
columns:[
  DataColumn( label: Text("Name"),),
  DataColumn( label: Text("Age"),),),
  DataColumn( label: Text("Class"),),
],
 // 可以一行一行的进行赋值,也可以使用List自动赋值
rows: [
  //第一行数据
  DataRow(cells: [
    DataCell(Text("Jam")),
    DataCell(Text("3")),
    DataCell(Text("Sleep")),
  ],),
  //第二行数据
  DataRow(cells: [
    DataCell(Text("Jam")),
    DataCell(Text("3")),
    DataCell(Text("Sleep")),
  ],),
  //第三行数据
  DataRow(cells: [
    DataCell(Text("Jam")),
    DataCell(Text("3")),
    DataCell(Text("Sleep")),
  ],),
],
),

在上面的代码中我们定义了一个三行三列的表格,列名通过columns属性控制,分别显示:Name,Age,Class.这些列名对应DataColumn类中的label属性。

接下来给rows属性赋值,它的类型是DataRow,行中的每一列类型为DataCell,它通过cells属性管理这些cell。注意cell的数量必须与列的数量保持一致。

这里只演示了核心代码,完整的代码在Github上ex029文件中。编译完整的程序就可以看到一个三行三列的表格,我在这里就不演示程序的运行结果了,建议大家自己动手去实践,这样才能真正体会到DataTable的特点。

看官们,关于DataTable Widget相关的内容就介绍到这里,欢迎大家在评论区交流与讨论!

import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext import pandas as pd import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import os from datetime import datetime class ExcelQuerySystem: def __init__(self, root): self.root = root self.root.title("Excel表格查询系统") self.root.geometry("1200x800") self.root.minsize(1000, 600) # 初始化变量 self.df = None self.filtered_df = None self.current_page = 1 self.rows_per_page = 20 self.sort_column = None self.sort_order = "asc" # 设置样式 self.style = ttk.Style() self.style.theme_use("clam") self.setup_styles() # 创建主布局 self.create_main_layout() # 创建菜单 self.create_menus() # 创建工具栏 self.create_toolbar() # 创建主内容区 self.create_main_content() # 初始状态禁用某些控件 self.update_widget_states() def setup_styles(self): """设置界面样式""" # 主色调:蓝色 primary_color = "#3498db" secondary_color = "#2980b9" accent_color = "#f39c12" # 配置样式 self.style.configure("Main.TFrame", background="white") self.style.configure("ToolBar.TFrame", background="#f5f5f5") self.style.configure("SidePanel.TFrame", background="#f8f9fa") self.style.configure("MainContent.TFrame", background="white") # 按钮样式 self.style.configure("Primary.TButton", background=primary_color, foreground="white", borderwidth=0, padding=6) self.style.map("Primary.TButton", background=[("active", secondary_color)]) self.style.configure("Accent.TButton", background=accent_color, foreground="white", borderwidth=0, padding=6) self.style.map("Accent.TButton", background=[("active", "#e67e22")]) # 标签样式 self.style.configure("Header.TLabel", font=("Arial", 14, "bold"), foreground="#2c3e50", padding=10) self.style.configure("Section.TLabel", font=("Arial", 12, "bold"), foreground="#34495e", padding=5) # 表格样式 self.style.configure("DataTable.Treeview", font=("Arial", 10), rowheight=25) self.style.configure("DataTable.Treeview.Heading", font=("Arial", 11, "bold"), background=primary_color, foreground="white") # 输入框样式 self.style.configure("Entry.TEntry", padding=5, bordercolor="#bdc3c7", relief="flat") # 组合框样式 self.style.configure("TCombobox", padding=5, bordercolor="#bdc3c7", relief="flat") def create_main_layout(self): """创建主布局""" # 主框架 self.main_frame = ttk.Frame(self.root, style="Main.TFrame") self.main_frame.pack(fill=tk.BOTH, expand=True) # 工具栏框架 self.toolbar_frame = ttk.Frame(self.main_frame, style="ToolBar.TFrame", height=40) self.toolbar_frame.pack(fill=tk.X, side=tk.TOP) # 内容区框架 self.content_frame = ttk.Frame(self.main_frame, style="Main.TFrame") self.content_frame.pack(fill=tk.BOTH, expand=True) # 左侧面板 self.side_panel = ttk.Frame(self.content_frame, style="SidePanel.TFrame", width=300) self.side_panel.pack(fill=tk.Y, side=tk.LEFT, padx=5, pady=5) # 右侧主内容 self.main_content = ttk.Frame(self.content_frame, style="MainContent.TFrame") self.main_content.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) def create_menus(self): """创建菜单""" menubar = tk.Menu(self.root) # 文件菜单 file_menu = tk.Menu(menubar, tearoff=0) file_menu.add_command(label="导入Excel", command=self.import_excel) file_menu.add_command(label="导出结果", command=self.export_results) file_menu.add_separator() file_menu.add_command(label="退出", command=self.root.quit) menubar.add_cascade(label="文件", menu=file_menu) # 帮助菜单 help_menu = tk.Menu(menubar, tearoff=0) help_menu.add_command(label="使用说明", command=self.show_help) help_menu.add_command(label="关于", command=self.show_about) menubar.add_cascade(label="帮助", menu=help_menu) self.root.config(menu=menubar) def create_toolbar(self): """创建工具栏""" # 导入按钮 self.import_btn = ttk.Button(self.toolbar_frame, text="导入Excel", command=self.import_excel, style="Primary.TButton") self.import_btn.pack(side=tk.LEFT, padx=5, pady=5) # 导出按钮 self.export_btn = ttk.Button(self.toolbar_frame, text="导出结果", command=self.export_results, style="Accent.TButton") self.export_btn.pack(side=tk.LEFT, padx=5, pady=5) # 统计按钮 self.stats_btn = ttk.Button(self.toolbar_frame, text="数据统计", command=self.show_statistics, style="Primary.TButton") self.stats_btn.pack(side=tk.LEFT, padx=5, pady=5) # 分隔符 separator = ttk.Separator(self.toolbar_frame, orient=tk.VERTICAL) separator.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5) # 状态标签 self.status_label = ttk.Label(self.toolbar_frame, text="就绪", font=("Arial", 10)) self.status_label.pack(side=tk.RIGHT, padx=10, pady=5) def create_main_content(self): """创建主内容区""" # 表格数据展示区 self.table_frame = ttk.Frame(self.main_content) self.table_frame.pack(fill=tk.BOTH, expand=True) # 表格 self.tree = ttk.Treeview(self.table_frame, style="DataTable.Treeview") self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 滚动条 self.scrollbar = ttk.Scrollbar(self.table_frame, orient=tk.VERTICAL, command=self.tree.yview) self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.tree.configure(yscrollcommand=self.scrollbar.set) # 分页控件 self.pagination_frame = ttk.Frame(self.main_content) self.pagination_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=5) self.prev_btn = ttk.Button(self.pagination_frame, text="上一页", command=self.prev_page) self.prev_btn.pack(side=tk.LEFT, padx=5) self.page_label = ttk.Label(self.pagination_frame, text="第 1 页") self.page_label.pack(side=tk.LEFT, padx=5) self.next_btn = ttk.Button(self.pagination_frame, text="下一页", command=self.next_page) self.next_btn.pack(side=tk.LEFT, padx=5) # 图表展示区 self.chart_frame = ttk.Frame(self.main_content) self.chart_frame.pack(fill=tk.BOTH, expand=True, pady=5) # 初始隐藏图表区 self.chart_frame.pack_forget() def create_side_panel_content(self): """创建侧边面板内容""" # 清空现有内容 for widget in self.side_panel.winfo_children(): widget.destroy() # 标题 title_label = ttk.Label(self.side_panel, text="查询条件", style="Section.TLabel") title_label.pack(fill=tk.X, pady=5) # 查询条件区域 self.query_frame = ttk.Frame(self.side_panel) self.query_frame.pack(fill=tk.X, padx=5, pady=5) # 添加查询条件行 self.add_query_row() # 添加条件按钮 self.add_condition_btn = ttk.Button(self.side_panel, text="+ 添加条件", command=self.add_query_row, style="Primary.TButton") self.add_condition_btn.pack(fill=tk.X, padx=5, pady=2) # 查询按钮 self.query_btn = ttk.Button(self.side_panel, text="执行查询", command=self.execute_query, style="Accent.TButton") self.query_btn.pack(fill=tk.X, padx=5, pady=10) # 重置按钮 self.reset_btn = ttk.Button(self.side_panel, text="重置查询", command=self.reset_query, style="Primary.TButton") self.reset_btn.pack(fill=tk.X, padx=5, pady=2) # 数据信息区域 info_label = ttk.Label(self.side_panel, text="数据信息", style="Section.TLabel") info_label.pack(fill=tk.X, padx=5, pady=10) self.info_text = scrolledtext.ScrolledText(self.side_panel, height=8, wrap=tk.WORD) self.info_text.pack(fill=tk.BOTH, padx=5, pady=5) self.info_text.config(state=tk.DISABLED) def add_query_row(self): """添加查询条件行""" row_frame = ttk.Frame(self.query_frame) row_frame.pack(fill=tk.X, pady=3) # 列选择 columns = list(self.df.columns) if self.df is not None else [] column_combo = ttk.Combobox(row_frame, values=columns, state="readonly", width=15) column_combo.pack(side=tk.LEFT, padx=2) # 操作符选择 operators = ["包含", "等于", "不等于", "大于", "小于", "大于等于", "小于等于"] operator_combo = ttk.Combobox(row_frame, values=operators, state="readonly", width=10) operator_combo.pack(side=tk.LEFT, padx=2) operator_combo.current(0) # 值输入 value_entry = ttk.Entry(row_frame) value_entry.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True) # 删除按钮 delete_btn = ttk.Button(row_frame, text="×", command=lambda: row_frame.destroy(), width=3, style="Accent.TButton") delete_btn.pack(side=tk.LEFT, padx=2) def update_widget_states(self): """更新控件状态""" if self.df is None: # 禁用导出、统计按钮 self.export_btn.config(state=tk.DISABLED) self.stats_btn.config(state=tk.DISABLED) self.query_btn.config(state=tk.DISABLED) self.reset_btn.config(state=tk.DISABLED) self.add_condition_btn.config(state=tk.DISABLED) self.prev_btn.config(state=tk.DISABLED) self.next_btn.config(state=tk.DISABLED) else: # 启用导出、统计按钮 self.export_btn.config(state=tk.NORMAL) self.stats_btn.config(state=tk.NORMAL) self.query_btn.config(state=tk.NORMAL) self.reset_btn_btn.config(state=tk.NORMAL) self.add_condition_btn.config(state=tk.NORMAL) # 更新根据数据量启用/禁用分页按钮 total_pages = self.get_total_pages() if total_pages <= 1: self.prev_btn.config(state=tk.DISABLED) self.next_btn.config(state=tk.DISABLED) else: self.prev_btn.config(state=tk.NORMAL if self.current_page > 1 else tk.DISABLED) self.next_btn.config(state=tk.NORMAL if self.current_page < total_pages_pages else tk.DISABLED) def import_excel(self): """导入Excel文件""" file_path = filedialog.askopenfilename( title="选择Excel文件", filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")] ) if not file_path: return try: # 读取Excel文件 self.df = pd.read_excel(file_path) self.filtered_df = self.df.copy() self.current_page = 1 # 更新状态 self.status_label.config(text=f"已导入: {os.path.basename(file_path)}") # 显示数据信息 self.update_info_text() # 创建侧边面板内容 self.create_side_panel_content() # 显示表格数据 self.display_table_data() # 更新控件状态 self.update_widget_states() messagebox.showinfo("导入成功", f"成功导入Excel文件,包含 {len(self.df)} 行数据") except Exception as e: messagebox.showerror("导入失败", f"导入Excel文件时出错: {str(e)}") self.status_label.config(text="导入失败") def update_info_text(self): """更新信息文本框""" if self.df is None: return info = f"数据文件: {self.status_label.cget('text').replace('已导入: ', '')}\n" info += f"总行数: {len(self.df)}\n" info += f"总列数: {len(self.df.columns)}\n\n" info += "列名及数据类型:\n" for col in self.df.columns: dtype = str(self.df[col].dtype) info += f"- {col}: {dtype}\n" self.info_text.config(state=tk.NORMAL) self.info_text.delete(1.0, tk.END) self.info_text.insert(tk.END, info) self.info_text.config(state=tk.DISABLED) def display_table_data(self): """显示表格数据""" # 清空现有表格 self.tree.delete(*self.tree.get_children()) # 如果没有数据,显示提示信息 if self.filtered_df is None or len(self.filtered_df) == 0: self.tree["columns"] = ["提示"] self.tree.column("#0", width=0, stretch=tk.NO) self.tree.column("提示", anchor=tk.CENTER, width=800) self.tree.heading("提示", text="没有符合条件的数据") self.tree.insert("", tk.END, values=["没有符合条件的数据"]) return # 获取当前页数据 start_idx = (self.current_page - 1) * self.rows_per_page end_idx = start_idx + self.rows_per_page page_data = self.filtered_df.iloc[start_idx:end_idx] # 设置表格列 columns = list(page_data.columns) self.tree["columns"] = columns # 隐藏第一列(默认的#0列) self.tree.column("#0", width=0, stretch=tk.NO) # 设置列属性 for col in columns: self.tree.column(col, anchor=tk.CENTER, width=100, minwidth=80) self.tree.heading(col, text=col, command=lambda c=col: self.sort_data(c)) # 添加数据行 for _, row in page_data.iterrows(): values = [str(val) if pd.notna(val) else "" for val in row.values] self.tree.insert("", tk.END, values=values) # 更新分页信息 total_pages = self.get_total_pages() self.page_label.config(text=f"第 {self.current_page} 页 / 共 {total_pages} 页") def get_total_pages(self): """计算总页数""" if self.filtered_df is None or len(self.filtered_df) == 0: return 1 return (len(self.filtered_df) + self.rows_per_page - 1) // self.rows_per_page def prev_page(self): """上一页""" if self.current_page > 1: self.current_page -= 1 self.display_table_data() self.update_widget_states() def next_page(self): """下一页""" total_pages = self.get_total_pages() if self.current_page < total_pages: self.current_page += 1 self.display_table_data() self.update_widget_states() def sort_data(self, column): """排序数据""" if self.filtered_df is None: return # 如果点击的是当前排序列,则切换排序顺序 if self.sort_column == column: self.sort_order = "desc" if self.sort_order == "asc" else "asc" else: self.sort_column = column self.sort_order = "asc" # 执行排序 self.filtered_df = self.filtered_df.sort_values( by=column, ascending=(self.sort_order == "asc") ) # 重置到第一页 self.current_page = 1 # 更新表格显示 self.display_table_data() def execute_query(self): """执行查询""" if self.df is None: return # 获取所有查询条件 conditions = [] valid = True for row_frame in self.query_frame.winfo_children(): widgets = row_frame.winfo_children() if len(widgets) < 3: continue column_combo = widgets[0] operator_combo = widgets[1] value_entry = widgets[2] column = column_combo.get() operator = operator_combo.get() value = value_entry.get().strip() if not column or not value: continue conditions.append((column, operator, value)) # 应用查询条件 try: self.filtered_df = self.df.copy() for column, operator, value in conditions: if column not in self.filtered_df.columns: continue # 根据数据类型处理查询值 dtype = self.filtered_df[column].dtype try: if "int" in str(dtype): value = int(value) elif "float" in str(dtype): value = float(value) elif "datetime" in str(dtype): value = pd.to_datetime(value) except ValueError: # 如果无法转换,则作为字符串处理 pass # 应用过滤条件 if operator == "包含": if isinstance(value, str): self.filtered_df = self.filtered_df[ self.filtered_df[column].astype(str).str.contains(value, na=False) ] elif operator == "等于": self.filtered_df = self.filtered_df[ self.filtered_df[column] == value ] elif operator == "不等于": self.filtered_df = self.filtered_df[ self.filtered_df[column] != value ] elif operator == "大于": self.filtered_df = self.filtered_df[ self.filtered_df[column] > value ] elif operator == "小于": self.filtered_df = self.filtered_df[ self.filtered_df[column] < value ] elif operator == "大于等于": self.filtered_df = self.filtered_df[ self.filtered_df[column] >= value ] elif operator == "小于等于": self.filtered_df = self.filtered_df[ self.filtered_df[column] <= value ] # 重置到第一页 self.current_page = 1 # 更新表格显示 self.display_table_data() # 更新状态 self.status_label.config(text=f"查询完成,找到 {len(self.filtered_df)} 条结果") # 更新控件状态 self.update_widget_states() except Exception as e: messagebox.showerror("查询失败", f"执行查询时出错: {str(e)}") def reset_query(self): """重置查询""" if self.df is None: return # 重置过滤后的数据 self.filtered_df = self.df.copy() self.current_page = 1 # 清空查询条件 for widget in self.query_frame.winfo_children(): widget.destroy() # 添加一行空的查询条件 self.add_query_row() # 更新表格显示 self.display_table_data() # 更新状态 self.status_label.config(text="查询已重置") # 更新控件状态 self.update_widget_states() def export_results(self): """导出查询结果""" if self.filtered_df is None or len(self.filtered_df) == 0: messagebox.showwarning("导出警告", "没有可导出的数据") return # 选择导出文件路径 file_path = filedialog.asksaveasfilename( title="导出结果", defaultextension=".xlsx", filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv"), ("所有文件", "*.*")] ) if not file_path: return try: # 根据文件扩展名选择导出格式 if file_path.endswith(".csv"): self.filtered_df.to_csv(file_path, index=False, encoding="utf-8-sig") else: self.filtered_df.to_excel(file_path, index=False) messagebox.showinfo("导出成功", f"成功导出 {len(self.filtered_df)} 行数据到 {file_path}") self.status_label.config(text=f"已导出: {os.path.basename(file_path)}") except Exception as e: messagebox.showerror("导出失败", f"导出数据时出错: {str(e)}") def show_statistics(self): """显示数据统计""" if self.filtered_df is None or len(self.filtered_df) == 0: messagebox.showwarning("统计警告", "没有可统计的数据") return # 显示图表区域 self.chart_frame.pack(fill=tk.BOTH, expand=True, pady=5) # 清空现有图表 for widget in self.chart_frame.winfo_children(): widget.destroy() # 创建统计信息标签 stats_label = ttk.Label(self.chart_frame, text="数据统计与可视化", style="Header.TLabel") stats_label.pack(fill=tk.X, pady=5) # 创建统计内容框架 stats_content_frame = ttk.Frame(self.chart_frame) stats_content_frame.pack(fill=tk.BOTH, expand=True) # 左侧统计信息 stats_text_frame = ttk.Frame(stats_content_frame, width=300) stats_text_frame.pack(fill=tk.Y, side=tk.LEFT, padx=5) # 生成统计信息 stats_text = self.generate_statistics_text() # 显示统计信息 stats_text_widget = scrolledtext.ScrolledText(stats_text_frame, wrap=tk.WORD) stats_text_widget.pack(fill=tk.BOTH, expand=True) stats_text_widget.insert(tk.END, stats_text) stats_text_widget.config(state=tk.DISABLED) # 右侧图表 chart_canvas_frame = ttk.Frame(stats_content_frame) chart_canvas_frame.pack(fill=tk.BOTH, expand=True, padx=5) # 创建图表 self.create_chart(chart_canvas_frame) def generate_statistics_text(self): """生成统计信息文本""" if self.filtered_df is None: return "" stats_text = f"数据概览:\n" stats_text += f"- 总记录数: {len(self.filtered_df)}\n" stats_text += f"- 总列数: {len(self.filtered_df.columns)}\n\n" # 数值型列的统计信息 numeric_columns = self.filtered_df.select_dtypes(include=["int64", "float64"]).columns if len(numeric_columns) > 0: stats_text += "数值型列统计:\n" for col in numeric_columns: stats_text += f"\n{col}:\n" stats_text += f" - 平均值: {self.filtered_df[col].mean():.2f}\n" stats_text += f" - 中位数: {self.filtered_df[col].median():.2f}\n" stats_text += f" - 最小值: {self.filtered_df[col].min():.2f}\n" stats_text += f" - 最大值: {self.filtered_df[col].max():.2f}\n" stats_text += f" - 标准差: {self.filtered_df[col].std():.2f}\n" # 字符串列的统计信息 string_columns = self.filtered_df.select_dtypes(include=["object"]).columns if len(string_columns) > 0: stats_text += "\n字符串列统计:\n" for col in string_columns: non_null_count = self.filtered_df[col].notna().sum() unique_count = self.filtered_df[col].nunique() stats_text += f"\n{col}:\n" stats_text += f" - 非空值数量: {non_null_count}\n" stats_text += f" - 唯一值数量: {unique_count}\n" # 如果唯一值数量不多,显示前5个最常见的值 if unique_count > 0 and unique_count <= 20: top_values = self.filtered_df[col].value_counts().head(5) stats_text += " - 常见值: " for val, count in top_values.items(): stats_text += f"{val}({count}), " stats_text = stats_text.rstrip(", ") + "\n" # 日期时间列的统计信息 datetime_columns = self.filtered_df.select_dtypes(include=["datetime64"]).columns if len(datetime_columns) > 0: stats_text += "\n日期时间列统计:\n" for col in datetime_columns: stats_text += f"\n{col}:\n" stats_text += f" - 最早日期: {self.filtered_df[col].min()}\n" stats_text += f" - 最晚日期: {self.filtered_df[col].max()}\n" stats_text += f" - 时间跨度: {(self.filtered_df[col].max() - self.filtered_df[col].min()).days} 天\n" return stats_text def create_chart(self, parent_frame): """创建数据可视化图表""" if self.filtered_df is None: return # 选择第一个数值型列进行可视化 numeric_columns = self.filtered_df.select_dtypes(include=["int64", "float64"]).columns if len(numeric_columns) == 0: # 如果没有数值型列,选择第一个字符串列进行柱状图 string_columns = self.filtered_df.select_dtypes(include=["object"]).columns if len(string_columns) == 0: return col = string_columns[0] value_counts = self.filtered_df[col].value_counts().head(10) # 创建柱状图 fig, ax = plt.subplots(figsize=(8, 6)) ax.bar(value_counts.index.astype(str), value_counts.values) ax.set_title(f"{col} 分布") ax.set_xlabel(col) ax.set_ylabel("数量") plt.xticks(rotation=45, ha="right") else: col = numeric_columns[0] # 创建直方图 fig, ax = plt.subplots(figsize=(8, 6)) ax.hist(self.filtered_df[col].dropna(), bins=20, edgecolor="black") ax.set_title(f"{col} 分布") ax.set_xlabel(col) ax.set_ylabel("频数") # 调整布局 plt.tight_layout() # 创建画布并显示 canvas = FigureCanvasTkAgg(fig, master=parent_frame) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) def show_help(self): """显示使用说明""" help_text = """ Excel表格查询系统使用说明 1. 导入Excel文件 - 点击"导入Excel"按钮或通过"文件"菜单选择Excel文件 - 支持.xlsx和.xls格式 2. 查询数据 - 在左侧面板设置查询条件 - 点击"+ 添加条件"可以增加多个查询条件 - 点击"执行查询"按钮应用筛选 - 点击"重置查询"按钮清除所有条件 3. 数据操作 - 点击表头可以按该列排序 - 使用分页控件浏览大量数据 - 点击"导出结果"按钮可以将查询结果导出为Excel或CSV文件 4. 数据统计 - 点击"数据统计"按钮查看数据统计信息和可视化图表 - 统计信息包括基本统计量、分布情况等 - 可视化图表根据数据类型自动生成 5. 其他功能 - 通过"帮助"菜单可以查看使用说明和关于信息 - 通过"文件"菜单可以退出系统 """ messagebox.showinfo("使用说明", help_text) def show_about(self): """显示关于信息""" about_text = """ Excel表格查询系统 v1.0 一个简单易用的Excel数据查询和分析工具, 无需编程知识即可快速查询、筛选和分析Excel表格数据。 主要功能: - Excel文件导入与预览 - 多条件组合查询 - 查询结果导出 - 数据统计与可视化 技术支持:Python + Tkinter + Pandas + Matplotlib © 2025 Excel表格查询系统 """ messagebox.showinfo("关于", about_text) def main(): """主函数""" root = tk.Tk() app = ExcelQuerySystem(root) root.mainloop() if __name__ == "__main__": main()
10-24
import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; class SosPage extends StatefulWidget { const SosPage({Key? key}) : super(key: key); @override State<SosPage> createState() => _SosPageState(); } class _SosPageState extends State<SosPage> { @override Widget build(BuildContext context) { return MaterialApp( title: '自定义弹窗表格应用', theme: ThemeData(primarySwatch: Colors.blue), home: ProjectListPage(), ); } } class TableRowData { final String fixedContent; String editableContent; final String presetContent; TableRowData({ required this.fixedContent, this.editableContent = "", required this.presetContent, }); Map<String, dynamic> toJson() => { 'fixedContent': fixedContent, 'editableContent': editableContent, 'presetContent': presetContent, }; factory TableRowData.fromJson(Map<String, dynamic> json) => TableRowData( fixedContent: json['fixedContent'], editableContent: json['editableContent'], presetContent: json['presetContent'], ); } class Project { String name; List<TableRowData> rows; Project({required this.name, required this.rows}); Map<String, dynamic> toJson() => { 'name': name, 'rows': rows.map((row) => row.toJson()).toList(), }; factory Project.fromJson(Map<String, dynamic> json) => Project( name: json['name'], rows: (json['rows'] as List) .map((row) => TableRowData.fromJson(row)) .toList(), ); } class ProjectListPage extends StatefulWidget { @override _ProjectListPageState createState() => _ProjectListPageState(); } class _ProjectListPageState extends State<ProjectListPage> { List<Project> projects = []; @override void initState() { super.initState(); _loadProjects(); } // 加载保存的项目 Future<void> _loadProjects() async { final prefs = await SharedPreferences.getInstance(); final String? projectsJson = prefs.getString('projects'); if (projectsJson != null) { final List<dynamic> jsonList = json.decode(projectsJson); setState(() { projects = jsonList.map((json) => Project.fromJson(json)).toList(); }); } } // 保存项目列表 Future<void> _saveProjects() async { final prefs = await SharedPreferences.getInstance(); final List<Map<String, dynamic>> projectsJson = projects.map((project) => project.toJson()).toList(); await prefs.setString('projects', json.encode(projectsJson)); } // 获取第一列固定内容 String _getFixedContent(int rowIndex) { final contents = [ "固定内容行 1", "固定内容行 2", "固定内容行 3", "固定内容行 4", "固定内容行 5", "固定内容行 6", "固定内容行 7", "固定内容行 8", "固定内容行 9" ]; return contents[rowIndex]; } // 获取预设弹窗内容 String _getPresetContent(int rowIndex) { final contents = [ "安全警告:参数超出安全范围\n建议值:25-50", "操作确认:此操作不可撤", "系统通知:数据已成功保存\n位", "提醒:请完成所有必填字段\n缺失字段I", "错误:输入格式不正确\n请输入数", "性能提示:优化算法复杂度", "警告:资源使用超过阈值\n当", "确认:是否提交当前修改?\n", "成功:操作执行完成\n" ]; return contents[rowIndex]; } // 添加新项目 void _addNewProject() { List<TableRowData> rows = List.generate( 9, (index) => TableRowData( fixedContent: _getFixedContent(index), presetContent: _getPresetContent(index), )); setState(() { projects.add(Project( name: '', rows: rows, )); _saveProjects(); }); } // 删除项目 void _deleteProject(int index) { setState(() { projects.removeAt(index); _saveProjects(); }); } // 导航到表格详情页 void _navigateToTableDetail(BuildContext context, Project project) { Navigator.push( context, MaterialPageRoute( builder: (context) => TableDetailPage(project: project), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('项目列表')), body: projects.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.table_chart, size: 64, color: Colors.grey), SizedBox(height: 16), Text('暂无表格项目', style: TextStyle(fontSize: 18)), SizedBox(height: 8), Text('点击右下角按钮创建新项目'), ], ), ) : ListView.builder( padding: EdgeInsets.all(8), itemCount: projects.length, itemBuilder: (context, index) { return Card( margin: EdgeInsets.only(bottom: 12), elevation: 0, child: ListTile( contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 20), minVerticalPadding: 24, // 设置最小垂直间距 title: TextField( controller: TextEditingController(text: projects[index].name), decoration: InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.zero, hintText: '输入项目名称', ), style: TextStyle(fontSize: 18), onChanged: (value) { setState(() { projects[index].name = value; _saveProjects(); }); }, ), trailing: Row( mainAxisSize: MainAxisSize.min, // 让Row尽量小 children: [ IconButton( icon: Icon(Icons.edit, color: Colors.blue), // 编辑图标,蓝色 onPressed: () => _navigateToTableDetail( context, projects[index]), // 编辑项目的函数,需要用户自己实现 ), IconButton( icon: Icon(Icons.delete, color: Colors.red), // 删除图标,红色 onPressed: () => _deleteProject(index), ), ], ), onTap: () => _navigateToTableDetail(context, projects[index]), ), ); }, ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: _addNewProject, tooltip: '添加新项目', ), ); } } class TableDetailPage extends StatefulWidget { final Project project; TableDetailPage({required this.project}); @override _TableDetailPageState createState() => _TableDetailPageState(); } class _TableDetailPageState extends State<TableDetailPage> { late Project _project; @override void initState() { super.initState(); _project = widget.project; } // 保存项目数据 Future<void> _saveProject() async { final prefs = await SharedPreferences.getInstance(); final List<Project> projects = []; // 加载现有项目 Future<void> _loadProjects() async { final String? projectsJson = prefs.getString('projects'); if (projectsJson != null) { final List<dynamic> jsonList = json.decode(projectsJson); projects .addAll(jsonList.map((json) => Project.fromJson(json)).toList()); } } // 更新当前项目 final index = projects.indexWhere((p) => p.name == _project.name); if (index != -1) { projects[index] = _project; } // 保存更新后的项目列表 Future<void> _saveProjects() async { final List<Map<String, dynamic>> projectsJson = projects.map((project) => project.toJson()).toList(); await prefs.setString('projects', json.encode(projectsJson)); } } // 编辑第二列内容 void _editSecondColumn(BuildContext context, int rowIndex) { String currentValue = _project.rows[rowIndex].editableContent; showDialog( context: context, builder: (context) => AlertDialog( title: Text('编辑内容'), content: TextField( autofocus: true, controller: TextEditingController(text: currentValue), onChanged: (value) => currentValue = value, ), actions: [ TextButton( child: Text('取消'), onPressed: () => Navigator.pop(context), ), TextButton( child: Text('保存'), onPressed: () { setState(() { _project.rows[rowIndex].editableContent = currentValue; _saveProject(); }); Navigator.pop(context); }, ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_project.name), leading: IconButton( icon: Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context), ), ), body: SingleChildScrollView( child: Container( padding: EdgeInsets.all(16), child: Table( border: TableBorder.all(color: Colors.grey[300]!), columnWidths: { 0: FixedColumnWidth(80), 1: FixedColumnWidth(240), 2: FixedColumnWidth(80), }, children: [ // 表头 TableRow( decoration: BoxDecoration(color: Colors.transparent), children: [ TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text('固定列', style: TextStyle(fontWeight: FontWeight.bold)), )), TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text('可编辑列', style: TextStyle(fontWeight: FontWeight.bold)), )), TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text('操作', style: TextStyle(fontWeight: FontWeight.bold)), )), ], ), // 表格内容(9行) ...List.generate(9, (rowIndex) { return TableRow( decoration: BoxDecoration( color: Colors.transparent, ), children: [ // 第一列:固定内容,不可修改 TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text( _project.rows[rowIndex].fixedContent, style: TextStyle(color: Colors.grey[700]), ), ), ), // 第二列:可编辑内容,点击编辑 TableCell( child: GestureDetector( onTap: () => _editSecondColumn(context, rowIndex), child: Padding( padding: EdgeInsets.all(12), child: Text( _project.rows[rowIndex].editableContent.isEmpty ? '点击编辑' : _project.rows[rowIndex].editableContent, style: TextStyle( color: _project .rows[rowIndex].editableContent.isEmpty ? Colors.grey : Colors.black, ), ), ), ), ), // 第三列:按钮,弹出预设内容 TableCell( child: Padding( padding: EdgeInsets.all(8), child: IconButton( icon: Icon(Icons.info_outline, size: 20), color: Colors.blue, onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( title: Text('系统信息'), content: SingleChildScrollView( child: Text( _project.rows[rowIndex].presetContent, style: TextStyle(fontSize: 16), ), ), actions: [ TextButton( child: Text('关闭'), onPressed: () => Navigator.pop(context), ), ], ), ); }, ), ), ), ], ); }), ], ), ), ), ); } } 在这个代码中的表格页加一个保存按钮
最新发布
11-03
在使用 JavaServer Faces (JSF) 时,若遇到 `<h:dataTable>` 组件无法通过 `<f:ajax>` 或 `<p:ajax>` 正确更新的问题,通常与组件 ID 的作用域、Ajax 请求的配置以及数据模型的绑定方式密切相关。 ### 三级标题 在 JSF 中,`<h:dataTable>` 是一个命名容器(`NamingContainer`),其内部组件的 ID 会被加上前缀以确保唯一性。因此,在配置 `<f:ajax>` 或 `<p:ajax>` 的 `update` 属性时,若未使用绝对路径(以 `:` 开头),则可能无法正确识别目标组件。例如,在 `<h:dataTable>` 内部的组件若 ID 为 `row`, 则其完整 ID 应为 `:formId:tableId:row`,而非 `formId:tableId:row`。[^1] 此外,`<h:dataTable>` 本身不支持 `rendered` 属性,这意味着即使 Ajax 请求成功执行,若数据模型未正确更新或未触发组件重新渲染,页面内容也不会发生变化。为确保 `<h:dataTable>` 能够正确更新,可以将其包裹在一个 `<h:panelGroup>` 或 `<p:outputPanel>` 中,并在 `update` 属性中指定该容器的 ID。例如: ```xhtml <h:panelGroup id="tableContainer"> <h:dataTable value="#{bean.items}" var="item"> <h:column> <h:outputText value="#{item.name}" /> </h:column> </h:dataTable> </h:panelGroup> <f:ajax event="action" execute="..." update=":tableContainer" /> ``` 在某些情况下,若 `<h:dataTable>` 的内容依赖于复杂的后端逻辑或动态数据源,还应确保数据模型在后台被正确更新。例如,在 `<p:ajax>` 的 `listener` 方法中更新数据列表,并在前端绑定该列表。[^1] ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

talk_8

真诚赞赏,手有余香

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值