I created this test code to simulate a Tk window with a left panel that is resizable. How it works:
- When the mouse pointer move over the ttk.Separator, a resizable arrow indicator will appear where the mouse pointer is.
- 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.
- 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()
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()