动态载入数据的无刷新TreeView控件(5)

本文深入探讨了TreeView控件的交互实现,包括鼠标选择、键盘导航及代码控制等多种交互方式。重点介绍了如何处理复杂的鼠标点击事件,如Shift+Click区间选择等功能,并详细解释了键盘事件的处理逻辑。
    今天讨论一下TreeView控件的交互问题。包括鼠标对TreeNode的选取(单选&多选)、Checked;键盘对TreeNode的选取(单选&多选)、Checked;通过代码和控件交互三种方式。最后提供一个现阶段完成版本的演示示例供大家测试。

    使用封装好的代码来操作TreeView的UI,是我们 上次主要讲述的内容。要实现交互,最重要的就是管理键盘和鼠标的事件。我原来讲过,关于TreeNode的UI外观,我们在TreeNodeBase那个类中进行处理,而把TreeNode的事件处理放在TreeNode类中来处理,这样的设计是为了提供一个清晰的编程结构。下面我们先看一下TreeNode类的定义和TreeNode.Render方法的代码:
ContractedBlock.gif < script  language ="javascript" > dot.gif </ script >


    当然,为了能灵活的使用鼠标和TreeViee交互,我们需要处理大部分的鼠标事件。包括click、mousedown、mouseover、mouseout和mousemove。同时在处理这些鼠标事件时,很多时候还需要和键盘配合来操作,比如:Shift+Click的区段选取,Ctrl+Click的check方式选取等。

    在TreeNode的Render方法中,处理节点展开和收缩的事件是比较简单的,因为那只是一个开/关状态的转换。在TreeNode上做Check操作时需要注意,为了让控件的脚本类(TreeNode的实例)和DHTML类之间属性值同步,我们需要完全控制Checkbox的状态的变化,并且为了避免后面我们使用键盘来Check节点时出错,我们还必须保证Checkbox始终不能获得焦点。

None.gif input.onfocus =  function(){ FindParentElement( this, 'TD').focus(); }; None.gif

    // 总是把焦点置于Checkbox的Parent元素上

    由于我们已经实现了一套对UI更新的机制,就是统一使用ApplyUIChange()方法来负责。所以除了mousedown事件外,其它的事件处理函数都非常的简单,只需要设置一下控件属性,然后调用ApplyUIChange()就行了,比如:__CheckBoxOnClick(),它的实现就非常的简单清晰。

None.gif TreeNode.__CheckBoxOnClick =  function()
None.gif {
None.gif     var elmtNode = FindParentElement( this, 'TR');
None.gif     if ( elmtNode && elmtNode.Comment == 'TreeNode' )
None.gif    {
None.gif         var objNode = elmtNode.Object;
None.gif         if ( objNode )
None.gif        {
None.gif            objNode.SetChecked( this.checked);
None.gif        }
None.gif    }   
None.gif };


    但是由于mousedown事件需要和键盘配合,并且本身它自己就承担着很多的交互功能,所以处理起来比较麻烦。而其中最麻烦的就是按住Shift键再Click的区段TreeNode选取功能。

None.gif if ( evt.shiftKey && !evt.ctrlKey )
ExpandedBlockStart.gif{
InBlock.gif     if ( innerCache.m_Selecteds.m_Count == 0 )
ExpandedSubBlockStart.gif    {
InBlock.gif         objNode.SetSelected( true);
InBlock.gif         innerCache.m_LastSelected = objNode;
ExpandedSubBlockEnd.gif    }
InBlock.gif    var startNode = innerCache.m_LastSelected;
InBlock.gif    var endNode = objNode; 
InBlock.gif    var posStart = GetAbsoluteLocation(startNode.m_Element).absoluteTop;
InBlock.gif    var posEnd = GetAbsoluteLocation(endNode.m_Element).absoluteTop;
InBlock.gif    innerCache.UnselectAll();
InBlock.gif     if ( startNode != endNode )
ExpandedSubBlockStart.gif    {  
InBlock.gif          if ( posStart > posEnd )
ExpandedSubBlockStart.gif         {
InBlock.gif                  var tmp = startNode;
InBlock.gif                  startNode = endNode;
InBlock.gif                  endNode = tmp;
ExpandedSubBlockEnd.gif         }
InBlock.gif         var curNode = startNode;
InBlock.gif          do
ExpandedSubBlockStart.gif         {
InBlock.gif                  curNode.SetSelected( true);
InBlock.gif                  curNode = curNode.GetNextRowNode();
ExpandedSubBlockEnd.gif         }
InBlock.gif          while(curNode != endNode);
InBlock.gif         endNode.SetSelected( true); 
ExpandedSubBlockEnd.gif    }
InBlock.gif     else
ExpandedSubBlockStart.gif    {
InBlock.gif         endNode.SetSelected( true);
ExpandedSubBlockEnd.gif    }   
ExpandedBlockEnd.gif}

    这个功能首先需要判断,当前事件是不是mousedown并且同时键盘Shift被按下了。上面第一个if就是做这个判断的,当确认了使这个事件条件后,我们需要判断当前的TreeView上是否存在最近(一种被选中时的循序的状态)被Selected的节点,这个节点将被用作区段选取的起点,鼠标mousedown(width shift)的节点将会是区段选取的终点。innerCache.m_Selecteds.m_Count == 0说明没有起点,那么我们就置当前被点下的点为起点(这是一个最近点),同时完成本次选取操作。如果在mouse down width shift key的时候,TreeView上有超过一个节点已被Selected,那么我们就取出起点(最近点)和终点,并开始计算起点和终点的位置关系,谁在上谁在下?然后把这两个节点整理为起点始终是在上面的节点,终点始终是在下面的节点(在屏幕上的相对位置),这样是为了使用同样的代码就能把两个方向的Selected工作都做了。然后清除TreeView上所有已选中的节点,从起点开始往终点Selected就行了:

None.gif  do
None.gif {
None.gif     curNode.SetSelected( true);
None.gif     curNode = curNode.GetNextRowNode();
None.gif }
None.gif  while(curNode != endNode);


    简单吧,可是这个curNode.GetNextRowNode()又不是很简单emembarrassed.gif,在处理键盘事件中我们再来详细说它。那么我们在键盘上需要处理那些操作呢?看下面的代码,我们处理:Up、Down、+、-、Space和Esc这几个按键。

ContractedBlock.gif < script  language ="javascript" > dot.gif </ script >

   上面的代码,switch前是为了取到被操作的TreeNode对象。+、-、Space和Esc都很简单,从代码中一眼就看明白了,麻烦的就是Up和Down这两个操作。其实在这里希望实现的操作都是WinControl的TreeView中支持的,只是被移到Web的控件上而已。Up和Down操作就是Selected最近那个节点的相邻节点,或上面的或下面的。如果我们在一个层次上来找一个节点的上一个和下一个,那是非常简单的index-1、index+1就行了,可是在树这样的层次结构中,寻找一个它的看起来的上一个或下一个展开的节点,就比较郁闷了。两个方法:

None.gif    var previousNode = currentNode.GetPreviousRowNode();
None.gif    var nextNode = currentNode.GetNextRowNode(); None.gif

    要说明白它们是干嘛的,都比较难。这么说吧,当这个TreeView展现我们面前时,我们暂时忽略节点之间的层级关系,把它们的节点看成向List的条目一样的结构,GetPreviousRowNode(),就是取上一行的Node,GetNextRowNode()就是取当前节点下一行的节点。

ContractedBlock.gif < script  language ="javascript" > dot.gif </ script >

    弄个图来配合代码看可能比较容易理解些emsmile.gif
  TreeView-4.png

    嗯,其它的问题先玩玩demo再继续讨论吧emsmile.gif
  

本文转自博客园鸟食轩的博客,原文链接:http://www.cnblogs.com/birdshome/,如需转载请自行联系原博主。

这有一段代码,增加一些功能,里面显示数据的表格,大小改成动态调整的,目前我里面的逻辑,正确的数据显示绿色,错误的是红色,在这个表格的左上角,加两个勾选,显示成功,和显示失败,默认是两个都显示,以下为代码import tkinter as tk from tkinter import filedialog, messagebox, ttk import pandas as pd import os import subprocess class ExcelViewerApp: def __init__(self, root): self.root = root self.root.title("TPC效率化工具") self.root.geometry("1000x700") # 使用网格布局的主容器 self.main_frame = tk.Frame(root, bg='#f0f0f0', padx=15, pady=15) self.main_frame.pack(fill=tk.BOTH, expand=True) # 配置网格 self.main_frame.columnconfigure(0, weight=0) self.main_frame.columnconfigure(1, weight=1) self.main_frame.columnconfigure(2, weight=0) self.main_frame.columnconfigure(3, weight=0) # === 主表控件 === # 文件路径行 tk.Label(self.main_frame, text="主表文件路径:", bg='#f0f0f0').grid(row=0, column=0, sticky='w', padx=(0, 5)) self.path_label = tk.Label( self.main_frame, text="未选择主表文件", anchor='w', relief=tk.SUNKEN, bg="#ffffff", padx=5, pady=5 ) self.path_label.grid(row=0, column=1, sticky='ew', padx=(0, 10)) # 工作表行 tk.Label(self.main_frame, text="主表工作表:", bg='#f0f0f0').grid(row=1, column=0, sticky='w', padx=(0, 5), pady=(10, 0)) self.sheet_label = tk.Label( self.main_frame, text="未选择主表工作表", anchor='w', relief=tk.SUNKEN, bg="#ffffff", padx=5, pady=5 ) self.sheet_label.grid(row=1, column=1, sticky='ew', padx=(0, 10), pady=(10, 0)) # 打开文件按钮 self.open_btn = tk.Button( self.main_frame, text="打开主表文件", command=self.open_excel, bg='#f0f0f0', fg='black', padx=15, pady=8, width=10 ) self.open_btn.grid(row=0, column=2, rowspan=2, sticky='ns', padx=(0, 5)) # 选择主表按钮 self.select_btn = tk.Button( self.main_frame, text="选择主表", command=self.load_excel, bg='#f0f0f0', fg='black', padx=15, pady=8, width=10 ) self.select_btn.grid(row=0, column=3, rowspan=2, sticky='ns', padx=(0, 5)) # === 副表控件 === (添加在下方) tk.Label(self.main_frame, text="副表文件路径:", bg='#f0f0f0').grid(row=2, column=0, sticky='w', padx=(0, 5), pady=(20, 0)) self.aux_path_label = tk.Label( self.main_frame, text="未选择副表文件", anchor='w', relief=tk.SUNKEN, bg="#ffffff", padx=5, pady=5 ) self.aux_path_label.grid(row=2, column=1, sticky='ew', padx=(0, 10), pady=(20, 0)) tk.Label(self.main_frame, text="副表工作表:", bg='#f0f0f0').grid(row=3, column=0, sticky='w', padx=(0, 5), pady=(10, 0)) self.aux_sheet_label = tk.Label( self.main_frame, text="未选择副表工作表", anchor='w', relief=tk.SUNKEN, bg="#ffffff", padx=5, pady=5 ) self.aux_sheet_label.grid(row=3, column=1, sticky='ew', padx=(0, 10), pady=(10, 0)) # 打开副表文件按钮 self.aux_open_btn = tk.Button( self.main_frame, text="打开副表文件", command=self.open_aux_excel, bg='#f0f0f0', fg='black', padx=15, pady=8, width=10 ) self.aux_open_btn.grid(row=2, column=2, rowspan=2, sticky='ns', padx=(0, 5), pady=(20, 0)) # 选择副表按钮 self.aux_select_btn = tk.Button( self.main_frame, text="选择副表", command=self.load_aux_excel, bg='#f0f0f0', fg='black', padx=15, pady=8, width=10 ) self.aux_select_btn.grid(row=2, column=3, rowspan=2, sticky='ns', padx=(0, 5), pady=(20, 0)) # 使用数组存储数据 self.dataset = [] # 主表数据 self.aux_dataset = [] # 副表数据 # 窗口居中 self.center_window(self.root) # 添加对比按钮 self.compare_btn = tk.Button( self.main_frame, text="对比数据", command=self.compare_data, bg='#4CAF50', fg='white', padx=15, pady=8, width=15 ) self.compare_btn.grid(row=4, column=1, columnspan=2, pady=20) # 创建结果显示区域 self.result_frame = tk.Frame(root, bg='#f0f0f0') self.result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 添加Treeview显示结果 self.tree = ttk.Treeview(self.result_frame) self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 添加滚动条 scrollbar = ttk.Scrollbar(self.result_frame, orient="vertical", command=self.tree.yview) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.tree.configure(yscrollcommand=scrollbar.set) # 状态标签 self.status_label = tk.Label(root, text="就绪", bg='#f0f0f0', anchor='w') self.status_label.pack(fill=tk.X, padx=10, pady=5) def center_window(self, window): window.update_idletasks() width = window.winfo_width() height = window.winfo_height() x = (window.winfo_screenwidth() // 2) - (width // 2) y = (window.winfo_screenheight() // 2) - (height // 2) window.geometry(f"+{x}+{y}") # ===== 主表功能 ===== def open_excel(self): """打开主表文件""" file_path = self.path_label.cget("text") if not file_path or file_path == "未选择主表文件": messagebox.showwarning("警告", "请先选择主表Excel文件") return try: if os.name == 'nt': os.startfile(file_path) elif os.name == 'posix': subprocess.call(('open', file_path)) else: subprocess.call(('xdg-open', file_path)) except Exception as e: messagebox.showerror("错误", f"打开主表文件失败: {str(e)}") def load_excel(self): """加载主表Excel""" file_path = filedialog.askopenfilename( title="选择主表Excel文件", filetypes=[("Excel文件", "*.xlsx *.xls")] ) if not file_path: return try: self.path_label.config(text=file_path) self.select_main_sheet(file_path) except Exception as e: messagebox.showerror("错误", f"读取主表文件失败: {str(e)}") def select_main_sheet(self, file_path): """选择主表工作表""" try: xl = pd.ExcelFile(file_path) sheet_names = xl.sheet_names selector = tk.Toplevel(self.root) selector.title("选择主表工作表") selector.geometry("300x150") tk.Label(selector, text="请选择主表工作表:").pack(pady=10) sheet_var = tk.StringVar(selector) combobox = ttk.Combobox( selector, textvariable=sheet_var, values=sheet_names, state="readonly", width=40 ) combobox.pack(pady=10, padx=20, fill=tk.X) combobox.current(0) tk.Button( selector, text="确认选择", command=lambda: self.process_main_sheet_selection( file_path, sheet_var.get(), selector ), bg='#f0f0f0', fg='black', padx=10, pady=5 ).pack(pady=15) self.center_window(selector) except Exception as e: messagebox.showerror("错误", f"读取主表文件失败: {str(e)}") def process_main_sheet_selection(self, file_path, sheet_name, selector): """处理主表工作表选择结果""" try: df = pd.read_excel(file_path, sheet_name=sheet_name, header=0, skiprows=list(range(0,9))) self.dataset = df.to_dict(orient='records') row_count = len(self.dataset) col_count = len(df.columns) if row_count > 0 else 0 self.sheet_label.config(text=f"{sheet_name} ({row_count}行×{col_count}列)") selector.destroy() messagebox.showinfo("加载成功", f"主表工作表 [{sheet_name}] 已载入\n" f"数据维度: {row_count}行 × {col_count}列" ) print(f"主表数据示例: {self.dataset[0] if self.dataset else '空'}") except Exception as e: messagebox.showerror("错误", f"加载主表数据失败: {str(e)}") # ===== 副表功能 ===== def open_aux_excel(self): """打开副表文件""" file_path = self.aux_path_label.cget("text") if not file_path or file_path == "未选择副表文件": messagebox.showwarning("警告", "请先选择副表Excel文件") return try: if os.name == 'nt': os.startfile(file_path) elif os.name == 'posix': subprocess.call(('open', file_path)) else: subprocess.call(('xdg-open', file_path)) except Exception as e: messagebox.showerror("错误", f"打开副表文件失败: {str(e)}") def load_aux_excel(self): """加载副表Excel""" file_path = filedialog.askopenfilename( title="选择副表Excel文件", filetypes=[("Excel文件", "*.xlsx *.xls")] ) if not file_path: return try: self.aux_path_label.config(text=file_path) self.select_aux_sheet(file_path) except Exception as e: messagebox.showerror("错误", f"读取副表文件失败: {str(e)}") def select_aux_sheet(self, file_path): """选择副表工作表""" try: xl = pd.ExcelFile(file_path) sheet_names = xl.sheet_names selector = tk.Toplevel(self.root) selector.title("选择副表工作表") selector.geometry("300x150") tk.Label(selector, text="请选择副表工作表:").pack(pady=10) sheet_var = tk.StringVar(selector) combobox = ttk.Combobox( selector, textvariable=sheet_var, values=sheet_names, state="readonly", width=40 ) combobox.pack(pady=10, padx=20, fill=tk.X) combobox.current(0) tk.Button( selector, text="确认选择", command=lambda: self.process_aux_sheet_selection( file_path, sheet_var.get(), selector ), bg='#f0f0f0', fg='black', padx=10, pady=5 ).pack(pady=15) self.center_window(selector) except Exception as e: messagebox.showerror("错误", f"读取副表文件失败: {str(e)}") def process_aux_sheet_selection(self, file_path, sheet_name, selector): """处理副表工作表选择结果""" try: #df = pd.read_excel(file_path, sheet_name=sheet_name, header=0, skiprows=list(range(0,9))) df = pd.read_excel(file_path, sheet_name=sheet_name, header=0) self.aux_dataset = df.to_dict(orient='records') row_count = len(self.aux_dataset) col_count = len(df.columns) if row_count > 0 else 0 self.aux_sheet_label.config(text=f"{sheet_name} ({row_count}行×{col_count}列)") selector.destroy() messagebox.showinfo("加载成功", f"副表工作表 [{sheet_name}] 已载入\n" f"数据维度: {row_count}行 × {col_count}列" ) print(f"副表数据示例: {self.aux_dataset[0] if self.aux_dataset else '空'}") except Exception as e: messagebox.showerror("错误", f"加载副表数据失败: {str(e)}") def compare_data(self): """对比主表和副表数据并设置背景色,仅显示和对比共有列,并显示具体错误列""" if not self.dataset or not self.aux_dataset: messagebox.showwarning("警告", "请先加载主表和副表数据") return # 获取主表和副表的列名 main_columns = set(self.dataset[0].keys()) if self.dataset else set() aux_columns = set(self.aux_dataset[0].keys()) if self.aux_dataset else set() # 获取两个表共有的列(包括Z2列,因为Z2用于匹配) common_columns = sorted(main_columns & aux_columns - {'Z2'}) # 确保Z2列存在(用于匹配的关键列) if 'Z2' not in main_columns or 'Z2' not in aux_columns: messagebox.showerror("错误", "主表或副表缺少Z2列,无法进行对比") return # 配置Treeview列 - 只显示共有列 self.tree["columns"] = common_columns self.tree["show"] = "headings" for col in common_columns: self.tree.heading(col, text=col) self.tree.column(col, width=100, anchor=tk.CENTER) # 添加Z2列作为第一列(用于显示匹配状态及详细错误) self.tree["columns"] = ["Z2"] + common_columns self.tree.heading("Z2", text="Z2(匹配状态)") self.tree.column("Z2", width=200, anchor=tk.CENTER) # 增加宽度以显示更多错误信息 # 清空现有数据 for item in self.tree.get_children(): self.tree.delete(item) # 创建用于设置颜色的tag self.tree.tag_configure('match', background='#DFF0D8') # 绿色 self.tree.tag_configure('mismatch', background='#F8D7DA') # 红色 self.tree.tag_configure('not_found', background='#F8D7DA') # 红色 # 统计结果 match_count = 0 mismatch_count = 0 not_found_count = 0 # 创建从Z2值到副表行的映射(提高查找效率) aux_map = {} for aux_row in self.aux_dataset: z2_value = aux_row.get('Z2', None) if z2_value is not None: aux_map.setdefault(z2_value, []).append(aux_row) # 记录所有匹配详情(Z2值→匹配列列表) mismatch_details = {} # 遍历主表每一行 for main_row in self.dataset: main_z2 = main_row.get('Z2', None) found_in_aux = False all_matched = True mismatched_columns = [] # 存储当前行匹配的列名 # 查找匹配的副表行 matching_aux_rows = aux_map.get(main_z2, []) if main_z2 is not None else [] if matching_aux_rows: found_in_aux = True # 检查所有匹配行中是否有完全匹配的 row_match = False for aux_row in matching_aux_rows: current_match = True # 只比较共有列 for col in common_columns: if col in main_row and col in aux_row: if main_row[col] != aux_row[col]: current_match = False # 记录匹配的列(避免重复) if col not in mismatched_columns: mismatched_columns.append(col) if current_match: row_match = True mismatched_columns = [] # 完全匹配时清空匹配列 break all_matched = row_match # 准备Treeview数据 if not found_in_aux: z2_display = f"{main_z2} ✗ (未找到)" not_found_count += 1 elif all_matched: z2_display = f"{main_z2} ✓" match_count += 1 else: # 显示匹配的具体列(最多显示3个) error_cols = ", ".join(mismatched_columns[:3]) if len(mismatched_columns) > 3: error_cols += f" 等{len(mismatched_columns)}处" z2_display = f"{main_z2} ✗ ({error_cols})" mismatch_count += 1 mismatch_details[main_z2] = mismatched_columns values = [z2_display] + [main_row.get(col, '') for col in common_columns] item_id = self.tree.insert("", "end", values=values) # 根据匹配情况设置背景色 if not found_in_aux: self.tree.item(item_id, tags=('not_found',)) elif all_matched: self.tree.item(item_id, tags=('match',)) else: self.tree.item(item_id, tags=('mismatch',)) # 更新状态(添加错误详情) detail_text = "" if mismatch_details: sample_errors = "\n".join([f"Z2={k}: {', '.join(v[:3])}{'...' if len(v)>3 else ''}" for k,v in list(mismatch_details.items())[:3]]) if len(mismatch_details) > 3: sample_errors += f"\n...等{len(mismatch_details)}行存在差异" detail_text = f"\n错误详情示例:\n{sample_errors}" self.status_label.config( text=(f"对比完成 | 匹配: {match_count}行 | 匹配: {mismatch_count}行 | 未找到: {not_found_count}行 | " f"共有列: {', '.join(common_columns)}{detail_text}") ) if __name__ == "__main__": root = tk.Tk() app = ExcelViewerApp(root) root.mainloop()
最新发布
09-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值