diff --git a/LICENSE b/LICENSE index 0e259d4..2c9bc28 100644 --- a/LICENSE +++ b/LICENSE @@ -1,121 +1,21 @@ -Creative Commons Legal Code +MIT License -CC0 1.0 Universal +Copyright (c) 2023 Tom Schimansky - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Statement of Purpose +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Readme.md b/Readme.md index dca878d..c592de1 100644 --- a/Readme.md +++ b/Readme.md @@ -105,6 +105,12 @@ how to position the text and image at once with the ``compound`` option: | _`image_example.py` on Windows 11_ ### +### Scrollable Frames +Scrollable frames are possible in vertical or horizontal orientation and can be combined +with any other widgets. +![](documentation_images/scrollable_frame_example_Windows.png) +| _`scrollable_frame_example.py` on Windows 11_ + ### Integration of TkinterMapView widget In the following example I used a TkinterMapView which integrates well with a CustomTkinter program. It's a tile based map widget which displays diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index 58be3cb..216a377 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -1,4 +1,4 @@ -__version__ = "5.0.4" +__version__ = "5.1.2" import os import sys @@ -33,6 +33,7 @@ from .windows.widgets import CTkSlider from .windows.widgets import CTkSwitch from .windows.widgets import CTkTabview from .windows.widgets import CTkTextbox +from .windows.widgets import CTkScrollableFrame # import windows from .windows import CTk diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json index bca0b2b..838e26b 100644 --- a/customtkinter/assets/themes/blue.json +++ b/customtkinter/assets/themes/blue.json @@ -121,12 +121,15 @@ "CTkTextbox": { "corner_radius": 6, "border_width": 0, - "fg_color": ["#F9F9FA", "gray23"], + "fg_color": ["#F9F9FA", "#1D1E1E"], "border_color": ["#979DA2", "#565B5E"], "text_color":["gray10", "#DCE4EE"], "scrollbar_button_color": ["gray55", "gray41"], "scrollbar_button_hover_color": ["gray40", "gray53"] }, + "CTkScrollableFrame": { + "label_fg_color": ["gray78", "gray23"] + }, "DropdownMenu": { "fg_color": ["gray90", "gray20"], "hover_color": ["gray75", "gray28"], diff --git a/customtkinter/assets/themes/dark-blue.json b/customtkinter/assets/themes/dark-blue.json index 1ecf8ab..b418cbf 100644 --- a/customtkinter/assets/themes/dark-blue.json +++ b/customtkinter/assets/themes/dark-blue.json @@ -127,6 +127,9 @@ "scrollbar_button_color": ["gray55", "gray41"], "scrollbar_button_hover_color": ["gray40", "gray53"] }, + "CTkScrollableFrame": { + "label_fg_color": ["gray80", "gray21"] + }, "DropdownMenu": { "fg_color": ["gray90", "gray20"], "hover_color": ["gray75", "gray28"], diff --git a/customtkinter/assets/themes/green.json b/customtkinter/assets/themes/green.json index 14cd8c6..cefab6e 100644 --- a/customtkinter/assets/themes/green.json +++ b/customtkinter/assets/themes/green.json @@ -127,6 +127,9 @@ "scrollbar_button_color": ["gray55", "gray41"], "scrollbar_button_hover_color": ["gray40", "gray53"] }, + "CTkScrollableFrame": { + "label_fg_color": ["gray78", "gray23"] + }, "DropdownMenu": { "fg_color": ["gray90", "gray20"], "hover_color": ["gray75", "gray28"], diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index ac56289..cc3c940 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -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): @@ -220,6 +225,10 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): self._iconbitmap_method_called = True super().wm_iconbitmap(bitmap, default) + def iconbitmap(self, bitmap=None, default=None): + self._iconbitmap_method_called = True + super().wm_iconbitmap(bitmap, default) + def _windows_set_titlebar_icon(self): try: # if not the user already called iconbitmap method, set icon @@ -263,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() @@ -294,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() @@ -307,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) diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index 59b43f6..a4eef57 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -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: diff --git a/customtkinter/windows/widgets/__init__.py b/customtkinter/windows/widgets/__init__.py index 2e21484..a75c63d 100644 --- a/customtkinter/windows/widgets/__init__.py +++ b/customtkinter/windows/widgets/__init__.py @@ -13,3 +13,4 @@ from .ctk_slider import CTkSlider from .ctk_switch import CTkSwitch from .ctk_tabview import CTkTabview from .ctk_textbox import CTkTextbox +from .ctk_scrollable_frame import CTkScrollableFrame diff --git a/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py b/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py index 0d19147..b7f757a 100644 --- a/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py +++ b/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py @@ -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): diff --git a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py index a75ab01..610f46a 100644 --- a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py +++ b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py @@ -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('', 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): @@ -179,8 +176,7 @@ class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClas elif isinstance(image, CTkImage): return image else: - warnings.warn(f"{type(self).__name__} Warning: Given image is not CTkImage but {type(image)}. " + - f"Image can not be scaled on HighDPI displays, use CTkImage instead.\n") + warnings.warn(f"{type(self).__name__} Warning: Given image is not CTkImage but {type(image)}. Image can not be scaled on HighDPI displays, use CTkImage instead.\n") return image def _update_dimensions_event(self, event): @@ -197,12 +193,15 @@ class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClas if master_widget is None: master_widget = self.master - if isinstance(master_widget, (windows.widgets.core_widget_classes.CTkBaseClass, windows.CTk, windows.CTkToplevel)): + if isinstance(master_widget, (windows.widgets.core_widget_classes.CTkBaseClass, windows.CTk, windows.CTkToplevel, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame)): if master_widget.cget("fg_color") is not None and master_widget.cget("fg_color") != "transparent": return master_widget.cget("fg_color") + elif isinstance(master_widget, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame): + return self._detect_color_of_master(master_widget.master.master.master) + # if fg_color of master is None, try to retrieve fg_color from master of master - elif hasattr(master_widget.master, "master"): + elif hasattr(master_widget, "master"): return self._detect_color_of_master(master_widget.master) elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget @@ -269,6 +268,8 @@ class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClas relheight=amount - height of this widget between 0.0 and 1.0 relative to height of master (1.0 is the same height as the master) bordermode="inside" or "outside" - whether to take border width of master widget into account """ + if "width" in kwargs or "height" in kwargs: + raise ValueError("'width' and 'height' arguments must be passed to the constructor of the widget, not the place method") self._last_geometry_manager_call = {"function": super().place, "kwargs": kwargs} return super().place(**self._apply_argument_scaling(kwargs)) diff --git a/customtkinter/windows/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py index 45ed025..d79a944 100644 --- a/customtkinter/windows/widgets/ctk_button.py +++ b/customtkinter/windows/widgets/ctk_button.py @@ -40,7 +40,7 @@ class CTkButton(CTkBaseClass): text: str = "CTkButton", font: Optional[Union[tuple, CTkFont]] = None, textvariable: Union[tkinter.Variable, None] = None, - image: Union[CTkImage, None] = None, + image: Union[CTkImage, "ImageTk.PhotoImage", None] = None, state: str = "normal", hover: bool = True, command: Union[Callable[[], None], None] = None, @@ -169,8 +169,11 @@ class CTkButton(CTkBaseClass): def _update_image(self): if self._image_label is not None: - self._image_label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(), - self._get_appearance_mode())) + if isinstance(self._image, CTkImage): + self._image_label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(), + self._get_appearance_mode())) + elif self._image is not None: + self._image_label.configure(image=self._image) def destroy(self): if isinstance(self._font, CTkFont): @@ -425,6 +428,7 @@ class CTkButton(CTkBaseClass): if "command" in kwargs: self._command = kwargs.pop("command") + self._set_cursor() if "compound" in kwargs: self._compound = kwargs.pop("compound") diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py index 6a2560b..db54386 100644 --- a/customtkinter/windows/widgets/ctk_entry.py +++ b/customtkinter/windows/widgets/ctk_entry.py @@ -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) diff --git a/customtkinter/windows/widgets/ctk_frame.py b/customtkinter/windows/widgets/ctk_frame.py index fe9e226..67bf161 100644 --- a/customtkinter/windows/widgets/ctk_frame.py +++ b/customtkinter/windows/widgets/ctk_frame.py @@ -24,8 +24,8 @@ class CTkFrame(CTkBaseClass): bg_color: Union[str, Tuple[str, str]] = "transparent", fg_color: Optional[Union[str, Tuple[str, str]]] = None, border_color: Optional[Union[str, Tuple[str, str]]] = None, - background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None, + background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None, overwrite_preferred_drawing_method: Union[str, None] = None, **kwargs): diff --git a/customtkinter/windows/widgets/ctk_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py new file mode 100644 index 0000000..685deb2 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_scrollable_frame.py @@ -0,0 +1,316 @@ +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 .scaling import CTkScalingBaseClass +from .core_widget_classes import CTkBaseClass +from .ctk_label import CTkLabel +from .font import CTkFont +from .theme import ThemeManager + + +class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass): + def __init__(self, + master: any, + width: int = 200, + height: int = 200, + corner_radius: Optional[Union[int, str]] = None, + border_width: Optional[Union[int, str]] = None, + + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + border_color: Optional[Union[str, Tuple[str, str]]] = None, + scrollbar_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + scrollbar_button_color: Optional[Union[str, Tuple[str, str]]] = None, + scrollbar_button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + label_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + label_text_color: Optional[Union[str, Tuple[str, str]]] = None, + + label_text: str = "", + label_font: Optional[Union[tuple, CTkFont]] = None, + label_anchor: str = "center", + orientation: Literal["vertical", "horizontal"] = "vertical"): + + self._orientation = orientation + + # dimensions independent of scaling + self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height + self._desired_height = height + + self._parent_frame = CTkFrame(master=master, width=0, height=0, corner_radius=corner_radius, + border_width=border_width, bg_color=bg_color, fg_color=fg_color, border_color=border_color) + self._parent_canvas = tkinter.Canvas(master=self._parent_frame, highlightthickness=0) + self._set_scroll_increments() + + if self._orientation == "horizontal": + self._scrollbar = CTkScrollbar(master=self._parent_frame, orientation="horizontal", command=self._parent_canvas.xview, + fg_color=scrollbar_fg_color, button_color=scrollbar_button_color, button_hover_color=scrollbar_button_hover_color) + 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, + fg_color=scrollbar_fg_color, button_color=scrollbar_button_color, button_hover_color=scrollbar_button_hover_color) + self._parent_canvas.configure(yscrollcommand=self._scrollbar.set) + + self._label_text = label_text + self._label = CTkLabel(self._parent_frame, text=label_text, anchor=label_anchor, font=label_font, + corner_radius=self._parent_frame.cget("corner_radius"), text_color=label_text_color, + fg_color=ThemeManager.theme["CTkScrollableFrame"]["label_fg_color"] if label_fg_color is None else label_fg_color) + + tkinter.Frame.__init__(self, master=self._parent_canvas, highlightthickness=0) + CTkAppearanceModeBaseClass.__init__(self) + CTkScalingBaseClass.__init__(self, scaling_type="widget") + + self._create_grid() + + self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + self.bind("", lambda e: self._parent_canvas.configure(scrollregion=self._parent_canvas.bbox("all"))) + self._parent_canvas.bind("", self._fit_frame_dimensions_to_canvas) + self.bind_all("", self._mouse_wheel_all, add="+") + self.bind_all("", self._keyboard_shift_press_all, add="+") + self.bind_all("", self._keyboard_shift_press_all, add="+") + self.bind_all("", self._keyboard_shift_release_all, add="+") + self.bind_all("", self._keyboard_shift_release_all, add="+") + self._create_window_id = self._parent_canvas.create_window(0, 0, window=self, anchor="nw") + + if self._parent_frame.cget("fg_color") == "transparent": + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + else: + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + + self._shift_pressed = False + + def destroy(self): + tkinter.Frame.destroy(self) + CTkAppearanceModeBaseClass.destroy(self) + CTkScalingBaseClass.destroy(self) + + def _create_grid(self): + border_spacing = self._apply_widget_scaling(self._parent_frame.cget("corner_radius") + self._parent_frame.cget("border_width")) + + if self._orientation == "horizontal": + self._parent_frame.grid_columnconfigure(0, weight=1) + self._parent_frame.grid_rowconfigure(1, weight=1) + self._parent_canvas.grid(row=1, column=0, sticky="nsew", padx=border_spacing, pady=(border_spacing, 0)) + self._scrollbar.grid(row=2, column=0, sticky="nsew", padx=border_spacing) + + if self._label_text is not None and self._label_text != "": + self._label.grid(row=0, column=0, sticky="ew", padx=border_spacing, pady=border_spacing) + else: + self._label.grid_forget() + + elif self._orientation == "vertical": + self._parent_frame.grid_columnconfigure(0, weight=1) + self._parent_frame.grid_rowconfigure(1, weight=1) + self._parent_canvas.grid(row=1, column=0, sticky="nsew", padx=(border_spacing, 0), pady=border_spacing) + self._scrollbar.grid(row=1, column=1, sticky="nsew", pady=border_spacing) + + if self._label_text is not None and self._label_text != "": + self._label.grid(row=0, column=0, columnspan=2, sticky="ew", padx=border_spacing, pady=border_spacing) + else: + self._label.grid_forget() + + def _set_appearance_mode(self, mode_string): + super()._set_appearance_mode(mode_string) + + if self._parent_frame.cget("fg_color") == "transparent": + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + else: + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + + def _set_scaling(self, new_widget_scaling, new_window_scaling): + super()._set_scaling(new_widget_scaling, new_window_scaling) + + self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def _set_dimensions(self, width=None, height=None): + if width is not None: + self._desired_width = width + if height is not None: + self._desired_height = height + + self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def configure(self, **kwargs): + if "width" in kwargs: + self._set_dimensions(width=kwargs.pop("width")) + + if "height" in kwargs: + self._set_dimensions(height=kwargs.pop("height")) + + if "corner_radius" in kwargs: + new_corner_radius = kwargs.pop("corner_radius") + self._parent_frame.configure(corner_radius=new_corner_radius) + if self._label is not None: + self._label.configure(corner_radius=new_corner_radius) + self._create_grid() + + if "border_width" in kwargs: + self._parent_frame.configure(border_width=kwargs.pop("border_width")) + self._create_grid() + + if "fg_color" in kwargs: + self._parent_frame.configure(fg_color=kwargs.pop("fg_color")) + + if self._parent_frame.cget("fg_color") == "transparent": + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + else: + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + self._parent_canvas.configure(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 "scrollbar_fg_color" in kwargs: + self._scrollbar.configure(fg_color=kwargs.pop("scrollbar_fg_color")) + + if "scrollbar_button_color" in kwargs: + self._scrollbar.configure(fg_color=kwargs.pop("scrollbar_button_color")) + + if "scrollbar_button_hover_color" in kwargs: + self._scrollbar.configure(fg_color=kwargs.pop("scrollbar_button_hover_color")) + + if "label_text" in kwargs: + self._label_text = kwargs.pop("label_text") + self._label.configure(text=self._label_text) + self._create_grid() + + if "label_font" in kwargs: + self._label.configure(font=kwargs.pop("label_font")) + + if "label_text_color" in kwargs: + self._label.configure(text_color=kwargs.pop("label_text_color")) + + if "label_fg_color" in kwargs: + self._label.configure(fg_color=kwargs.pop("label_fg_color")) + + if "label_anchor" in kwargs: + self._label.configure(anchor=kwargs.pop("label_anchor")) + + self._parent_frame.configure(**kwargs) + + def cget(self, attribute_name: str): + if attribute_name == "width": + return self._desired_width + elif attribute_name == "height": + return self._desired_height + + elif attribute_name == "label_text": + return self._label_text + elif attribute_name == "label_font": + return self._label.cget("font") + elif attribute_name == "label_text_color": + return self._label.cget("_text_color") + elif attribute_name == "label_fg_color": + return self._label.cget("fg_color") + elif attribute_name == "label_anchor": + return self._label.cget("anchor") + + elif attribute_name.startswith("scrollbar_fg_color"): + return self._scrollbar.cget("fg_color") + elif attribute_name.startswith("scrollbar_button_color"): + return self._scrollbar.cget("button_color") + elif attribute_name.startswith("scrollbar_button_hover_color"): + return self._scrollbar.cget("button_hover_color") + + else: + return self._parent_frame.cget(attribute_name) + + 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 _mouse_wheel_all(self, event): + if self.check_if_master_is_canvas(event.widget): + 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: + 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 + + def _keyboard_shift_release_all(self, event): + self._shift_pressed = False + + def check_if_master_is_canvas(self, widget): + if widget == self._parent_canvas: + return True + elif widget.master is not None: + return self.check_if_master_is_canvas(widget.master) + else: + return False + + def pack(self, **kwargs): + self._parent_frame.pack(**kwargs) + + def place(self, **kwargs): + self._parent_frame.place(**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): + return 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) diff --git a/customtkinter/windows/widgets/ctk_textbox.py b/customtkinter/windows/widgets/ctk_textbox.py index 1fa1331..eeee9ef 100644 --- a/customtkinter/windows/widgets/ctk_textbox.py +++ b/customtkinter/windows/widgets/ctk_textbox.py @@ -119,7 +119,7 @@ class CTkTextbox(CTkBaseClass): self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True) - self.after(50, self._check_if_scrollbars_needed) + self.after(50, self._check_if_scrollbars_needed, None, True) self._draw() def _create_grid_for_text_and_scrollbars(self, re_grid_textbox=False, re_grid_x_scrollbar=False, re_grid_y_scrollbar=False): @@ -151,7 +151,7 @@ class CTkTextbox(CTkBaseClass): else: self._y_scrollbar.grid_forget() - def _check_if_scrollbars_needed(self, event=None, continue_loop: bool = True): + def _check_if_scrollbars_needed(self, event=None, continue_loop: bool = False): """ Method hides or places the scrollbars if they are needed on key release event of tkinter.text widget """ if self._scrollbars_activated: @@ -349,7 +349,6 @@ class CTkTextbox(CTkBaseClass): return self._textbox.focus_force() def insert(self, index, text, tags=None): - self._check_if_scrollbars_needed() return self._textbox.insert(index, text, tags) def get(self, index1, index2=None): diff --git a/documentation_images/scrollable_frame_example_Windows.png b/documentation_images/scrollable_frame_example_Windows.png new file mode 100644 index 0000000..9b74602 Binary files /dev/null and b/documentation_images/scrollable_frame_example_Windows.png differ diff --git a/examples/complex_example.py b/examples/complex_example.py index 978a706..072bc64 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -87,21 +87,9 @@ class App(customtkinter.CTk): self.radio_button_3 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=2) self.radio_button_3.grid(row=3, column=2, pady=10, padx=20, sticky="n") - # create checkbox and switch frame - self.checkbox_slider_frame = customtkinter.CTkFrame(self) - self.checkbox_slider_frame.grid(row=1, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew") - self.checkbox_1 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) - self.checkbox_1.grid(row=1, column=0, pady=(20, 10), padx=20, sticky="n") - self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) - self.checkbox_2.grid(row=2, column=0, pady=10, padx=20, sticky="n") - self.switch_1 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame, command=lambda: print("switch 1 toggle")) - self.switch_1.grid(row=3, column=0, pady=10, padx=20, sticky="n") - self.switch_2 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame) - self.switch_2.grid(row=4, column=0, pady=(10, 20), padx=20, sticky="n") - # create slider and progressbar frame self.slider_progressbar_frame = customtkinter.CTkFrame(self, fg_color="transparent") - self.slider_progressbar_frame.grid(row=1, column=1, columnspan=2, padx=(20, 0), pady=(20, 0), sticky="nsew") + self.slider_progressbar_frame.grid(row=1, column=1, padx=(20, 0), pady=(20, 0), sticky="nsew") self.slider_progressbar_frame.grid_columnconfigure(0, weight=1) self.slider_progressbar_frame.grid_rowconfigure(4, weight=1) self.seg_button_1 = customtkinter.CTkSegmentedButton(self.slider_progressbar_frame) @@ -117,12 +105,32 @@ class App(customtkinter.CTk): self.progressbar_3 = customtkinter.CTkProgressBar(self.slider_progressbar_frame, orientation="vertical") self.progressbar_3.grid(row=0, column=2, rowspan=5, padx=(10, 20), pady=(10, 10), sticky="ns") + # create scrollable frame + self.scrollable_frame = customtkinter.CTkScrollableFrame(self, label_text="CTkScrollableFrame") + self.scrollable_frame.grid(row=1, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew") + self.scrollable_frame.grid_columnconfigure(0, weight=1) + self.scrollable_frame_switches = [] + for i in range(100): + switch = customtkinter.CTkSwitch(master=self.scrollable_frame, text=f"CTkSwitch {i}") + switch.grid(row=i, column=0, padx=10, pady=(0, 20)) + self.scrollable_frame_switches.append(switch) + + # create checkbox and switch frame + self.checkbox_slider_frame = customtkinter.CTkFrame(self) + self.checkbox_slider_frame.grid(row=1, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew") + self.checkbox_1 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) + self.checkbox_1.grid(row=1, column=0, pady=(20, 0), padx=20, sticky="n") + self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) + self.checkbox_2.grid(row=2, column=0, pady=(20, 0), padx=20, sticky="n") + self.checkbox_3 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) + self.checkbox_3.grid(row=3, column=0, pady=20, padx=20, sticky="n") + # set default values self.sidebar_button_3.configure(state="disabled", text="Disabled CTkButton") - self.checkbox_2.configure(state="disabled") - self.switch_2.configure(state="disabled") + self.checkbox_3.configure(state="disabled") self.checkbox_1.select() - self.switch_1.select() + self.scrollable_frame_switches[0].select() + self.scrollable_frame_switches[4].select() self.radio_button_3.configure(state="disabled") self.appearance_mode_optionemenu.set("Dark") self.scaling_optionemenu.set("100%") diff --git a/examples/scrollable_frame_example.py b/examples/scrollable_frame_example.py new file mode 100644 index 0000000..9386f70 --- /dev/null +++ b/examples/scrollable_frame_example.py @@ -0,0 +1,133 @@ +import customtkinter +import os +from PIL import Image + + +class ScrollableCheckBoxFrame(customtkinter.CTkScrollableFrame): + def __init__(self, master, item_list, command=None, **kwargs): + super().__init__(master, **kwargs) + + self.command = command + self.checkbox_list = [] + for i, item in enumerate(item_list): + self.add_item(item) + + def add_item(self, item): + checkbox = customtkinter.CTkCheckBox(self, text=item) + if self.command is not None: + checkbox.configure(command=self.command) + checkbox.grid(row=len(self.checkbox_list), column=0, pady=(0, 10)) + self.checkbox_list.append(checkbox) + + def remove_item(self, item): + for checkbox in self.checkbox_list: + if item == checkbox.cget("text"): + checkbox.destroy() + self.checkbox_list.remove(checkbox) + return + + def get_checked_items(self): + return [checkbox.cget("text") for checkbox in self.checkbox_list if checkbox.get() == 1] + + +class ScrollableRadiobuttonFrame(customtkinter.CTkScrollableFrame): + def __init__(self, master, item_list, command=None, **kwargs): + super().__init__(master, **kwargs) + + self.command = command + self.radiobutton_variable = customtkinter.StringVar() + self.radiobutton_list = [] + for i, item in enumerate(item_list): + self.add_item(item) + + def add_item(self, item): + radiobutton = customtkinter.CTkRadioButton(self, text=item, value=item, variable=self.radiobutton_variable) + if self.command is not None: + radiobutton.configure(command=self.command) + radiobutton.grid(row=len(self.radiobutton_list), column=0, pady=(0, 10)) + self.radiobutton_list.append(radiobutton) + + def remove_item(self, item): + for radiobutton in self.radiobutton_list: + if item == radiobutton.cget("text"): + radiobutton.destroy() + self.radiobutton_list.remove(radiobutton) + return + + def get_checked_item(self): + return self.radiobutton_variable.get() + + +class ScrollableLabelButtonFrame(customtkinter.CTkScrollableFrame): + def __init__(self, master, command=None, **kwargs): + super().__init__(master, **kwargs) + self.grid_columnconfigure(0, weight=1) + + self.command = command + self.radiobutton_variable = customtkinter.StringVar() + self.label_list = [] + self.button_list = [] + + def add_item(self, item, image=None): + label = customtkinter.CTkLabel(self, text=item, image=image, compound="left", padx=5, anchor="w") + button = customtkinter.CTkButton(self, text="Command", width=100, height=24) + if self.command is not None: + button.configure(command=lambda: self.command(item)) + label.grid(row=len(self.label_list), column=0, pady=(0, 10), sticky="w") + button.grid(row=len(self.button_list), column=1, pady=(0, 10), padx=5) + self.label_list.append(label) + self.button_list.append(button) + + def remove_item(self, item): + for label, button in zip(self.label_list, self.button_list): + if item == label.cget("text"): + label.destroy() + button.destroy() + self.label_list.remove(label) + self.button_list.remove(button) + return + + +class App(customtkinter.CTk): + def __init__(self): + super().__init__() + + self.title("CTkScrollableFrame example") + self.grid_rowconfigure(0, weight=1) + self.columnconfigure(2, weight=1) + + # create scrollable checkbox frame + self.scrollable_checkbox_frame = ScrollableCheckBoxFrame(master=self, width=200, command=self.checkbox_frame_event, + item_list=[f"item {i}" for i in range(50)]) + self.scrollable_checkbox_frame.grid(row=0, column=0, padx=15, pady=15, sticky="ns") + self.scrollable_checkbox_frame.add_item("new item") + + # create scrollable radiobutton frame + self.scrollable_radiobutton_frame = ScrollableRadiobuttonFrame(master=self, width=500, command=self.radiobutton_frame_event, + item_list=[f"item {i}" for i in range(100)], + label_text="ScrollableRadiobuttonFrame") + self.scrollable_radiobutton_frame.grid(row=0, column=1, padx=15, pady=15, sticky="ns") + self.scrollable_radiobutton_frame.configure(width=200) + self.scrollable_radiobutton_frame.remove_item("item 3") + + # create scrollable label and button frame + current_dir = os.path.dirname(os.path.abspath(__file__)) + self.scrollable_label_button_frame = ScrollableLabelButtonFrame(master=self, width=300, command=self.label_button_frame_event, corner_radius=0) + self.scrollable_label_button_frame.grid(row=0, column=2, padx=0, pady=0, sticky="nsew") + for i in range(20): # add items with images + self.scrollable_label_button_frame.add_item(f"image and item {i}", image=customtkinter.CTkImage(Image.open(os.path.join(current_dir, "test_images", "chat_light.png")))) + + def checkbox_frame_event(self): + print(f"checkbox frame modified: {self.scrollable_checkbox_frame.get_checked_items()}") + + def radiobutton_frame_event(self): + print(f"radiobutton frame modified: {self.scrollable_radiobutton_frame.get_checked_item()}") + + def label_button_frame_event(self, item): + print(f"label button frame clicked: {item}") + + +if __name__ == "__main__": + customtkinter.set_appearance_mode("dark") + app = App() + app.mainloop() diff --git a/pyproject.toml b/pyproject.toml index 6899822..d8a2316 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" github_url = "https://github.com/TomSchimansky/CustomTkinter" [tool.tbump.version] -current = "5.0.4" +current = "5.1.2" # Example of a semver regexp. # Make sure this matches current_version before diff --git a/setup.cfg b/setup.cfg index 66555cc..0ed1020 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = customtkinter -version = 5.0.4 +version = 5.1.2 description = Create modern looking GUIs with Python long_description = A modern and customizable python UI-library based on Tkinter: https://github.com/TomSchimansky/CustomTkinter long_description_content_type = text/markdown diff --git a/test/manual_integration_tests/test_scrollable_frame.py b/test/manual_integration_tests/test_scrollable_frame.py new file mode 100644 index 0000000..808a4ca --- /dev/null +++ b/test/manual_integration_tests/test_scrollable_frame.py @@ -0,0 +1,39 @@ +import customtkinter + +customtkinter.set_default_color_theme("dark-blue") + + +app = customtkinter.CTk() +app.grid_columnconfigure(2, weight=1) +app.grid_rowconfigure(1, weight=1) + +toplevel = customtkinter.CTkToplevel() +switch = customtkinter.CTkSwitch(toplevel, text="Mode", command=lambda: customtkinter.set_appearance_mode("dark" if switch.get() == 1 else "light")) +switch.grid(row=0, column=0, padx=50, pady=50) + +frame_1 = customtkinter.CTkScrollableFrame(app, orientation="vertical", label_text="should not appear", fg_color="transparent") +frame_1.grid(row=0, column=0, padx=20, pady=20) +frame_1.configure(label_text=None) + +frame_2 = customtkinter.CTkScrollableFrame(app, orientation="vertical", label_text="CTkScrollableFrame") +frame_2.grid(row=1, column=0, padx=20, pady=20) + +frame_3 = customtkinter.CTkScrollableFrame(app, orientation="horizontal") +frame_3.grid(row=0, column=1, padx=20, pady=20) + +frame_4 = customtkinter.CTkScrollableFrame(app, orientation="horizontal", label_fg_color="transparent") +frame_4.grid(row=1, column=1, padx=20, pady=20) +frame_4.configure(label_text="CTkScrollableFrame") + +frame_5 = customtkinter.CTkScrollableFrame(app, orientation="vertical", label_text="CTkScrollableFrame", corner_radius=0) +frame_5.grid(row=0, column=2, rowspan=2, sticky="nsew") + +for i in range(100): + customtkinter.CTkCheckBox(frame_1).grid(row=i, padx=10, pady=10) + customtkinter.CTkCheckBox(frame_2).grid(row=i, padx=10, pady=10) + customtkinter.CTkCheckBox(frame_3).grid(row=0, column=i, padx=10, pady=10) + customtkinter.CTkCheckBox(frame_4).grid(row=0, column=i, padx=10, pady=10) + customtkinter.CTkCheckBox(frame_5).grid(row=i, padx=10, pady=10) + + +app.mainloop()