Skip to content
Advertisement

A tkinter GUI with a resizable left panel: resizable arrow indicator appearance issue

I created this test code to simulate a Tk window with a left panel that is resizable. How it works:

  1. When the mouse pointer move over the ttk.Separator, a resizable arrow indicator will appear where the mouse pointer is.
  2. Pressing the left mouse button and moving the mouse pointer, the width of the left panel will resize corresponding to the x position of the mouse pointer.
  3. The resizable arrow indicator should also move in sync with the mouse pointer.

I am able to perform steps 1 & 2. However, for step 3, I have an issue. The resizable arrow indicator in step 1 does not disappear while the resizable arrow indicator position in step 3 does follow the mouse pointer occasionally: there appears to be a competition btw these two steps.

How do I fix this issue?

Test code:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import tkinter as tk
import tkinter.ttk as ttk
    

class App(ttk.Frame):
    def __init__(self, master):
        self.master = master
        self.master.title('App')
        self.master.geometry('1500x140')
        self.mouse_pointer_x = tk.IntVar()
        self.mouse_pointer_y = tk.IntVar()
        super().__init__(master, style='App.TFrame', borderwidth=20)
        self._set_style()
        self._create_widgets()
        self.bind('<Motion>', self._store_mouse_pointer_coordinate)

    def _set_style(self):
        self.style = ttk.Style()
        self.style.configure('App.TFrame', background='pink')
        self.style.configure('lframe.TFrame', background='green')
        self.style.configure('rframe.TFrame', background='orange')
        self.style.configure('TSeparator', background='red')

    def _create_widgets(self):
        self.lframe = ttk.Frame(self, style='lframe.TFrame', borderwidth=20)
        self.rframe = ttk.Frame(self, style='rframe.TFrame', borderwidth=20)
        self.divider = ttk.Separator(self, orient=tk.VERTICAL)
        self.lframe.grid(row=0, column=0, sticky='nsew')
        self.divider.grid(row=0, column=1, sticky='nsew', padx=20)
        self.rframe.grid(row=0, column=2, sticky='nsew')

        harrow = './resize-arrow-24.png'
        self.icon_harrow = tk.PhotoImage(file=harrow)
        self.harrow = ttk.Label(self, image=self.icon_harrow)
        self.harrow.place(x=0, y=0)
        self.harrow.place_forget()

        self.ltv = self._create_treeview(self.lframe)
        self.rtv = self._create_treeview(self.rframe)
        self.ltv.grid(row=0, column=0, sticky='nsew')
        self.rtv.grid(row=0, column=0, sticky='nsew')

        self.divider.bind('<Enter>', self._show_divider)
        self.divider.bind('<Leave>', self._hide_divider)
        self.divider.bind("<B1-Motion>", self._button1_press_move)

    def _create_treeview(self, parent):
        # Create Treeview
        SearchCols = ('#01', '#02', '#03', '#04', '#05', '#06')
        tv = ttk.Treeview(parent, columns=SearchCols, height=2,
                          displaycolumn=['#05', '#06', '#01',
                                         '#02', '#03', '#04'],
                          style='search.Treeview',
                          selectmode='extended', takefocus=True)
        # Setup column & it's headings
        tv.column('#0', stretch=0, minwidth=100, width=100, anchor='w')
        tv.column('#01', stretch=0, anchor='n', width=70)
        tv.column('#02', stretch=0, anchor='n', width=80)
        tv.column('#03', stretch=0, anchor='n', width=75)
        tv.column('#04', stretch=0, anchor='w')
        tv.column('#05', stretch=0, anchor='e', width=80)
        tv.column('#06', stretch=0, anchor='n', width=70)
        tv.heading('#0', text=' Directory ', anchor='w')
        tv.heading('#01', text='#01', anchor='center')
        tv.heading('#02', text='#02', anchor='center')
        tv.heading('#03', text='#03', anchor='center')
        tv.heading('#04', text='#04', anchor='w')
        tv.heading('#05', text='#05', anchor='center')
        tv.heading('#06', text='#06', anchor='center')
        # #0, #01, #02 denotes the 0, 1st, 2nd columns
        return tv

    # Event Handlers
    def _store_mouse_pointer_coordinate(self, event):
        self.mouse_pointer_x.set(event.x)
        self.mouse_pointer_y.set(event.y)
        print(self.mouse_pointer_x.get(), self.mouse_pointer_y.get())

    def _show_divider(self, event):
        x = self.mouse_pointer_x.get()
        y = self.mouse_pointer_y.get()
        self.harrow.place_configure(x=x-31, y=y-36)
        self.harrow.lower(belowThis=self.divider)

    def _hide_divider(self, event):
        self.harrow.place_forget()

    def _button1_press_move(self, event):
        # Configure self.lframe
        new_width = self.lframe.winfo_pointerx() - self.lframe['borderwidth']*2
        self.lframe['width'] = new_width
        print(f'self.lframe["width"]={self.lframe["width"]}')
        self.lframe.grid_propagate(0)
        # Configure self.harrow
        new_height = event.y
        self.harrow.place_forget()
        self.harrow.place_configure(x=new_width+9, y=new_height-4)
        self.harrow.lower(belowThis=self.divider)
        self.update_idletasks()

if __name__ == '__main__':
    root = tk.Tk()
    root.resizable(width=False, height=False)
    root.title('App')
    root.geometry('1300x400+0+24')
    root.rowconfigure(0, weight=1)
    root.columnconfigure(0, weight=1)

    app = App(root)
    app.grid(row=0, column=0, sticky='nsew')

    root.mainloop()

resize-arrow-24.png: resize-arrow-24.png

Advertisement

Answer

To resolve my resizable arrow indicator issue, I had to introduce event handlers to unbind and rebind events <Enter> and <Leave> when B1 is pressed and released on the ttk.Separator widget. See revised test code below. See Revised test code.

An enhancement to this script is to transform the mouse pointer appearance into a resizable arrow indicator when it enters the widget ttk.Separator as mentioned by @Atlas435 in the comment section of my question. This can be done by using the cursor option of the ttk.Separator widget. This approach also eliminates needing to implement the solution mentioned above and makes the code more concise. See Improved revised test code

From hindsight, I released I had written a python class to create an alternative vertically oriented ttk.PanedWindow widget using ttk.Frame and ttk.Separator widgets. @BryanOakley and @HenryYik thanks for pointing this fact to me.

Revised test code:

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import tkinter as tk
import tkinter.ttk as ttk


class App(ttk.Frame):
    def __init__(self, master):
        self.master = master
        self.mouse_pointer_x = tk.IntVar()
        self.mouse_pointer_y = tk.IntVar()
        super().__init__(master, style='App.TFrame', borderwidth=20)
        self._set_style()
        self._create_widgets()
        self.bind('<Motion>', self._store_mouse_pointer_coordinate)

    def _set_style(self):
        self.style = ttk.Style()
        self.style.configure('App.TFrame', background='pink')
        self.style.configure('lframe.TFrame', background='green')
        self.style.configure('rframe.TFrame', background='orange')
        self.style.configure('TSeparator', background='red')

    def _create_widgets(self):
        self.lframe = ttk.Frame(self, style='lframe.TFrame', borderwidth=20)
        self.rframe = ttk.Frame(self, style='rframe.TFrame', borderwidth=20)
        self.divider = ttk.Separator(self, orient=tk.VERTICAL)
        self.lframe.grid(row=0, column=0, sticky='nsew')
        self.divider.grid(row=0, column=1, sticky='nsew', padx=20)
        self.rframe.grid(row=0, column=2, sticky='nsew')

        harrow = './resize-arrow-24.png'
        self.icon_harrow = tk.PhotoImage(file=harrow)
        self.harrow = ttk.Label(self, image=self.icon_harrow)
        self.harrow.place(x=0, y=0)
        self.harrow.place_forget()

        self.ltv = self._create_treeview(self.lframe)
        self.rtv = self._create_treeview(self.rframe)
        self.ltv.grid(row=0, column=0, sticky='nsew')
        self.rtv.grid(row=0, column=0, sticky='nsew')

        self.divider_bind_enter = self.divider.bind('<Enter>',
                                                    self._show_divider)
        self.divider_bind_leave = self.divider.bind('<Leave>',
                                                    self._hide_divider)
        self.divider.bind("<ButtonPress-1>", self._divider_B1_press)
        self.divider.bind("<ButtonRelease-1>", self._divider_B1_release)
        self.divider.bind("<B1-Motion>", self._divider_B1_press_move)

    def _create_treeview(self, parent):
        # Create Treeview
        SearchCols = ('#01', '#02', '#03', '#04', '#05', '#06')
        tv = ttk.Treeview(parent, columns=SearchCols, height=2,
                          displaycolumn=['#05', '#06', '#01',
                                         '#02', '#03', '#04'],
                          style='search.Treeview',
                          selectmode='extended', takefocus=True)
        # Setup column & it's headings
        tv.column('#0', stretch=0, minwidth=100, width=100, anchor='w')
        tv.column('#01', stretch=0, anchor='n', width=70)
        tv.column('#02', stretch=0, anchor='n', width=80)
        tv.column('#03', stretch=0, anchor='n', width=75)
        tv.column('#04', stretch=0, anchor='w')
        tv.column('#05', stretch=0, anchor='e', width=80)
        tv.column('#06', stretch=0, anchor='n', width=70)
        tv.heading('#0', text=' Directory ', anchor='w')
        tv.heading('#01', text='#01', anchor='center')
        tv.heading('#02', text='#02', anchor='center')
        tv.heading('#03', text='#03', anchor='center')
        tv.heading('#04', text='#04', anchor='w')
        tv.heading('#05', text='#05', anchor='center')
        tv.heading('#06', text='#06', anchor='center')
        # #0, #01, #02 denotes the 0, 1st, 2nd columns
        return tv

    # Event Handlers
    def _store_mouse_pointer_coordinate(self, event):
        self.mouse_pointer_x.set(event.x)
        self.mouse_pointer_y.set(event.y)
        print(self.mouse_pointer_x.get(), self.mouse_pointer_y.get())

    def _show_divider(self, event):
        x = self.mouse_pointer_x.get()
        y = self.mouse_pointer_y.get()
        self.harrow.place_configure(x=x-31, y=y-33)
        self.harrow.lower(belowThis=self.divider)

    def _hide_divider(self, event):
        self.harrow.place_forget()

    def _divider_B1_press(self, event):
        print('unbind self.divider <Enter> & <Leave> events')
        self.divider.unbind('<Enter>', self.divider_bind_enter)
        self.divider.unbind('<Leave>', self.divider_bind_leave)

    def _divider_B1_release(self, event):
        print('bind self.divider <Enter> & <Leave> events')
        self.divider_bind_enter = self.divider.bind('<Enter>',
                                                    self._show_divider)
        self.divider_bind_leave = self.divider.bind('<Leave>',
                                                    self._hide_divider)

    def _divider_B1_press_move(self, event):
        # Configure self.lframe
        new_width = self.lframe.winfo_pointerx() - self.lframe['borderwidth']*2
        self.lframe['width'] = new_width
        print(f'self.lframe["width"]={self.lframe["width"]}')
        self.lframe.grid_propagate(0)
        # Configure self.harrow
        new_height = event.y
        self.harrow.place_forget()
        self.harrow.place_configure(x=new_width+9, y=new_height-6)
        self.harrow.lower(belowThis=self.divider)
        self.update_idletasks()


if __name__ == '__main__':
    root = tk.Tk()
    #root.resizable(width=False, height=False)
    root.title('App')
    root.geometry('1500x140+0+24')
    root.rowconfigure(0, weight=1)
    root.columnconfigure(0, weight=1)

    app = App(root)
    app.grid(row=0, column=0, sticky='nsew')

    root.mainloop()

Improved revised test code (a VerticalPanedWindow widget):

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import tkinter as tk
import tkinter.ttk as ttk


class VerticalPanedWindow(ttk.Frame):
    '''A ttk styled Vertical PanedWindow.'''
    def __init__(self, master, bg='pink',  borderwidth=0,
                 divider_fg='red', divider_padx=1,
                 lframe_bg='green', lframe_borderwidth=0,
                 rframe_bg='orange', rframe_borderwidth=0, ):
        self.master = master
        self.bg = bg
        self.borderwidth = borderwidth
        self.divider_fg = divider_fg
        self.divider_padx = divider_padx
        self.lframe_bg = lframe_bg
        self.lframe_borderwidth = lframe_borderwidth
        self.rframe_bg = rframe_bg
        self.rframe_borderwidth = rframe_borderwidth
        self.mouse_pointer_x = tk.IntVar()
        self.mouse_pointer_y = tk.IntVar()
        super().__init__(master, style='App.TFrame', borderwidth=borderwidth)
        self._set_style()
        self._create_widgets()
        self.bind('<Motion>', self._store_mouse_pointer_coordinate)

    def _set_style(self):
        self.style = ttk.Style()
        self.style.configure('App.TFrame', background=self.bg)
        self.style.configure('lframe.TFrame', background=self.lframe_bg)
        self.style.configure('rframe.TFrame', background=self.rframe_bg)
        self.style.configure('TSeparator', background=self.divider_fg)

    def _create_widgets(self):
        self.lframe = ttk.Frame(self, style='lframe.TFrame',
                                borderwidth=self.lframe_borderwidth)
        self.rframe = ttk.Frame(self, style='rframe.TFrame',
                                borderwidth=self.rframe_borderwidth)
        self.divider = ttk.Separator(self, orient=tk.VERTICAL,
                                     cursor='sb_h_double_arrow')
        self.lframe.grid(row=0, column=0, sticky='nsew')
        self.rframe.grid(row=0, column=2, sticky='nsew')
        self.divider.grid(row=0, column=1, sticky='nsew',
                          padx=self.divider_padx)
        self.divider.bind("<B1-Motion>", self._divider_B1_press_move)

    # Event Handlers
    def _store_mouse_pointer_coordinate(self, event):
        self.mouse_pointer_x.set(event.x)
        self.mouse_pointer_y.set(event.y)
        print(self.mouse_pointer_x.get(), self.mouse_pointer_y.get())

    def _divider_B1_press_move(self, event):
        # Configure self.lframe
        eventx = event.x
        mpx = self.mouse_pointer_x.get()
        new_x = mpx + eventx
        self.mouse_pointer_x.set(new_x)
        self.lframe['width'] = new_x
        self.lframe.grid_propagate(0)
        print(f' {eventx} {mpx} {new_x} {self.lframe["width"]}')


def _create_treeview(parent):
    # Create Treeview
    SearchCols = ('#01', '#02', '#03', '#04', '#05', '#06')
    tv = ttk.Treeview(parent, columns=SearchCols, height=2,
                      displaycolumn=['#05', '#06', '#01',
                                     '#02', '#03', '#04'],
                      style='search.Treeview',
                      selectmode='extended', takefocus=True)
    # Setup column & it's headings
    tv.column('#0', stretch=0, minwidth=100, width=100, anchor='w')
    tv.column('#01', stretch=0, anchor='n', width=70)
    tv.column('#02', stretch=0, anchor='n', width=80)
    tv.column('#03', stretch=0, anchor='n', width=75)
    tv.column('#04', stretch=0, anchor='w')
    tv.column('#05', stretch=0, anchor='e', width=80)
    tv.column('#06', stretch=0, anchor='n', width=70)
    tv.heading('#0', text=' Directory ', anchor='w')
    tv.heading('#01', text='#01', anchor='center')
    tv.heading('#02', text='#02', anchor='center')
    tv.heading('#03', text='#03', anchor='center')
    tv.heading('#04', text='#04', anchor='w')
    tv.heading('#05', text='#05', anchor='center')
    tv.heading('#06', text='#06', anchor='center')
    # #0, #01, #02 denotes the 0, 1st, 2nd columns
    return tv


if __name__ == '__main__':
    root = tk.Tk()
    # root.resizable(width=False, height=False)
    root.title('VerticalPanedWindow')
    root.geometry('1500x140')
    root.rowconfigure(0, weight=1)
    root.columnconfigure(0, weight=1)

    # Use customs colors and borderwidth values
    # color = '#240240237'
    # app = VerticalPanedWindow(
    #     root, bg=color,  borderwidth=20,
    #     divider_fg=color, divider_padx=20,
    #     lframe_bg=color, lframe_borderwidth=20,
    #     rframe_bg=color, rframe_borderwidth=20, )
    # Use customs borderwidth values
    app = VerticalPanedWindow(root, borderwidth=20, divider_padx=20,
                              lframe_borderwidth=20, rframe_borderwidth=20)
    # Use default options value
    # app = VerticalPanedWindow(root)
    app.grid(row=0, column=0, sticky='nsew')
    ltv = _create_treeview(app.lframe)
    rtv = _create_treeview(app.rframe)
    ltv.grid(row=0, column=0, sticky='nsew')
    rtv.grid(row=0, column=0, sticky='nsew')

    root.mainloop()
User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement