Skip to content

Commit

Permalink
progress on scrollable frame
Browse files Browse the repository at this point in the history
  • Loading branch information
TomSchimansky committed Feb 5, 2023
1 parent 110e9bb commit 2359a6c
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 49 deletions.
21 changes: 16 additions & 5 deletions customtkinter/windows/ctk_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ def __init__(self,
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)
Expand Down Expand Up @@ -137,24 +140,26 @@ def iconify(self):

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):
Expand Down Expand Up @@ -267,9 +272,11 @@ def _windows_set_titlebar_color(self, color_mode: str):
# 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()

Expand Down Expand Up @@ -298,7 +305,7 @@ def _windows_set_titlebar_color(self, color_mode: str):
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()
Expand All @@ -311,6 +318,10 @@ def _windows_set_titlebar_color(self, color_mode: str):
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)

Expand Down
8 changes: 8 additions & 0 deletions customtkinter/windows/ctk_toplevel.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ def __init__(self, *args,
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)
Expand Down Expand Up @@ -238,6 +241,7 @@ def _windows_set_titlebar_color(self, color_mode: str):
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()

Expand Down Expand Up @@ -268,6 +272,10 @@ def _windows_set_titlebar_color(self, color_mode: str):
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
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
Expand Down Expand Up @@ -74,7 +71,7 @@ class GeometryCallDict(TypedDict):
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):
Expand Down
12 changes: 6 additions & 6 deletions customtkinter/windows/widgets/ctk_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def _set_dimensions(self, width=None, height=None):

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 """
Expand All @@ -153,14 +153,14 @@ def destroy(self):
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),
Expand Down Expand Up @@ -342,13 +342,13 @@ def get(self):
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)
Expand Down
155 changes: 122 additions & 33 deletions customtkinter/windows/widgets/ctk_scrollable_frame.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -21,58 +24,120 @@ def __init__(self,
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._orientation = orientation

self._activate_x_scrollbars = activate_x_scrollbars
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, 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()

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)
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
Expand All @@ -96,3 +161,27 @@ def place(self, **kwargs):

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)

0 comments on commit 2359a6c

Please sign in to comment.