49 Commits

Author SHA1 Message Date
d719950f80 fixed tabview initial fg_color, removed tests from simple_example.py 2023-07-27 14:40:19 +02:00
16990a566f added anchor to tabview #1766, fixed tabview fg_color update #1803, added index method to tabview and segmented button, added rename method to tabview #1192, fixed license in Readme.md 2023-07-27 14:06:13 +02:00
f629e4c5cb fix simple example 2023-07-08 21:32:20 +02:00
9cfd5f4515 added missing arguments to CTkSLider configure #1790 2023-07-08 21:24:59 +02:00
e116faf910 add font option to input dialog #838 2023-06-20 13:38:47 +02:00
b8f1eea411 fix license classifier 2023-06-19 14:01:01 +02:00
8b7386bc64 fix license classifier 2023-06-19 13:57:11 +02:00
8fc2d31584 Bump to 5.2.0 2023-06-19 13:54:51 +02:00
290cafbc39 Merge pull request #1399 from Sahil481/customtkinter-sahil-contribute
Fix configuring anchor on CTkButton #1394 #1393
2023-06-19 13:19:50 +02:00
9bd6d7bdde Merge pull request #1729 from dishb/fix-cancel-event
Fix incorrect event in input dialog
2023-06-19 13:00:37 +02:00
6841f94a99 🛠️ [fix] fix issue in #1631 2023-06-16 12:54:16 -07:00
a0c0da6c9a Merge pull request #1721 from dishb/fix-readme-1
README has unnecessary tkinter import
2023-06-16 13:17:47 +02:00
10d5cd4c2a 🛠️ [fix] small mistake in the README example code 2023-06-15 16:07:18 -07:00
abe33f7e81 fix error in CTkFont with multiple destroy calls #1692 2023-06-15 12:49:07 +02:00
37b375d58b fixed wrong json class names and added fix for backwards compatibility in ThemeManager #1538 2023-05-27 13:25:30 +02:00
cd85a1c782 added text_color_disabled to CTkLabel #1557, change license in setup.cfg 2023-05-27 12:48:46 +02:00
1960c0b1e0 added text_color_disabled to configure of CTkOptionMenu #1559 2023-05-08 19:11:22 +02:00
6d2f31c23c added border_width, corner_radius to configure of CTkSegmentedButton, removed border_color because has no effect #1562 2023-05-08 19:06:39 +02:00
c6b16ce815 added checkmark_color and text_color_disabled to CTkCheckbox configure method #1586 2023-05-08 13:08:44 +02:00
72157e8d26 Bump to 5.1.3 2023-05-05 18:55:53 +02:00
ccf9fdfc9a add new website links 2023-05-05 18:54:32 +02:00
d3883012b7 change tabview select method #1508 2023-04-29 01:38:55 +02:00
86ba99c2ab fixed scrollbar colors for scrollable frame #1509 2023-04-26 10:53:08 +02:00
b60859dfd6 added text color configuration for switch #1497 2023-04-24 13:45:16 +02:00
121f5713a8 fixed platform independant path for theme loading #1498 2023-04-24 13:37:17 +02:00
2ee496ed28 fixed fg_color key for CTkSwitch #1482 2023-04-23 11:36:50 +02:00
838bc7885b update Readme.md 2023-04-22 17:33:22 +02:00
d23d99deb0 update Readme.md 2023-04-22 17:33:04 +02:00
fe5ace8ab7 update Readme.md 2023-04-22 16:54:24 +02:00
ffbf851a92 update Readme.md 2023-04-22 16:41:18 +02:00
220bfea1a6 added 'master' o valid CTkToplevel arguments #1468 2023-04-21 01:47:19 +02:00
9f653fab1d Fixed a bug
Fixed #1394
2023-03-29 22:14:43 +05:30
09e584634c Bump to 5.1.2 2023-02-06 20:07:47 +01:00
bc5d527d68 added scrollable frame to other theme files #1193 2023-02-06 20:07:29 +01:00
6e9258a444 Bump to 5.1.1 2023-02-06 12:55:59 +01:00
f47cf024b2 fixed CTkScrollableFrame and example 2023-02-06 12:55:46 +01:00
59df37e920 Bump to 5.1.0 2023-02-05 22:58:13 +01:00
b177f85328 scrollable frame fix 2023-02-05 22:39:07 +01:00
786a5148de finished scrollable frame, added example and test for scrollable frame 2023-02-05 21:41:23 +01:00
2359a6ce39 progress on scrollable frame 2023-02-05 03:02:21 +01:00
110e9bbcbf Merge remote-tracking branch 'origin/master' 2023-02-04 16:54:12 +01:00
a478334fb7 start working on scrollable frame 2023-02-04 16:53:48 +01:00
4d52febd99 Change license to MIT 2023-02-04 14:52:17 +01:00
9e2584c958 override iconbitmap #1106 2023-01-24 12:45:29 +01:00
9fcd963fd2 prevent width and height args in place method #1094 2023-01-22 22:00:18 +01:00
4b600b9179 Bump to 5.0.5 2023-01-22 21:37:49 +01:00
7901edba30 remove unnecessary check_scollbar call in textbox #1020 2023-01-21 22:37:40 +01:00
39447072ac change text fg color 2023-01-21 22:24:54 +01:00
7cb8f64dec fix switch and radiobutton background color #867, check if user set titlebar icon on Windows 2023-01-21 14:22:18 +01:00
37 changed files with 957 additions and 319 deletions

134
LICENSE
View File

@ -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.

View File

@ -10,13 +10,19 @@
![PyPI](https://img.shields.io/pypi/v/customtkinter)
![PyPI - Downloads](https://img.shields.io/pypi/dm/customtkinter?color=green&label=downloads)
![Downloads](https://static.pepy.tech/personalized-badge/customtkinter?period=total&units=international_system&left_color=grey&right_color=green&left_text=downloads)
![PyPI - License](https://img.shields.io/pypi/l/customtkinter)
![PyPI - License](https://img.shields.io/badge/license_MIT)
![](https://tokei.rs/b1/github/tomschimansky/customtkinter)
</div>
---
<div align="center">
<h3>
Official website: https://customtkinter.tomschimansky.com
</h3>
</div>
CustomTkinter is a python UI-library based on Tkinter, which provides new, modern and
fully customizable widgets. They are created and used like normal Tkinter widgets and
can also be used in combination with normal Tkinter elements. The widgets
@ -43,14 +49,13 @@ pip3 install customtkinter
## Documentation
The **official** documentation can be found in the Wiki Tab here:
The **official** documentation can be found here:
**--> [Documentation](https://github.com/TomSchimansky/CustomTkinter/wiki)**.
**➡️ https://customtkinter.tomschimansky.com/documentation**.
## Example Program
To test customtkinter you can try this simple example with only a single button:
```python
import tkinter
import customtkinter
customtkinter.set_appearance_mode("System") # Modes: system (default), light, dark
@ -64,7 +69,7 @@ def button_function():
# Use CTkButton instead of tkinter Button
button = customtkinter.CTkButton(master=app, text="CTkButton", command=button_function)
button.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
button.place(relx=0.5, rely=0.5, anchor=customtkinter.CENTER)
app.mainloop()
```
@ -105,6 +110,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

View File

@ -1,4 +1,4 @@
__version__ = "5.0.4"
__version__ = "5.2.0"
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

View File

@ -34,7 +34,7 @@
"text_color":["gray10", "#DCE4EE"],
"placeholder_text_color": ["gray52", "gray62"]
},
"CTkCheckbox": {
"CTkCheckBox": {
"corner_radius": 6,
"border_width": 3,
"fg_color": ["#3B8ED0", "#1F6AA5"],
@ -48,14 +48,14 @@
"corner_radius": 1000,
"border_width": 3,
"button_length": 0,
"fg_Color": ["#939BA2", "#4A4D50"],
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["#3B8ED0", "#1F6AA5"],
"button_color": ["gray36", "#D5D9DE"],
"button_hover_color": ["gray20", "gray100"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkRadiobutton": {
"CTkRadioButton": {
"corner_radius": 1000,
"border_width_checked": 6,
"border_width_unchecked": 3,
@ -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"],

View File

@ -34,7 +34,7 @@
"text_color": ["gray14", "gray84"],
"placeholder_text_color": ["gray52", "gray62"]
},
"CTkCheckbox": {
"CTkCheckBox": {
"corner_radius": 6,
"border_width": 3,
"fg_color": ["#3a7ebf", "#1f538d"],
@ -48,14 +48,14 @@
"corner_radius": 1000,
"border_width": 3,
"button_length": 0,
"fg_Color": ["#939BA2", "#4A4D50"],
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["#3a7ebf", "#1f538d"],
"button_color": ["gray36", "#D5D9DE"],
"button_hover_color": ["gray20", "gray100"],
"text_color": ["gray14", "gray84"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkRadiobutton": {
"CTkRadioButton": {
"corner_radius": 1000,
"border_width_checked": 6,
"border_width_unchecked": 3,
@ -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"],

View File

@ -34,7 +34,7 @@
"text_color":["gray10", "#DCE4EE"],
"placeholder_text_color": ["gray52", "gray62"]
},
"CTkCheckbox": {
"CTkCheckBox": {
"corner_radius": 6,
"border_width": 3,
"fg_color": ["#2CC985", "#2FA572"],
@ -48,14 +48,14 @@
"corner_radius": 1000,
"border_width": 3,
"button_length": 0,
"fg_Color": ["#939BA2", "#4A4D50"],
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["#2CC985", "#2FA572"],
"button_color": ["gray36", "#D5D9DE"],
"button_hover_color": ["gray20", "gray100"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkRadiobutton": {
"CTkRadioButton": {
"corner_radius": 1000,
"border_width_checked": 6,
"border_width_unchecked": 3,
@ -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"],

View File

@ -5,6 +5,7 @@ from .widgets import CTkEntry
from .widgets import CTkButton
from .widgets.theme import ThemeManager
from .ctk_toplevel import CTkToplevel
from .widgets.font import CTkFont
class CTkInputDialog(CTkToplevel):
@ -24,6 +25,7 @@ class CTkInputDialog(CTkToplevel):
entry_text_color: Optional[Union[str, Tuple[str, str]]] = None,
title: str = "CTkDialog",
font: Optional[Union[tuple, CTkFont]] = None,
text: str = "CTkDialog"):
super().__init__(fg_color=fg_color)
@ -39,9 +41,11 @@ class CTkInputDialog(CTkToplevel):
self._user_input: Union[str, None] = None
self._running: bool = False
self._title = title
self._text = text
self._font = font
self.title(title)
self.title(self._title)
self.lift() # lift window on top
self.attributes("-topmost", True) # stay on top
self.protocol("WM_DELETE_WINDOW", self._on_closing)
@ -50,7 +54,6 @@ class CTkInputDialog(CTkToplevel):
self.grab_set() # make other windows not clickable
def _create_widgets(self):
self.grid_columnconfigure((0, 1), weight=1)
self.rowconfigure(0, weight=1)
@ -59,14 +62,16 @@ class CTkInputDialog(CTkToplevel):
wraplength=300,
fg_color="transparent",
text_color=self._text_color,
text=self._text,)
text=self._text,
font=self._font)
self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew")
self._entry = CTkEntry(master=self,
width=230,
fg_color=self._entry_fg_color,
border_color=self._entry_border_color,
text_color=self._entry_text_color)
text_color=self._entry_text_color,
font=self._font)
self._entry.grid(row=1, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew")
self._ok_button = CTkButton(master=self,
@ -76,6 +81,7 @@ class CTkInputDialog(CTkToplevel):
hover_color=self._button_hover_color,
text_color=self._button_text_color,
text='Ok',
font=self._font,
command=self._ok_event)
self._ok_button.grid(row=2, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew")
@ -86,7 +92,8 @@ class CTkInputDialog(CTkToplevel):
hover_color=self._button_hover_color,
text_color=self._button_text_color,
text='Cancel',
command=self._ok_event)
font=self._font,
command=self._cancel_event)
self._cancel_button.grid(row=2, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew")
self.after(150, lambda: self._entry.focus()) # set focus to entry with slight delay, otherwise it won't work

View File

@ -1,4 +1,5 @@
import tkinter
import tkinterDnD
from distutils.version import StrictVersion as Version
import sys
import os
@ -12,8 +13,10 @@ from .widgets.appearance_mode import CTkAppearanceModeBaseClass
from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
TK_CLASS = tkinterDnD.Tk
class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
class CTk(TK_CLASS, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
"""
Main app window with dark titlebar on Windows and macOS.
For detailed information check out the documentation.
@ -35,19 +38,11 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
self._enable_macos_dark_title_bar()
# call init methods of super classes
tkinter.Tk.__init__(self, **pop_from_dict_by_set(kwargs, self._valid_tk_constructor_arguments))
TK_CLASS.__init__(self, **pop_from_dict_by_set(kwargs, self._valid_tk_constructor_arguments))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="window")
check_kwargs_empty(kwargs, raise_error=True)
try:
# Set Windows titlebar icon
if sys.platform.startswith("win"):
customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.after(200, lambda: self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico")))
except Exception:
pass
self._current_width = 600 # initial window size, independent of scaling
self._current_height = 500
self._min_width: int = 0
@ -61,23 +56,31 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
# set bg of tkinter.Tk
super().configure(bg=self._apply_appearance_mode(self._fg_color))
# set title and initial geometry
# set title
self.title("CTk")
# self.geometry(f"{self._current_width}x{self._current_height}")
# indicator variables
self._iconbitmap_method_called = False # indicates if wm_iconbitmap method got called
self._state_before_windows_set_titlebar_color = None
self._window_exists = False # indicates if the window is already shown through update() or mainloop() after init
self._withdraw_called_before_window_exists = False # indicates if withdraw() 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
# 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)
# set titlebar color (Windows only)
if sys.platform.startswith("win"):
self._windows_set_titlebar_color(self._get_appearance_mode())
self.bind('<Configure>', self._update_dimensions_event)
self.bind('<FocusIn>', self._focus_in_event)
self._block_update_dimensions_event = False
def destroy(self):
self._disable_macos_dark_title_bar()
@ -140,24 +143,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):
@ -219,6 +224,23 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
else:
return super().cget(attribute_name)
def wm_iconbitmap(self, bitmap=None, default=None):
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
if not self._iconbitmap_method_called:
customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico"))
except Exception:
pass
@classmethod
def _enable_macos_dark_title_bar(cls):
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
@ -253,9 +275,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()
@ -284,7 +308,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()
@ -297,6 +321,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

@ -19,7 +19,7 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl
For detailed information check out the documentation.
"""
_valid_tk_toplevel_arguments: set = {"bd", "borderwidth", "class", "container", "cursor", "height",
_valid_tk_toplevel_arguments: set = {"master", "bd", "borderwidth", "class", "container", "cursor", "height",
"highlightbackground", "highlightthickness", "menu", "relief",
"screen", "takefocus", "use", "visual", "width"}
@ -62,19 +62,28 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl
# set title of tkinter.Toplevel
super().title("CTkToplevel")
# indicator variables
self._iconbitmap_method_called = True
self._state_before_windows_set_titlebar_color = None
self._windows_set_titlebar_color_called = False # indicates if windows_set_titlebar_color was called, stays True until revert_withdraw_after_windows_set_titlebar_color is called
self._withdraw_called_after_windows_set_titlebar_color = False # indicates if withdraw() 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
# 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)
# set titlebar color (Windows only)
if sys.platform.startswith("win"):
self._windows_set_titlebar_color(self._get_appearance_mode())
self.bind('<Configure>', self._update_dimensions_event)
self.bind('<FocusIn>', self._focus_in_event)
self._block_update_dimensions_event = False
def destroy(self):
self._disable_macos_dark_title_bar()
@ -190,6 +199,19 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl
else:
return super().cget(attribute_name)
def wm_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
if not self._iconbitmap_method_called:
customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico"))
except Exception:
pass
@classmethod
def _enable_macos_dark_title_bar(cls):
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
@ -219,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()
@ -249,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

@ -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

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):
@ -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))

View File

@ -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")
@ -432,6 +436,7 @@ class CTkButton(CTkBaseClass):
if "anchor" in kwargs:
self._anchor = kwargs.pop("anchor")
self._create_grid()
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)

View File

@ -51,20 +51,20 @@ class CTkCheckBox(CTkBaseClass):
self._checkbox_height = checkbox_height
# color
self._fg_color = ThemeManager.theme["CTkCheckbox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._hover_color = ThemeManager.theme["CTkCheckbox"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
self._border_color = ThemeManager.theme["CTkCheckbox"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._checkmark_color = ThemeManager.theme["CTkCheckbox"]["checkmark_color"] if checkmark_color is None else self._check_color_type(checkmark_color)
self._fg_color = ThemeManager.theme["CTkCheckBox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._hover_color = ThemeManager.theme["CTkCheckBox"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
self._border_color = ThemeManager.theme["CTkCheckBox"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._checkmark_color = ThemeManager.theme["CTkCheckBox"]["checkmark_color"] if checkmark_color is None else self._check_color_type(checkmark_color)
# shape
self._corner_radius = ThemeManager.theme["CTkCheckbox"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkCheckbox"]["border_width"] if border_width is None else border_width
self._corner_radius = ThemeManager.theme["CTkCheckBox"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkCheckBox"]["border_width"] if border_width is None else border_width
# text
self._text = text
self._text_label: Union[tkinter.Label, None] = None
self._text_color = ThemeManager.theme["CTkCheckbox"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkCheckbox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
self._text_color = ThemeManager.theme["CTkCheckBox"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkCheckBox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
@ -265,12 +265,20 @@ class CTkCheckBox(CTkBaseClass):
self._hover_color = self._check_color_type(kwargs.pop("hover_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
if "checkmark_color" in kwargs:
self._checkmark_color = self._check_color_type(kwargs.pop("checkmark_color"))
require_redraw = True
if "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
if "text_color_disabled" in kwargs:
self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
require_redraw = True
if "hover" in 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

@ -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):

View File

@ -14,6 +14,8 @@ class CTkLabel(CTkBaseClass):
"""
Label with rounded corners. Default is fg_color=None (transparent fg_color).
For detailed information check out the documentation.
state argument will probably be removed because it has no effect
"""
# attributes that are passed to and managed by the tkinter entry only:
@ -29,6 +31,7 @@ class CTkLabel(CTkBaseClass):
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
text: str = "CTkLabel",
font: Optional[Union[tuple, CTkFont]] = None,
@ -45,6 +48,14 @@ class CTkLabel(CTkBaseClass):
self._fg_color = ThemeManager.theme["CTkLabel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(text_color)
if text_color_disabled is None:
if "text_color_disabled" in ThemeManager.theme["CTkLabel"]:
self._text_color_disabled = ThemeManager.theme["CTkLabel"]["text_color"]
else:
self._text_color_disabled = self._text_color
else:
self._text_color_disabled = self._check_color_type(text_color_disabled)
# shape
self._corner_radius = ThemeManager.theme["CTkLabel"]["corner_radius"] if corner_radius is None else corner_radius
@ -159,6 +170,7 @@ class CTkLabel(CTkBaseClass):
outline=self._apply_appearance_mode(self._bg_color))
self._label.configure(fg=self._apply_appearance_mode(self._text_color),
disabledforeground=self._apply_appearance_mode(self._text_color_disabled),
bg=self._apply_appearance_mode(self._bg_color))
else:
self._canvas.itemconfig("inner_parts",
@ -166,6 +178,7 @@ class CTkLabel(CTkBaseClass):
outline=self._apply_appearance_mode(self._fg_color))
self._label.configure(fg=self._apply_appearance_mode(self._text_color),
disabledforeground=self._apply_appearance_mode(self._text_color_disabled),
bg=self._apply_appearance_mode(self._fg_color))
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
@ -184,6 +197,10 @@ class CTkLabel(CTkBaseClass):
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
if "text_color_disabled" in kwargs:
self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
require_redraw = True
if "text" in kwargs:
self._text = kwargs.pop("text")
self._label.configure(text=self._text)
@ -228,6 +245,8 @@ class CTkLabel(CTkBaseClass):
return self._fg_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text_color_disabled":
return self._text_color_disabled
elif attribute_name == "text":
return self._text

View File

@ -243,6 +243,10 @@ class CTkOptionMenu(CTkBaseClass):
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
if "text_color_disabled" in kwargs:
self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
require_redraw = True
if "dropdown_fg_color" in kwargs:
self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_fg_color"))
@ -261,8 +265,12 @@ class CTkOptionMenu(CTkBaseClass):
self._update_font()
if "command" in kwargs:
self._command = kwargs.pop("command")
if "dropdown_font" in kwargs:
self._dropdown_menu.configure(font=kwargs.pop("dropdown_font"))
if "values" in kwargs:
self._values = kwargs.pop("values")
self._dropdown_menu.configure(values=self._values)
if "variable" in kwargs:
if self._variable is not None: # remove old callback
@ -277,19 +285,15 @@ class CTkOptionMenu(CTkBaseClass):
else:
self._variable = None
if "values" in kwargs:
self._values = kwargs.pop("values")
self._dropdown_menu.configure(values=self._values)
if "dropdown_font" in kwargs:
self._dropdown_menu.configure(font=kwargs.pop("dropdown_font"))
if "state" in kwargs:
self._state = kwargs.pop("state")
require_redraw = True
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "state" in kwargs:
self._state = kwargs.pop("state")
require_redraw = True
if "command" in kwargs:
self._command = kwargs.pop("command")
if "dynamic_resizing" in kwargs:
self._dynamic_resizing = kwargs.pop("dynamic_resizing")

View File

@ -50,20 +50,20 @@ class CTkRadioButton(CTkBaseClass):
self._radiobutton_height = radiobutton_height
# color
self._fg_color = ThemeManager.theme["CTkRadiobutton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._hover_color = ThemeManager.theme["CTkRadiobutton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
self._border_color = ThemeManager.theme["CTkRadiobutton"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._fg_color = ThemeManager.theme["CTkRadioButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._hover_color = ThemeManager.theme["CTkRadioButton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
self._border_color = ThemeManager.theme["CTkRadioButton"]["border_color"] if border_color is None else self._check_color_type(border_color)
# shape
self._corner_radius = ThemeManager.theme["CTkRadiobutton"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width_unchecked = ThemeManager.theme["CTkRadiobutton"]["border_width_unchecked"] if border_width_unchecked is None else border_width_unchecked
self._border_width_checked = ThemeManager.theme["CTkRadiobutton"]["border_width_checked"] if border_width_checked is None else border_width_checked
self._corner_radius = ThemeManager.theme["CTkRadioButton"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width_unchecked = ThemeManager.theme["CTkRadioButton"]["border_width_unchecked"] if border_width_unchecked is None else border_width_unchecked
self._border_width_checked = ThemeManager.theme["CTkRadioButton"]["border_width_checked"] if border_width_checked is None else border_width_checked
# text
self._text = text
self._text_label: Union[tkinter.Label, None] = None
self._text_color = ThemeManager.theme["CTkRadiobutton"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkRadiobutton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
self._text_color = ThemeManager.theme["CTkRadioButton"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkRadioButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
@ -85,6 +85,7 @@ class CTkRadioButton(CTkBaseClass):
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=1)
self.grid_rowconfigure(0, weight=1)
self._bg_canvas = CTkCanvas(master=self,
highlightthickness=0,

View File

@ -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("<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, add="+")
self.bind_all("<KeyPress-Shift_L>", self._keyboard_shift_press_all, add="+")
self.bind_all("<KeyPress-Shift_R>", self._keyboard_shift_press_all, add="+")
self.bind_all("<KeyRelease-Shift_L>", self._keyboard_shift_release_all, add="+")
self.bind_all("<KeyRelease-Shift_R>", 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(button_color=kwargs.pop("scrollbar_button_color"))
if "scrollbar_button_hover_color" in kwargs:
self._scrollbar.configure(button_hover_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)

View File

@ -10,6 +10,7 @@ from .theme import ThemeManager
from .font import CTkFont
from .ctk_button import CTkButton
from .ctk_frame import CTkFrame
from .utility import check_kwargs_empty
class CTkSegmentedButton(CTkFrame):
@ -40,10 +41,9 @@ class CTkSegmentedButton(CTkFrame):
variable: Union[tkinter.Variable, None] = None,
dynamic_resizing: bool = True,
command: Union[Callable[[str], None], None] = None,
state: str = "normal",
**kwargs):
state: str = "normal"):
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
super().__init__(master=master, bg_color=bg_color, width=width, height=height)
self._sb_fg_color = ThemeManager.theme["CTkSegmentedButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
@ -186,7 +186,7 @@ class CTkSegmentedButton(CTkFrame):
for index, value in enumerate(self._value_list):
self.grid_columnconfigure(index, weight=1, minsize=self._current_height)
self._buttons_dict[value].grid(row=0, column=index, sticky="ew")
self._buttons_dict[value].grid(row=0, column=index, sticky="nsew")
def _create_buttons_from_values(self):
assert len(self._buttons_dict) == 0
@ -197,6 +197,23 @@ class CTkSegmentedButton(CTkFrame):
self._configure_button_corners_for_index(index)
def configure(self, **kwargs):
if "width" in kwargs:
super().configure(width=kwargs.pop("width"))
if "height" in kwargs:
super().configure(height=kwargs.pop("height"))
if "corner_radius" in kwargs:
self._sb_corner_radius = kwargs.pop("corner_radius")
super().configure(corner_radius=self._sb_corner_radius)
for button in self._buttons_dict.values():
button.configure(corner_radius=self._sb_corner_radius)
if "border_width" in kwargs:
self._sb_border_width = kwargs.pop("border_width")
for button in self._buttons_dict.values():
button.configure(border_width=self._sb_border_width)
if "bg_color" in kwargs:
super().configure(bg_color=kwargs.pop("bg_color"))
@ -296,14 +313,20 @@ class CTkSegmentedButton(CTkFrame):
for button in self._buttons_dict.values():
button.configure(state=self._state)
super().configure(**kwargs)
check_kwargs_empty(kwargs, raise_error=True)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
if attribute_name == "width":
return super().cget(attribute_name)
elif attribute_name == "height":
return super().cget(attribute_name)
elif attribute_name == "corner_radius":
return self._sb_corner_radius
elif attribute_name == "border_width":
return self._sb_border_width
elif attribute_name == "bg_color":
return super().cget(attribute_name)
elif attribute_name == "fg_color":
return self._sb_fg_color
elif attribute_name == "selected_color":
@ -331,7 +354,7 @@ class CTkSegmentedButton(CTkFrame):
return self._command
else:
return super().cget(attribute_name)
raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.")
def set(self, value: str, from_variable_callback: bool = False, from_button_callback: bool = False):
if value == self._current_value:
@ -360,6 +383,9 @@ class CTkSegmentedButton(CTkFrame):
def get(self) -> str:
return self._current_value
def index(self, value: str) -> int:
return self._value_list.index(value)
def insert(self, index: int, value: str):
if value not in self._buttons_dict:
if value != "":

View File

@ -199,15 +199,30 @@ class CTkSlider(CTkBaseClass):
outline=self._apply_appearance_mode(self._button_color))
def configure(self, require_redraw=False, **kwargs):
if "state" in kwargs:
self._state = kwargs.pop("state")
self._set_cursor()
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "button_corner_radius" in kwargs:
self._button_corner_radius = kwargs.pop("button_corner_radius")
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "button_length" in kwargs:
self._button_length = kwargs.pop("button_length")
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True)
require_redraw = True
if "progress_color" in kwargs:
self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True)
require_redraw = True
@ -220,20 +235,17 @@ class CTkSlider(CTkBaseClass):
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True)
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "from_" in kwargs:
self._from_ = kwargs.pop("from_")
if "to" in kwargs:
self._to = kwargs.pop("to")
if "state" in kwargs:
self._state = kwargs.pop("state")
self._set_cursor()
require_redraw = True
if "number_of_steps" in kwargs:
self._number_of_steps = kwargs.pop("number_of_steps")
@ -255,6 +267,10 @@ class CTkSlider(CTkBaseClass):
else:
self._variable = None
if "orientation" in kwargs:
self._orientation = kwargs.pop("orientation")
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:

View File

@ -54,7 +54,7 @@ class CTkSwitch(CTkBaseClass):
# color
self._border_color = self._check_color_type(border_color, transparency=True)
self._fg_color = ThemeManager.theme["CTkSwitch"]["fg_Color"] if fg_color is None else self._check_color_type(fg_color)
self._fg_color = ThemeManager.theme["CTkSwitch"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._progress_color = ThemeManager.theme["CTkSwitch"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True)
self._button_color = ThemeManager.theme["CTkSwitch"]["button_color"] if button_color is None else self._check_color_type(button_color)
self._button_hover_color = ThemeManager.theme["CTkSwitch"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
@ -92,6 +92,7 @@ class CTkSwitch(CTkBaseClass):
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=1)
self.grid_rowconfigure(0, weight=1)
self._bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
@ -221,23 +222,29 @@ class CTkSwitch(CTkBaseClass):
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
if self._border_color == "transparent":
self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._bg_color),
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=self._apply_appearance_mode(self._bg_color))
else:
self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._border_color),
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._border_color),
outline=self._apply_appearance_mode(self._border_color))
self._canvas.itemconfig("inner_parts", fill=self._apply_appearance_mode(self._fg_color),
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
if self._progress_color == "transparent":
self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._fg_color),
self._canvas.itemconfig("progress_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
else:
self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._progress_color),
self._canvas.itemconfig("progress_parts",
fill=self._apply_appearance_mode(self._progress_color),
outline=self._apply_appearance_mode(self._progress_color))
self._canvas.itemconfig("slider_parts", fill=self._apply_appearance_mode(self._button_color),
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_color),
outline=self._apply_appearance_mode(self._button_color))
if self._state == tkinter.DISABLED:
@ -292,6 +299,10 @@ class CTkSwitch(CTkBaseClass):
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True)
require_redraw = True
if "progress_color" in kwargs:
self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True)
require_redraw = True
@ -304,8 +315,12 @@ class CTkSwitch(CTkBaseClass):
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True)
if "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
if "text_color_disabled" in kwargs:
self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
require_redraw = True
if "hover" in kwargs:

View File

@ -15,8 +15,8 @@ class CTkTabview(CTkBaseClass):
For detailed information check out the documentation.
"""
_top_spacing: int = 10 # px on top of the buttons
_top_button_overhang: int = 8 # px
_outer_spacing: int = 10 # px on top or below the button
_outer_button_overhang: int = 8 # px
_button_height: int = 26
_segmented_button_border_width: int = 3
@ -41,6 +41,7 @@ class CTkTabview(CTkBaseClass):
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
command: Union[Callable, None] = None,
anchor: str = "center",
state: str = "normal",
**kwargs):
@ -65,12 +66,13 @@ class CTkTabview(CTkBaseClass):
# shape
self._corner_radius = ThemeManager.theme["CTkFrame"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkFrame"]["border_width"] if border_width is None else border_width
self._anchor = anchor
self._canvas = CTkCanvas(master=self,
bg=self._apply_appearance_mode(self._bg_color),
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height - self._top_spacing - self._top_button_overhang))
height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang))
self._draw_engine = DrawEngine(self._canvas)
self._segmented_button = CTkSegmentedButton(self,
@ -99,9 +101,9 @@ class CTkTabview(CTkBaseClass):
self._draw()
def _segmented_button_callback(self, selected_name):
self._tab_dict[self._current_name].grid_forget()
self._current_name = selected_name
self._grid_forget_all_tabs()
self._set_grid_tab_by_name(self._current_name)
self._set_grid_current_tab()
if self._command is not None:
self._command()
@ -124,7 +126,7 @@ class CTkTabview(CTkBaseClass):
super()._set_scaling(*args, **kwargs)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height - self._top_spacing - self._top_button_overhang))
height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang))
self._configure_grid()
self._draw(no_color_updates=True)
@ -132,56 +134,82 @@ class CTkTabview(CTkBaseClass):
super()._set_dimensions(width, height)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height - self._top_spacing - self._top_button_overhang))
height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang))
self._draw()
def _configure_segmented_button_background_corners(self):
""" needs to be called for changes in fg_color, bg_color """
if self._fg_color is not None:
self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._fg_color, self._fg_color))
else:
if self._fg_color == "transparent":
self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
def _configure_tab_background_corners_by_name(self, name: str):
""" needs to be called for changes in fg_color, bg_color, border_width """
self._tab_dict[name].configure(background_corner_colors=None)
else:
if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._fg_color, self._fg_color))
else:
self._segmented_button.configure(background_corner_colors=(self._fg_color, self._fg_color, self._bg_color, self._bg_color))
def _configure_grid(self):
""" create 3 x 4 grid system """
self.grid_rowconfigure(0, weight=0, minsize=self._apply_widget_scaling(self._top_spacing))
self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._top_button_overhang))
self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._top_button_overhang))
self.grid_rowconfigure(3, weight=1)
if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
self.grid_rowconfigure(0, weight=0, minsize=self._apply_widget_scaling(self._outer_spacing))
self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._outer_button_overhang))
self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._outer_button_overhang))
self.grid_rowconfigure(3, weight=1)
else:
self.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._outer_button_overhang))
self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._outer_button_overhang))
self.grid_rowconfigure(3, weight=0, minsize=self._apply_widget_scaling(self._outer_spacing))
self.grid_columnconfigure(0, weight=1)
def _set_grid_canvas(self):
self._canvas.grid(row=2, rowspan=2, column=0, columnspan=1, sticky="nsew")
if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
self._canvas.grid(row=2, rowspan=2, column=0, columnspan=1, sticky="nsew")
else:
self._canvas.grid(row=0, rowspan=2, column=0, columnspan=1, sticky="nsew")
def _set_grid_segmented_button(self):
""" needs to be called for changes in corner_radius """
self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="ns")
""" needs to be called for changes in corner_radius, anchor """
def _set_grid_tab_by_name(self, name: str):
if self._anchor.lower() in ("center", "n", "s"):
self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="ns")
elif self._anchor.lower() in ("nw", "w", "sw"):
self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="nsw")
elif self._anchor.lower() in ("ne", "e", "se"):
self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="nse")
def _set_grid_current_tab(self):
""" needs to be called for changes in corner_radius, border_width """
self._tab_dict[name].grid(row=3, column=0, sticky="nsew",
padx=self._apply_widget_scaling(max(self._corner_radius, self._border_width)),
pady=self._apply_widget_scaling(max(self._corner_radius, self._border_width)))
if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
self._tab_dict[self._current_name].grid(row=3, column=0, sticky="nsew",
padx=self._apply_widget_scaling(max(self._corner_radius, self._border_width)),
pady=self._apply_widget_scaling(max(self._corner_radius, self._border_width)))
else:
self._tab_dict[self._current_name].grid(row=0, column=0, sticky="nsew",
padx=self._apply_widget_scaling(max(self._corner_radius, self._border_width)),
pady=self._apply_widget_scaling(max(self._corner_radius, self._border_width)))
def _grid_forget_all_tabs(self):
for frame in self._tab_dict.values():
frame.grid_forget()
def _grid_forget_all_tabs(self, exclude_name=None):
for name, frame in self._tab_dict.items():
if name != exclude_name:
frame.grid_forget()
def _create_tab(self) -> CTkFrame:
new_tab = CTkFrame(self,
height=0,
width=0,
fg_color=self._fg_color,
border_width=0,
corner_radius=self._corner_radius)
corner_radius=0)
if self._fg_color == "transparent":
new_tab.configure(fg_color=self._apply_appearance_mode(self._bg_color),
bg_color=self._apply_appearance_mode(self._bg_color))
else:
new_tab.configure(fg_color=self._apply_appearance_mode(self._fg_color),
bg_color=self._apply_appearance_mode(self._fg_color))
return new_tab
def _draw(self, no_color_updates: bool = False):
@ -191,7 +219,7 @@ class CTkTabview(CTkBaseClass):
return
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._top_spacing - self._top_button_overhang),
self._apply_widget_scaling(self._current_height - self._outer_spacing - self._outer_button_overhang),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width))
@ -200,27 +228,37 @@ class CTkTabview(CTkBaseClass):
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=self._apply_appearance_mode(self._bg_color))
for tab in self._tab_dict.values():
tab.configure(fg_color=self._apply_appearance_mode(self._bg_color),
bg_color=self._apply_appearance_mode(self._bg_color))
else:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
for tab in self._tab_dict.values():
tab.configure(fg_color=self._apply_appearance_mode(self._fg_color),
bg_color=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._border_color),
outline=self._apply_appearance_mode(self._border_color))
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._bg_color)) # configure bg color of tkinter.Frame, cuase canvas does not fill frame
tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._bg_color)) # configure bg color of tkinter.Frame, cause canvas does not fill frame
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
self._set_grid_segmented_button()
self._set_grid_current_tab()
self._set_grid_canvas()
self._configure_segmented_button_background_corners()
self._segmented_button.configure(corner_radius=self._corner_radius)
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
self._configure_segmented_button_background_corners()
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
@ -242,6 +280,10 @@ class CTkTabview(CTkBaseClass):
if "command" in kwargs:
self._command = kwargs.pop("command")
if "anchor" in kwargs:
self._anchor = kwargs.pop("anchor")
self._configure_grid()
self._set_grid_segmented_button()
if "state" in kwargs:
self._segmented_button.configure(state=kwargs.pop("state"))
@ -274,6 +316,8 @@ class CTkTabview(CTkBaseClass):
elif attribute_name == "command":
return self._command
elif attribute_name == "anchor":
return self._anchor
elif attribute_name == "state":
return self._segmented_button.cget(attribute_name)
@ -296,17 +340,16 @@ class CTkTabview(CTkBaseClass):
if len(self._tab_dict) == 0:
self._set_grid_segmented_button()
self._name_list.insert(index, name)
self._name_list.append(name)
self._tab_dict[name] = self._create_tab()
self._segmented_button.insert(index, name)
self._configure_tab_background_corners_by_name(name)
# if created tab is only tab select this tab
if len(self._tab_dict) == 1:
self._current_name = name
self._segmented_button.set(self._current_name)
self._grid_forget_all_tabs()
self._set_grid_tab_by_name(self._current_name)
self._set_grid_current_tab()
return self._tab_dict[name]
else:
@ -316,6 +359,10 @@ class CTkTabview(CTkBaseClass):
""" appends new tab with given name """
return self.insert(len(self._tab_dict), name)
def index(self, name) -> int:
""" get index of tab with given name """
return self._segmented_button.index(name)
def move(self, new_index: int, name: str):
if 0 <= new_index < len(self._name_list):
if name in self._tab_dict:
@ -325,6 +372,22 @@ class CTkTabview(CTkBaseClass):
else:
raise ValueError(f"CTkTabview new_index {new_index} not in range of name list with len {len(self._name_list)}")
def rename(self, old_name: str, new_name: str):
if new_name in self._name_list:
raise ValueError(f"new_name '{new_name}' already exists")
# segmented button
old_index = self._segmented_button.index(old_name)
self._segmented_button.delete(old_name)
self._segmented_button.insert(old_index, new_name)
# name list
self._name_list.remove(old_name)
self._name_list.append(new_name)
# tab dictionary
self._tab_dict[new_name] = self._tab_dict.pop(old_name)
def delete(self, name: str):
""" delete tab by name """
@ -344,7 +407,7 @@ class CTkTabview(CTkBaseClass):
self._current_name = self._name_list[0]
self._segmented_button.set(self._current_name)
self._grid_forget_all_tabs()
self._set_grid_tab_by_name(self._current_name)
self._set_grid_current_tab()
# more tabs are left
else:
@ -360,8 +423,8 @@ class CTkTabview(CTkBaseClass):
if name in self._tab_dict:
self._current_name = name
self._segmented_button.set(name)
self._grid_forget_all_tabs()
self._set_grid_tab_by_name(name)
self._set_grid_current_tab()
self.after(100, lambda: self._grid_forget_all_tabs(exclude_name=name))
else:
raise ValueError(f"CTkTabview has no tab named '{name}'")

View File

@ -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):

View File

@ -52,7 +52,10 @@ class CTkFont(Font):
def remove_size_configure_callback(self, callback: Callable):
""" remove function, that gets called when font got configured """
self._size_configure_callback_list.remove(callback)
try:
self._size_configure_callback_list.remove(callback)
except ValueError:
pass
def create_scaled_tuple(self, font_scaling: float) -> Tuple[str, int, str]:
""" return scaled tuple representation of font in the form (family: str, size: int, style: str)"""

View File

@ -4,6 +4,6 @@ from .theme_manager import ThemeManager
try:
ThemeManager.load_theme("blue")
except FileNotFoundError as err:
raise FileNotFoundError(f"{err}\n\nThe .json theme file for CustomTkinter could not be found.\n" +
raise FileNotFoundError(f"{err}\nThe .json theme file for CustomTkinter could not be found.\n" +
f"If packaging with pyinstaller was used, have a look at the wiki:\n" +
f"https://github.com/TomSchimansky/CustomTkinter/wiki/Packaging#windows-pyinstaller-auto-py-to-exe")

View File

@ -1,5 +1,6 @@
import sys
import os
import pathlib
import json
from typing import List, Union
@ -15,7 +16,8 @@ class ThemeManager:
script_directory = os.path.dirname(os.path.abspath(__file__))
if theme_name_or_path in cls._built_in_themes:
with open(os.path.join(script_directory, "../../../assets", "themes", f"{theme_name_or_path}.json"), "r") as f:
customtkinter_path = pathlib.Path(script_directory).parent.parent.parent
with open(os.path.join(customtkinter_path, "assets", "themes", f"{theme_name_or_path}.json"), "r") as f:
cls.theme = json.load(f)
else:
with open(theme_name_or_path, "r") as f:
@ -35,6 +37,12 @@ class ThemeManager:
else:
cls.theme[key] = cls.theme[key]["Linux"]
# fix name inconsistencies
if "CTkCheckbox" in cls.theme.keys():
cls.theme["CTkCheckBox"] = cls.theme.pop("CTkCheckbox")
if "CTkRadiobutton" in cls.theme.keys():
cls.theme["CTkRadioButton"] = cls.theme.pop("CTkRadiobutton")
@classmethod
def save_theme(cls):
if cls._currently_loaded_theme is not None:

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -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%")

View File

@ -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()

View File

@ -63,7 +63,7 @@ text_1.insert("0.0", "CTkTextbox\n\n\n\n")
segmented_button_1 = customtkinter.CTkSegmentedButton(master=frame_1, values=["CTkSegmentedButton", "Value 2"])
segmented_button_1.pack(pady=10, padx=10)
tabview_1 = customtkinter.CTkTabview(master=frame_1, width=200, height=70)
tabview_1 = customtkinter.CTkTabview(master=frame_1, width=300)
tabview_1.pack(pady=10, padx=10)
tabview_1.add("CTkTabview")
tabview_1.add("Tab 2")

View File

@ -3,11 +3,10 @@ requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[tool.tbump]
# Uncomment this if your project is hosted on GitHub:
github_url = "https://github.com/TomSchimansky/CustomTkinter"
[tool.tbump.version]
current = "5.0.4"
current = "5.2.0"
# Example of a semver regexp.
# Make sure this matches current_version before
@ -33,17 +32,3 @@ src = "setup.cfg"
[[tool.tbump.file]]
src = "customtkinter/__init__.py"
search = "__version__ = \"{current_version}\""
# You can specify a list of commands to
# run after the files have been patched
# and before the git commit is made
# [[tool.tbump.before_commit]]
# name = "check changelog"
# cmd = "grep -q {new_version} Changelog.rst"
# Or run some commands after the git tag and the branch
# have been pushed:
# [[tool.tbump.after_push]]
# name = "publish"
# cmd = "./publish.sh"

View File

@ -1,18 +1,23 @@
[metadata]
name = customtkinter
version = 5.0.4
version = 5.2.0
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 = A modern and customizable python UI-library based on Tkinter: https://customtkinter.tomschimansky.com
long_description_content_type = text/markdown
url = https://github.com/TomSchimansky/CustomTkinter
url = https://customtkinter.tomschimansky.com
author = Tom Schimansky
license = Creative Commons Zero v1.0 Universal
license_file = LICENSE
classifiers =
License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Programming Language :: Python :: 3 :: Only
[project.urls]
homepage = https://customtkinter.tomschimansky.com
documentation = https://customtkinter.tomschimansky.com/documentation
repository = https://github.com/tomschimansky/customtkinter
[options]
python_requires = >=3.7
packages =

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -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()