ttk.Combobox

问题描述

为了编辑 ttk.Treeview 的元素,我搜索了每当用户双击它时都会从树视图中弹出一个 ttk.Entry一个 ttk.ComboBox

为此,我找到了一个 tkk.Entry 派生类,我对其进行了一些改编以满足我的需要(也可能是最初编写它的人没有完成它)。 看来这个工作正常。
然后我从 ttk.ComboBox 派生了一个类,灵感来自前一个

这是代码,包含两个类、树视图和一个主窗口,可让一切正常工作。

import tkinter as tk
from tkinter import ttk


class EntryPopup(ttk.Entry):

    def __init__(self,parent,iid,col,text,**kw):
        super().__init__(parent,**kw)
        self.tv = parent
        self.iid = iid
        self.col = col

        self.insert(0,text)
        self.select_range(0,tk.END)

        self.focus_force()
        self.bind("<Return>",self.on_return)
        self.bind("<Escape>",lambda *ignore: self.destroy())
        self.bind("<FocusOut>",self.on_return)

    def on_return(self,event):
        if self.col == '#0':
            self.tv.item(self.iid,text=self.get())
        else:
            self.tv.set(self.iid,self.col,self.get())
        self.destroy()


class ComboPopup(ttk.ComboBox):

    def __init__(self,**kw)
        self.tv = parent
        self.iid = iid
        self.col = col
        self.set(self.tv.set(iid,col))

        self.focus()
        self.bind("<Escape>",self.focus_out())
        self.bind("<<ComboBoxSelected>>",self.new_selection())

    def new_selection(self):
        self.tv.set(self.iid,self.get())

    def focus_out(self):
        self.new_selection()
        self.destroy()


class App(ttk.Frame):

    def __init__(self,*args,**kwargs):
        ttk.Frame.__init__(self,parent)
        self.parent = parent
        self.entryPopup = None

        # Create Treeview
        self.tree = ttk.Treeview(self.parent,column=('A','B'),selectmode='browse',height=7)
        self.tree.pack(expand=True,fill='both',side='top')

        # Setup column heading
        self.tree.heading('#0',text=' Items',anchor='center')
        self.tree.heading('#1',text=' A',anchor='center')
        self.tree.heading('#2',text=' B',anchor='center')

        self.tree.bind("<Double-Button>",self.on_double_click)

        self.tree.insert('','end',text="First item",value=("A's value","B's value"))
        self.tree.insert('',text="Second item","B's value"))

    def on_double_click(self,event):
        # What row and column was clicked on
        rowid = self.tree.identify_row(event.y)
        column = self.tree.identify_column(event.x)

        # get cell position info
        x,y,width,height = self.tree.bBox(rowid,column)

        # y-axis offset
        pady = height / 2

        # place Entry popup properly
        if column == '#0':
            text = self.tree.item(rowid,'text')
            self.entryPopup = EntryPopup(self.tree,rowid,column,text)
        else:
            self.entryPopup = ComboPopup(self.tree,values=("A","b","3"))
        self.entryPopup.place(x=x,y=y + pady,width=width,height=1.25 * height,anchor='w')


if __name__ == '__main__':
    root = tk.Tk()
    root.geometry('900x600+300+300')
    App(root)
    root.mainloop()

因此,如果用户双击第一列的单元格,则会在那里生成一个 EntryPopup 对象,如果在另一列中,则它是一个 ComboPopup 对象。

EntryPopup 对象中,在 <Escape> 事件中,对象只是被销毁,没有任何改变;在 <Return><FocusOut> 上所做的任何更改都会保留。 我认为在 <FocusOut> 上也执行此操作很重要的原因是当用户进入另一个单元格(例如,通过单击它)时会发生这种情况。

现在在 ComboPopup 对象中,<FocusOut> 事件是一个问题。 显然,当用户首先尝试访问组合框时会触发此事件,这引发了我无法理解的错误

文件“/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/tkinter/__init__.py”,第 1340 行,在 _bind 中 return self.tk.call(what + (sequence,)) _tkinter.TclError: 错误的窗口路径名“.!treeview.!combopopup”

另一方面,如果我将 <FocusOut> 的行为设置为与 <Escape> 一样,则不会出现错误,但我也永远不会在组合框中保留用户的选择。
最后,如果我将它设置为 <Return> 事件,则不会出现错误并且选择的值被保留,但组合框仍然存在,在这种情况下,我可以为每个单元格设置一个组合框,而它们只会去如果触发 <Escape> 事件,则再次丢失引入的数据。

我已经解决这个问题有一段时间了,我认为我自己无法找到解决方案。
也许有一个简单的解决方法

解决方法

我想我有一个令人满意的方法来解决我在这篇文章中遇到的代码问题。
我的意思是,我没有完全修复 <FocusOut> 行为,而是将其替换为从树视图调用任何鼠标单击或双击。

请注意,如何控制同一个小部件上的“单击”和“双击”事件并不明显,因为在双击中有两次单击。
幸运的是,我找到了 this answer which addresses that problem(我觉得奇怪的是它在我的之前没有任何赞成票)。

上述所有内容都适用于这种新方法,结果代码为

import tkinter as tk
from tkinter import ttk


class EntryPopup(ttk.Entry):

    def __init__(self,parent,iid,col,text,**kw):
        super().__init__(parent,**kw)
        self.tv = parent
        self.iid = iid
        self.col = col

        self.insert(0,text)
        self.select_range(0,tk.END)

        self.focus_force()
        self.bind("<Return>",self.on_return)
        self.bind("<Escape>",self.on_escape)

    def on_return(self,event):
        self.close()

    def on_escape(self,event):
        self.destroy()

    def close(self):
        if self.col == '#0':
            self.tv.item(self.iid,text=self.get())
        else:
            self.tv.set(self.iid,self.col,self.get())
        self.destroy()


class ComboPopup(ttk.Combobox):

    def __init__(self,**kw)
        self.tv = parent
        self.iid = iid
        self.col = col
        self.old_text = self.tv.set(iid,col)
        self.set(self.old_text)

        self.focus()
        self.bind("<Escape>",self.on_escape)
        self.bind("<<ComboboxSelected>>",self.new_selection)
        self.bind("<Return>",self.on_return)

    def new_selection(self,event=None):
        self.tv.set(self.iid,self.get())

    def on_return(self,event):
        self.tv.set(self.iid,self.old_text)
        self.destroy()

    def close(self):
        self.new_selection()
        self.destroy()


class App(ttk.Frame):

    def __init__(self,*args,**kwargs):
        ttk.Frame.__init__(self,parent)
        self.parent = parent
        self.cellPopup = None
        self.single_click = False

        # Create Treeview
        self.tree = ttk.Treeview(self.parent,column=('A','B'),selectmode='browse',height=7)
        self.tree.pack(expand=True,fill='both',side='top')

        # Setup column heading
        self.tree.heading('#0',text=' Items',anchor='center')
        self.tree.heading('#1',text=' A',anchor='center')
        self.tree.heading('#2',text=' B',anchor='center')

        self.tree.bind("<Double-Button-1>",lambda event: self.on_click(event,101))
        self.tree.bind("<Button-1>",1))

        self.tree.insert('','end',text="First item",value=("A's value 1","B's value 1"))
        self.tree.insert('',text="Second item",value=("A's value 2","B's value 2"))

    def destroy_cell_popup(self):
        if self.single_click:
            self.single_click = False
            if self.cellPopup and self.cellPopup.winfo_exists():
                self.cellPopup.close()
                self.cellPopup = None

    def on_click(self,event,extra=None):
        """ Executed,when a row is single or double-clicked.
        Opens EntryPopup or ComboPopup above the item's column,so it is possible to select text """
        if extra == 1:
            self.single_click = True
            self.parent.after(200,self.destroy_cell_popup)
            return
        elif extra == 101:
            self.single_click = False
            if self.cellPopup and self.cellPopup.winfo_exists():
                self.cellPopup.close()
                self.cellPopup = None

        # What row and column was clicked on
        rowid = self.tree.identify_row(event.y)
        column = self.tree.identify_column(event.x)

        # Check if the double click was on some row or empty space
        if rowid == '':
            return

        # get cell position info
        x,y,width,height = self.tree.bbox(rowid,column)

        # y-axis offset
        pady = height / 2

        # place Entry popup properly
        if column == '#0':
            text = self.tree.item(rowid,'text')
            self.cellPopup = EntryPopup(self.tree,rowid,column,text)
        else:
            self.cellPopup = ComboPopup(self.tree,values=("A","b","3"))
        self.cellPopup.place(x=x,y=y + pady,width=width,height=1.25 * height,anchor='w')


if __name__ == '__main__':
    root = tk.Tk()
    root.geometry('900x600+300+300')
    App(root)
    root.mainloop()

它似乎完美无缺。 现在,无论如何...