progress on scrollable frame

This commit is contained in:
TomSchimansky 2023-02-05 03:02:21 +01:00
parent 110e9bbcbf
commit 2359a6ce39
6 changed files with 154 additions and 49 deletions

View File

@ -64,6 +64,9 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
self._iconify_called_before_window_exists = False # indicates if iconify() was called before window is first shown through update() or mainloop() self._iconify_called_before_window_exists = False # indicates if iconify() was called before window is first shown through update() or mainloop()
self._block_update_dimensions_event = False self._block_update_dimensions_event = False
# save focus before calling withdraw
self.focused_widget_before_widthdraw = None
# set CustomTkinter titlebar icon (Windows only) # set CustomTkinter titlebar icon (Windows only)
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
self.after(200, self._windows_set_titlebar_icon) self.after(200, self._windows_set_titlebar_icon)
@ -137,24 +140,26 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
def update(self): def update(self):
if self._window_exists is False: if self._window_exists is False:
self._window_exists = True
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists: if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists:
# print("window dont exists -> deiconify in update") # print("window dont exists -> deiconify in update")
self.deiconify() self.deiconify()
self._window_exists = True
super().update() super().update()
def mainloop(self, *args, **kwargs): def mainloop(self, *args, **kwargs):
if not self._window_exists: if not self._window_exists:
self._window_exists = True
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
self._windows_set_titlebar_color(self._get_appearance_mode())
if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists: if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists:
# print("window dont exists -> deiconify in mainloop") # print("window dont exists -> deiconify in mainloop")
self.deiconify() self.deiconify()
self._window_exists = True
super().mainloop(*args, **kwargs) super().mainloop(*args, **kwargs)
def resizable(self, width: bool = None, height: bool = None): def resizable(self, width: bool = None, height: bool = None):
@ -267,9 +272,11 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
# print("window_exists -> state_before_windows_set_titlebar_color: ", self.state_before_windows_set_titlebar_color) # print("window_exists -> state_before_windows_set_titlebar_color: ", self.state_before_windows_set_titlebar_color)
if self._state_before_windows_set_titlebar_color != "iconic" or self._state_before_windows_set_titlebar_color != "withdrawn": if self._state_before_windows_set_titlebar_color != "iconic" or self._state_before_windows_set_titlebar_color != "withdrawn":
self.focused_widget_before_widthdraw = self.focus_get()
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
else: else:
# print("window dont exists -> withdraw and update") # print("window dont exists -> withdraw and update")
self.focused_widget_before_widthdraw = self.focus_get()
super().withdraw() super().withdraw()
super().update() super().update()
@ -298,7 +305,7 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
except Exception as err: except Exception as err:
print(err) print(err)
if self._window_exists: if self._window_exists or True:
# print("window_exists -> return to original state: ", self.state_before_windows_set_titlebar_color) # print("window_exists -> return to original state: ", self.state_before_windows_set_titlebar_color)
if self._state_before_windows_set_titlebar_color == "normal": if self._state_before_windows_set_titlebar_color == "normal":
self.deiconify() self.deiconify()
@ -311,6 +318,10 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
else: else:
pass # wait for update or mainloop to be called pass # wait for update or mainloop to be called
if self.focused_widget_before_widthdraw is not None:
self.after(1, self.focused_widget_before_widthdraw.focus)
self.focused_widget_before_widthdraw = None
def _set_appearance_mode(self, mode_string: str): def _set_appearance_mode(self, mode_string: str):
super()._set_appearance_mode(mode_string) super()._set_appearance_mode(mode_string)

View File

@ -70,6 +70,9 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl
self._iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color self._iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color
self._block_update_dimensions_event = False self._block_update_dimensions_event = False
# save focus before calling withdraw
self.focused_widget_before_widthdraw = None
# set CustomTkinter titlebar icon (Windows only) # set CustomTkinter titlebar icon (Windows only)
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
self.after(200, self._windows_set_titlebar_icon) self.after(200, self._windows_set_titlebar_icon)
@ -238,6 +241,7 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl
if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation: if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation:
self._state_before_windows_set_titlebar_color = self.state() self._state_before_windows_set_titlebar_color = self.state()
self.focused_widget_before_widthdraw = self.focus_get()
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
super().update() super().update()
@ -268,6 +272,10 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl
self._windows_set_titlebar_color_called = True self._windows_set_titlebar_color_called = True
self.after(5, self._revert_withdraw_after_windows_set_titlebar_color) self.after(5, self._revert_withdraw_after_windows_set_titlebar_color)
if self.focused_widget_before_widthdraw is not None:
self.after(10, self.focused_widget_before_widthdraw.focus)
self.focused_widget_before_widthdraw = None
def _revert_withdraw_after_windows_set_titlebar_color(self): def _revert_withdraw_after_windows_set_titlebar_color(self):
""" if in a short time (5ms) after """ """ if in a short time (5ms) after """
if self._windows_set_titlebar_color_called: if self._windows_set_titlebar_color_called:

View File

@ -9,7 +9,7 @@ class CTkAppearanceModeBaseClass:
- destroy() must be called when sub-class is destroyed - destroy() must be called when sub-class is destroyed
- _set_appearance_mode() abstractmethod, gets called when appearance mode changes, must be overridden - _set_appearance_mode() abstractmethod, gets called when appearance mode changes, must be overridden
- _apply_appearance_mode() - _apply_appearance_mode() to convert tuple color
""" """
def __init__(self): def __init__(self):

View File

@ -9,9 +9,6 @@ try:
except ImportError: except ImportError:
from typing_extensions import TypedDict from typing_extensions import TypedDict
# removed due to circular import
# from ...ctk_tk import CTk
# from ...ctk_toplevel import CTkToplevel
from .... import windows # import windows for isinstance checks from .... import windows # import windows for isinstance checks
from ..theme import ThemeManager from ..theme import ThemeManager
@ -74,7 +71,7 @@ class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClas
super().bind('<Configure>', self._update_dimensions_event) super().bind('<Configure>', self._update_dimensions_event)
# overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well # overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well
if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame, tkinter.LabelFrame, ttk.Frame, ttk.LabelFrame, ttk.Notebook)) and not isinstance(self.master, CTkBaseClass): if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame, tkinter.LabelFrame, ttk.Frame, ttk.LabelFrame, ttk.Notebook)) and not isinstance(self.master, (CTkBaseClass, CTkAppearanceModeBaseClass)):
master_old_configure = self.master.config master_old_configure = self.master.config
def new_configure(*args, **kwargs): def new_configure(*args, **kwargs):

View File

@ -133,7 +133,7 @@ class CTkEntry(CTkBaseClass):
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height)) height=self._apply_widget_scaling(self._desired_height))
self._draw() self._draw(no_color_updates=True)
def _update_font(self): def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """ """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
@ -153,14 +153,14 @@ class CTkEntry(CTkBaseClass):
def _draw(self, no_color_updates=False): def _draw(self, no_color_updates=False):
super()._draw(no_color_updates) super()._draw(no_color_updates)
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width), requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height), self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width)) self._apply_widget_scaling(self._border_width))
if requires_recoloring or no_color_updates is False: if requires_recoloring or no_color_updates is False:
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
if self._apply_appearance_mode(self._fg_color) == "transparent": if self._apply_appearance_mode(self._fg_color) == "transparent":
self._canvas.itemconfig("inner_parts", self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color), fill=self._apply_appearance_mode(self._bg_color),
@ -342,13 +342,13 @@ class CTkEntry(CTkBaseClass):
return self._entry.get() return self._entry.get()
def focus(self): def focus(self):
return self._entry.focus() self._entry.focus()
def focus_set(self): def focus_set(self):
return self._entry.focus_set() self._entry.focus_set()
def focus_force(self): def focus_force(self):
return self._entry.focus_force() self._entry.focus_force()
def index(self, index): def index(self, index):
return self._entry.index(index) return self._entry.index(index)

View File

@ -1,15 +1,18 @@
from typing import Union, Tuple, List, Optional from typing import Union, Tuple, Optional
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
import tkinter import tkinter
import sys
from .ctk_frame import CTkFrame from .ctk_frame import CTkFrame
from .ctk_scrollbar import CTkScrollbar from .ctk_scrollbar import CTkScrollbar
from .appearance_mode import CTkAppearanceModeBaseClass
from .core_widget_classes import CTkBaseClass
class CTkScrollableFrame(tkinter.Frame): class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass):
_xscrollincrement = 4 # horizontal scrolling speed
_yscrollincrement = 8 # vertical scrolling speed
def __init__(self, def __init__(self,
master: any, master: any,
width: int = 200, width: int = 200,
@ -21,57 +24,119 @@ class CTkScrollableFrame(tkinter.Frame):
fg_color: Optional[Union[str, Tuple[str, str]]] = None, fg_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Optional[Union[str, Tuple[str, str]]] = None, border_color: Optional[Union[str, Tuple[str, str]]] = None,
activate_x_scrollbars: bool = False, orientation: Literal["vertical", "horizontal"] = "vertical"):
activate_y_scrollbars: bool = True):
self._activate_x_scrollbars = activate_x_scrollbars self._orientation = orientation
self._activate_y_scrollbars = activate_y_scrollbars
self.parent_frame = CTkFrame(master=master, width=width, height=height, corner_radius=corner_radius, border_width=border_width) self.parent_frame = CTkFrame(master=master, width=width, height=height, corner_radius=corner_radius,
self.parent_canvas = tkinter.Canvas(master=self.parent_frame, yscrollincrement=self._yscrollincrement, xscrollincrement=self._xscrollincrement) border_width=border_width, bg_color=bg_color, fg_color=fg_color, border_color=border_color)
if self._activate_x_scrollbars: self.parent_frame.grid_propagate(0)
self.x_scrollbar = CTkScrollbar(master=self.parent_frame, orientation="horizontal", command=self.parent_canvas.xview) self.parent_canvas = tkinter.Canvas(master=self.parent_frame, highlightthickness=0, width=0, height=0)
self.parent_canvas.configure(xscrollcommand=self.x_scrollbar.set) self._set_scroll_increments()
if self._activate_y_scrollbars:
self.y_scrollbar = CTkScrollbar(master=self.parent_frame, orientation="vertical", command=self.parent_canvas.yview) if self._orientation == "horizontal":
self.parent_canvas.configure(yscrollcommand=self.y_scrollbar.set) self.scrollbar = CTkScrollbar(master=self.parent_frame, orientation="horizontal", command=self.parent_canvas.xview)
self.parent_canvas.configure(xscrollcommand=self.scrollbar.set)
elif self._orientation == "vertical":
self.scrollbar = CTkScrollbar(master=self.parent_frame, orientation="vertical", command=self.parent_canvas.yview)
self.parent_canvas.configure(yscrollcommand=self.scrollbar.set)
self._create_grid() self._create_grid()
super().__init__(master=self.parent_canvas, width=0) tkinter.Frame.__init__(self, master=self.parent_canvas, highlightthickness=0)
CTkAppearanceModeBaseClass.__init__(self)
self.bind("<Configure>", lambda e: self.parent_canvas.configure(scrollregion=self.parent_canvas.bbox("all"))) self.bind("<Configure>", lambda e: self.parent_canvas.configure(scrollregion=self.parent_canvas.bbox("all")))
self.parent_canvas.bind("<Configure>", self._fit_frame_dimensions_to_canvas)
self.bind_all("<MouseWheel>", self._mouse_wheel_all) self.bind_all("<MouseWheel>", self._mouse_wheel_all)
self.bind_all("<KeyPress-Shift_L>", self._keyboard_shift_press_all) self.bind_all("<KeyPress-Shift_L>", self._keyboard_shift_press_all)
self.bind_all("<KeyPress-Shift_R>", self._keyboard_shift_press_all) self.bind_all("<KeyPress-Shift_R>", self._keyboard_shift_press_all)
self.bind_all("<KeyRelease-Shift_L>", self._keyboard_shift_release_all) self.bind_all("<KeyRelease-Shift_L>", self._keyboard_shift_release_all)
self.bind_all("<KeyRelease-Shift_R>", self._keyboard_shift_release_all) self.bind_all("<KeyRelease-Shift_R>", self._keyboard_shift_release_all)
self.parent_canvas.bind("<Configure>", self._parent_canvas_configure)
self._create_window_id = self.parent_canvas.create_window(0, 0, window=self, anchor="nw") self._create_window_id = self.parent_canvas.create_window(0, 0, window=self, anchor="nw")
tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self.parent_frame.cget("fg_color")))
self._shift_pressed = False self._shift_pressed = False
self.mouse_over_widget = False
def destroy(self):
tkinter.Frame.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
def _set_appearance_mode(self, mode_string):
super()._set_appearance_mode(mode_string)
tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self.parent_frame.cget("fg_color")))
def configure(self, **kwargs):
if "fg_color" in kwargs:
self.parent_frame.configure(fg_color=kwargs.pop("fg_color"))
tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self.parent_frame.cget("fg_color")))
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self.parent_frame.cget("fg_color"))
if "corner_radius" in kwargs:
self.parent_frame.configure(corner_radius=kwargs.pop("corner_radius"))
self._create_grid()
if "border_width" in kwargs:
self.parent_frame.configure(border_width=kwargs.pop("border_width"))
self._create_grid()
self.parent_frame.configure(**kwargs)
def _fit_frame_dimensions_to_canvas(self, event):
if self._orientation == "horizontal":
self.parent_canvas.itemconfigure(self._create_window_id, height=self.parent_canvas.winfo_height())
elif self._orientation == "vertical":
self.parent_canvas.itemconfigure(self._create_window_id, width=self.parent_canvas.winfo_width())
def _set_scroll_increments(self):
if sys.platform.startswith("win"):
self.parent_canvas.configure(xscrollincrement=1, yscrollincrement=1)
elif sys.platform == "darwin":
self.parent_canvas.configure(xscrollincrement=4, yscrollincrement=8)
def _create_grid(self): def _create_grid(self):
border_spacing = self.parent_frame.cget("corner_radius") + self.parent_frame.cget("border_width")
self.parent_frame.grid_columnconfigure(0, weight=1) self.parent_frame.grid_columnconfigure(0, weight=1)
self.parent_frame.grid_rowconfigure(0, weight=1) self.parent_frame.grid_rowconfigure(0, weight=1)
self.parent_canvas.grid(row=0, column=0, sticky="nsew")
if self._activate_x_scrollbars: if self._orientation == "horizontal":
self.parent_frame.grid_rowconfigure(1, weight=0) self.parent_frame.grid_rowconfigure(1, weight=0)
self.x_scrollbar.grid(row=1, column=0, sticky="nsew") self.parent_canvas.grid(row=0, column=0, sticky="nsew",
if self._activate_y_scrollbars: padx=border_spacing, pady=(border_spacing, 0))
self.scrollbar.grid(row=1, column=0, sticky="nsew",
padx=border_spacing)
elif self._orientation == "vertical":
self.parent_frame.grid_columnconfigure(1, weight=0) self.parent_frame.grid_columnconfigure(1, weight=0)
self.y_scrollbar.grid(row=0, column=1, sticky="nsew") self.parent_canvas.grid(row=0, column=0, sticky="nsew",
pady=border_spacing, padx=(border_spacing, 0))
def _parent_canvas_configure(self, event): self.scrollbar.grid(row=0, column=1, sticky="nsew",
#self.parent_canvas.itemconfigure(self._create_window_id, width=event.width, height=event.height) pady=border_spacing)
pass
def _mouse_wheel_all(self, event): def _mouse_wheel_all(self, event):
if self.check_if_master_is_canvas(event.widget): if self.check_if_master_is_canvas(event.widget):
if sys.platform.startswith("win"):
if self._shift_pressed: if self._shift_pressed:
if self.parent_canvas.xview() != (0.0, 1.0):
self.parent_canvas.xview("scroll", -int(event.delta/6), "units")
else:
if self.parent_canvas.yview() != (0.0, 1.0):
self.parent_canvas.yview("scroll", -int(event.delta/6), "units")
elif sys.platform == "darwin":
if self._shift_pressed:
if self.parent_canvas.xview() != (0.0, 1.0):
self.parent_canvas.xview("scroll", -event.delta, "units") self.parent_canvas.xview("scroll", -event.delta, "units")
else: else:
if self.parent_canvas.yview() != (0.0, 1.0):
self.parent_canvas.yview("scroll", -event.delta, "units")
else:
if self._shift_pressed:
if self.parent_canvas.xview() != (0.0, 1.0):
self.parent_canvas.xview("scroll", -event.delta, "units")
else:
if self.parent_canvas.yview() != (0.0, 1.0):
self.parent_canvas.yview("scroll", -event.delta, "units") self.parent_canvas.yview("scroll", -event.delta, "units")
def _keyboard_shift_press_all(self, event): def _keyboard_shift_press_all(self, event):
@ -96,3 +161,27 @@ class CTkScrollableFrame(tkinter.Frame):
def grid(self, **kwargs): def grid(self, **kwargs):
self.parent_frame.grid(**kwargs) self.parent_frame.grid(**kwargs)
def pack_forget(self):
self.parent_frame.pack_forget()
def place_forget(self, **kwargs):
self.parent_frame.place_forget()
def grid_forget(self, **kwargs):
self.parent_frame.grid_forget()
def grid_remove(self, **kwargs):
self.parent_frame.grid_remove()
def grid_propagate(self, **kwargs):
self.parent_frame.grid_propagate()
def grid_info(self, **kwargs):
self.parent_frame.grid_info()
def lift(self, aboveThis=None):
self.parent_frame.lift(aboveThis)
def lower(self, belowThis=None):
self.parent_frame.lower(belowThis)