You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

滚动TreeView时如何同步移动弹出式ttk.Combobox的下拉列表?

解决ttk.Combobox下拉列表随TreeView滚动同步移动的问题

这个问题的核心在于ttk.Combobox的下拉列表(PopdownWindow)是一个独立的Tcl窗口,默认不会跟随输入框自动移动。我们需要手动获取这个窗口的引用,然后在TreeView滚动时同步调整它的位置。

关键原理

ttk.Combobox的下拉窗口可以通过Tcl内置命令ttk::combobox::PopdownWindow获取,拿到这个窗口的Tcl路径后,我们可以把它包装成tkinter的Toplevel对象,从而用tkinter的方法控制它的位置。

修改后的完整代码

import tkinter as tk
from tkinter import ttk

class ComboPopup(ttk.Combobox):
    def __init__(self, parent, itemId, col, **kw):
        super().__init__(parent, **kw)
        self.tv = parent
        self.iId = itemId
        self.column = col
        choices = ["option1", "option2", "option3"]
        v = tk.StringVar()
        self.config(state="readonly", textvariable=v, values=choices, width=9)
        self.focus_force()
        existingChoice = 1
        self.current(existingChoice)
        self.bind("<Return>", self.onReturn)
        self.bind("<Escape>", self.onEscape)
        # 跟踪下拉窗口是否展开
        self.popdown_shown = False
        # 绑定下拉窗口展开/收起相关事件
        self.bind("<<ComboboxSelected>>", lambda e: setattr(self, "popdown_shown", False))
        self.bind("<Button-1>", self.check_popdown)

    def check_popdown(self, event):
        # 点击箭头按钮时标记下拉窗口展开状态
        if event.x >= self.winfo_width() - 20:
            self.popdown_shown = True
            # 延迟获取下拉窗口引用,确保窗口已完成创建
            self.after(100, self.get_popdown_window)

    def get_popdown_window(self):
        # 获取下拉窗口的Tcl路径并包装为tkinter Toplevel对象
        pd_path = self.tk.call('ttk::combobox::PopdownWindow', self)
        if pd_path:
            self.popdown = tk.Toplevel(self, name=pd_path)

    def saveCombo(self):
        self.tv.set(self.iId, column=self.column, value=self.get())
        print("EntryPopup::saveEdit---{}".format(self.iId))

    def onEscape(self, event):
        print("ComboPopup::onEscape")
        self.tv.focus_set()
        self.destroy()

    def onReturn(self, event):
        self.tv.focus_set()
        self.saveCombo()
        self.destroy()

class EditableDataTable(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.tree = None
        self.comboPopup = None
        self.curSelectedRowId = ""
        columns = ("Col1", "Col2")
        # 创建带垂直滚动条的TreeView
        self.tree = ttk.Treeview(self, columns=columns, show="headings")
        self.tree.grid(column=0, row=0, sticky='news')
        self.tree.heading("#1", text="col1")
        self.tree.heading("#2", text="col2")
        self.vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=self.vsb.set)
        self.vsb.grid(column=1, row=0, sticky='ns')
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)

        # 填充测试数据
        col1 = []
        col2 = []
        for r in range(50):
            col1.append("data 1-{}".format(r))
            col2.append("data 2-{}".format(r))
        for i in range(min(len(col1),len(col2))):
            self.tree.insert('', i, values=(col1[i], col2[i]))

        self.tree.bind('<Double-1>', self.onDoubleClick)
        self.tree.bind("<MouseWheel>", self.onMousewheel)
        # 绑定滚动条拖动事件,确保拖动时也同步移动下拉窗口
        self.vsb.bind("<B1-Motion>", self.onMousewheel)

    def onMousewheel(self, event):
        if self.comboPopup and ttk.Combobox.winfo_exists(self.comboPopup):
            try:
                iid = self.comboPopup.iId
                x, y, width, height = self.tree.bbox(iid, column=self.comboPopup.column)
                # 移动Combobox输入框到当前行对应位置
                self.comboPopup.place(x=x, y=y+height//2, anchor='w', width=width)
                # 如果下拉窗口处于展开状态,同步调整它的位置
                if self.comboPopup.popdown_shown and hasattr(self.comboPopup, 'popdown'):
                    try:
                        # 保持下拉窗口相对于输入框的位置偏移
                        pd_x = self.comboPopup.winfo_x()
                        pd_y = self.comboPopup.winfo_y() + self.comboPopup.winfo_height()
                        self.comboPopup.popdown.geometry(f"+{pd_x}+{pd_y}")
                    except tk.TclError:
                        # 下拉窗口已被关闭,重置状态
                        self.comboPopup.popdown_shown = False
            except ValueError:
                # 当前行已不可见,隐藏输入框
                self.comboPopup.place_forget()

    def createPopup(self, row, column):
        x, y, width, height = self.tree.bbox(row, column)
        # y轴偏移,让输入框对齐单元格
        pady = height // 2
        self.comboPopup = ComboPopup(self.tree, row, column)
        self.comboPopup.place(x=x, y=y+pady, anchor='w', width=width)

    def onDoubleClick(self, event):
        rowid = self.tree.identify_row(event.y)
        column = self.tree.identify_column(event.x)
        self.createPopup(rowid, column)

root = tk.Tk()
for row in range(2):
    root.grid_rowconfigure(row, weight=1)
root.grid_columnconfigure(0, weight=1)

label = tk.Label(root, text="Double-click to edit and press 'Enter'")
label.grid(row=0, column=0, sticky='news', padx=10, pady=5)

dataTable = EditableDataTable(root)
dataTable.grid(row=1, column=0, sticky="news", pady=10, padx=10)

root.geometry("450x300")
root.mainloop()

代码说明

  1. 跟踪下拉窗口状态:在ComboPopup类中添加popdown_shown属性,通过点击事件判断用户是否展开了下拉列表,并标记状态。
  2. 获取下拉窗口引用:使用ttk::combobox::PopdownWindow命令获取下拉窗口的Tcl路径,将其包装为tk.Toplevel对象,这样就能用tkinter的方法操作它。
  3. 同步滚动位置:在onMousewheel方法中,不仅移动Combobox输入框的位置,还会检查下拉窗口是否展开,如果展开则同步调整它的位置,保持相对于输入框的偏移。
  4. 覆盖滚动场景:绑定了滚动条的<B1-Motion>事件,确保拖动滚动条时也能同步移动下拉窗口,覆盖所有滚动操作场景。

这样修改后,当TreeView滚动时,Combobox的输入框和下拉列表都会同步移动,解决了你的问题。

内容的提问来源于stack exchange,提问作者Jakaria

火山引擎 最新活动