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._block_update_dimensions_event = False
# save focus before calling withdraw
self.focused_widget_before_widthdraw = None
# set CustomTkinter titlebar icon (Windows only)
if sys.platform.startswith("win"):
self.after(200, self._windows_set_titlebar_icon)
@ -137,24 +140,26 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
def update(self):
if self._window_exists is False:
self._window_exists = True
if sys.platform.startswith("win"):
if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists:
# print("window dont exists -> deiconify in update")
self.deiconify()
self._window_exists = True
super().update()
def mainloop(self, *args, **kwargs):
if not self._window_exists:
self._window_exists = True
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:
# print("window dont exists -> deiconify in mainloop")
self.deiconify()
self._window_exists = True
super().mainloop(*args, **kwargs)
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)
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
else:
# print("window dont exists -> withdraw and update")
self.focused_widget_before_widthdraw = self.focus_get()
super().withdraw()
super().update()
@ -298,7 +305,7 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
except Exception as 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)
if self._state_before_windows_set_titlebar_color == "normal":
self.deiconify()
@ -311,6 +318,10 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
else:
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):
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._block_update_dimensions_event = False
# save focus before calling withdraw
self.focused_widget_before_widthdraw = None
# set CustomTkinter titlebar icon (Windows only)
if sys.platform.startswith("win"):
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:
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().update()
@ -268,6 +272,10 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl
self._windows_set_titlebar_color_called = True
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):
""" if in a short time (5ms) after """
if self._windows_set_titlebar_color_called:

View File

@ -9,7 +9,7 @@ class CTkAppearanceModeBaseClass:
- destroy() must be called when sub-class is destroyed
- _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):

View File

@ -9,9 +9,6 @@ try:
except ImportError:
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 ..theme import ThemeManager
@ -74,7 +71,7 @@ class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClas
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
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
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),
height=self._apply_widget_scaling(self._desired_height))
self._draw()
self._draw(no_color_updates=True)
def _update_font(self):
""" 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):
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),
self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width))
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":
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
@ -342,13 +342,13 @@ class CTkEntry(CTkBaseClass):
return self._entry.get()
def focus(self):
return self._entry.focus()
self._entry.focus()
def focus_set(self):
return self._entry.focus_set()
self._entry.focus_set()
def focus_force(self):
return self._entry.focus_force()
self._entry.focus_force()
def index(self, 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 sys
from .ctk_frame import CTkFrame
from .ctk_scrollbar import CTkScrollbar
from .appearance_mode import CTkAppearanceModeBaseClass
from .core_widget_classes import CTkBaseClass
class CTkScrollableFrame(tkinter.Frame):
_xscrollincrement = 4 # horizontal scrolling speed
_yscrollincrement = 8 # vertical scrolling speed
class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass):
def __init__(self,
master: any,
width: int = 200,
@ -21,58 +24,120 @@ class CTkScrollableFrame(tkinter.Frame):
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Optional[Union[str, Tuple[str, str]]] = None,
activate_x_scrollbars: bool = False,
activate_y_scrollbars: bool = True):
orientation: Literal["vertical", "horizontal"] = "vertical"):
self._activate_x_scrollbars = activate_x_scrollbars
self._activate_y_scrollbars = activate_y_scrollbars
self._orientation = orientation
self.parent_frame = CTkFrame(master=master, width=width, height=height, corner_radius=corner_radius, border_width=border_width)
self.parent_canvas = tkinter.Canvas(master=self.parent_frame, yscrollincrement=self._yscrollincrement, xscrollincrement=self._xscrollincrement)
if self._activate_x_scrollbars:
self.x_scrollbar = CTkScrollbar(master=self.parent_frame, orientation="horizontal", command=self.parent_canvas.xview)
self.parent_canvas.configure(xscrollcommand=self.x_scrollbar.set)
if self._activate_y_scrollbars:
self.y_scrollbar = CTkScrollbar(master=self.parent_frame, orientation="vertical", command=self.parent_canvas.yview)
self.parent_canvas.configure(yscrollcommand=self.y_scrollbar.set)
self.parent_frame = CTkFrame(master=master, width=width, height=height, corner_radius=corner_radius,
border_width=border_width, bg_color=bg_color, fg_color=fg_color, border_color=border_color)
self.parent_frame.grid_propagate(0)
self.parent_canvas = tkinter.Canvas(master=self.parent_frame, highlightthickness=0, width=0, height=0)
self._set_scroll_increments()
if self._orientation == "horizontal":
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()
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.parent_canvas.bind("<Configure>", self._fit_frame_dimensions_to_canvas)
self.bind_all("<MouseWheel>", self._mouse_wheel_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("<KeyRelease-Shift_L>", 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")
tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self.parent_frame.cget("fg_color")))
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):
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_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.x_scrollbar.grid(row=1, column=0, sticky="nsew")
if self._activate_y_scrollbars:
self.parent_canvas.grid(row=0, column=0, sticky="nsew",
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.y_scrollbar.grid(row=0, column=1, sticky="nsew")
def _parent_canvas_configure(self, event):
#self.parent_canvas.itemconfigure(self._create_window_id, width=event.width, height=event.height)
pass
self.parent_canvas.grid(row=0, column=0, sticky="nsew",
pady=border_spacing, padx=(border_spacing, 0))
self.scrollbar.grid(row=0, column=1, sticky="nsew",
pady=border_spacing)
def _mouse_wheel_all(self, event):
if self.check_if_master_is_canvas(event.widget):
if self._shift_pressed:
self.parent_canvas.xview("scroll", -event.delta, "units")
if sys.platform.startswith("win"):
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")
else:
if self.parent_canvas.yview() != (0.0, 1.0):
self.parent_canvas.yview("scroll", -event.delta, "units")
else:
self.parent_canvas.yview("scroll", -event.delta, "units")
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")
def _keyboard_shift_press_all(self, event):
self._shift_pressed = True
@ -96,3 +161,27 @@ class CTkScrollableFrame(tkinter.Frame):
def grid(self, **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)