How to move popup window when scrolling a tkinter treeview?

Viewed 448

I have a tkinter treeview with a vertical scrollbar. To make it (look like) editable, I create a popup Entry when the user double-clicks on a cell of the treeview. However, I can't make the popup to move when the treeview is scrolled.

import tkinter  as tk
from tkinter import ttk

class EntryPopup(ttk.Entry):
    def __init__(self, parent, itemId, col, **kw):
        super().__init__(parent, **kw)
        self.tv = parent
        self.iId = itemId
        self.column = col
        self['exportselection'] = False

        self.focus_force()
        self.bind("<Return>", self.onReturn)

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

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


class EditableDataTable(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        self.parent = parent
        self.tree = None
        self.entryPopup = None
        columns = ("Col1", "Col2")
        # Create a treeview with vertical scrollbar.
        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)

        self.entryPopup = None
        self.curSelectedRowId = ""

        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)

    def createPopup(self, row, column):
        x,y,width,height = self.tree.bbox(row, column)

        # y-axis offset
        pady = height // 2
        self.entryPopup = EntryPopup(self.tree, row, column)
        self.entryPopup.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()

To reproduce the problem, double-click on the treeview. While the edit box is open, move your mouse pointer to hover over the treeview. Now scroll using the mouse wheel. The treeview moves but the popup edit box does not.

2 Answers

I have done something similar before by binding a function to mousewheel and recalculate all the new x & y location of your hovering widgets.

class EditableDataTable(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        self.parent = parent
        self.tree = None
        self.entryPopup = None
        self.list_of_entries = []

        ...

        self.tree.bind("<MouseWheel>", self._on_mousewheel)

    def _on_mousewheel(self, event):
        if self.list_of_entries:
            def _move():
                for i in self.list_of_entries:
                    try:
                        iid = i.iId
                        x, y, width, height = self.tree.bbox(iid, column="Col2") #hardcoded to col2
                        i.place(x=x, y=y+height//2, anchor='w', width=width)
                    except ValueError:
                        i.place_forget()
                    except tk.TclError:
                        pass
            self.master.after(5, _move)

    def createPopup(self, row, column):
        x,y,width,height = self.tree.bbox(row, column)
        # y-axis offset
        pady = height // 2
        self.entryPopup = EntryPopup(self.tree, row, column)
        self.list_of_entries.append(self.entryPopup)
        self.entryPopup.place(x=x, y=y+pady, anchor='w', width=width)

Note that this only works on the second column, but should be easy enough to implement for the rest.

You will need to do the math and move the entry widget when the tree is scrolled. I have edited your code and I programmed the scrollbar buttons only. If you click the button the entry widget will scroll with the tree. I did not program the wheelmouse scrolling or dragging the scrollbar. You should be able to figure out the rest.

import tkinter as tk
import tkinter.font as tkfont
from tkinter import ttk

class EntryPopup(ttk.Entry):
    def __init__(self, parent, itemId, col, **kw):
        super().__init__(parent, **kw)
        self.tv = parent
        self.iId = itemId
        self.column = col
        self['exportselection'] = False

        self.focus_force()
        self.bind("<Return>", self.onReturn)

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

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


class EditableDataTable(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        self.parent = parent
        self.tree = None
        self.entryPopup = None
        columns = ("Col1", "Col2")
        # Create a treeview with vertical scrollbar.
        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)

        self.entryPopup = None
        self.curSelectedRowId = ""

        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.vsb.bind('<ButtonPress-1>', self.func)

    def func(self, event):
        print(self.vsb.identify(event.x, event.y))
        if hasattr(self.entryPopup, 'y'):
            item = self.vsb.identify(event.x, event.y)

            if item == 'uparrow':
                self.entryPopup.y += 20
            elif item == 'downarrow':
                self.entryPopup.y -= 20

            self.entryPopup.place(x=self.entryPopup.x, y=self.entryPopup.y, )

    def createPopup(self, row, column):
        x, y, width, height = self.tree.bbox(row, column)

        # y-axis offset
        pady = height // 2
        self.entryPopup = EntryPopup(self.tree, row, column)
        self.entryPopup.x = x
        self.entryPopup.y = y+pady
        self.entryPopup.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()
Related