330 Commits

Author SHA1 Message Date
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
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
359226e468 Bump to 5.0.4 2023-01-21 13:43:54 +01:00
dc751e46d3 add example images 2023-01-21 13:43:18 +01:00
79d5da439b fix readonly background for combobox #983 2023-01-10 15:03:45 +01:00
fac2fa5e68 Merge remote-tracking branch 'origin/master' 2023-01-07 18:37:22 +01:00
2de1b94575 fixed dropdown_fg_color attribute in configure of CTkOptionMenu 2023-01-07 18:37:00 +01:00
a79502dc03 replaced sys.stderr with warnings.warn #932 2023-01-07 01:21:28 +01:00
8a537076ce added icon on Windows for CTkToplevel, fixed #960 2023-01-07 01:16:15 +01:00
1396a7e484 fixed #925 2022-12-25 21:03:33 +01:00
5bbd72b5dc fixed #941 2022-12-25 20:54:40 +01:00
84bfc776b0 Merge remote-tracking branch 'origin/master' 2022-12-10 13:40:29 +01:00
7f5ac69259 Bump to 5.0.3 2022-12-10 13:40:06 +01:00
90157252d0 added icons folder to MANIFEST.in 2022-12-10 13:39:33 +01:00
392586eaa1 removed macOS icon change 2022-12-10 13:38:27 +01:00
f3710de173 changed windows icon 2022-12-10 13:29:35 +01:00
9f8b54563d add icons 2022-12-10 13:17:55 +01:00
28228316eb fix image configure for button #807 2022-12-08 19:33:39 +01:00
61adb1da07 fix readme 2022-12-08 10:35:11 +01:00
042fac7242 fix image type hints #795 2022-12-08 10:34:41 +01:00
a49dde63b3 fix switch #801 2022-12-07 22:15:31 +01:00
f11d727879 fix combobox #800 2022-12-07 22:10:35 +01:00
77595da9f2 Bump to 5.0.2 2022-12-06 23:19:54 +01:00
d43229ef6e fixed long description in setup.cfg 2022-12-06 23:19:43 +01:00
f068cee972 test 2022-12-06 19:29:04 +01:00
62063d6f64 test 2022-12-06 19:28:34 +01:00
3d86b5a14f fix widget bind if clause error 2022-12-06 19:25:00 +01:00
5a17b1243e fixed background for entry readonly state #643 2022-12-06 19:16:05 +01:00
7572f095c2 cget now returns copy of lists 2022-12-06 18:47:39 +01:00
868b2a2f42 allow multiple style strings in font tuple #759 2022-12-06 18:29:09 +01:00
6a3fa7fa29 fixed support for python3.7 #737 2022-12-06 18:13:59 +01:00
a564bc35ef fixed progressbar start stop speed increase #775, fixed transparent textbox #779, fixed binding for all widgets #250 #374 #380 #477 #480 2022-12-06 18:09:20 +01:00
dd223a15b5 fixed progressbar start stop speed increase #775 2022-12-06 11:27:35 +01:00
2c7b2c5030 fixed radiobutton disabled command call bug #677, fixed key error for theme in scrollbar #711, removed bind_all and unbind_all from baseclass, added CTkCanvas and CTkBaseClass for top level import 2022-12-06 11:09:34 +01:00
f4af512290 Bump to 5.0.1 2022-12-01 10:06:54 +01:00
482a6e60b7 fix PIL Image import error 2022-12-01 09:33:13 +01:00
83dedea59c fixed changelog 2022-12-01 00:31:48 +01:00
d3d1469af5 Bump to 5.0.0 2022-12-01 00:09:02 +01:00
e7d1fc1b82 Merge branch 'develop'
# Conflicts:
#	Readme.md
2022-12-01 00:02:43 +01:00
4a9a6f41ad readme fix 2022-12-01 00:00:20 +01:00
03782aa5a4 readme fix 2022-11-30 23:12:37 +01:00
60c72a4289 added image_example image for readme 2022-11-30 23:09:42 +01:00
f3d90821ce finished image example 2022-11-30 22:59:55 +01:00
0d8a79d783 added update_idletasks call to base class set_appearance_mode 2022-11-30 22:03:42 +01:00
50a8bb140e fixed assets font path 2022-11-30 21:51:36 +01:00
bfbe4a4e5c added example test images 2022-11-30 21:35:13 +01:00
ae0c5a3397 created image example 2022-11-30 21:31:40 +01:00
f0bbc188a2 edit readme 2022-11-30 01:30:21 +01:00
f491270a3d edit readme 2022-11-30 01:28:42 +01:00
f2bf7f50e1 edit readme 2022-11-30 01:27:38 +01:00
5282b8ca3e edit readme 2022-11-30 01:21:01 +01:00
6d56b9a76f edit readme 2022-11-30 01:13:44 +01:00
9aee46af10 added tkintermapview video 2022-11-30 01:10:25 +01:00
61abcb2fea added readme videos 2022-11-30 00:53:58 +01:00
c5d3c490e7 added Windows video to Readme.md 2022-11-30 00:36:21 +01:00
88751659cb remove video from master 2022-11-30 00:35:44 +01:00
39b71f6bf9 added videos 2022-11-29 23:27:54 +01:00
6231f9b4ee Fixed Windows readme video 2022-11-29 23:26:21 +01:00
3879708622 Added Windows readme video 2022-11-29 23:24:14 +01:00
03249110f3 restructured imports from modules, created imports in __init__.py 2022-11-29 19:06:33 +01:00
1254a39161 removed default geometry call from CTk, fixed CTkInputDialog 2022-11-29 13:04:51 +01:00
1db775a6f6 remove image_example.py 2022-11-28 00:55:20 +01:00
7f56839b58 created image_example.py 2022-11-28 00:52:57 +01:00
83bcd57396 added example image 2022-11-28 00:50:56 +01:00
359ce2e060 added example image 2022-11-28 00:49:20 +01:00
be4537734c test fixes 2022-11-28 00:49:01 +01:00
15e594ff47 test fixes 2022-11-28 00:46:43 +01:00
dbd8968ee7 readme chnage images 2022-11-28 00:02:16 +01:00
3bfa532fa5 example enhancements 2022-11-27 23:51:42 +01:00
4e3ad8b118 Merge remote-tracking branch 'origin/develop' into develop 2022-11-27 23:18:19 +01:00
8b9f42c6eb example fixes 2022-11-27 23:18:10 +01:00
9098f27b07 change readme images 2022-11-27 23:02:40 +01:00
e13bd9b9cd change readme images 2022-11-27 22:56:25 +01:00
843ed8ac69 change readme images 2022-11-27 22:49:30 +01:00
b720a3d1be change readme images 2022-11-27 22:47:55 +01:00
72422dcafb change readme images 2022-11-27 22:38:03 +01:00
087b540493 change readme layout 2022-11-27 22:34:28 +01:00
06d65ef505 change readme images 2022-11-27 22:31:43 +01:00
968d25019a chnage readme 2022-11-27 22:30:40 +01:00
695abe05f4 chnage readme 2022-11-27 22:28:48 +01:00
1424eeac6c theme chnages 2022-11-27 21:33:12 +01:00
596b017992 changed theme files 2022-11-27 21:24:17 +01:00
f11caec099 changed theme colors 2022-11-27 21:10:40 +01:00
81e979b567 added save_theme to ThemeManager 2022-11-27 12:22:25 +01:00
01e64f38e8 changed theme file format 2022-11-27 02:48:09 +01:00
a921eb824d new test 2022-11-14 14:29:59 +01:00
fcfda7ed24 remove tabview colors in themes 2022-11-13 18:37:41 +01:00
b62543f332 changed tabview test 2022-11-13 17:10:04 +01:00
3f44877984 fixed small bugs 2022-11-12 23:17:37 +01:00
d306a9d010 fixed some bugs on Windows 2022-11-12 20:59:10 +01:00
2f27611f3e removed CTkFiledialog, add filedialog 2022-11-12 14:01:54 +01:00
e269091ffe fixed small bugs, add CTkFiledialog which is tkinter.filedialog 2022-11-12 13:28:16 +01:00
f302b045e3 fix input dialog for linux 2022-11-11 14:09:50 +01:00
779434dc29 added fix for empty value for segmented button 2022-11-11 13:08:30 +01:00
cf62427f66 fixed entry color when disabled 2022-11-11 13:06:26 +01:00
f18ac0c81a added .invoke() method to button #605 2022-11-11 00:32:52 +01:00
2c31f18dc1 renewed CTkInputDialog 2022-11-11 00:06:25 +01:00
cbbc9efda3 changed transparent color value from None to "transparent", fixed type hints, added color type and value checking 2022-11-10 23:27:55 +01:00
1387e834b5 fixed some configure bugs #590 2022-11-06 14:44:08 +01:00
cea48c3501 fixed some configure bugs #585 #584 #583 #580 #579 #578, added check_image method 2022-11-06 14:40:15 +01:00
62b330ddba added CTkImage, compound support for label 2022-11-03 13:48:31 +01:00
7374e7a3bc architecture fixes 2022-11-01 00:37:30 +01:00
302313916a enhanced inherit structure 2022-10-29 21:56:00 +02:00
c2a5b4881e changeed driectory structure, moved scaling and appearance mode functionality to super classes 2022-10-29 13:11:55 +02:00
e5484cb6cd renamed ThemeManage.single_color to _apply_appearance_mode and moved it to baseclass and windows 2022-10-29 01:20:32 +02:00
08a0835fd0 added new button grid system, fixed CTkSwitch grid positioning, moved ctk_canvas.py and draw_engine.py to widgets/core_rendering, created CTkImage 2022-10-29 00:42:33 +02:00
bf1835922b added new button grid system, fixed CTkSwitch grid positioning, moved ctk_canvas.py and draw_engine.py to widgets/core_rendering, 2022-10-23 22:33:57 +02:00
90d11e2f3f added missing arguments to configure method of combobox, fixed switch canvas positioning 2022-10-23 13:18:48 +02:00
db563b3511 removed spacing_scaling, now widget_scaling is used 2022-10-22 15:42:22 +02:00
6bf877eebc added mising configure argument for CTkButton #544 2022-10-22 15:24:15 +02:00
fc3a41d074 remove old complex example 2022-10-22 15:14:23 +02:00
7ea2edeef2 added CTkFont support to all widgets 2022-10-22 15:08:13 +02:00
25531cdf50 removed Settings class, moved settings to widgets and windows, added 'jusitfy' attribute to combobox and 'anchor' to optionmenu #528, added 'hover' attribute to all widgets and configure and cget methods, fixed missing attributes in configure 2022-10-22 14:24:04 +02:00
9d7eca7bb1 fixed focus bug of CTk and CTkToplevel on macOS 2022-10-22 01:20:08 +02:00
2288c255ac added CTkFont support for checkbox, combobox. Added padding to entry in combobox, combined grid commands for combobox in configure_grid_system method 2022-10-21 21:54:42 +02:00
42fb7f2186 finished CTkFont support for button, label, checkbox, created test_font.py 2022-10-21 21:28:31 +02:00
1ae794272b removed bg and background argument support from CTk and CTkToplevel 2022-10-16 20:13:19 +02:00
53b0d04e4b fix dropdown arrow size for scaling 2022-10-15 14:02:43 +02:00
a7b278cca0 fix checkmark recoloring after scaling 2022-10-15 13:58:51 +02:00
9ffe61dd54 added no_color_updates option for set_scaling methods, added transparency effect on Windows when window scaling, changed scaling loop times 2022-10-15 01:02:54 +02:00
205cdae5f9 added move() method to CTkTabview 2022-10-14 19:58:16 +02:00
103358d037 added move() method to CTkSegmentedButton 2022-10-14 19:49:18 +02:00
53c7e1a3c2 added configure and cget to CTkTabview 2022-10-14 19:36:52 +02:00
362fcbaf13 added additional force_focus call for CTk and CTkToplevel on macOS to fix bug where window disappears when window is selected from Mission Control 2022-10-14 19:26:16 +02:00
5977dcbaeb added _set_dimensions method to every widget, now called from CTkBaseClass, added separate width and height attributes for small canvas in CTkCheckbox, CTkRadioButton, CTkSwitch 2022-10-14 18:51:44 +02:00
eabfa67335 moved width and height configuring to CTkBaseClass 2022-10-14 12:52:07 +02:00
6ba384eb0b finished basic tabview mechanics 2022-10-14 01:15:35 +02:00
7abdf21021 decreased border with and padding for tkinter.Label in CTkButton, CTkLabek, CTkOptionMenu 2022-10-13 17:28:33 +02:00
466ba7747e worked on CTkTabview, fixed small issues 2022-10-10 00:48:08 +02:00
dbf5577cf0 removed print from CTkSegmentedButton 2022-10-08 13:50:11 +02:00
327957e97a finished CTkSegmentedButton, created test_segmented_button.py 2022-10-08 01:50:09 +02:00
1696016d54 Merge remote-tracking branch 'origin/develop' into develop 2022-10-07 17:04:14 +02:00
db58423253 fixed dropdown menu configuring 2022-10-07 16:54:46 +02:00
63195943b2 added master kwarg for all widgets and removed *args 2022-10-06 15:53:52 +02:00
4616561c13 added focus wrapper functions to all widgets 2022-10-05 21:11:46 +02:00
44323affa2 fixed textbox border spacing 2022-10-05 19:06:40 +02:00
4e20fb01d7 updated changelog 2022-10-05 18:51:42 +02:00
0cbec6fea1 finished textbox, combined CTkTextbox and CTkScrolledTextbox into one class, enhanced test_text.py 2022-10-05 18:39:45 +02:00
14b39aa7b4 added CTkScrolledTextbox 2022-10-05 10:35:41 +02:00
35265fb675 added all textbox methods, fixed input dialog geometry grid 2022-10-04 20:17:56 +02:00
2964b39410 enhanced bind and unbind with double bind call on two tkinter widgets for some CTk widgets 2022-10-04 16:55:34 +02:00
8b85225133 fixed typo in all unbind methods 2022-10-04 03:16:59 +02:00
40474d6905 added unbind method for all widgets, fixed bind methods, fixed textbox configure 2022-10-04 03:03:43 +02:00
06332a1061 readme fix 2022-10-04 02:29:02 +02:00
12f0942e73 added bind method to all widgets, added kwargs managing for CTkLabel 2022-10-04 02:26:29 +02:00
b953464038 updated changelog 2022-10-04 00:28:03 +02:00
92873373a1 updated changelog 2022-10-04 00:21:36 +02:00
68bdcafb26 updated changelog 2022-10-04 00:15:01 +02:00
18cdf9daa1 updated changelog 2022-10-04 00:13:37 +02:00
1374e04f04 removed .config(), added kwargs managing and filtering, added kwargs exceptions, fixed cursor color for combobox 2022-10-03 23:50:59 +02:00
bfc42c25ef Added .cget() methods to all widgets and changed text_font to font for all widgets 2022-10-03 00:33:06 +02:00
8b6f9a1d7e added .config method to all widgets 2022-10-02 03:59:54 +02:00
d103fc42f1 added cget method to base class and button #395 2022-10-02 03:53:27 +02:00
d6075ad544 added type hints and private hints to all widget classes, fixed #497, removed get and set methods from some widgets 2022-10-02 03:23:10 +02:00
5717fc68e2 Merge remote-tracking branch 'origin/master' 2022-09-21 09:36:09 +02:00
77adb7d024 fixed resizable call for CTkToplevel shortly after toplevel creation 2022-09-21 09:35:42 +02:00
fb600c9811 fixed resizable call for CTkToplevel shortly after toplevel creation 2022-09-21 09:35:21 +02:00
31c0afe15b Bump to 4.6.3 2022-09-17 13:41:58 +02:00
dab93a6a5e Fixed variable configuring for CTkOptionMennu 2022-09-17 13:39:22 +02:00
89fa0a8758 Bump to 4.6.2 2022-09-17 12:47:43 +02:00
67ab575b78 changed CTkCheckBox command to be only triggered by .toggle() 2022-09-17 12:41:45 +02:00
b350224f06 Bump to 4.6.1 2022-09-17 01:21:00 +02:00
7dd945adbb revert linux font path to ~/.fonts/ 2022-09-17 01:20:33 +02:00
3f156f5648 Bump to 4.6.0 2022-09-17 00:55:31 +02:00
8fba7b4481 update CHANGELOG.md 2022-09-17 00:54:32 +02:00
ee85b27271 fixed CTkProgressBar intermediate mode rendering on Windows #115 2022-09-17 00:52:20 +02:00
5204683df4 updated command calls of CTkSwitch, now commmand is called for .toggle() only 2022-09-17 00:39:27 +02:00
98c4c669a6 added indeterminate mode to CTkProgressBar 2022-09-17 00:18:31 +02:00
078918e77b changed methods for command callback for optionmenu and combobox 2022-09-15 23:55:55 +02:00
c16c891115 changed combobox and optionemnu command to only get triggered by manual selection #440 2022-09-15 18:46:24 +02:00
66f9fa2386 updated Readme.md 2022-09-15 16:20:20 +02:00
e52b5fb799 updated Readme.md 2022-09-15 16:20:05 +02:00
6aac63d851 updated Readme.md 2022-09-15 16:18:56 +02:00
d7dda9cb39 updated Readme.md 2022-09-15 16:17:49 +02:00
39f369a8d4 Bump to 4.5.11 2022-09-15 13:59:58 +02:00
423d0886c9 enhanced geometry test 2022-09-15 13:54:32 +02:00
d2f8fd012f added zoom and appearance mode behavior tests for CTkToplevel #66 2022-09-15 13:48:32 +02:00
dcde8d69d8 added CTk zoom behavior test 2022-09-15 13:39:03 +02:00
81f3f9a622 added tests for CTk window behavior when switchng appearance mode and hiding at program start #66 #277 2022-09-15 13:28:02 +02:00
65c45abe32 changed linux font directory to ~/.local/share/fonts/ #340 2022-09-15 12:31:43 +02:00
64c8b8345d fixed bug when configuring place_holder in CTkEntry widget #330 2022-09-13 21:47:50 +02:00
9a144bfc6b small fixes 2022-08-19 00:18:01 +02:00
d9db3b64af fixed withdraw and iconify functionality for CTk and CTkToplevel #277 #305 #302 2022-08-19 00:13:00 +02:00
2db46afaf0 fixed withdraw and iconify functionality for CTk window before mainloop or update #277 #305 #302 2022-08-16 18:14:30 +02:00
8c9183006c added text_font configuration for all CTk widgets #266 2022-08-16 14:05:15 +02:00
f39ee5764a fixed simple_example.py 2022-08-06 01:12:22 +02:00
69216469a4 refactored example_button_images.py to class structure 2022-08-06 00:55:47 +02:00
d890d243a5 Merge remote-tracking branch 'origin/master' 2022-08-05 20:38:10 +02:00
deebaa9163 enhanced geometry string parsing for CTk and CTkToplevel #345 #287 2022-08-05 20:38:05 +02:00
5a4c28b178 removed print from CTk class 2022-08-05 16:08:46 +02:00
73ab410a96 Merge remote-tracking branch 'origin/master' 2022-08-05 15:37:42 +02:00
9bdf2436f5 CTk scaling fixes for Windows 2022-08-05 15:37:23 +02:00
91efc0ffc1 Merge pull request #352 from splewdge/master
Fix for issue #351
2022-08-05 08:49:38 -04:00
99550ab7fd Merge remote-tracking branch 'origin/master' 2022-08-05 14:23:22 +02:00
46b20d6605 fixed bug in CTkBaseClass #354 2022-08-05 14:23:06 +02:00
013e186ca6 Update ctk_button.py 2022-08-04 12:27:27 +01:00
6df8a1f44a Merge pull request #347 from felipetesc/master
white version of sweetkind
2022-08-02 14:16:30 -04:00
FTE
4516a5edb1 added white version of sweetkind
added white version of sweetkind
2022-08-02 14:51:28 -03:00
FTE
bd19d2f3e6 added white variant 2022-08-02 14:48:31 -03:00
ec8cecb575 fixed window closing returning None of CTkInputDialog 2022-07-25 11:25:48 +02:00
156a1863f5 Bump to 4.5.10 2022-07-23 20:03:59 +02:00
e295674e00 fixed bug in CTkLabel with multiline strings 2022-07-23 19:09:53 +02:00
36702326fa fixed checkbox size bug when rescaling 2022-07-18 13:02:39 +02:00
3bee19f8ce Bump to 4.5.9 2022-07-18 12:34:43 +02:00
ac6fb661a4 removed print from CTk 2022-07-18 12:34:27 +02:00
1a57294ae9 Bump to 4.5.8 2022-07-17 21:45:45 +02:00
ddd49377d4 fixed geomtry method for CTk and CTkToplevel 2022-07-17 21:45:29 +02:00
6bfddda399 Bump to 4.5.7 2022-07-17 21:41:28 +02:00
67f2072e07 added return value of geometry method for CTk and CTkToplevel 2022-07-17 21:41:13 +02:00
b3c0388958 added corner radius to CTkLabel in complex_example.py 2022-07-17 20:37:24 +02:00
228729305b Bump to 4.5.6 2022-07-17 20:35:12 +02:00
d9ff3d998c fixed command function bug in CTkSwitch 2022-07-17 20:34:54 +02:00
6a43dfd9bf added corner_radius to .configure() of CTkButton 2022-07-14 13:57:02 +02:00
db4f5ec919 Bump to 4.5.5 2022-07-12 21:57:50 +02:00
78f4e1e2ee Merge remote-tracking branch 'origin/master' into develop 2022-07-12 21:54:29 +02:00
acaeceb96d added methods to CTkTextbox 2022-07-12 21:52:52 +02:00
be126c70ae Merge pull request #247 from Ripeey/Theme-Scrollbar-Patch
Add scrollbar default values in .json theme files
2022-07-10 15:10:43 -04:00
d45904b1e4 Update missing scrollbar configs in green and sweetkind themes. 2022-07-11 04:57:39 +05:30
4fbcce75a0 Update dark-blue.json 2022-07-10 23:19:14 +05:30
92de2c4183 fixed CTkButton fg_color type hint 2022-07-07 18:23:50 +02:00
1c5c3450f9 Bump to 4.5.4 2022-07-07 18:22:39 +02:00
c95c0b7050 added readonly state to CTkComboBox 2022-07-07 18:19:23 +02:00
de33629e7d fixed entry placeholder for textvariables and added test_entry_placeholder.py 2022-07-07 18:07:54 +02:00
162997c7da added type hints to CTkButton 2022-07-07 16:21:30 +02:00
767379462e enhanced .configure() process for all widgets 2022-07-07 16:02:51 +02:00
a2fcb5dee1 fixed variables for CTkCheckbox, CTkSwitch, CTkRadiobutton 2022-07-05 14:39:12 +02:00
039cb1d17c Merge pull request #157 from mohsen1365ir/master
Enhanced checkbox functionality
2022-07-02 08:16:00 -04:00
f9890ba3e9 Merge branch 'master' into master 2022-07-02 08:14:20 -04:00
cdaf8f5f5c changed default CTkLabel corner_radius for better positioning 2022-07-02 14:10:41 +02:00
7e8bbf2968 Bump to 4.5.3 2022-07-02 01:11:54 +02:00
a3fb12f7cf Fixed textvariable support for CTkCheckBox, CTkSwitch, CTkRadiobutton 2022-07-02 00:54:21 +02:00
bb6678ae15 added support for anchor attribute in CTkLabel 2022-07-01 22:09:45 +02:00
b30692d1af Bump to 4.5.2 2022-07-01 21:39:19 +02:00
21448d3a07 Merge branch 'master' into develop 2022-07-01 21:38:51 +02:00
1f030f04f9 fixed placeholder_text bug for CTkEntry 2022-07-01 21:30:31 +02:00
c9653e7793 fixed updating of bg_color with set_scaling in CTkBaseClass 2022-06-30 15:53:32 +02:00
f587109618 Bump to 4.5.1 2022-06-28 11:33:21 +02:00
8bfd763786 Merge branch 'master' into develop 2022-06-28 11:30:58 +02:00
2a0ae06426 made CTkBaseClass public 2022-06-28 11:28:11 +02:00
16b9ce3c5f added .focus() for CTkEntry 2022-06-28 11:26:43 +02:00
e15bc5933d fixed scaling issues for combobox and optionemnu, chatched error in appearance_mode_tracker 2022-06-28 11:16:28 +02:00
11c7363d28 updated CHANGELOG.md 2022-06-23 22:28:29 +02:00
d4d0cf1188 Merge branch 'develop'
# Conflicts:
#	customtkinter/__init__.py
#	pyproject.toml
#	setup.cfg
2022-06-23 22:22:11 +02:00
b891032e2e Bump to 4.5.0 2022-06-23 22:18:06 +02:00
28308065bc fixed horizontal scrollbar for Windows 2022-06-23 22:14:19 +02:00
d4ae8cab7d fixed optionmenu and combobox bugs 2022-06-23 21:05:44 +02:00
ec3fdc40ff fixed scrollbar for horizontal orientation 2022-06-23 17:41:12 +02:00
a7b175ae65 changed scrollbar colors, added custom exception message for pyinstaller file problem 2022-06-20 23:44:35 +02:00
43900c7fef fixed scrollbar for Windows 2022-06-19 22:12:19 +02:00
79ecd2e946 added CTkScrollbar 2022-06-19 21:16:19 +02:00
22b4dfb2d3 Bump to 4.4.1 2022-06-17 21:13:30 +02:00
9146e02718 updated CHANGELOG.md 2022-06-17 21:10:15 +02:00
3b259e4d01 refined dropdown word spacing for linux 2022-06-16 16:55:17 +02:00
7a99aa318c fixed disabled color for combobox 2022-06-15 18:31:20 -04:00
9ff6cc8268 optimized char mapping on canvas for Linux 2022-06-15 18:15:24 -04:00
3a1d12f8ea removed old dropdown menu 2022-06-15 23:26:21 +02:00
45e47f5970 fixed dropdown menu bug on Linux 2022-06-15 14:51:24 +02:00
0e510dec53 Bump to 4.4.0 2022-06-15 02:08:28 +02:00
fc952294f0 chnaged complex_example.py and fixed command-variable execution order in CTkSlider 2022-06-15 02:07:51 +02:00
d8b5104028 adopted new dropdown menu for combobox 2022-06-15 01:24:02 +02:00
20e16969f2 fixed dropdown_menu_fallback.py for Linux 2022-06-14 18:31:10 -04:00
a86dbd4d07 fixed dropdown_menu_fallback.py for macOS 2022-06-15 00:14:35 +02:00
413cedd093 refined dropdown_menu_fallback.py 2022-06-14 23:58:21 +02:00
91e7e3077c enhanced dropdown_menu_fallback.py 2022-06-13 15:08:13 +02:00
4cf6a9f5c9 ctk_radiobutton command blocked if disabled 2022-06-08 07:24:24 -04:00
6e36ec818e checkbox.check_state initialization fixed 2022-06-08 07:11:52 -04:00
0f7cb22b1b Enhanced checkbox functionality 2022-06-05 06:32:11 -04:00
9c479bc1de Bump to 4.3.0 2022-06-02 01:25:21 +02:00
ecf6b8d9cf added combobox values to themes 2022-06-02 01:25:13 +02:00
807064a888 ComboBox and DropDown fixes for Windows 2022-06-02 00:15:24 +02:00
550653c6c3 small fixes for CTkComboBox 2022-06-01 23:50:50 +02:00
0aa9dfc70f removed combobox from simple_example.py 2022-05-31 23:16:13 +02:00
a9f51f1aa1 fixed CTkComboBox for Windows 2022-05-31 22:44:22 +02:00
2746e2a05f added CTkComboBox 2022-05-31 22:32:21 +02:00
f5fdd77584 Bump to 4.2.0 2022-05-30 17:30:27 +02:00
940ed128bd fixed geometry string scaling 2022-05-30 17:20:27 +02:00
60b13bf215 fixes for DropdownMenu 2022-05-30 16:46:36 +02:00
a50e2ea9ca added test_optionmenu.py 2022-05-30 15:48:41 +02:00
15558b4d0f added dropdown arrow with font files 2022-05-30 15:35:23 +02:00
aa46c56da9 small fixes in examples 2022-05-30 14:35:33 +02:00
cf6f513afc added click support for labels of CTkCheckBox, CTkRadioButton, CTkSwitch 2022-05-30 13:39:10 +02:00
9d618386e1 fixed scaling for DropdownMenu 2022-05-27 01:09:54 +02:00
9bd55cc159 implemented overwrite_preferred_drawing_method parameter in DrawEngine 2022-05-27 00:30:06 +02:00
8a87b6f926 updated color for blue theme 2022-05-27 00:12:37 +02:00
a1afc3056b removed scaling with ctypes shcore for Windows < 8.1 2022-05-26 19:16:01 +02:00
34da9505e9 DropDown fixes for Windows 2022-05-25 22:40:30 +02:00
91a8687736 added DropdownMenu and CTkOptionMenu 2022-05-25 22:14:38 +02:00
e96165d212 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	customtkinter/draw_engine.py
2022-05-25 18:42:42 +02:00
aa8c96a2c4 added overwrite_preferred_drawing_method parameter to DrawEngine 2022-05-25 18:40:07 +02:00
fd8135129c Merge remote-tracking branch 'origin/master'
# Conflicts:
#	test/manual_integration_tests/test_new_menu_design.py
2022-05-25 18:38:49 +02:00
5f88db11aa updated test_new_menu_design.py 2022-05-25 18:37:55 +02:00
1fed35a193 added draw_rounded_rect_with_border_vertical_split() function to DrawEngine 2022-05-25 17:04:00 +02:00
4b3b406250 updated test_new_menu_design.py 2022-05-24 13:50:34 +02:00
f49c83d2dc Bump to 4.1.0 2022-05-24 01:03:14 +02:00
4389c3e86b added configurable dimensions to some widgets 2022-05-24 01:00:58 +02:00
25297c2598 Bump to 4.0.4 2022-05-23 22:35:57 +02:00
4e155aedd6 fixed bug in fg_color detection of master 2022-05-23 22:35:38 +02:00
3a5d34cef6 Merge pull request #106 from bengy3d/linux-font-hotfix
Fixed loading fonts on linux
2022-05-23 16:27:01 +02:00
e42db49ca5 Fixed loading fonts on linux 2022-05-23 16:16:09 +02:00
a7c0fc2a3c Bump to 4.0.3 2022-05-23 11:01:38 +02:00
9be2a76b25 set minimum python version to 3.7 2022-05-23 11:01:29 +02:00
b1ac3b6d45 Merge pull request #102 from demberto/issue101-fix
Fixes #101
2022-05-23 10:42:32 +02:00
a6b563abb1 Fixes #101 2022-05-23 11:21:09 +05:30
161 changed files with 11393 additions and 4809 deletions

View File

@ -1,17 +1,79 @@
# Changelog
All notable changes to this project will be documented in this file.
All notable changes to this project will be documented in this file!
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
ToDo:
- cursor configuring
- overwrite winfo methods
- set icon (self.call("wm", "iconphoto", self._w, tkinter.PhotoImage(file="test_images/CustomTkinter_logo_single.png")))
- add option to change label position for checkbox, switch, radiobutton #628
## [5.0.0] - 2022-11-13
### Added
- Added CTkTextbox with automatic x and y scrollbars, corner_radius, border_width, border_spacing
- Added CTkSegmentedButton
- Added CTkTabview
- Added .cget() method to all widgets and windows
- Added .bind() and .focus() methods to almost all widgets
- Added 'anchor' option to CTkButton to position image and text inside the button
- Added 'anchor' option to CTkOptionMenu and 'justify' option to CTkComboBox
- Added CTkFont class
- Added CTkImage class to replace PIL.ImageTk.PhotoImage, supports scaling and two images for appearance mode, supports configuring
- Added missing configure options for multiple widgets
### Changed
- Changed value for transparent colors (same as background) from None to 'transparent'
- Changed 'text_font' attribute to 'font' in all widgets, changed 'dropdown_text_font' to 'dropdown_font'
- Changed 'dropdown_color' attribute to 'dropdown_fg_color' for combobox, optionmenu
- Changed 'orient' attribute of CTkProgressBar and CTkSlider to 'orientation'
- Width and height attributes of CTkCheckBox, CTkRadioButton, CTkSwitch now describe the outer dimensions of the whole widget. The button/switch size is described by separate attributes like checkbox_width, checkbox_height
- font attribute must be tuple or CTkFont now, all size values are measured in pixel now
- Changed dictionary key 'window_bg_color' to 'window' in theme files
- CTkInputDialog attributes completely changed
- CTkScrollbar attributes scrollbar_color, scrollbar_hover_color changed to button_color, button_hover_color
### Removed
- Removed setter and getter functions like set_text in CTkButton
- Removed bg and background attribute from CTk and CTkToplevel, always use fg_color
- Removed Settings class and moved settings to widget and window classes
- removed customtkinter.set_spacing_scaling(), now set_widget_scaling() is used for spacing too
## [4.6.0] - 2022-09-17
### Added
- CTkProgressBar indeterminate mode, automatic progress loop with .start() and .stop()
## [4.5.0] - 2022-06-23
### Added
- CTkScrollbar (vertical, horizontal)
## [4.4.0] - 2022-06-14
### Changed
- Changed custom dropdown menu to normal tkinter.Menu because of multiple platform specific bugs
## [4.3.0] - 2022-06-1
### Added
- Added CTkComboBox
- Small fixes for new dropdown menu
## [4.2.0] - 2022-05-30
### Added
- CTkOptionMenu with custom dropdown menu
- Support for clicking on labels of CTkCheckBox, CTkRadioButton, CTkSwitch
## [4.1.0] - 2022-05-24
### Added
- Configure width and height for frame, button, label, progressbar, slider, entry
## [4.0.0] - 2022-05-22
### Added
- This changelog file
- Adopted semantic versioning
- Added HighDPI scaling to all widgets and geometry managers (place, pack, grid)
- Restructured CTkSettings and renamed a few manager classes
### Changed
- Orientation attribute for slider and progressbar
### Removed
- A few unnecessary tests

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

@ -1,4 +1,5 @@
include customtkinter/assets/*
include customtkinter/assets/fonts/*
include customtkinter/assets/fonts/Roboto/*
include customtkinter/assets/themes/*
include customtkinter/assets/icons/*
include customtkinter/assets/themes/*

View File

@ -1,24 +1,43 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./documentation_images/CustomTkinter_logo_dark.png">
<img src="./documentation_images/CustomTkinter_logo_light.png">
</picture>
</p>
<div align="center">
![PyPI](https://img.shields.io/pypi/v/customtkinter)
![PyPI - Downloads](https://img.shields.io/pypi/dm/customtkinter?color=green&label=pip%20downloads)
![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)
![Total lines](https://img.shields.io/tokei/lines/github.com/tomschimansky/customtkinter?color=green&label=total%20lines)
![](https://tokei.rs/b1/github/tomschimansky/customtkinter)
# CustomTkinter UI-Library
</div>
![](documentation_images/Windows_dark.png)
| _`complex_example.py` on Windows 11 with dark mode and 'dark-blue' theme_
---
![](documentation_images/macOS_light.png)
| _`complex_example.py` on macOS in light mode and standard 'blue' theme_
###
<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
and the window colors either adapt to the system appearance or the manually set mode
('light', 'dark'). With CustomTkinter you'll get a consistent and modern look across all
('light', 'dark'), and all CustomTkinter widgets and windows support HighDPI scaling
(Windows, macOS). With CustomTkinter you'll get a consistent and modern look across all
desktop platforms (Windows, macOS, Linux).
![](documentation_images/complex_example_dark_Windows.png)
| _`complex_example.py` on Windows 11 with dark mode and 'blue' theme_
![](documentation_images/complex_example_light_macOS.png)
| _`complex_example.py` on macOS in light mode and standard 'blue' theme_
###
## Installation
Install the module with pip:
@ -30,7 +49,9 @@ pip3 install customtkinter
## Documentation
A detailed documentation can be found in the Wiki Tab here: **[Documentation](https://github.com/TomSchimansky/CustomTkinter/wiki)**.
The **official** documentation can be found here:
**➡️ https://customtkinter.tomschimansky.com/documentation**.
## Example Program
To test customtkinter you can try this simple example with only a single button:
@ -53,31 +74,31 @@ button.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
app.mainloop()
```
which gives the following (macOS dark mode on):
which results in the following window on macOS:
![](documentation_images/macOS_button_dark.png)
<img src="documentation_images/single_button_macOS.png" width="400"/>
In the [examples folder](https://github.com/TomSchimansky/CustomTkinter/tree/master/examples), you
can find more example programs and in the [Documentation](https://github.com/TomSchimansky/CustomTkinter/wiki)
you can find further information on the appearance mode, the themes and all widgets.
you can find further information on the appearance mode, scaling, themes and all widgets.
## More Examples and Showcase
### Appearance mode change
### Appearance mode change and scaling change
On Windows 10/11 you get a dark window header, which changes with set
appearance mode or the system, when you use `customtkinter.CTk()`
to create the window, and it works with all python versions:
CustomTkinter can adapt to the Windows 10/11 light or dark mode:
![](documentation_images/Windows_system_mode_change.gif)
| _`complex_example.py` on Windows 11 with system mode change and standard 'blue' theme_
https://user-images.githubusercontent.com/66446067/204672968-6584f360-4c52-434f-9c16-25761341368b.mp4
| _`complex_example.py` on Windows 11 with system appearance mode change and standard 'blue' theme_
###
On macOS however you either need python3.10 or higher or the anaconda python
version to get a dark window header at all (Tcl/Tk >= 8.6.9 required).
On macOS you either need python3.10 or higher or the anaconda python
version to get a dark window header (Tcl/Tk >= 8.6.9 required):
![](documentation_images/macOS_system_mode_change.gif)
| _`complex_example.py` on macOS with system mode change and standard 'blue' theme_
https://user-images.githubusercontent.com/66446067/204673854-b6cbcfda-d9a1-4425-92a3-5b57d7f2fd6b.mp4
| _`complex_example.py` on macOS with system appearance mode change, user-scaling change and standard 'blue' theme_
###
### Button with images
@ -86,17 +107,24 @@ pass a PhotoImage object to the CTkButton with the ``image`` argument.
If you want no text at all you have to set ``text=""`` or you specify
how to position the text and image at once with the ``compound`` option:
![](documentation_images/macOS_button_images.png)
| _`example_button_images.py` on macOS_
![](documentation_images/image_example_dark_Windows.png)
| _`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
OpenStreetMap or other tile based maps:
![](documentation_images/tkintermapview_example.gif)
| _`examples/map_with_customtkinter.py` from TkinterMapView repository on macOS_
https://user-images.githubusercontent.com/66446067/204675835-1584a8da-5acc-4993-b4a9-e70f06fa14b0.mp4
You can find the TkinterMapView library and the example program here:
| _`examples/map_with_customtkinter.py` from TkinterMapView repository on Windows 11_
You can find the TkinterMapView library and example program here:
https://github.com/TomSchimansky/TkinterMapView

View File

View File

@ -1,57 +1,52 @@
__version__ = "4.0.2"
__version__ = "5.1.3"
import os
import sys
from tkinter import Variable, StringVar, IntVar, DoubleVar, BooleanVar
from tkinter.constants import *
import tkinter.filedialog as filedialog
# import manager classes
from .settings import Settings
from .appearance_mode_tracker import AppearanceModeTracker
from .theme_manager import ThemeManager
from .scaling_tracker import ScalingTracker
from .font_manager import FontManager
from .draw_engine import DrawEngine
from .windows.widgets.appearance_mode import AppearanceModeTracker
from .windows.widgets.font import FontManager
from .windows.widgets.scaling import ScalingTracker
from .windows.widgets.theme import ThemeManager
from .windows.widgets.core_rendering import DrawEngine
AppearanceModeTracker.init_appearance_mode()
ThemeManager.load_theme("blue") # load default theme
FontManager.init_font_manager()
# determine draw method based on current platform
if sys.platform == "darwin":
DrawEngine.preferred_drawing_method = "polygon_shapes"
else:
DrawEngine.preferred_drawing_method = "font_shapes"
# load Roboto fonts (used on Windows/Linux)
script_directory = os.path.dirname(os.path.abspath(__file__))
FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf"))
FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Medium.ttf"))
# load font necessary for rendering the widgets (used on Windows/Linux)
if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "CustomTkinter_shapes_font-fine.otf")) is False:
# change draw method if font loading failed
if DrawEngine.preferred_drawing_method == "font_shapes":
sys.stderr.write("customtkinter.__init__ warning: " +
"Preferred drawing method 'font_shapes' can not be used because the font file could not be loaded.\n" +
"Using 'circle_shapes' instead. The rendering quality will be bad!")
DrawEngine.preferred_drawing_method = "circle_shapes"
# import base widgets
from .windows.widgets.core_rendering import CTkCanvas
from .windows.widgets.core_widget_classes import CTkBaseClass
# import widgets
from .widgets.ctk_button import CTkButton
from .widgets.ctk_checkbox import CTkCheckBox
from .widgets.ctk_entry import CTkEntry
from .widgets.ctk_slider import CTkSlider
from .widgets.ctk_frame import CTkFrame
from .widgets.ctk_progressbar import CTkProgressBar
from .widgets.ctk_label import CTkLabel
from .widgets.ctk_radiobutton import CTkRadioButton
from .widgets.ctk_canvas import CTkCanvas
from .widgets.ctk_switch import CTkSwitch
from .windows.widgets import CTkButton
from .windows.widgets import CTkCheckBox
from .windows.widgets import CTkComboBox
from .windows.widgets import CTkEntry
from .windows.widgets import CTkFrame
from .windows.widgets import CTkLabel
from .windows.widgets import CTkOptionMenu
from .windows.widgets import CTkProgressBar
from .windows.widgets import CTkRadioButton
from .windows.widgets import CTkScrollbar
from .windows.widgets import CTkSegmentedButton
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.ctk_tk import CTk
from .windows.ctk_toplevel import CTkToplevel
from .windows.ctk_input_dialog import CTkInputDialog
from .windows import CTk
from .windows import CTkToplevel
from .windows import CTkInputDialog
# import font classes
from .windows.widgets.font import CTkFont
# import image classes
from .windows.widgets.image import CTkImage
_ = Variable, StringVar, IntVar, DoubleVar, BooleanVar, CENTER, filedialog # prevent IDE from removing unused imports
def set_appearance_mode(mode_string: str):
@ -77,11 +72,6 @@ def set_widget_scaling(scaling_value: float):
ScalingTracker.set_widget_scaling(scaling_value)
def set_spacing_scaling(scaling_value: float):
""" set scaling for geometry manager calls (place, pack, grid)"""
ScalingTracker.set_spacing_scaling(scaling_value)
def set_window_scaling(scaling_value: float):
""" set scaling for window dimensions """
ScalingTracker.set_window_scaling(scaling_value)
@ -89,4 +79,4 @@ def set_window_scaling(scaling_value: float):
def deactivate_automatic_dpi_awareness():
""" deactivate DPI awareness of current process (windll.shcore.SetProcessDpiAwareness(0)) """
ScalingTracker.deactivate_automatic_dpi_awareness = False
ScalingTracker.deactivate_automatic_dpi_awareness = True

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,68 +1,155 @@
{
"color": {
"window_bg_color": ["gray95", "gray12"],
"button":["#5B97D3", "#3373B8"],
"button_hover": ["#4A7BAD", "#1D538D"],
"button_border": ["gray40", "#D5D9DE"],
"checkbox_border": ["gray40", "#D5D9DE"],
"checkmark": ["white", "gray90"],
"entry": ["white", "gray24"],
"entry_border": ["gray70", "gray32"],
"entry_placeholder_text": ["gray52", "gray62"],
"frame_border": ["#A7C2E0", "#5FB4DD"],
"frame_low": ["#E3E4E5", "gray16"],
"frame_high": ["#D7D8D9", "gray22"],
"label": [null, null],
"text": ["gray20", "#D5D9DE"],
"text_disabled": ["gray60", "#777B80"],
"text_button_disabled": ["gray40", "gray74"],
"progressbar": ["#6B6B6B", "gray0"],
"progressbar_progress": ["#5B97D3", "#3373B8"],
"progressbar_border": ["gray", "gray"],
"slider": ["#6B6B6B", "gray0"],
"slider_progress": ["white", "gray40"],
"slider_button": ["#5B97D3", "#3373B8"],
"slider_button_hover": ["#4A7BAD", "#1D538D"],
"switch": ["gray70", "gray35"],
"switch_progress": ["#5B97D3", "#3373B8"],
"switch_button": ["gray36", "#D5D9DE"],
"switch_button_hover": ["gray20", "gray100"]
"CTk": {
"fg_color": ["gray92", "gray14"]
},
"text": {
"CTkToplevel": {
"fg_color": ["gray92", "gray14"]
},
"CTkFrame": {
"corner_radius": 6,
"border_width": 0,
"fg_color": ["gray86", "gray17"],
"top_fg_color": ["gray81", "gray20"],
"border_color": ["gray65", "gray28"]
},
"CTkButton": {
"corner_radius": 6,
"border_width": 0,
"fg_color": ["#3B8ED0", "#1F6AA5"],
"hover_color": ["#36719F", "#144870"],
"border_color": ["#3E454A", "#949A9F"],
"text_color": ["#DCE4EE", "#DCE4EE"],
"text_color_disabled": ["gray74", "gray60"]
},
"CTkLabel": {
"corner_radius": 0,
"fg_color": "transparent",
"text_color": ["gray10", "#DCE4EE"]
},
"CTkEntry": {
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#F9F9FA", "#343638"],
"border_color": ["#979DA2", "#565B5E"],
"text_color":["gray10", "#DCE4EE"],
"placeholder_text_color": ["gray52", "gray62"]
},
"CTkCheckbox": {
"corner_radius": 6,
"border_width": 3,
"fg_color": ["#3B8ED0", "#1F6AA5"],
"border_color": ["#3E454A", "#949A9F"],
"hover_color": ["#3B8ED0", "#1F6AA5"],
"checkmark_color": ["#DCE4EE", "gray90"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkSwitch": {
"corner_radius": 1000,
"border_width": 3,
"button_length": 0,
"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": {
"corner_radius": 1000,
"border_width_checked": 6,
"border_width_unchecked": 3,
"fg_color": ["#3B8ED0", "#1F6AA5"],
"border_color": ["#3E454A", "#949A9F"],
"hover_color": ["#36719F", "#144870"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkProgressBar": {
"corner_radius": 1000,
"border_width": 0,
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["#3B8ED0", "#1F6AA5"],
"border_color": ["gray", "gray"]
},
"CTkSlider": {
"corner_radius": 1000,
"button_corner_radius": 1000,
"border_width": 6,
"button_length": 0,
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["gray40", "#AAB0B5"],
"button_color": ["#3B8ED0", "#1F6AA5"],
"button_hover_color": ["#36719F", "#144870"]
},
"CTkOptionMenu": {
"corner_radius": 6,
"fg_color": ["#3B8ED0", "#1F6AA5"],
"button_color": ["#36719F", "#144870"],
"button_hover_color": ["#27577D", "#203A4F"],
"text_color": ["#DCE4EE", "#DCE4EE"],
"text_color_disabled": ["gray74", "gray60"]
},
"CTkComboBox": {
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#F9F9FA", "#343638"],
"border_color": ["#979DA2", "#565B5E"],
"button_color": ["#979DA2", "#565B5E"],
"button_hover_color": ["#6E7174", "#7A848D"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray50", "gray45"]
},
"CTkScrollbar": {
"corner_radius": 1000,
"border_spacing": 4,
"fg_color": "transparent",
"button_color": ["gray55", "gray41"],
"button_hover_color": ["gray40", "gray53"]
},
"CTkSegmentedButton": {
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#979DA2", "gray29"],
"selected_color": ["#3B8ED0", "#1F6AA5"],
"selected_hover_color": ["#36719F", "#144870"],
"unselected_color": ["#979DA2", "gray29"],
"unselected_hover_color": ["gray70", "gray41"],
"text_color": ["#DCE4EE", "#DCE4EE"],
"text_color_disabled": ["gray74", "gray60"]
},
"CTkTextbox": {
"corner_radius": 6,
"border_width": 0,
"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"],
"text_color": ["gray10", "gray90"]
},
"CTkFont": {
"macOS": {
"font": "SF Display",
"size": -13
"family": "SF Display",
"size": 13,
"weight": "normal"
},
"Windows": {
"font": "Roboto",
"size": -13
"family": "Roboto",
"size": 13,
"weight": "normal"
},
"Linux": {
"font": "Roboto",
"size": -13
"family": "Roboto",
"size": 13,
"weight": "normal"
}
},
"shape": {
"button_corner_radius": 8,
"button_border_width": 0,
"checkbox_corner_radius": 7,
"checkbox_border_width": 3,
"radiobutton_corner_radius": 1000,
"radiobutton_border_width_unchecked": 3,
"radiobutton_border_width_checked": 6,
"entry_border_width": 2,
"frame_corner_radius": 8,
"frame_border_width": 0,
"label_corner_radius": 8,
"progressbar_border_width": 0,
"progressbar_corner_radius": 1000,
"slider_border_width": 6,
"slider_corner_radius": 8,
"slider_button_length": 0,
"slider_button_corner_radius": 1000,
"switch_border_width": 3,
"switch_corner_radius": 1000,
"switch_button_corner_radius": 1000,
"switch_button_length": 0
}
}

View File

@ -1,69 +1,155 @@
{
"color": {
"window_bg_color": ["gray98", "gray10"],
"button": ["#608BD5", "#395E9C"],
"button_hover": ["#A4BDE6", "#748BB3"],
"button_border": ["gray40", "gray70"],
"checkbox_border": ["gray40", "gray60"],
"checkmark": ["white", "gray90"],
"entry": ["white", "gray24"],
"entry_border": ["gray70", "gray32"],
"entry_placeholder_text": ["gray52", "gray62"],
"frame_border": ["#A7C2E0", "#5FB4DD"],
"frame_low": ["gray92", "gray16"],
"frame_high": ["gray86", "gray20"],
"label": [null, null],
"text": ["gray12", "gray90"],
"text_disabled": ["gray60", "gray50"],
"text_button_disabled": ["gray40", "gray74"],
"progressbar": ["#6B6B6B", "gray0"],
"progressbar_progress": ["#608BD5", "#395E9C"],
"progressbar_border": ["gray", "gray"],
"slider": ["#6B6B6B", "gray6"],
"slider_progress": ["gray70", "gray30"],
"slider_button": ["#608BD5", "#395E9C"],
"slider_button_hover": ["#A4BDE6", "#748BB3"],
"switch": ["gray70", "gray35"],
"switch_progress": ["#608BD5", "#395E9C"],
"switch_button": ["gray38", "gray70"],
"switch_button_hover": ["gray30", "gray90"],
"darken_factor": 0.8
"CTk": {
"fg_color": ["gray95", "gray10"]
},
"text": {
"CTkToplevel": {
"fg_color": ["gray95", "gray10"]
},
"CTkFrame": {
"corner_radius": 6,
"border_width": 0,
"fg_color": ["gray90", "gray13"],
"top_fg_color": ["gray85", "gray16"],
"border_color": ["gray65", "gray28"]
},
"CTkButton": {
"corner_radius": 6,
"border_width": 0,
"fg_color": ["#3a7ebf", "#1f538d"],
"hover_color": ["#325882", "#14375e"],
"border_color": ["#3E454A", "#949A9F"],
"text_color": ["#DCE4EE", "#DCE4EE"],
"text_color_disabled": ["gray74", "gray60"]
},
"CTkLabel": {
"corner_radius": 0,
"fg_color": "transparent",
"text_color": ["gray14", "gray84"]
},
"CTkEntry": {
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#F9F9FA", "#343638"],
"border_color": ["#979DA2", "#565B5E"],
"text_color": ["gray14", "gray84"],
"placeholder_text_color": ["gray52", "gray62"]
},
"CTkCheckbox": {
"corner_radius": 6,
"border_width": 3,
"fg_color": ["#3a7ebf", "#1f538d"],
"border_color": ["#3E454A", "#949A9F"],
"hover_color": ["#325882", "#14375e"],
"checkmark_color": ["#DCE4EE", "gray90"],
"text_color": ["gray14", "gray84"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkSwitch": {
"corner_radius": 1000,
"border_width": 3,
"button_length": 0,
"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": {
"corner_radius": 1000,
"border_width_checked": 6,
"border_width_unchecked": 3,
"fg_color": ["#3a7ebf", "#1f538d"],
"border_color": ["#3E454A", "#949A9F"],
"hover_color": ["#325882", "#14375e"],
"text_color": ["gray14", "gray84"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkProgressBar": {
"corner_radius": 1000,
"border_width": 0,
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["#3a7ebf", "#1f538d"],
"border_color": ["gray", "gray"]
},
"CTkSlider": {
"corner_radius": 1000,
"button_corner_radius": 1000,
"border_width": 6,
"button_length": 0,
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["gray40", "#AAB0B5"],
"button_color": ["#3a7ebf", "#1f538d"],
"button_hover_color": ["#325882", "#14375e"]
},
"CTkOptionMenu": {
"corner_radius": 6,
"fg_color": ["#3a7ebf", "#1f538d"],
"button_color": ["#325882", "#14375e"],
"button_hover_color": ["#234567", "#1e2c40"],
"text_color": ["#DCE4EE", "#DCE4EE"],
"text_color_disabled": ["gray74", "gray60"]
},
"CTkComboBox": {
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#F9F9FA", "#343638"],
"border_color": ["#979DA2", "#565B5E"],
"button_color": ["#979DA2", "#565B5E"],
"button_hover_color": ["#6E7174", "#7A848D"],
"text_color": ["gray14", "gray84"],
"text_color_disabled": ["gray50", "gray45"]
},
"CTkScrollbar": {
"corner_radius": 1000,
"border_spacing": 4,
"fg_color": "transparent",
"button_color": ["gray55", "gray41"],
"button_hover_color": ["gray40", "gray53"]
},
"CTkSegmentedButton": {
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#979DA2", "gray29"],
"selected_color": ["#3a7ebf", "#1f538d"],
"selected_hover_color": ["#325882", "#14375e"],
"unselected_color": ["#979DA2", "gray29"],
"unselected_hover_color": ["gray70", "gray41"],
"text_color": ["#DCE4EE", "#DCE4EE"],
"text_color_disabled": ["gray74", "gray60"]
},
"CTkTextbox": {
"corner_radius": 6,
"border_width": 0,
"fg_color": ["gray100", "gray20"],
"border_color": ["#979DA2", "#565B5E"],
"text_color": ["gray14", "gray84"],
"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"],
"text_color": ["gray14", "gray84"]
},
"CTkFont": {
"macOS": {
"font": "SF Display",
"size": -13
"family": "SF Display",
"size": 13,
"weight": "normal"
},
"Windows": {
"font": "Roboto",
"size": -13
"family": "Roboto",
"size": 13,
"weight": "normal"
},
"Linux": {
"font": "Roboto",
"size": -13
"family": "Roboto",
"size": 13,
"weight": "normal"
}
},
"shape": {
"button_corner_radius": 8,
"button_border_width": 0,
"checkbox_corner_radius": 7,
"checkbox_border_width": 3,
"radiobutton_corner_radius": 1000,
"radiobutton_border_width_unchecked": 3,
"radiobutton_border_width_checked": 6,
"entry_border_width": 2,
"frame_corner_radius": 10,
"frame_border_width": 0,
"label_corner_radius": 8,
"progressbar_border_width": 0,
"progressbar_corner_radius": 1000,
"slider_border_width": 6,
"slider_corner_radius": 8,
"slider_button_length": 0,
"slider_button_corner_radius": 1000,
"switch_border_width": 3,
"switch_corner_radius": 1000,
"switch_button_corner_radius": 1000,
"switch_button_length": 0
}
}

View File

@ -1,69 +1,155 @@
{
"color": {
"window_bg_color": ["gray92", "gray12"],
"button": ["#72CF9F", "#11B384"],
"button_hover": ["#0E9670", "#0D8A66"],
"button_border": ["gray40", "gray70"],
"checkbox_border": ["gray40", "gray60"],
"checkmark": ["white", "gray90"],
"entry": ["white", "gray24"],
"entry_border": ["gray70", "gray32"],
"entry_placeholder_text": ["gray52", "gray62"],
"frame_border": ["#A7C2E0", "#5FB4DD"],
"frame_low": ["gray87", "gray18"],
"frame_high": ["gray82", "gray22"],
"label": [null, null],
"text": ["gray20", "gray90"],
"text_disabled": ["gray60", "gray50"],
"text_button_disabled": ["gray40", "gray74"],
"progressbar": ["#6B6B6B", "#222222"],
"progressbar_progress": ["#72CF9F", "#11B384"],
"progressbar_border": ["gray", "gray"],
"slider": ["#6B6B6B", "#222222"],
"slider_progress": ["white", "#555555"],
"slider_button": ["#72CF9F", "#11B384"],
"slider_button_hover": ["#0E9670", "#0D8A66"],
"switch": ["gray70", "gray35"],
"switch_progress": ["#72CF9F", "#11B384"],
"switch_button": ["gray38", "gray70"],
"switch_button_hover": ["gray30", "gray90"],
"darken_factor": 0.8
"CTk": {
"fg_color": ["gray92", "gray14"]
},
"text": {
"CTkToplevel": {
"fg_color": ["gray92", "gray14"]
},
"CTkFrame": {
"corner_radius": 6,
"border_width": 0,
"fg_color": ["gray86", "gray17"],
"top_fg_color": ["gray81", "gray20"],
"border_color": ["gray65", "gray28"]
},
"CTkButton": {
"corner_radius": 6,
"border_width": 0,
"fg_color": ["#2CC985", "#2FA572"],
"hover_color": ["#0C955A", "#106A43"],
"border_color": ["#3E454A", "#949A9F"],
"text_color": ["gray98", "#DCE4EE"],
"text_color_disabled": ["gray78", "gray68"]
},
"CTkLabel": {
"corner_radius": 0,
"fg_color": "transparent",
"text_color": ["gray10", "#DCE4EE"]
},
"CTkEntry": {
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#F9F9FA", "#343638"],
"border_color": ["#979DA2", "#565B5E"],
"text_color":["gray10", "#DCE4EE"],
"placeholder_text_color": ["gray52", "gray62"]
},
"CTkCheckbox": {
"corner_radius": 6,
"border_width": 3,
"fg_color": ["#2CC985", "#2FA572"],
"border_color": ["#3E454A", "#949A9F"],
"hover_color": ["#0C955A", "#106A43"],
"checkmark_color": ["#DCE4EE", "gray90"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkSwitch": {
"corner_radius": 1000,
"border_width": 3,
"button_length": 0,
"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": {
"corner_radius": 1000,
"border_width_checked": 6,
"border_width_unchecked": 3,
"fg_color": ["#2CC985", "#2FA572"],
"border_color": ["#3E454A", "#949A9F"],
"hover_color":["#0C955A", "#106A43"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray60", "gray45"]
},
"CTkProgressBar": {
"corner_radius": 1000,
"border_width": 0,
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["#2CC985", "#2FA572"],
"border_color": ["gray", "gray"]
},
"CTkSlider": {
"corner_radius": 1000,
"button_corner_radius": 1000,
"border_width": 6,
"button_length": 0,
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["gray40", "#AAB0B5"],
"button_color": ["#2CC985", "#2FA572"],
"button_hover_color": ["#0C955A", "#106A43"]
},
"CTkOptionMenu": {
"corner_radius": 6,
"fg_color": ["#2cbe79", "#2FA572"],
"button_color": ["#0C955A", "#106A43"],
"button_hover_color": ["#0b6e3d", "#17472e"],
"text_color": ["gray98", "#DCE4EE"],
"text_color_disabled": ["gray78", "gray68"]
},
"CTkComboBox": {
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#F9F9FA", "#343638"],
"border_color": ["#979DA2", "#565B5E"],
"button_color": ["#979DA2", "#565B5E"],
"button_hover_color": ["#6E7174", "#7A848D"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray50", "gray45"]
},
"CTkScrollbar": {
"corner_radius": 1000,
"border_spacing": 4,
"fg_color": "transparent",
"button_color": ["gray55", "gray41"],
"button_hover_color": ["gray40", "gray53"]
},
"CTkSegmentedButton": {
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#979DA2", "gray29"],
"selected_color": ["#2CC985", "#2FA572"],
"selected_hover_color": ["#0C955A", "#106A43"],
"unselected_color": ["#979DA2", "gray29"],
"unselected_hover_color": ["gray70", "gray41"],
"text_color": ["gray98", "#DCE4EE"],
"text_color_disabled": ["gray78", "gray68"]
},
"CTkTextbox": {
"corner_radius": 6,
"border_width": 0,
"fg_color": ["#F9F9FA", "gray23"],
"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"],
"text_color": ["gray10", "gray90"]
},
"CTkFont": {
"macOS": {
"font": "SF Display",
"size": -13
"family": "SF Display",
"size": 13,
"weight": "normal"
},
"Windows": {
"font": "Roboto",
"size": -13
"family": "Roboto",
"size": 13,
"weight": "normal"
},
"Linux": {
"font": "Roboto",
"size": -13
"family": "Roboto",
"size": 13,
"weight": "normal"
}
},
"shape": {
"button_corner_radius": 6,
"button_border_width": 0,
"checkbox_corner_radius": 7,
"checkbox_border_width": 3,
"radiobutton_corner_radius": 1000,
"radiobutton_border_width_unchecked": 3,
"radiobutton_border_width_checked": 6,
"entry_border_width": 2,
"frame_corner_radius": 10,
"frame_border_width": 0,
"label_corner_radius": 8,
"progressbar_border_width": 0,
"progressbar_corner_radius": 1000,
"slider_border_width": 6,
"slider_corner_radius": 8,
"slider_button_length": 0,
"slider_button_corner_radius": 1000,
"switch_border_width": 3,
"switch_corner_radius": 1000,
"switch_button_corner_radius": 1000,
"switch_button_length": 0
}
}
}

View File

@ -1,69 +0,0 @@
{
"color": {
"window_bg_color": ["#181b28", "#181b28"],
"button": ["#212435", "#212435"],
"button_hover": ["#171926", "#171926"],
"button_border": ["#080b12", "#080b12"],
"checkbox_border": ["#01e9c4", "#01e9c4"],
"checkmark": ["#01e9c4", "#01e9c4"],
"entry": ["#212435", "#212435"],
"entry_border": ["#080b12", "#080b12"],
"entry_placeholder_text": ["#cdc8ce", "#cdc8ce"],
"frame_border": ["#10121f", "#10121f"],
"frame_low": ["#181b28", "#181b28"],
"frame_high": ["#181b28", "#181b28"],
"label": [null, null],
"text": ["#cdc8ce", "#cdc8ce"],
"text_disabled": ["#7a8894", "#7a8894"],
"text_button_disabled": ["#7a8894", "#7a8894"],
"progressbar": ["#c452f8", "#c452f8"],
"progressbar_progress": ["#363844", "#363844"],
"progressbar_border": ["#0d101f", "#0d101f"],
"slider": ["#c452f8", "#c452f8"],
"slider_progress": ["#363844", "#363844"],
"slider_button": ["#5b40c5", "#5b40c5"],
"slider_button_hover": ["#c452f8", "#c452f8"],
"switch": ["#1f2233", "#1f2233"],
"switch_progress": ["#00e6c3", "#00e6c3"],
"switch_button": ["#2e324a", "#2e324a"],
"switch_button_hover": ["#2e324a", "#2e324a"],
"darken_factor": 0.1
},
"text": {
"macOS": {
"font": "SF Display",
"size": -13
},
"Windows": {
"font": "Roboto",
"size": -13
},
"Linux": {
"font": "Roboto",
"size": -13
}
},
"shape": {
"button_corner_radius": 8,
"button_border_width": 2,
"checkbox_corner_radius": 7,
"checkbox_border_width": 3,
"radiobutton_corner_radius": 1000,
"radiobutton_border_width_unchecked": 3,
"radiobutton_border_width_checked": 6,
"entry_border_width": 2,
"frame_corner_radius": 10,
"frame_border_width": 2,
"label_corner_radius": 8,
"progressbar_border_width": 2,
"progressbar_corner_radius": 1000,
"slider_border_width": 6,
"slider_corner_radius": 8,
"slider_button_length": 0,
"slider_button_corner_radius": 1000,
"switch_border_width": 3,
"switch_corner_radius": 1000,
"switch_button_corner_radius": 1000,
"switch_button_length": 2
}
}

View File

@ -1,703 +0,0 @@
from __future__ import annotations
import sys
import math
import tkinter
from typing import Union, TYPE_CHECKING
if TYPE_CHECKING:
from .widgets.ctk_canvas import CTkCanvas
class DrawEngine:
"""
This is the core of the CustomTkinter library where all the drawing on the tkinter.Canvas happens.
A year of experimenting and trying out different drawing methods have led to the current state of this
class, and I don't think there's much I can do to make the rendering look better than this with the
limited capabilities the tkinter.Canvas offers.
Functions:
- draw_rounded_rect_with_border()
- draw_rounded_progress_bar_with_border()
- draw_rounded_slider_with_border_and_button()
- draw_checkmark()
"""
preferred_drawing_method: str = None # 'polygon_shapes', 'font_shapes', 'circle_shapes'
def __init__(self, canvas: CTkCanvas):
self._canvas = canvas
self._existing_tags = set()
def _calc_optimal_corner_radius(self, user_corner_radius: Union[float, int]) -> Union[float, int]:
# optimize for drawing with polygon shapes
if self.preferred_drawing_method == "polygon_shapes":
if sys.platform == "darwin":
return user_corner_radius
else:
return round(user_corner_radius)
# optimize forx drawing with antialiased font shapes
elif self.preferred_drawing_method == "font_shapes":
return round(user_corner_radius)
# optimize for drawing with circles and rects
elif self.preferred_drawing_method == "circle_shapes":
user_corner_radius = 0.5 * round(user_corner_radius / 0.5) # round to 0.5 steps
# make sure the value is always with .5 at the end for smoother corners
if user_corner_radius == 0:
return 0
elif user_corner_radius % 1 == 0:
return user_corner_radius + 0.5
else:
return user_corner_radius
def draw_rounded_rect_with_border(self, width: int, height: int, corner_radius: Union[float, int], border_width: Union[float, int]) -> bool:
""" Draws a rounded rectangle with a corner_radius and border_width on the canvas. The border elements have a 'border_parts' tag,
the main foreground elements have an 'inner_parts' tag to color the elements accordingly.
returns bool if recoloring is necessary """
width = math.floor(width / 2) * 2 # round (floor) current_width and current_height and restrict them to even values only
height = math.floor(height / 2) * 2
corner_radius = round(corner_radius)
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
corner_radius = min(width / 2, height / 2)
border_width = round(border_width)
corner_radius = self._calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
if corner_radius >= border_width:
inner_corner_radius = corner_radius - border_width
else:
inner_corner_radius = 0
if self.preferred_drawing_method == "polygon_shapes":
return self._draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius)
elif self.preferred_drawing_method == "font_shapes":
return self._draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ())
elif self.preferred_drawing_method == "circle_shapes":
return self._draw_rounded_rect_with_border_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius)
def _draw_rounded_rect_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool:
requires_recoloring = False
# create border button parts (only if border exists)
if border_width > 0:
if not self._canvas.find_withtag("border_parts"):
self._canvas.create_polygon((0, 0, 0, 0), tags=("border_line_1", "border_parts"))
requires_recoloring = True
self._canvas.coords("border_line_1",
(corner_radius,
corner_radius,
width - corner_radius,
corner_radius,
width - corner_radius,
height - corner_radius,
corner_radius,
height - corner_radius))
self._canvas.itemconfig("border_line_1",
joinstyle=tkinter.ROUND,
width=corner_radius * 2)
else:
self._canvas.delete("border_parts")
# create inner button parts
if not self._canvas.find_withtag("inner_parts"):
self._canvas.create_polygon((0, 0, 0, 0), tags=("inner_line_1", "inner_parts"), joinstyle=tkinter.ROUND)
requires_recoloring = True
if corner_radius <= border_width:
bottom_right_shift = -1 # weird canvas rendering inaccuracy that has to be corrected in some cases
else:
bottom_right_shift = 0
self._canvas.coords("inner_line_1",
border_width + inner_corner_radius,
border_width + inner_corner_radius,
width - (border_width + inner_corner_radius) + bottom_right_shift,
border_width + inner_corner_radius,
width - (border_width + inner_corner_radius) + bottom_right_shift,
height - (border_width + inner_corner_radius) + bottom_right_shift,
border_width + inner_corner_radius,
height - (border_width + inner_corner_radius) + bottom_right_shift)
self._canvas.itemconfig("inner_line_1",
width=inner_corner_radius * 2)
if requires_recoloring: # new parts were added -> manage z-order
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
return requires_recoloring
def _draw_rounded_rect_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
exclude_parts: tuple) -> bool:
requires_recoloring = False
# create border button parts
if border_width > 0:
if corner_radius > 0:
# create canvas border corner parts if not already created, but only if needed, and delete if not needed
if not self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" in exclude_parts:
self._canvas.delete("border_oval_1_a", "border_oval_1_b")
if not self._canvas.find_withtag("border_oval_2_a") and width > 2 * corner_radius and "border_oval_2" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("border_oval_2_a") and (not width > 2 * corner_radius or "border_oval_2" in exclude_parts):
self._canvas.delete("border_oval_2_a", "border_oval_2_b")
if not self._canvas.find_withtag("border_oval_3_a") and height > 2 * corner_radius \
and width > 2 * corner_radius and "border_oval_3" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("border_oval_3_a") and (not (height > 2 * corner_radius
and width > 2 * corner_radius) or "border_oval_3" in exclude_parts):
self._canvas.delete("border_oval_3_a", "border_oval_3_b")
if not self._canvas.find_withtag("border_oval_4_a") and height > 2 * corner_radius and "border_oval_4" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("border_oval_4_a") and (not height > 2 * corner_radius or "border_oval_4" in exclude_parts):
self._canvas.delete("border_oval_4_a", "border_oval_4_b")
# change position of border corner parts
self._canvas.coords("border_oval_1_a", corner_radius, corner_radius, corner_radius)
self._canvas.coords("border_oval_1_b", corner_radius, corner_radius, corner_radius)
self._canvas.coords("border_oval_2_a", width - corner_radius, corner_radius, corner_radius)
self._canvas.coords("border_oval_2_b", width - corner_radius, corner_radius, corner_radius)
self._canvas.coords("border_oval_3_a", width - corner_radius, height - corner_radius, corner_radius)
self._canvas.coords("border_oval_3_b", width - corner_radius, height - corner_radius, corner_radius)
self._canvas.coords("border_oval_4_a", corner_radius, height - corner_radius, corner_radius)
self._canvas.coords("border_oval_4_b", corner_radius, height - corner_radius, corner_radius)
else:
self._canvas.delete("border_corner_part") # delete border corner parts if not needed
# create canvas border rectangle parts if not already created
if not self._canvas.find_withtag("border_rectangle_1"):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_rectangle_part", "border_parts"), width=0)
self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_2", "border_rectangle_part", "border_parts"), width=0)
requires_recoloring = True
# change position of border rectangle parts
self._canvas.coords("border_rectangle_1", (0, corner_radius, width, height - corner_radius))
self._canvas.coords("border_rectangle_2", (corner_radius, 0, width - corner_radius, height))
else:
self._canvas.delete("border_parts")
# create inner button parts
if inner_corner_radius > 0:
# create canvas border corner parts if not already created, but only if they're needed and delete if not needed
if not self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" in exclude_parts:
self._canvas.delete("inner_oval_1_a", "inner_oval_1_b")
if not self._canvas.find_withtag("inner_oval_2_a") and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_2" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_2_a") and (not width - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_2" in exclude_parts):
self._canvas.delete("inner_oval_2_a", "inner_oval_2_b")
if not self._canvas.find_withtag("inner_oval_3_a") and height - (2 * border_width) > 2 * inner_corner_radius \
and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_3" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_3_a") and (not (height - (2 * border_width) > 2 * inner_corner_radius
and width - (2 * border_width) > 2 * inner_corner_radius) or "inner_oval_3" in exclude_parts):
self._canvas.delete("inner_oval_3_a", "inner_oval_3_b")
if not self._canvas.find_withtag("inner_oval_4_a") and height - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_4" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_4_a") and (not height - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_4" in exclude_parts):
self._canvas.delete("inner_oval_4_a", "inner_oval_4_b")
# change position of border corner parts
self._canvas.coords("inner_oval_1_a", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_1_b", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_2_a", width - border_width - inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_2_b", width - border_width - inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_3_a", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_3_b", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_4_a", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_4_b", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
else:
self._canvas.delete("inner_corner_part") # delete inner corner parts if not needed
# create canvas inner rectangle parts if not already created
if not self._canvas.find_withtag("inner_rectangle_1"):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_1", "inner_rectangle_part", "inner_parts"), width=0)
requires_recoloring = True
if not self._canvas.find_withtag("inner_rectangle_2") and inner_corner_radius * 2 < height - (border_width * 2):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_2", "inner_rectangle_part", "inner_parts"), width=0)
requires_recoloring = True
elif self._canvas.find_withtag("inner_rectangle_2") and not inner_corner_radius * 2 < height - (border_width * 2):
self._canvas.delete("inner_rectangle_2")
# change position of inner rectangle parts
self._canvas.coords("inner_rectangle_1", (border_width + inner_corner_radius,
border_width,
width - border_width - inner_corner_radius,
height - border_width))
self._canvas.coords("inner_rectangle_2", (border_width,
border_width + inner_corner_radius,
width - border_width,
height - inner_corner_radius - border_width))
if requires_recoloring: # new parts were added -> manage z-order
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
return requires_recoloring
def _draw_rounded_rect_with_border_circle_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool:
requires_recoloring = False
# border button parts
if border_width > 0:
if corner_radius > 0:
if not self._canvas.find_withtag("border_oval_1"):
self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_1", "border_corner_part", "border_parts"), width=0)
self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_2", "border_corner_part", "border_parts"), width=0)
self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_3", "border_corner_part", "border_parts"), width=0)
self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_4", "border_corner_part", "border_parts"), width=0)
self._canvas.tag_lower("border_parts")
requires_recoloring = True
self._canvas.coords("border_oval_1", 0, 0, corner_radius * 2 - 1, corner_radius * 2 - 1)
self._canvas.coords("border_oval_2", width - corner_radius * 2, 0, width - 1, corner_radius * 2 - 1)
self._canvas.coords("border_oval_3", 0, height - corner_radius * 2, corner_radius * 2 - 1, height - 1)
self._canvas.coords("border_oval_4", width - corner_radius * 2, height - corner_radius * 2, width - 1, height - 1)
else:
self._canvas.delete("border_corner_part")
if not self._canvas.find_withtag("border_rectangle_1"):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_rectangle_part", "border_parts"), width=0)
self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_2", "border_rectangle_part", "border_parts"), width=0)
self._canvas.tag_lower("border_parts")
requires_recoloring = True
self._canvas.coords("border_rectangle_1", (0, corner_radius, width, height - corner_radius))
self._canvas.coords("border_rectangle_2", (corner_radius, 0, width - corner_radius, height))
else:
self._canvas.delete("border_parts")
# inner button parts
if inner_corner_radius > 0:
if not self._canvas.find_withtag("inner_oval_1"):
self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_1", "inner_corner_part", "inner_parts"), width=0)
self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_2", "inner_corner_part", "inner_parts"), width=0)
self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_3", "inner_corner_part", "inner_parts"), width=0)
self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_4", "inner_corner_part", "inner_parts"), width=0)
self._canvas.tag_raise("inner_parts")
requires_recoloring = True
self._canvas.coords("inner_oval_1", (border_width, border_width,
border_width + inner_corner_radius * 2 - 1, border_width + inner_corner_radius * 2 - 1))
self._canvas.coords("inner_oval_2", (width - border_width - inner_corner_radius * 2, border_width,
width - border_width - 1, border_width + inner_corner_radius * 2 - 1))
self._canvas.coords("inner_oval_3", (border_width, height - border_width - inner_corner_radius * 2,
border_width + inner_corner_radius * 2 - 1, height - border_width - 1))
self._canvas.coords("inner_oval_4", (width - border_width - inner_corner_radius * 2, height - border_width - inner_corner_radius * 2,
width - border_width - 1, height - border_width - 1))
else:
self._canvas.delete("inner_corner_part") # delete inner corner parts if not needed
if not self._canvas.find_withtag("inner_rectangle_1"):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_1", "inner_rectangle_part", "inner_parts"), width=0)
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_2", "inner_rectangle_part", "inner_parts"), width=0)
self._canvas.tag_raise("inner_parts")
requires_recoloring = True
self._canvas.coords("inner_rectangle_1", (border_width + inner_corner_radius,
border_width,
width - border_width - inner_corner_radius,
height - border_width))
self._canvas.coords("inner_rectangle_2", (border_width,
border_width + inner_corner_radius,
width - border_width,
height - inner_corner_radius - border_width))
return requires_recoloring
def draw_rounded_progress_bar_with_border(self, width: int, height: int, corner_radius: Union[float, int], border_width: Union[float, int],
progress_value: float, orientation: str) -> bool:
""" Draws a rounded bar on the canvas, which is split in half according to the argument 'progress_value' (0 - 1).
The border elements get the 'border_parts' tag", the main elements get the 'inner_parts' tag and
the progress elements get the 'progress_parts' tag. The 'orientation' argument defines from which direction the progress starts (n, w, s, e).
returns bool if recoloring is necessary """
width = math.floor(width / 2) * 2 # round current_width and current_height and restrict them to even values only
height = math.floor(height / 2) * 2
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
corner_radius = min(width / 2, height / 2)
border_width = round(border_width)
corner_radius = self._calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
if corner_radius >= border_width:
inner_corner_radius = corner_radius - border_width
else:
inner_corner_radius = 0
if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes":
return self._draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
progress_value, orientation)
elif self.preferred_drawing_method == "font_shapes":
return self._draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
progress_value, orientation)
def _draw_rounded_progress_bar_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
progress_value: float, orientation: str) -> bool:
requires_recoloring = self._draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius)
if corner_radius <= border_width:
bottom_right_shift = 0 # weird canvas rendering inaccuracy that has to be corrected in some cases
else:
bottom_right_shift = 0
# create progress parts
if not self._canvas.find_withtag("progress_parts"):
self._canvas.create_polygon((0, 0, 0, 0), tags=("progress_line_1", "progress_parts"), joinstyle=tkinter.ROUND)
self._canvas.tag_raise("progress_parts", "inner_parts")
requires_recoloring = True
if orientation == "w":
self._canvas.coords("progress_line_1",
border_width + inner_corner_radius,
border_width + inner_corner_radius,
border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value,
border_width + inner_corner_radius,
border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value,
height - (border_width + inner_corner_radius) + bottom_right_shift,
border_width + inner_corner_radius,
height - (border_width + inner_corner_radius) + bottom_right_shift)
elif orientation == "s":
self._canvas.coords("progress_line_1",
border_width + inner_corner_radius,
border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value),
width - (border_width + inner_corner_radius),
border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value),
width - (border_width + inner_corner_radius),
height - (border_width + inner_corner_radius) + bottom_right_shift,
border_width + inner_corner_radius,
height - (border_width + inner_corner_radius) + bottom_right_shift)
self._canvas.itemconfig("progress_line_1", width=inner_corner_radius * 2)
return requires_recoloring
def _draw_rounded_progress_bar_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
progress_value: float, orientation: str) -> bool:
requires_recoloring, requires_recoloring_2 = False, False
if inner_corner_radius > 0:
# create canvas border corner parts if not already created
if not self._canvas.find_withtag("progress_oval_1_a"):
self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_1_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_1_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180)
self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_2_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_2_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
if not self._canvas.find_withtag("progress_oval_3_a") and round(inner_corner_radius) * 2 < height - 2 * border_width:
self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_3_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_3_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180)
self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_4_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_4_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("progress_oval_3_a") and not round(inner_corner_radius) * 2 < height - 2 * border_width:
self._canvas.delete("progress_oval_3_a", "progress_oval_3_b", "progress_oval_4_a", "progress_oval_4_b")
if not self._canvas.find_withtag("progress_rectangle_1"):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("progress_rectangle_1", "progress_rectangle_part", "progress_parts"), width=0)
requires_recoloring = True
if not self._canvas.find_withtag("progress_rectangle_2") and inner_corner_radius * 2 < height - (border_width * 2):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("progress_rectangle_2", "progress_rectangle_part", "progress_parts"), width=0)
requires_recoloring = True
elif self._canvas.find_withtag("progress_rectangle_2") and not inner_corner_radius * 2 < height - (border_width * 2):
self._canvas.delete("progress_rectangle_2")
# horizontal orientation from the bottom
if orientation == "w":
requires_recoloring_2 = self._draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
("inner_oval_1", "inner_oval_4"))
# set positions of progress corner parts
self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("progress_oval_1_b", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("progress_oval_2_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value,
border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("progress_oval_2_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value,
border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("progress_oval_3_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value,
height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("progress_oval_3_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value,
height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("progress_oval_4_a", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("progress_oval_4_b", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
# set positions of progress rect parts
self._canvas.coords("progress_rectangle_1",
border_width + inner_corner_radius,
border_width,
border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value,
height - border_width)
self._canvas.coords("progress_rectangle_2",
border_width,
border_width + inner_corner_radius,
border_width + 2 * inner_corner_radius + (width - 2 * inner_corner_radius - 2 * border_width) * progress_value,
height - inner_corner_radius - border_width)
# vertical orientation from the bottom
if orientation == "s":
requires_recoloring_2 = self._draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
("inner_oval_3", "inner_oval_4"))
# set positions of progress corner parts
self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius,
border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), inner_corner_radius)
self._canvas.coords("progress_oval_1_b", border_width + inner_corner_radius,
border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), inner_corner_radius)
self._canvas.coords("progress_oval_2_a", width - border_width - inner_corner_radius,
border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), inner_corner_radius)
self._canvas.coords("progress_oval_2_b", width - border_width - inner_corner_radius,
border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), inner_corner_radius)
self._canvas.coords("progress_oval_3_a", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("progress_oval_3_b", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("progress_oval_4_a", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("progress_oval_4_b", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
# set positions of progress rect parts
self._canvas.coords("progress_rectangle_1",
border_width + inner_corner_radius,
border_width + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value),
width - border_width - inner_corner_radius,
height - border_width)
self._canvas.coords("progress_rectangle_2",
border_width,
border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value),
width - border_width,
height - inner_corner_radius - border_width)
return requires_recoloring or requires_recoloring_2
def draw_rounded_slider_with_border_and_button(self, width: int, height: int, corner_radius: Union[float, int], border_width: Union[float, int],
button_length: Union[float, int], button_corner_radius: Union[float, int], slider_value: float,
orientation: str) -> bool:
width = math.floor(width / 2) * 2 # round current_width and current_height and restrict them to even values only
height = math.floor(height / 2) * 2
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
corner_radius = min(width / 2, height / 2)
if button_corner_radius > width / 2 or button_corner_radius > height / 2: # restrict button_corner_radius if it's too larger
button_corner_radius = min(width / 2, height / 2)
button_length = round(button_length)
border_width = round(border_width)
button_corner_radius = round(button_corner_radius)
corner_radius = self._calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
if corner_radius >= border_width:
inner_corner_radius = corner_radius - border_width
else:
inner_corner_radius = 0
if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes":
return self._draw_rounded_slider_with_border_and_button_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
button_length, button_corner_radius, slider_value, orientation)
elif self.preferred_drawing_method == "font_shapes":
return self._draw_rounded_slider_with_border_and_button_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
button_length, button_corner_radius, slider_value, orientation)
def _draw_rounded_slider_with_border_and_button_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
button_length: int, button_corner_radius: int, slider_value: float, orientation: str) -> bool:
# draw normal progressbar
requires_recoloring = self._draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
slider_value, orientation)
# create slider button part
if not self._canvas.find_withtag("slider_parts"):
self._canvas.create_polygon((0, 0, 0, 0), tags=("slider_line_1", "slider_parts"), joinstyle=tkinter.ROUND)
self._canvas.tag_raise("slider_parts") # manage z-order
requires_recoloring = True
if corner_radius <= border_width:
bottom_right_shift = -1 # weird canvas rendering inaccuracy that has to be corrected in some cases
else:
bottom_right_shift = 0
if orientation == "w":
slider_x_position = corner_radius + (button_length / 2) + (width - 2 * corner_radius - button_length) * slider_value
self._canvas.coords("slider_line_1",
slider_x_position - (button_length / 2), button_corner_radius,
slider_x_position + (button_length / 2), button_corner_radius,
slider_x_position + (button_length / 2), height - button_corner_radius,
slider_x_position - (button_length / 2), height - button_corner_radius)
self._canvas.itemconfig("slider_line_1",
width=button_corner_radius * 2)
elif orientation == "s":
slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_value)
self._canvas.coords("slider_line_1",
button_corner_radius, slider_y_position - (button_length / 2),
button_corner_radius, slider_y_position + (button_length / 2),
width - button_corner_radius, slider_y_position + (button_length / 2),
width - button_corner_radius, slider_y_position - (button_length / 2))
self._canvas.itemconfig("slider_line_1",
width=button_corner_radius * 2)
return requires_recoloring
def _draw_rounded_slider_with_border_and_button_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
button_length: int, button_corner_radius: int, slider_value: float, orientation: str) -> bool:
# draw normal progressbar
requires_recoloring = self._draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
slider_value, orientation)
# create 4 circles (if not needed, then less)
if not self._canvas.find_withtag("slider_oval_1_a"):
self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_1_a", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_1_b", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
if not self._canvas.find_withtag("slider_oval_2_a") and button_length > 0:
self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_a", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_b", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("slider_oval_2_a") and not button_length > 0:
self._canvas.delete("slider_oval_2_a", "slider_oval_2_b")
if not self._canvas.find_withtag("slider_oval_4_a") and height > 2 * button_corner_radius:
self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_4_a", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_4_b", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("slider_oval_4_a") and not height > 2 * button_corner_radius:
self._canvas.delete("slider_oval_4_a", "slider_oval_4_b")
if not self._canvas.find_withtag("slider_oval_3_a") and button_length > 0 and height > 2 * button_corner_radius:
self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_3_a", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_3_b", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("border_oval_3_a") and not (button_length > 0 and height > 2 * button_corner_radius):
self._canvas.delete("slider_oval_3_a", "slider_oval_3_b")
# create the 2 rectangles (if needed)
if not self._canvas.find_withtag("slider_rectangle_1") and button_length > 0:
self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_1", "slider_rectangle_part", "slider_parts"), width=0)
requires_recoloring = True
elif self._canvas.find_withtag("slider_rectangle_1") and not button_length > 0:
self._canvas.delete("slider_rectangle_1")
if not self._canvas.find_withtag("slider_rectangle_2") and height > 2 * button_corner_radius:
self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_2", "slider_rectangle_part", "slider_parts"), width=0)
requires_recoloring = True
elif self._canvas.find_withtag("slider_rectangle_2") and not height > 2 * button_corner_radius:
self._canvas.delete("slider_rectangle_2")
# set positions of circles and rectangles
if orientation == "w":
slider_x_position = corner_radius + (button_length / 2) + (width - 2 * corner_radius - button_length) * slider_value
self._canvas.coords("slider_oval_1_a", slider_x_position - (button_length / 2), button_corner_radius, button_corner_radius)
self._canvas.coords("slider_oval_1_b", slider_x_position - (button_length / 2), button_corner_radius, button_corner_radius)
self._canvas.coords("slider_oval_2_a", slider_x_position + (button_length / 2), button_corner_radius, button_corner_radius)
self._canvas.coords("slider_oval_2_b", slider_x_position + (button_length / 2), button_corner_radius, button_corner_radius)
self._canvas.coords("slider_oval_3_a", slider_x_position + (button_length / 2), height - button_corner_radius, button_corner_radius)
self._canvas.coords("slider_oval_3_b", slider_x_position + (button_length / 2), height - button_corner_radius, button_corner_radius)
self._canvas.coords("slider_oval_4_a", slider_x_position - (button_length / 2), height - button_corner_radius, button_corner_radius)
self._canvas.coords("slider_oval_4_b", slider_x_position - (button_length / 2), height - button_corner_radius, button_corner_radius)
self._canvas.coords("slider_rectangle_1",
slider_x_position - (button_length / 2), 0,
slider_x_position + (button_length / 2), height)
self._canvas.coords("slider_rectangle_2",
slider_x_position - (button_length / 2) - button_corner_radius, button_corner_radius,
slider_x_position + (button_length / 2) + button_corner_radius, height - button_corner_radius)
elif orientation == "s":
slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_value)
self._canvas.coords("slider_oval_1_a", button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius)
self._canvas.coords("slider_oval_1_b", button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius)
self._canvas.coords("slider_oval_2_a", button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius)
self._canvas.coords("slider_oval_2_b", button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius)
self._canvas.coords("slider_oval_3_a", width - button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius)
self._canvas.coords("slider_oval_3_b", width - button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius)
self._canvas.coords("slider_oval_4_a", width - button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius)
self._canvas.coords("slider_oval_4_b", width - button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius)
self._canvas.coords("slider_rectangle_1",
0, slider_y_position - (button_length / 2),
width, slider_y_position + (button_length / 2))
self._canvas.coords("slider_rectangle_2",
button_corner_radius, slider_y_position - (button_length / 2) - button_corner_radius,
width - button_corner_radius, slider_y_position + (button_length / 2) + button_corner_radius)
if requires_recoloring: # new parts were added -> manage z-order
self._canvas.tag_raise("slider_parts")
return requires_recoloring
def draw_checkmark(self, width: int, height: int, size: Union[int, float]) -> bool:
""" Draws a rounded rectangle with a corner_radius and border_width on the canvas. The border elements have a 'border_parts' tag,
the main foreground elements have an 'inner_parts' tag to color the elements accordingly.
returns bool if recoloring is necessary """
size = round(size)
requires_recoloring = False
if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes":
x, y, radius = width / 2, height / 2, size / 2.8
if not self._canvas.find_withtag("checkmark"):
self._canvas.create_line(0, 0, 0, 0, tags=("checkmark", "create_line"), width=round(height / 8), joinstyle=tkinter.MITER, capstyle=tkinter.ROUND)
self._canvas.tag_raise("checkmark")
requires_recoloring = True
self._canvas.coords("checkmark",
x + radius, y - radius,
x - radius / 4, y + radius * 0.8,
x - radius, y + radius / 6)
elif self.preferred_drawing_method == "font_shapes":
if not self._canvas.find_withtag("checkmark"):
self._canvas.create_text(0, 0, text="Z", font=("CustomTkinter_shapes_font", -size), tags=("checkmark", "create_text"), anchor=tkinter.CENTER)
self._canvas.tag_raise("checkmark")
requires_recoloring = True
self._canvas.coords("checkmark", round(width / 2), round(height / 2))
return requires_recoloring

View File

@ -1,5 +0,0 @@
class Settings:
cursor_manipulation_enabled = True
deactivate_macos_window_header_manipulation = False
deactivate_windows_window_header_manipulation = False

View File

@ -1,80 +0,0 @@
import sys
import os
import json
class ThemeManager:
theme = {} # contains all the theme data
built_in_themes = ["blue", "green", "dark-blue", "sweetkind"]
@classmethod
def load_theme(cls, theme_name_or_path: str):
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:
cls.theme = json.load(f)
else:
with open(theme_name_or_path, "r") as f:
cls.theme = json.load(f)
if sys.platform == "darwin":
cls.theme["text"] = cls.theme["text"]["macOS"]
elif sys.platform.startswith("win"):
cls.theme["text"] = cls.theme["text"]["Windows"]
else:
cls.theme["text"] = cls.theme["text"]["Linux"]
@staticmethod
def single_color(color, appearance_mode: int) -> str:
""" color can be either a single hex color string or a color name or it can be a
tuple color with (light_color, dark_color). The functions then returns
always a single color string """
if type(color) == tuple or type(color) == list:
return color[appearance_mode]
else:
return color
@staticmethod
def rgb2hex(rgb_color: tuple) -> str:
return "#{:02x}{:02x}{:02x}".format(round(rgb_color[0]), round(rgb_color[1]), round(rgb_color[2]))
@staticmethod
def hex2rgb(hex_color: str) -> tuple:
return tuple(int(hex_color.strip("#")[i:i+2], 16) for i in (0, 2, 4))
@classmethod
def linear_blend(cls, color_1: str, color_2: str, blend_factor: float) -> str:
""" Blends two hex colors linear, where blend_factor of 0
results in color_1 and blend_factor of 1 results in color_2. """
if color_1 is None or color_2 is None:
return None
rgb_1 = cls.hex2rgb(color_1)
rgb_2 = cls.hex2rgb(color_2)
new_rgb = (rgb_1[0] + (rgb_2[0] - rgb_1[0]) * blend_factor,
rgb_1[1] + (rgb_2[1] - rgb_1[1]) * blend_factor,
rgb_1[2] + (rgb_2[2] - rgb_1[2]) * blend_factor)
return cls.rgb2hex(new_rgb)
@classmethod
def multiply_hex_color(cls, hex_color: str, factor: float = 1.0) -> str:
try:
rgb_color = ThemeManager.hex2rgb(hex_color)
dark_rgb_color = (min(255, rgb_color[0] * factor),
min(255, rgb_color[1] * factor),
min(255, rgb_color[2] * factor))
return ThemeManager.rgb2hex(dark_rgb_color)
except Exception as err:
# sys.stderr.write("ERROR (CTkColorManager): failed to darken the following color: " + str(hex_color) + " " + str(err))
return hex_color
@classmethod
def set_main_color(cls, main_color, main_color_hover):
cls.MAIN_COLOR = main_color
cls.MAIN_HOVER_COLOR = main_color_hover

View File

@ -1,3 +0,0 @@
from .ctk_canvas import CTkCanvas
CTkCanvas.init_font_character_mapping()

View File

@ -1,375 +0,0 @@
import tkinter
import sys
import math
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..settings import Settings
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
class CTkButton(CTkBaseClass):
""" tkinter custom button with border, rounded corners and hover effect """
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
hover_color="default_theme",
border_color="default_theme",
border_width="default_theme",
command=None,
textvariable=None,
width=120,
height=30,
corner_radius="default_theme",
text_font="default_theme",
text_color="default_theme",
text_color_disabled="default_theme",
text="CTkButton",
hover=True,
image=None,
compound=tkinter.LEFT,
state=tkinter.NORMAL,
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
self.configure_basic_grid()
# color variables
self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self.border_color = ThemeManager.theme["color"]["button_border"] if border_color == "default_theme" else border_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width
# text and font and image
self.image = image
self.image_label = None
self.text = text
self.text_label = None
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# callback and hover functionality
self.function = command
self.textvariable = textvariable
self.state = state
self.hover = hover
self.compound = compound
self.click_animation_running = False
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
self.draw_engine = DrawEngine(self.canvas)
# event bindings
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.clicked)
self.canvas.bind("<Button-1>", self.clicked)
self.bind('<Configure>', self.update_dimensions_event)
self.set_cursor()
self.draw() # initial draw
def configure_basic_grid(self):
# Configuration of a grid system (2x2) in which all parts of CTkButton are centered
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(1, weight=1)
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
if self.text_label is not None:
self.text_label.destroy()
self.text_label = None
if self.image_label is not None:
self.image_label.destroy()
self.image_label = None
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height)),
self.draw()
def draw(self, no_color_updates=False):
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 no_color_updates is False or requires_recoloring:
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
# set color for the button border parts (outline)
self.canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self.border_color, self.appearance_mode),
fill=ThemeManager.single_color(self.border_color, self.appearance_mode))
# set color for inner button parts
if self.fg_color is None:
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self.bg_color, self.appearance_mode),
fill=ThemeManager.single_color(self.bg_color, self.appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode))
# create text label if text given
if self.text is not None and self.text != "":
if self.text_label is None:
self.text_label = tkinter.Label(master=self,
font=self.apply_font_scaling(self.text_font),
textvariable=self.textvariable)
self.text_label.bind("<Enter>", self.on_enter)
self.text_label.bind("<Leave>", self.on_leave)
self.text_label.bind("<Button-1>", self.clicked)
self.text_label.bind("<Button-1>", self.clicked)
if no_color_updates is False:
# set text_label fg color (text color)
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode))
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self.appearance_mode)))
else:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode))
if self.fg_color is None:
self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
else:
self.text_label.configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
self.text_label.configure(text=self.text) # set text
else:
# delete text_label if no text given
if self.text_label is not None:
self.text_label.destroy()
self.text_label = None
# create image label if image given
if self.image is not None:
if self.image_label is None:
self.image_label = tkinter.Label(master=self)
self.image_label.bind("<Enter>", self.on_enter)
self.image_label.bind("<Leave>", self.on_leave)
self.image_label.bind("<Button-1>", self.clicked)
self.image_label.bind("<Button-1>", self.clicked)
if no_color_updates is False:
# set image_label bg color (background color of label)
if self.fg_color is None:
self.image_label.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
else:
self.image_label.configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
self.image_label.configure(image=self.image) # set image
else:
# delete text_label if no text given
if self.image_label is not None:
self.image_label.destroy()
self.image_label = None
# create grid layout with just an image given
if self.image_label is not None and self.text_label is None:
self.image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) # bottom pady with +1 for rounding to even
# create grid layout with just text given
if self.image_label is None and self.text_label is not None:
self.text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
padx=self.apply_widget_scaling(self.corner_radius),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) # bottom pady with +1 for rounding to even
# create grid layout of image and text label in 2x2 grid system with given compound
if self.image_label is not None and self.text_label is not None:
if self.compound == tkinter.LEFT or self.compound == "left":
self.image_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
self.text_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
elif self.compound == tkinter.TOP or self.compound == "top":
self.image_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
pady=(self.apply_widget_scaling(self.border_width), 2))
self.text_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
pady=(2, self.apply_widget_scaling(self.border_width)))
elif self.compound == tkinter.RIGHT or self.compound == "right":
self.image_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
self.text_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
elif self.compound == tkinter.BOTTOM or self.compound == "bottom":
self.image_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
pady=(2, self.apply_widget_scaling(self.border_width)))
self.text_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
pady=(self.apply_widget_scaling(self.border_width), 2))
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw() at the end
if "text" in kwargs:
self.set_text(kwargs["text"])
del kwargs["text"]
if "state" in kwargs:
self.state = kwargs["state"]
self.set_cursor()
require_redraw = True
del kwargs["state"]
if "image" in kwargs:
self.set_image(kwargs["image"])
del kwargs["image"]
if "compound" in kwargs:
self.compound = kwargs["compound"]
require_redraw = True
del kwargs["compound"]
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
require_redraw = True
del kwargs["fg_color"]
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
require_redraw = True
del kwargs["border_color"]
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "hover_color" in kwargs:
self.hover_color = kwargs["hover_color"]
require_redraw = True
del kwargs["hover_color"]
if "text_color" in kwargs:
self.text_color = kwargs["text_color"]
require_redraw = True
del kwargs["text_color"]
if "command" in kwargs:
self.function = kwargs["command"]
del kwargs["command"]
if "textvariable" in kwargs:
self.textvariable = kwargs["textvariable"]
if self.text_label is not None:
self.text_label.configure(textvariable=self.textvariable)
del kwargs["textvariable"]
super().configure(*args, **kwargs)
if require_redraw:
self.draw()
def set_cursor(self):
if Settings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and self.function is not None and Settings.cursor_manipulation_enabled:
self.configure(cursor="arrow")
elif sys.platform.startswith("win") and self.function is not None and Settings.cursor_manipulation_enabled:
self.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and self.function is not None and Settings.cursor_manipulation_enabled:
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and self.function is not None and Settings.cursor_manipulation_enabled:
self.configure(cursor="hand2")
def set_text(self, text):
self.text = text
self.draw()
def set_image(self, image):
self.image = image
self.draw()
def on_enter(self, event=0):
if self.hover is True and self.state == tkinter.NORMAL:
if self.hover_color is None:
inner_parts_color = self.fg_color
else:
inner_parts_color = self.hover_color
# set color of inner button parts to hover color
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(inner_parts_color, self.appearance_mode),
fill=ThemeManager.single_color(inner_parts_color, self.appearance_mode))
# set text_label bg color to button hover color
if self.text_label is not None:
self.text_label.configure(bg=ThemeManager.single_color(inner_parts_color, self.appearance_mode))
# set image_label bg color to button hover color
if self.image_label is not None:
self.image_label.configure(bg=ThemeManager.single_color(inner_parts_color, self.appearance_mode))
def on_leave(self, event=0):
self.click_animation_running = False
if self.hover is True:
if self.fg_color is None:
inner_parts_color = self.bg_color
else:
inner_parts_color = self.fg_color
# set color of inner button parts
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(inner_parts_color, self.appearance_mode),
fill=ThemeManager.single_color(inner_parts_color, self.appearance_mode))
# set text_label bg color (label color)
if self.text_label is not None:
self.text_label.configure(bg=ThemeManager.single_color(inner_parts_color, self.appearance_mode))
# set image_label bg color (image bg color)
if self.image_label is not None:
self.image_label.configure(bg=ThemeManager.single_color(inner_parts_color, self.appearance_mode))
def click_animation(self):
if self.click_animation_running:
self.on_enter()
def clicked(self, event=0):
if self.function is not None:
if self.state is not tkinter.DISABLED:
# click animation: change color with .on_leave() and back to normal after 100ms with click_animation()
self.on_leave()
self.click_animation_running = True
self.after(100, self.click_animation)
self.function()

View File

@ -1,349 +0,0 @@
import tkinter
import sys
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..settings import Settings
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
class CTkCheckBox(CTkBaseClass):
""" tkinter custom checkbox with border, rounded corners and hover effect """
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
hover_color="default_theme",
border_color="default_theme",
border_width="default_theme",
checkmark_color="default_theme",
width=24,
height=24,
corner_radius="default_theme",
text_font="default_theme",
text_color="default_theme",
text="CTkCheckBox",
text_color_disabled="default_theme",
hover=True,
command=None,
state=tkinter.NORMAL,
onvalue=1,
offvalue=0,
variable=None,
textvariable=None,
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self.border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
self.checkmark_color = ThemeManager.theme["color"]["checkmark"] if checkmark_color == "default_theme" else checkmark_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["checkbox_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["checkbox_border_width"] if border_width == "default_theme" else border_width
# text
self.text = text
self.text_label = None
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# callback and hover functionality
self.function = command
self.state = state
self.hover = hover
self.check_state = False
self.onvalue = onvalue
self.offvalue = offvalue
self.variable: tkinter.Variable = variable
self.variable_callback_blocked = False
self.textvariable = textvariable
self.variable_callback_name = None
# configure grid system (1x3)
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,
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, rowspan=1)
self.draw_engine = DrawEngine(self.canvas)
if self.hover is True:
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.toggle)
self.canvas.bind("<Button-1>", self.toggle)
# set select state according to variable
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
if self.variable.get() == self.onvalue:
self.select(from_variable_callback=True)
elif self.variable.get() == self.offvalue:
self.deselect(from_variable_callback=True)
self.set_cursor()
self.draw() # initial draw
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.bg_canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def destroy(self):
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
super().destroy()
def draw(self, no_color_updates=False):
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 self.check_state is True:
self.draw_engine.draw_checkmark(self.apply_widget_scaling(self.current_width),
self.apply_widget_scaling(self.current_height),
self.apply_widget_scaling(self.current_height * 0.58))
else:
self.canvas.delete("checkmark")
self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
if self.check_state is True:
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode))
self.canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode))
if "create_line" in self.canvas.gettags("checkmark"):
self.canvas.itemconfig("checkmark", fill=ThemeManager.single_color(self.checkmark_color, self.appearance_mode))
else:
self.canvas.itemconfig("checkmark", fill=ThemeManager.single_color(self.checkmark_color, self.appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self.bg_color, self.appearance_mode),
fill=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self.border_color, self.appearance_mode),
fill=ThemeManager.single_color(self.border_color, self.appearance_mode))
if self.text_label is None:
self.text_label = tkinter.Label(master=self,
bd=0,
text=self.text,
justify=tkinter.LEFT,
font=self.apply_font_scaling(self.text_font))
self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self.text_label["anchor"] = "w"
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self.appearance_mode)))
else:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode))
self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.set_text(self.text)
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw()
if "text" in kwargs:
self.set_text(kwargs["text"])
del kwargs["text"]
if "state" in kwargs:
self.state = kwargs["state"]
self.set_cursor()
require_redraw = True
del kwargs["state"]
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
require_redraw = True
del kwargs["fg_color"]
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "hover_color" in kwargs:
self.hover_color = kwargs["hover_color"]
require_redraw = True
del kwargs["hover_color"]
if "text_color" in kwargs:
self.text_color = kwargs["text_color"]
require_redraw = True
del kwargs["text_color"]
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
require_redraw = True
del kwargs["border_color"]
if "command" in kwargs:
self.function = kwargs["command"]
del kwargs["command"]
if "variable" in kwargs:
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
self.variable = kwargs["variable"]
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
if self.variable.get() == self.onvalue:
self.select(from_variable_callback=True)
elif self.variable.get() == self.offvalue:
self.deselect(from_variable_callback=True)
else:
self.variable = None
del kwargs["variable"]
super().configure(*args, **kwargs)
if require_redraw:
self.draw()
def set_cursor(self):
if Settings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
def set_text(self, text):
self.text = text
if self.text_label is not None:
self.text_label.configure(text=self.text)
else:
sys.stderr.write("ERROR (CTkButton): Cant change text because checkbox has no text.")
def on_enter(self, event=0):
if self.hover is True and self.state == tkinter.NORMAL:
if self.check_state is True:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.hover_color, self.appearance_mode),
outline=ThemeManager.single_color(self.hover_color, self.appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.hover_color, self.appearance_mode),
outline=ThemeManager.single_color(self.hover_color, self.appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.hover_color, self.appearance_mode),
outline=ThemeManager.single_color(self.hover_color, self.appearance_mode))
def on_leave(self, event=0):
if self.hover is True:
if self.check_state is True:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.bg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self.appearance_mode),
outline=ThemeManager.single_color(self.border_color, self.appearance_mode))
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
if self.variable.get() == self.onvalue:
self.select(from_variable_callback=True)
elif self.variable.get() == self.offvalue:
self.deselect(from_variable_callback=True)
def toggle(self, event=0):
if self.state == tkinter.NORMAL:
if self.check_state is True:
self.check_state = False
self.draw()
else:
self.check_state = True
self.draw()
if self.function is not None:
self.function()
if self.variable is not None:
self.variable_callback_blocked = True
self.variable.set(self.onvalue if self.check_state is True else self.offvalue)
self.variable_callback_blocked = False
def select(self, from_variable_callback=False):
self.check_state = True
self.draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.onvalue)
self.variable_callback_blocked = False
if self.function is not None:
try:
self.function()
except:
pass
def deselect(self, from_variable_callback=False):
self.check_state = False
self.draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.offvalue)
self.variable_callback_blocked = False
if self.function is not None:
try:
self.function()
except:
pass
def get(self):
return self.onvalue if self.check_state is True else self.offvalue

View File

@ -1,204 +0,0 @@
import tkinter
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
class CTkEntry(CTkBaseClass):
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
text_color="default_theme",
placeholder_text_color="default_theme",
text_font="default_theme",
placeholder_text=None,
corner_radius="default_theme",
border_width="default_theme",
border_color="default_theme",
width=120,
height=30,
state=tkinter.NORMAL,
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
if "master" in kwargs:
super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs["master"])
del kwargs["master"]
else:
super().__init__(*args, bg_color=bg_color, width=width, height=height)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.placeholder_text_color = ThemeManager.theme["color"]["entry_placeholder_text"] if placeholder_text_color == "default_theme" else placeholder_text_color
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self.border_color = ThemeManager.theme["color"]["entry_border"] if border_color == "default_theme" else border_color
self.placeholder_text = placeholder_text
self.placeholder_text_active = False
self.pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back
self.state = state
self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.canvas.grid(column=0, row=0, sticky="we")
self.draw_engine = DrawEngine(self.canvas)
self.entry = tkinter.Entry(master=self,
bd=0,
width=1,
highlightthickness=0,
font=self.apply_font_scaling(self.text_font),
state=self.state,
**kwargs)
self.entry.grid(column=0, row=0, sticky="we",
padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6))
super().bind('<Configure>', self.update_dimensions_event)
self.entry.bind('<FocusOut>', self.set_placeholder)
self.entry.bind('<FocusIn>', self.clear_placeholder)
self.draw()
self.set_placeholder()
def set_scaling(self, *args, **kwargs):
super().set_scaling( *args, **kwargs)
self.entry.configure(font=self.apply_font_scaling(self.text_font))
self.entry.grid(column=0, row=0, sticky="we",
padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6))
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def set_placeholder(self, event=None):
if self.placeholder_text is not None:
if not self.placeholder_text_active and self.entry.get() == "":
self.placeholder_text_active = True
self.pre_placeholder_arguments = {"show": self.entry.cget("show")}
self.entry.config(fg=ThemeManager.single_color(self.placeholder_text_color, self.appearance_mode), show="")
self.entry.delete(0, tkinter.END)
self.entry.insert(0, self.placeholder_text)
def clear_placeholder(self, event=None):
if self.placeholder_text_active:
self.placeholder_text_active = False
self.entry.config(fg=ThemeManager.single_color(self.text_color, self.appearance_mode))
self.entry.delete(0, tkinter.END)
for argument, value in self.pre_placeholder_arguments.items():
self.entry[argument] = value
def draw(self, no_color_updates=False):
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
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:
if ThemeManager.single_color(self.fg_color, self.appearance_mode) is not None:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
self.entry.configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode),
disabledbackground=ThemeManager.single_color(self.fg_color, self.appearance_mode),
highlightcolor=ThemeManager.single_color(self.fg_color, self.appearance_mode),
fg=ThemeManager.single_color(self.text_color, self.appearance_mode),
disabledforeground=ThemeManager.single_color(self.text_color, self.appearance_mode),
insertbackground=ThemeManager.single_color(self.text_color, self.appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.bg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.entry.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode),
disabledbackground=ThemeManager.single_color(self.bg_color, self.appearance_mode),
highlightcolor=ThemeManager.single_color(self.bg_color, self.appearance_mode),
fg=ThemeManager.single_color(self.text_color, self.appearance_mode),
disabledforeground=ThemeManager.single_color(self.text_color, self.appearance_mode),
insertbackground=ThemeManager.single_color(self.text_color, self.appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self.appearance_mode),
outline=ThemeManager.single_color(self.border_color, self.appearance_mode))
if self.placeholder_text_active:
self.entry.config(fg=ThemeManager.single_color(self.placeholder_text_color, self.appearance_mode))
def bind(self, *args, **kwargs):
self.entry.bind(*args, **kwargs)
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw() at the end
if "state" in kwargs:
self.state = kwargs["state"]
self.entry.configure(state=self.state)
del kwargs["state"]
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
del kwargs["fg_color"]
require_redraw = True
if "text_color" in kwargs:
self.text_color = kwargs["text_color"]
del kwargs["text_color"]
require_redraw = True
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
del kwargs["border_color"]
require_redraw = True
if "corner_radius" in kwargs:
self.corner_radius = kwargs["corner_radius"]
if self.corner_radius * 2 > self.current_height:
self.corner_radius = self.current_height / 2
elif self.corner_radius * 2 > self.current_width:
self.corner_radius = self.current_width / 2
self.entry.grid(column=0, row=0, sticky="we", padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6))
del kwargs["corner_radius"]
require_redraw = True
if "placeholder_text" in kwargs:
pass
self.entry.configure(*args, **kwargs)
if require_redraw is True:
self.draw()
def delete(self, *args, **kwargs):
self.entry.delete(*args, **kwargs)
self.set_placeholder()
def insert(self, *args, **kwargs):
self.clear_placeholder()
return self.entry.insert(*args, **kwargs)
def get(self):
if self.placeholder_text_active:
return ""
else:
return self.entry.get()

View File

@ -1,136 +0,0 @@
import tkinter
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..settings import Settings
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
class CTkFrame(CTkBaseClass):
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
border_color="default_theme",
border_width="default_theme",
corner_radius="default_theme",
width=200,
height=200,
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color
if fg_color == "default_theme":
if isinstance(self.master, CTkFrame):
if self.master.fg_color == ThemeManager.theme["color"]["frame_low"]:
self.fg_color = ThemeManager.theme["color"]["frame_high"]
else:
self.fg_color = ThemeManager.theme["color"]["frame_low"]
else:
self.fg_color = ThemeManager.theme["color"]["frame_low"]
else:
self.fg_color = fg_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.draw_engine = DrawEngine(self.canvas)
self.bind('<Configure>', self.update_dimensions_event)
self.draw()
def winfo_children(self):
""" winfo_children of CTkFrame without self.canvas widget,
because it's not a child but part of the CTkFrame itself """
child_widgets = super().winfo_children()
try:
child_widgets.remove(self.canvas)
return child_widgets
except ValueError:
return child_widgets
def set_scaling(self, *args, **kwargs):
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.draw()
def draw(self, no_color_updates=False):
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 no_color_updates is False or requires_recoloring:
if self.fg_color is None:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.bg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self.appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self.appearance_mode),
outline=ThemeManager.single_color(self.border_color, self.appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.canvas.tag_lower("inner_parts")
self.canvas.tag_lower("border_parts")
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw() at the end
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
require_redraw = True
del kwargs["fg_color"]
# check if CTk widgets are children of the frame and change their bg_color to new frame fg_color
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self.fg_color)
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
require_redraw = True
del kwargs["border_color"]
if "corner_radius" in kwargs:
self.corner_radius = kwargs["corner_radius"]
require_redraw = True
del kwargs["corner_radius"]
if "border_width" in kwargs:
self.border_width = kwargs["border_width"]
require_redraw = True
del kwargs["border_width"]
super().configure(*args, **kwargs)
if require_redraw:
self.draw()

View File

@ -1,128 +0,0 @@
import tkinter
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
class CTkLabel(CTkBaseClass):
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
text_color="default_theme",
corner_radius="default_theme",
width=120,
height=25,
text="CTkLabel",
text_font="default_theme",
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
if "master" in kwargs:
super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs["master"])
del kwargs["master"]
else:
super().__init__(*args, bg_color=bg_color, width=width, height=height)
# color
self.fg_color = ThemeManager.theme["color"]["label"] if fg_color == "default_theme" else fg_color
if self.fg_color is None:
self.fg_color = self.bg_color
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["label_corner_radius"] if corner_radius == "default_theme" else corner_radius
# text
self.text = text
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas)
self.text_label = tkinter.Label(master=self,
highlightthickness=0,
bd=0,
text=self.text,
font=self.apply_font_scaling(self.text_font),
**kwargs)
self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius))
self.bind('<Configure>', self.update_dimensions_event)
self.draw()
def set_scaling(self, *args, **kwargs):
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.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius))
self.draw()
def draw(self, no_color_updates=False):
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),
0)
if no_color_updates is False or requires_recoloring:
if ThemeManager.single_color(self.fg_color, self.appearance_mode) is not None:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode),
bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.bg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode),
bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw() at the end
if "text" in kwargs:
self.set_text(kwargs["text"])
del kwargs["text"]
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
require_redraw = True
del kwargs["fg_color"]
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "text_color" in kwargs:
self.text_color = kwargs["text_color"]
require_redraw = True
del kwargs["text_color"]
self.text_label.configure(*args, **kwargs)
if require_redraw:
self.draw()
def set_text(self, text):
self.text = text
self.text_label.configure(text=self.text, width=len(self.text))

View File

@ -1,184 +0,0 @@
import tkinter
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..draw_engine import DrawEngine
from ..settings import Settings
from .widget_base_class import CTkBaseClass
class CTkProgressBar(CTkBaseClass):
""" tkinter custom progressbar, always horizontal, values are from 0 to 1 """
def __init__(self, *args,
variable=None,
bg_color=None,
border_color="default_theme",
fg_color="default_theme",
progress_color="default_theme",
corner_radius="default_theme",
width=None,
height=None,
border_width="default_theme",
orient="horizontal",
**kwargs):
# set default dimensions according to orientation
if width is None:
if orient.lower() == "vertical":
width = 8
else:
width = 200
if height is None:
if orient.lower() == "vertical":
height = 200
else:
height = 8
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.border_color = ThemeManager.theme["color"]["progressbar_border"] if border_color == "default_theme" else border_color
self.fg_color = ThemeManager.theme["color"]["progressbar"] if fg_color == "default_theme" else fg_color
self.progress_color = ThemeManager.theme["color"]["progressbar_progress"] if progress_color == "default_theme" else progress_color
# control variable
self.variable = variable
self.variable_callback_blocked = False
self.variable_callback_name = None
# shape
self.corner_radius = ThemeManager.theme["shape"]["progressbar_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width
self.value = 0.5
self.orient = orient
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas)
# Each time an item is resized due to pack position mode, the binding Configure is called on the widget
self.bind('<Configure>', self.update_dimensions_event)
self.draw() # initial draw
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.variable_callback_blocked = True
self.set(self.variable.get(), from_variable_callback=True)
self.variable_callback_blocked = False
def set_scaling(self, *args, **kwargs):
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.draw()
def destroy(self):
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
super().destroy()
def draw(self, no_color_updates=False):
if self.orient.lower() == "horizontal":
orientation = "w"
elif self.orient.lower() == "vertical":
orientation = "s"
else:
orientation = "w"
requires_recoloring = self.draw_engine.draw_rounded_progress_bar_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),
self.value,
orientation)
if no_color_updates is False or requires_recoloring:
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self.appearance_mode),
outline=ThemeManager.single_color(self.border_color, self.appearance_mode))
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
self.canvas.itemconfig("progress_parts",
fill=ThemeManager.single_color(self.progress_color, self.appearance_mode),
outline=ThemeManager.single_color(self.progress_color, self.appearance_mode))
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw() at the end
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
del kwargs["fg_color"]
require_redraw = True
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
del kwargs["border_color"]
require_redraw = True
if "progress_color" in kwargs:
self.progress_color = kwargs["progress_color"]
del kwargs["progress_color"]
require_redraw = True
if "border_width" in kwargs:
self.border_width = kwargs["border_width"]
del kwargs["border_width"]
require_redraw = True
if "variable" in kwargs:
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
self.variable = kwargs["variable"]
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.set(self.variable.get(), from_variable_callback=True)
else:
self.variable = None
del kwargs["variable"]
super().configure(*args, **kwargs)
if require_redraw is True:
self.draw()
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
self.set(self.variable.get(), from_variable_callback=True)
def set(self, value, from_variable_callback=False):
self.value = value
if self.value > 1:
self.value = 1
elif self.value < 0:
self.value = 0
self.draw(no_color_updates=True)
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(round(self.value) if isinstance(self.variable, tkinter.IntVar) else self.value)
self.variable_callback_blocked = False

View File

@ -1,302 +0,0 @@
import tkinter
import sys
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..settings import Settings
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
class CTkRadioButton(CTkBaseClass):
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
hover_color="default_theme",
border_color="default_theme",
border_width_unchecked="default_theme",
border_width_checked="default_theme",
width=22,
height=22,
corner_radius="default_theme",
text_font="default_theme",
text_color="default_theme",
text="CTkRadioButton",
text_color_disabled="default_theme",
hover=True,
command=None,
state=tkinter.NORMAL,
value=0,
variable=None,
textvariable=None,
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self.border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["radiobutton_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width_unchecked = ThemeManager.theme["shape"]["radiobutton_border_width_unchecked"] if border_width_unchecked == "default_theme" else border_width_unchecked
self.border_width_checked = ThemeManager.theme["shape"]["radiobutton_border_width_checked"] if border_width_checked == "default_theme" else border_width_checked
self.border_width = self.border_width_unchecked
# text
self.text = text
self.text_label = None
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# callback and control variables
self.function = command
self.state = state
self.hover = hover
self.check_state = False
self.value = value
self.variable: tkinter.Variable = variable
self.variable_callback_blocked = False
self.textvariable = textvariable
self.variable_callback_name = None
# configure grid system (3x1)
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.bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1)
self.draw_engine = DrawEngine(self.canvas)
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.invoke)
self.canvas.bind("<Button-1>", self.invoke)
self.set_cursor()
self.draw() # initial draw
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
if self.variable.get() == self.value:
self.select(from_variable_callback=True)
else:
self.deselect(from_variable_callback=True)
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.bg_canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def destroy(self):
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
super().destroy()
def draw(self, no_color_updates=False):
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))
self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
if self.check_state is False:
self.canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self.border_color, self.appearance_mode),
fill=ThemeManager.single_color(self.border_color, self.appearance_mode))
else:
self.canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode))
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self.bg_color, self.appearance_mode),
fill=ThemeManager.single_color(self.bg_color, self.appearance_mode))
if self.text_label is None:
self.text_label = tkinter.Label(master=self,
bd=0,
text=self.text,
justify=tkinter.LEFT,
font=self.apply_font_scaling(self.text_font))
self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self.text_label["anchor"] = "w"
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color_disabled, self.appearance_mode))
else:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode))
self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.set_text(self.text)
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw()
if "text" in kwargs:
self.set_text(kwargs["text"])
del kwargs["text"]
if "state" in kwargs:
self.state = kwargs["state"]
self.set_cursor()
require_redraw = True
del kwargs["state"]
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
require_redraw = True
del kwargs["fg_color"]
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "hover_color" in kwargs:
self.hover_color = kwargs["hover_color"]
require_redraw = True
del kwargs["hover_color"]
if "text_color" in kwargs:
self.text_color = kwargs["text_color"]
require_redraw = True
del kwargs["text_color"]
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
require_redraw = True
del kwargs["border_color"]
if "border_width" in kwargs:
self.border_width = kwargs["border_width"]
require_redraw = True
del kwargs["border_width"]
if "command" in kwargs:
self.function = kwargs["command"]
del kwargs["command"]
if "variable" in kwargs:
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
self.variable = kwargs["variable"]
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
if self.variable.get() == self.value:
self.select(from_variable_callback=True)
else:
self.deselect(from_variable_callback=True)
else:
self.variable = None
del kwargs["variable"]
super().configure(*args, **kwargs)
if require_redraw:
self.draw()
def set_cursor(self):
if Settings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
def set_text(self, text):
self.text = text
if self.text_label is not None:
self.text_label.configure(text=self.text)
else:
sys.stderr.write("ERROR (CTkButton): Cant change text because radiobutton has no text.")
def on_enter(self, event=0):
if self.hover is True and self.state == tkinter.NORMAL:
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.hover_color, self.appearance_mode),
outline=ThemeManager.single_color(self.hover_color, self.appearance_mode))
def on_leave(self, event=0):
if self.hover is True:
if self.check_state is True:
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
else:
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self.appearance_mode),
outline=ThemeManager.single_color(self.border_color, self.appearance_mode))
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
if self.variable.get() == self.value:
self.select(from_variable_callback=True)
else:
self.deselect(from_variable_callback=True)
def invoke(self, event=0):
if self.state == tkinter.NORMAL:
if self.check_state is False:
self.check_state = True
self.select()
if self.function is not None:
try:
self.function()
except:
pass
def select(self, from_variable_callback=False):
self.check_state = True
self.border_width = self.border_width_checked
self.draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.value)
self.variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
self.check_state = False
self.border_width = self.border_width_unchecked
self.draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set("")
self.variable_callback_blocked = False

View File

@ -1,316 +0,0 @@
import tkinter
import sys
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..settings import Settings
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
class CTkSlider(CTkBaseClass):
""" tkinter custom slider, always horizontal """
def __init__(self, *args,
bg_color=None,
border_color=None,
fg_color="default_theme",
progress_color="default_theme",
button_color="default_theme",
button_hover_color="default_theme",
from_=0,
to=1,
number_of_steps=None,
width=None,
height=None,
corner_radius="default_theme",
button_corner_radius="default_theme",
border_width="default_theme",
button_length="default_theme",
command=None,
variable=None,
orient="horizontal",
**kwargs):
# set default dimensions according to orientation
if width is None:
if orient.lower() == "vertical":
width = 16
else:
width = 200
if height is None:
if orient.lower() == "vertical":
height = 200
else:
height = 16
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.border_color = border_color
self.fg_color = ThemeManager.theme["color"]["slider"] if fg_color == "default_theme" else fg_color
self.progress_color = ThemeManager.theme["color"]["slider_progress"] if progress_color == "default_theme" else progress_color
self.button_color = ThemeManager.theme["color"]["slider_button"] if button_color == "default_theme" else button_color
self.button_hover_color = ThemeManager.theme["color"]["slider_button_hover"] if button_hover_color == "default_theme" else button_hover_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["slider_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.button_corner_radius = ThemeManager.theme["shape"]["slider_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius
self.border_width = ThemeManager.theme["shape"]["slider_border_width"] if border_width == "default_theme" else border_width
self.button_length = ThemeManager.theme["shape"]["slider_button_length"] if button_length == "default_theme" else button_length
self.value = 0.5 # initial value of slider in percent
self.orient = orient
self.hover_state = False
self.from_ = from_
self.to = to
self.number_of_steps = number_of_steps
self.output_value = self.from_ + (self.value * (self.to - self.from_))
if self.corner_radius < self.button_corner_radius:
self.corner_radius = self.button_corner_radius
# callback and control variables
self.callback_function = command
self.variable: tkinter.Variable = variable
self.variable_callback_blocked = False
self.variable_callback_name = None
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas)
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.clicked)
self.canvas.bind("<B1-Motion>", self.clicked)
# Each time an item is resized due to pack position mode, the binding Configure is called on the widget
self.bind('<Configure>', self.update_dimensions_event)
self.set_cursor()
self.draw() # initial draw
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.variable_callback_blocked = True
self.set(self.variable.get(), from_variable_callback=True)
self.variable_callback_blocked = False
def set_scaling(self, *args, **kwargs):
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.draw()
def destroy(self):
# remove variable_callback from variable callbacks if variable exists
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
super().destroy()
def set_cursor(self):
if Settings.cursor_manipulation_enabled:
if sys.platform == "darwin":
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win"):
self.configure(cursor="hand2")
def draw(self, no_color_updates=False):
if self.orient.lower() == "horizontal":
orientation = "w"
elif self.orient.lower() == "vertical":
orientation = "s"
else:
orientation = "w"
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(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),
self.apply_widget_scaling(self.button_length),
self.apply_widget_scaling(self.button_corner_radius),
self.value, orientation)
if no_color_updates is False or requires_recoloring:
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
if self.border_color is None:
self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.bg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self.appearance_mode))
else:
self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.border_color, self.appearance_mode),
outline=ThemeManager.single_color(self.border_color, self.appearance_mode))
self.canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
if self.progress_color is None:
self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
else:
self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.progress_color, self.appearance_mode),
outline=ThemeManager.single_color(self.progress_color, self.appearance_mode))
self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_color, self.appearance_mode),
outline=ThemeManager.single_color(self.button_color, self.appearance_mode))
def clicked(self, event=None):
if self.orient.lower() == "horizontal":
self.value = (event.x / self.current_width) / self.widget_scaling
else:
self.value = 1 - (event.y / self.current_height) / self.widget_scaling
if self.value > 1:
self.value = 1
if self.value < 0:
self.value = 0
self.output_value = self.round_to_step_size(self.from_ + (self.value * (self.to - self.from_)))
self.value = (self.output_value - self.from_) / (self.to - self.from_)
self.draw(no_color_updates=False)
if self.callback_function is not None:
self.callback_function(self.output_value)
if self.variable is not None:
self.variable_callback_blocked = True
self.variable.set(round(self.output_value) if isinstance(self.variable, tkinter.IntVar) else self.output_value)
self.variable_callback_blocked = False
def on_enter(self, event=0):
self.hover_state = True
self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_hover_color, self.appearance_mode),
outline=ThemeManager.single_color(self.button_hover_color, self.appearance_mode))
def on_leave(self, event=0):
self.hover_state = False
self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_color, self.appearance_mode),
outline=ThemeManager.single_color(self.button_color, self.appearance_mode))
def round_to_step_size(self, value):
if self.number_of_steps is not None:
step_size = (self.to - self.from_) / self.number_of_steps
value = self.to - (round((self.to - value) / step_size) * step_size)
return value
else:
return value
def get(self):
return self.output_value
def set(self, output_value, from_variable_callback=False):
if self.from_ < self.to:
if output_value > self.to:
output_value = self.to
elif output_value < self.from_:
output_value = self.from_
else:
if output_value < self.to:
output_value = self.to
elif output_value > self.from_:
output_value = self.from_
self.output_value = self.round_to_step_size(output_value)
self.value = (self.output_value - self.from_) / (self.to - self.from_)
self.draw(no_color_updates=False)
if self.callback_function is not None:
self.callback_function(self.output_value)
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(round(self.output_value) if isinstance(self.variable, tkinter.IntVar) else self.output_value)
self.variable_callback_blocked = False
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
self.set(self.variable.get(), from_variable_callback=True)
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw() at the end
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
require_redraw = True
del kwargs["fg_color"]
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "progress_color" in kwargs:
if kwargs["progress_color"] is None:
self.progress_color = self.fg_color
else:
self.progress_color = kwargs["progress_color"]
require_redraw = True
del kwargs["progress_color"]
if "button_color" in kwargs:
self.button_color = kwargs["button_color"]
require_redraw = True
del kwargs["button_color"]
if "button_hover_color" in kwargs:
self.button_hover_color = kwargs["button_hover_color"]
require_redraw = True
del kwargs["button_hover_color"]
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
require_redraw = True
del kwargs["border_color"]
if "border_width" in kwargs:
self.border_width = kwargs["border_width"]
require_redraw = True
del kwargs["border_width"]
if "from_" in kwargs:
self.from_ = kwargs["from_"]
del kwargs["from_"]
if "to" in kwargs:
self.to = kwargs["to"]
del kwargs["to"]
if "number_of_steps" in kwargs:
self.number_of_steps = kwargs["number_of_steps"]
del kwargs["number_of_steps"]
if "command" in kwargs:
self.callback_function = kwargs["command"]
del kwargs["command"]
if "variable" in kwargs:
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
self.variable = kwargs["variable"]
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.set(self.variable.get(), from_variable_callback=True)
else:
self.variable = None
del kwargs["variable"]
super().configure(*args, **kwargs)
if require_redraw:
self.draw()

View File

@ -1,350 +0,0 @@
import tkinter
import sys
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..settings import Settings
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
class CTkSwitch(CTkBaseClass):
def __init__(self, *args,
text="CTkSwitch",
text_font="default_theme",
text_color="default_theme",
text_color_disabled="default_theme",
bg_color=None,
border_color=None,
fg_color="default_theme",
progress_color="default_theme",
button_color="default_theme",
button_hover_color="default_theme",
width=36,
height=18,
corner_radius="default_theme",
# button_corner_radius="default_theme",
border_width="default_theme",
button_length="default_theme",
command=None,
onvalue=1,
offvalue=0,
variable=None,
textvariable=None,
state=tkinter.NORMAL,
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.border_color = border_color
self.fg_color = ThemeManager.theme["color"]["switch"] if fg_color == "default_theme" else fg_color
self.progress_color = ThemeManager.theme["color"]["switch_progress"] if progress_color == "default_theme" else progress_color
self.button_color = ThemeManager.theme["color"]["switch_button"] if button_color == "default_theme" else button_color
self.button_hover_color = ThemeManager.theme["color"]["switch_button_hover"] if button_hover_color == "default_theme" else button_hover_color
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
# text
self.text = text
self.text_label = None
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# shape
self.corner_radius = ThemeManager.theme["shape"]["switch_corner_radius"] if corner_radius == "default_theme" else corner_radius
# self.button_corner_radius = ThemeManager.theme["shape"]["switch_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius
self.border_width = ThemeManager.theme["shape"]["switch_border_width"] if border_width == "default_theme" else border_width
self.button_length = ThemeManager.theme["shape"]["switch_button_length"] if button_length == "default_theme" else button_length
self.hover_state = False
self.check_state = False # True if switch is activated
self.state = state
self.onvalue = onvalue
self.offvalue = offvalue
# if self.corner_radius < self.button_corner_radius:
# self.corner_radius = self.button_corner_radius
# callback and control variables
self.callback_function = command
self.variable: tkinter.Variable = variable
self.variable_callback_blocked = False
self.variable_callback_name = None
self.textvariable = textvariable
# configure grid system (3x1)
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=0)
self.bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas)
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.toggle)
self.set_cursor()
self.draw() # initial draw
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
if self.variable.get() == self.onvalue:
self.select(from_variable_callback=True)
elif self.variable.get() == self.offvalue:
self.deselect(from_variable_callback=True)
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.bg_canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def destroy(self):
# remove variable_callback from variable callbacks if variable exists
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
super().destroy()
def set_cursor(self):
if Settings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
else:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
def draw(self, no_color_updates=False):
if self.check_state is True:
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(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),
self.apply_widget_scaling(self.button_length),
self.apply_widget_scaling(self.corner_radius),
1, "w")
else:
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(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),
self.apply_widget_scaling(self.button_length),
self.apply_widget_scaling(self.corner_radius),
0, "w")
if no_color_updates is False or requires_recoloring:
self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
if self.border_color is None:
self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.bg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self.appearance_mode))
else:
self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.border_color, self.appearance_mode),
outline=ThemeManager.single_color(self.border_color, self.appearance_mode))
self.canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
if self.progress_color is None:
self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode))
else:
self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.progress_color, self.appearance_mode),
outline=ThemeManager.single_color(self.progress_color, self.appearance_mode))
self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_color, self.appearance_mode),
outline=ThemeManager.single_color(self.button_color, self.appearance_mode))
if self.text_label is None:
self.text_label = tkinter.Label(master=self,
bd=0,
text=self.text,
justify=tkinter.LEFT,
font=self.apply_font_scaling(self.text_font))
self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self.text_label["anchor"] = "w"
if self.textvariable is not None:
self.text_label.configure(textvariable=self.textvariable)
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self.appearance_mode)))
else:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode))
self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.set_text(self.text)
def set_text(self, text):
self.text = text
if self.text_label is not None:
self.text_label.configure(text=self.text)
def toggle(self, event=None):
if self.state is not tkinter.DISABLED:
if self.check_state is True:
self.check_state = False
else:
self.check_state = True
self.draw(no_color_updates=True)
if self.callback_function is not None:
self.callback_function()
if self.variable is not None:
self.variable_callback_blocked = True
self.variable.set(self.onvalue if self.check_state is True else self.offvalue)
self.variable_callback_blocked = False
def select(self, from_variable_callback=False):
if self.state is not tkinter.DISABLED or from_variable_callback:
self.check_state = True
self.draw(no_color_updates=True)
if self.callback_function is not None:
self.callback_function()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.onvalue)
self.variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
if self.state is not tkinter.DISABLED or from_variable_callback:
self.check_state = False
self.draw(no_color_updates=True)
if self.callback_function is not None:
self.callback_function()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.offvalue)
self.variable_callback_blocked = False
def get(self):
return self.onvalue if self.check_state is True else self.offvalue
def on_enter(self, event=0):
self.hover_state = True
if self.state is not tkinter.DISABLED:
self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_hover_color, self.appearance_mode),
outline=ThemeManager.single_color(self.button_hover_color, self.appearance_mode))
def on_leave(self, event=0):
self.hover_state = False
self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_color, self.appearance_mode),
outline=ThemeManager.single_color(self.button_color, self.appearance_mode))
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
if self.variable.get() == self.onvalue:
self.select(from_variable_callback=True)
elif self.variable.get() == self.offvalue:
self.deselect(from_variable_callback=True)
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw() at the end
if "state" in kwargs:
self.state = kwargs["state"]
self.set_cursor()
require_redraw = True
del kwargs["state"]
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
require_redraw = True
del kwargs["fg_color"]
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "progress_color" in kwargs:
if kwargs["progress_color"] is None:
self.progress_color = self.fg_color
else:
self.progress_color = kwargs["progress_color"]
require_redraw = True
del kwargs["progress_color"]
if "button_color" in kwargs:
self.button_color = kwargs["button_color"]
require_redraw = True
del kwargs["button_color"]
if "button_hover_color" in kwargs:
self.button_hover_color = kwargs["button_hover_color"]
require_redraw = True
del kwargs["button_hover_color"]
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
require_redraw = True
del kwargs["border_color"]
if "border_width" in kwargs:
self.border_width = kwargs["border_width"]
require_redraw = True
del kwargs["border_width"]
if "command" in kwargs:
self.callback_function = kwargs["command"]
del kwargs["command"]
if "textvariable" in kwargs:
self.text_label.configure(textvariable=kwargs["textvariable"])
del kwargs["textvariable"]
if "variable" in kwargs:
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
self.variable = kwargs["variable"]
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
if self.variable.get() == self.onvalue:
self.select(from_variable_callback=True)
elif self.variable.get() == self.offvalue:
self.deselect(from_variable_callback=True)
else:
self.variable = None
del kwargs["variable"]
super().configure(*args, **kwargs)
if require_redraw:
self.draw()

View File

@ -1,213 +0,0 @@
import tkinter
import tkinter.ttk as ttk
import copy
import re
from typing import Callable, Union, TypedDict
from ..windows.ctk_tk import CTk
from ..windows.ctk_toplevel import CTkToplevel
from ..appearance_mode_tracker import AppearanceModeTracker
from ..scaling_tracker import ScalingTracker
from ..theme_manager import ThemeManager
class CTkBaseClass(tkinter.Frame):
def __init__(self, *args, bg_color=None, width, height, **kwargs):
super().__init__(*args, width=width, height=height, **kwargs) # set desired size of underlying tkinter.Frame
self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color
self.current_width = width # current_width and current_height in pixel, represent current size of the widget (not the desired size by init)
self.current_height = height # current_width and current_height are independent of the scale
self.desired_width = width
self.desired_height = height
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
ScalingTracker.add_widget(self.set_scaling, self)
self.widget_scaling = ScalingTracker.get_widget_scaling(self)
self.spacing_scaling = ScalingTracker.get_spacing_scaling(self)
super().configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
# save latest geometry function and kwargs
class GeometryCallDict(TypedDict):
function: Callable
kwargs: dict
self.last_geometry_manager_call: Union[GeometryCallDict, None] = None
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self.set_appearance_mode, self)
self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
super().configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
# overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget too
if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame)) and not isinstance(self.master, (CTkBaseClass, CTk, CTkToplevel)):
master_old_configure = self.master.config
def new_configure(*args, **kwargs):
if "bg" in kwargs:
self.configure(bg_color=kwargs["bg"])
elif "background" in kwargs:
self.configure(bg_color=kwargs["background"])
# args[0] is dict when attribute gets changed by widget[<attribute>] syntax
elif len(args) > 0 and type(args[0]) == dict:
if "bg" in args[0]:
self.configure(bg_color=args[0]["bg"])
elif "background" in args[0]:
self.configure(bg_color=args[0]["background"])
master_old_configure(*args, **kwargs)
self.master.config = new_configure
self.master.configure = new_configure
def destroy(self):
AppearanceModeTracker.remove(self.set_appearance_mode)
super().destroy()
def place(self, **kwargs):
self.last_geometry_manager_call = {"function": super().place, "kwargs": kwargs}
super().place(**self.apply_argument_scaling(kwargs))
def pack(self, **kwargs):
self.last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs}
super().pack(**self.apply_argument_scaling(kwargs))
def grid(self, **kwargs):
self.last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs}
super().grid(**self.apply_argument_scaling(kwargs))
def apply_argument_scaling(self, kwargs: dict) -> dict:
scaled_kwargs = copy.copy(kwargs)
if "pady" in scaled_kwargs:
if isinstance(scaled_kwargs["pady"], (int, float, str)):
scaled_kwargs["pady"] = self.apply_spacing_scaling(scaled_kwargs["pady"])
elif isinstance(scaled_kwargs["pady"], tuple):
scaled_kwargs["pady"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["pady"]])
if "padx" in kwargs:
if isinstance(scaled_kwargs["padx"], (int, float, str)):
scaled_kwargs["padx"] = self.apply_spacing_scaling(scaled_kwargs["padx"])
elif isinstance(scaled_kwargs["padx"], tuple):
scaled_kwargs["padx"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["padx"]])
if "x" in scaled_kwargs:
scaled_kwargs["x"] = self.apply_spacing_scaling(scaled_kwargs["x"])
if "y" in scaled_kwargs:
scaled_kwargs["y"] = self.apply_spacing_scaling(scaled_kwargs["y"])
return scaled_kwargs
def config(self, *args, **kwargs):
self.configure(*args, **kwargs)
def configure(self, *args, **kwargs):
""" basic configure with bg_color support, to be overridden """
require_redraw = False
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
super().configure(*args, **kwargs)
if require_redraw:
self.draw()
def update_dimensions_event(self, event):
# only redraw if dimensions changed (for performance)
if round(self.current_width) != round(event.width / self.widget_scaling) or round(self.current_height) != round(event.height / self.widget_scaling):
self.current_width = (event.width / self.widget_scaling) # adjust current size according to new size given by event
self.current_height = (event.height / self.widget_scaling) # current_width and current_height are independent of the scale
self.draw(no_color_updates=True) # faster drawing without color changes
def detect_color_of_master(self):
""" detect color of self.master widget to set correct bg_color """
if isinstance(self.master, CTkBaseClass) and hasattr(self.master, "fg_color"): # master is CTkFrame
return self.master.fg_color
elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget
try:
ttk_style = ttk.Style()
return ttk_style.lookup(self.master.winfo_class(), 'background')
except Exception:
return "#FFFFFF", "#000000"
else: # master is normal tkinter widget
try:
return self.master.cget("bg") # try to get bg color by .cget() method
except Exception:
return "#FFFFFF", "#000000"
def set_appearance_mode(self, mode_string):
if mode_string.lower() == "dark":
self.appearance_mode = 1
elif mode_string.lower() == "light":
self.appearance_mode = 0
if isinstance(self.master, (CTkBaseClass, CTk)) and hasattr(self.master, "fg_color"):
self.bg_color = self.master.fg_color
else:
self.bg_color = self.master.cget("bg")
self.draw()
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self.widget_scaling = new_widget_scaling
self.spacing_scaling = new_spacing_scaling
super().configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
if self.last_geometry_manager_call is not None:
self.last_geometry_manager_call["function"](**self.apply_argument_scaling(self.last_geometry_manager_call["kwargs"]))
def apply_widget_scaling(self, value):
if isinstance(value, (int, float)):
return value * self.widget_scaling
else:
return value
def apply_spacing_scaling(self, value):
if isinstance(value, (int, float)):
return value * self.spacing_scaling
else:
return value
def apply_font_scaling(self, font):
if type(font) == tuple or type(font) == list:
font_list = list(font)
for i in range(len(font_list)):
if (type(font_list[i]) == int or type(font_list[i]) == float) and font_list[i] < 0:
font_list[i] = int(font_list[i] * self.widget_scaling)
return tuple(font_list)
elif type(font) == str:
for negative_number in re.findall(r" -\d* ", font):
font = font.replace(negative_number, f" {int(int(negative_number) * self.widget_scaling)} ")
return font
elif isinstance(font, tkinter.font.Font):
new_font_object = copy.copy(font)
if font.cget("size") < 0:
new_font_object.config(size=int(font.cget("size") * self.widget_scaling))
return new_font_object
else:
return font
def draw(self, no_color_updates=False):
""" abstract of draw method to be overridden """
pass

View File

@ -0,0 +1,3 @@
from .ctk_tk import CTk
from .ctk_toplevel import CTkToplevel
from .ctk_input_dialog import CTkInputDialog

View File

@ -1,110 +1,110 @@
import tkinter
import time
from typing import Union, Tuple, Optional
from ..widgets.ctk_label import CTkLabel
from ..widgets.ctk_entry import CTkEntry
from ..widgets.ctk_frame import CTkFrame
from ..windows.ctk_toplevel import CTkToplevel
from ..widgets.ctk_button import CTkButton
from ..appearance_mode_tracker import AppearanceModeTracker
from ..theme_manager import ThemeManager
from .widgets import CTkLabel
from .widgets import CTkEntry
from .widgets import CTkButton
from .widgets.theme import ThemeManager
from .ctk_toplevel import CTkToplevel
class CTkInputDialog:
class CTkInputDialog(CTkToplevel):
"""
Dialog with extra window, message, entry widget, cancel and ok button.
For detailed information check out the documentation.
"""
def __init__(self,
master=None,
title="CTkDialog",
text="CTkDialog",
fg_color="default_theme",
hover_color="default_theme",
border_color="default_theme"):
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
button_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
button_text_color: Optional[Union[str, Tuple[str, str]]] = None,
entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
entry_border_color: Optional[Union[str, Tuple[str, str]]] = None,
entry_text_color: Optional[Union[str, Tuple[str, str]]] = None,
self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
self.master = master
title: str = "CTkDialog",
text: str = "CTkDialog"):
self.user_input = None
self.running = False
super().__init__(fg_color=fg_color)
self.height = len(text.split("\n"))*20 + 150
self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color)
self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color)
self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color)
self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color)
self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color)
self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color)
self.text = text
self.window_bg_color = ThemeManager.theme["color"]["window_bg_color"]
self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self.border_color = ThemeManager.theme["color"]["button_hover"] if border_color == "default_theme" else border_color
self._user_input: Union[str, None] = None
self._running: bool = False
self._text = text
self.top = CTkToplevel()
self.top.geometry(f"{280}x{self.height}")
self.top.minsize(280, self.height)
self.top.maxsize(280, self.height)
self.top.title(title)
self.top.lift()
self.top.focus_force()
self.top.grab_set()
self.title(title)
self.lift() # lift window on top
self.attributes("-topmost", True) # stay on top
self.protocol("WM_DELETE_WINDOW", self._on_closing)
self.after(10, self._create_widgets) # create widgets with slight delay, to avoid white flickering of background
self.resizable(False, False)
self.grab_set() # make other windows not clickable
self.top.after(10, self.create_widgets) # create widgets with slight delay, to avoid white flickering of background
def _create_widgets(self):
def create_widgets(self):
self.label_frame = CTkFrame(master=self.top,
corner_radius=0,
fg_color=self.window_bg_color,
width=300,
height=self.height-100)
self.label_frame.place(relx=0.5, rely=0, anchor=tkinter.N)
self.grid_columnconfigure((0, 1), weight=1)
self.rowconfigure(0, weight=1)
self.button_and_entry_frame = CTkFrame(master=self.top,
corner_radius=0,
fg_color=self.window_bg_color,
width=300,
height=100)
self.button_and_entry_frame.place(relx=0.5, rely=1, anchor=tkinter.S)
self._label = CTkLabel(master=self,
width=300,
wraplength=300,
fg_color="transparent",
text_color=self._text_color,
text=self._text,)
self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew")
self.myLabel = CTkLabel(master=self.label_frame,
text=self.text,
width=300,
fg_color=None,
height=self.height-100)
self.myLabel.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
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)
self._entry.grid(row=1, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew")
self.entry = CTkEntry(master=self.button_and_entry_frame,
width=230)
self.entry.place(relx=0.5, rely=0.15, anchor=tkinter.CENTER)
self._ok_button = CTkButton(master=self,
width=100,
border_width=0,
fg_color=self._button_fg_color,
hover_color=self._button_hover_color,
text_color=self._button_text_color,
text='Ok',
command=self._ok_event)
self._ok_button.grid(row=2, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew")
self.ok_button = CTkButton(master=self.button_and_entry_frame,
text='Ok',
width=100,
command=self.ok_event,
fg_color=self.fg_color,
hover_color=self.hover_color,
border_color=self.border_color)
self.ok_button.place(relx=0.28, rely=0.65, anchor=tkinter.CENTER)
self._cancel_button = CTkButton(master=self,
width=100,
border_width=0,
fg_color=self._button_fg_color,
hover_color=self._button_hover_color,
text_color=self._button_text_color,
text='Cancel',
command=self._ok_event)
self._cancel_button.grid(row=2, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew")
self.cancel_button = CTkButton(master=self.button_and_entry_frame,
text='Cancel',
width=100,
command=self.cancel_event,
fg_color=self.fg_color,
hover_color=self.hover_color,
border_color=self.border_color)
self.cancel_button.place(relx=0.72, rely=0.65, anchor=tkinter.CENTER)
self.after(150, lambda: self._entry.focus()) # set focus to entry with slight delay, otherwise it won't work
self._entry.bind("<Return>", self._ok_event)
self.entry.entry.focus_force()
self.entry.bind("<Return>", self.ok_event)
def _ok_event(self, event=None):
self._user_input = self._entry.get()
self.grab_release()
self.destroy()
def ok_event(self, event=None):
self.user_input = self.entry.get()
self.running = False
def _on_closing(self):
self.grab_release()
self.destroy()
def cancel_event(self):
self.running = False
def _cancel_event(self):
self.grab_release()
self.destroy()
def get_input(self):
self.running = True
while self.running:
self.top.update()
time.sleep(0.01)
time.sleep(0.05)
self.top.destroy()
return self.user_input
self.master.wait_window(self)
return self._user_input

View File

@ -4,215 +4,257 @@ import sys
import os
import platform
import ctypes
import re
from typing import Union, Tuple
from typing import Union, Tuple, Optional
from ..appearance_mode_tracker import AppearanceModeTracker
from ..theme_manager import ThemeManager
from ..scaling_tracker import ScalingTracker
from ..settings import Settings
from .widgets.theme import ThemeManager
from .widgets.scaling import CTkScalingBaseClass
from .widgets.appearance_mode import CTkAppearanceModeBaseClass
from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
class CTk(tkinter.Tk):
def __init__(self, *args,
fg_color="default_theme",
class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
"""
Main app window with dark titlebar on Windows and macOS.
For detailed information check out the documentation.
"""
_valid_tk_constructor_arguments: set = {"screenName", "baseName", "className", "useTk", "sync", "use"}
_valid_tk_configure_arguments: set = {'bd', 'borderwidth', 'class', 'menu', 'relief', 'screen',
'use', 'container', 'cursor', 'height',
'highlightthickness', 'padx', 'pady', 'takefocus', 'visual', 'width'}
_deactivate_macos_window_header_manipulation: bool = False
_deactivate_windows_window_header_manipulation: bool = False
def __init__(self,
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
**kwargs):
ScalingTracker.activate_high_dpi_awareness() # make process DPI aware
self.enable_macos_dark_title_bar()
self._enable_macos_dark_title_bar()
super().__init__(*args, **kwargs)
# call init methods of super classes
tkinter.Tk.__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)
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self.set_appearance_mode, self)
self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
self._current_width = 600 # initial window size, independent of scaling
self._current_height = 500
self._min_width: int = 0
self._min_height: int = 0
self._max_width: int = 1_000_000
self._max_height: int = 1_000_000
self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
ScalingTracker.add_widget(self.set_scaling, self)
self.window_scaling = ScalingTracker.get_window_scaling(self)
self._fg_color = ThemeManager.theme["CTk"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self.current_width = 600 # initial window size, always without scaling
self.current_height = 500
self.min_width: int = 0
self.min_height: int = 0
self.max_width: int = 1_000_000
self.max_height: int = 1_000_000
self.last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
# set bg of tkinter.Tk
super().configure(bg=self._apply_appearance_mode(self._fg_color))
self.fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
# set title
self.title("CTk")
if "bg" in kwargs:
self.fg_color = kwargs["bg"]
del kwargs["bg"]
elif "background" in kwargs:
self.fg_color = kwargs["background"]
del kwargs["background"]
# 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
super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
super().title("CTk")
self.geometry(f"{self.current_width}x{self.current_height}")
self.window_exists = False # indicates if the window is already shown through .update or .mainloop
# save focus before calling withdraw
self.focused_widget_before_widthdraw = None
# set CustomTkinter titlebar icon (Windows only)
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self.after(200, self._windows_set_titlebar_icon)
self.bind('<Configure>', self.update_dimensions_event)
# set titlebar color (Windows only)
if sys.platform.startswith("win"):
self._windows_set_titlebar_color(self._get_appearance_mode())
def update_dimensions_event(self, event=None):
detected_width = self.winfo_width() # detect current window size
detected_height = self.winfo_height()
if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling):
self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event
self.current_height = round(detected_height / self.window_scaling) # current_width and current_height are independent of the scale
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self.window_scaling = new_window_scaling
# force new dimensions on window by using min, max, and geometry
super().minsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
super().maxsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
super().geometry(f"{self.apply_window_scaling(self.current_width)}x"+f"{self.apply_window_scaling(self.current_height)}")
# set new scaled min and max with 400ms delay (otherwise it won't work for some reason)
self.after(400, self.set_scaled_min_max)
def set_scaled_min_max(self):
if self.min_width is not None or self.min_height is not None:
super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
if self.max_width is not None or self.max_height is not None:
super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
self.bind('<Configure>', self._update_dimensions_event)
self.bind('<FocusIn>', self._focus_in_event)
def destroy(self):
AppearanceModeTracker.remove(self.set_appearance_mode)
ScalingTracker.remove_window(self.set_scaling, self)
self.disable_macos_dark_title_bar()
super().destroy()
self._disable_macos_dark_title_bar()
# call destroy methods of super classes
tkinter.Tk.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
def _focus_in_event(self, event):
# sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
if sys.platform == "darwin":
self.lift()
def _update_dimensions_event(self, event=None):
if not self._block_update_dimensions_event:
detected_width = super().winfo_width() # detect current window size
detected_height = super().winfo_height()
# detected_width = event.width
# detected_height = event.height
if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height):
self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event
self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale
def _set_scaling(self, new_widget_scaling, new_window_scaling):
super()._set_scaling(new_widget_scaling, new_window_scaling)
# Force new dimensions on window by using min, max, and geometry. Without min, max it won't work.
super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}")
# set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window)
self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11)
def block_update_dimensions_event(self):
self._block_update_dimensions_event = False
def unblock_update_dimensions_event(self):
self._block_update_dimensions_event = False
def _set_scaled_min_max(self):
if self._min_width is not None or self._min_height is not None:
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
if self._max_width is not None or self._max_height is not None:
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def withdraw(self):
if self._window_exists is False:
self._withdraw_called_before_window_exists = True
super().withdraw()
def iconify(self):
if self._window_exists is False:
self._iconify_called_before_window_exists = True
super().iconify()
def update(self):
if self.window_exists is False:
self.deiconify()
self.window_exists = True
if self._window_exists is False:
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.deiconify()
self.window_exists = True
if not self._window_exists:
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, *args, **kwargs):
super().resizable(*args, **kwargs)
self.last_resizable_args = (args, kwargs)
def resizable(self, width: bool = None, height: bool = None):
current_resizable_values = super().resizable(width, height)
self._last_resizable_args = ([], {"width": width, "height": height})
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self._windows_set_titlebar_color(self._get_appearance_mode())
def minsize(self, width=None, height=None):
self.min_width = width
self.min_height = height
if self.current_width < width: self.current_width = width
if self.current_height < height: self.current_height = height
super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
return current_resizable_values
def maxsize(self, width=None, height=None):
self.max_width = width
self.max_height = height
if self.current_width > width: self.current_width = width
if self.current_height > height: self.current_height = height
super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
def minsize(self, width: int = None, height: int = None):
self._min_width = width
self._min_height = height
if self._current_width < width:
self._current_width = width
if self._current_height < height:
self._current_height = height
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
def geometry(self, geometry_string):
super().geometry(self.apply_geometry_scaling(geometry_string))
def maxsize(self, width: int = None, height: int = None):
self._max_width = width
self._max_height = height
if self._current_width > width:
self._current_width = width
if self._current_height > height:
self._current_height = height
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
# update width and height attributes
numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers
self.current_width = max(self.min_width, min(numbers[0], self.max_width)) # bound value between min and max
self.current_height = max(self.min_height, min(numbers[1], self.max_height))
def geometry(self, geometry_string: str = None):
if geometry_string is not None:
super().geometry(self._apply_geometry_scaling(geometry_string))
def apply_geometry_scaling(self, geometry_string):
numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers
if len(numbers) == 2:
return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\
f"{self.apply_window_scaling(numbers[1]):.0f}"
elif len(numbers) == 4:
return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\
f"{self.apply_window_scaling(numbers[1]):.0f}+" +\
f"{self.apply_window_scaling(numbers[2]):.0f}+" +\
f"{self.apply_window_scaling(numbers[3]):.0f}"
# update width and height attributes
width, height, x, y = self._parse_geometry_string(geometry_string)
if width is not None and height is not None:
self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max
self._current_height = max(self._min_height, min(height, self._max_height))
else:
return geometry_string
return self._reverse_geometry_scaling(super().geometry())
def apply_window_scaling(self, value):
if isinstance(value, (int, float)):
return int(value * self.window_scaling)
else:
return value
def config(self, *args, **kwargs):
self.configure(*args, **kwargs)
def configure(self, *args, **kwargs):
bg_changed = False
if "bg" in kwargs:
self.fg_color = kwargs["bg"]
bg_changed = True
kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
elif "background" in kwargs:
self.fg_color = kwargs["background"]
bg_changed = True
kwargs["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
elif "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
del kwargs["fg_color"]
bg_changed = True
elif len(args) > 0 and type(args[0]) == dict:
if "bg" in args[0]:
self.fg_color=args[0]["bg"]
bg_changed = True
args[0]["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
elif "background" in args[0]:
self.fg_color=args[0]["background"]
bg_changed = True
args[0]["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
if bg_changed:
from ..widgets.widget_base_class import CTkBaseClass
def configure(self, **kwargs):
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
super().configure(bg=self._apply_appearance_mode(self._fg_color))
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self.fg_color)
try:
child.configure(bg_color=self._fg_color)
except Exception:
pass
super().configure(*args, **kwargs)
super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_configure_arguments))
check_kwargs_empty(kwargs)
@staticmethod
def enable_macos_dark_title_bar():
if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
def cget(self, attribute_name: str) -> any:
if attribute_name == "fg_color":
return self._fg_color
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
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
# This command allows dark-mode for all programs
@staticmethod
def disable_macos_dark_title_bar():
if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
@classmethod
def _disable_macos_dark_title_bar(cls):
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
# This command reverts the dark-mode setting for all programs.
def windows_set_titlebar_color(self, color_mode: str):
def _windows_set_titlebar_color(self, color_mode: str):
"""
Set the titlebar color of the window to light or dark theme on Microsoft Windows.
@ -223,10 +265,19 @@ class CTk(tkinter.Tk):
https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
"""
if sys.platform.startswith("win") and not Settings.deactivate_windows_window_header_manipulation:
if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation:
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
if not self.window_exists:
if self._window_exists:
self._state_before_windows_set_titlebar_color = self.state()
# 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()
if color_mode.lower() == "dark":
@ -254,19 +305,27 @@ class CTk(tkinter.Tk):
except Exception as err:
print(err)
if self.window_exists:
self.deiconify()
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()
elif self._state_before_windows_set_titlebar_color == "iconic":
self.iconify()
elif self._state_before_windows_set_titlebar_color == "zoomed":
self.state("zoomed")
else:
self.state(self._state_before_windows_set_titlebar_color) # other states
else:
pass # wait for update or mainloop to be called
def set_appearance_mode(self, mode_string):
if mode_string.lower() == "dark":
self.appearance_mode = 1
elif mode_string.lower() == "light":
self.appearance_mode = 0
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)
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self._windows_set_titlebar_color(mode_string)
super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
super().configure(bg=self._apply_appearance_mode(self._fg_color))

View File

@ -4,198 +4,230 @@ import sys
import os
import platform
import ctypes
import re
from typing import Union, Tuple
from typing import Union, Tuple, Optional
from ..appearance_mode_tracker import AppearanceModeTracker
from ..theme_manager import ThemeManager
from ..settings import Settings
from ..scaling_tracker import ScalingTracker
from .widgets.theme import ThemeManager
from .widgets.scaling import CTkScalingBaseClass
from .widgets.appearance_mode import CTkAppearanceModeBaseClass
from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
class CTkToplevel(tkinter.Toplevel):
class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
"""
Toplevel window with dark titlebar on Windows and macOS.
For detailed information check out the documentation.
"""
_valid_tk_toplevel_arguments: set = {"master", "bd", "borderwidth", "class", "container", "cursor", "height",
"highlightbackground", "highlightthickness", "menu", "relief",
"screen", "takefocus", "use", "visual", "width"}
_deactivate_macos_window_header_manipulation: bool = False
_deactivate_windows_window_header_manipulation: bool = False
def __init__(self, *args,
fg_color="default_theme",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
**kwargs):
self.enable_macos_dark_title_bar()
super().__init__(*args, **kwargs)
self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
self._enable_macos_dark_title_bar()
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
ScalingTracker.add_widget(self.set_scaling, self)
self.window_scaling = ScalingTracker.get_window_scaling(self)
# call init methods of super classes
super().__init__(*args, **pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="window")
check_kwargs_empty(kwargs, raise_error=True)
self.current_width = 200 # initial window size, always without scaling
self.current_height = 200
self.min_width: int = 0
self.min_height: int = 0
self.max_width: int = 1_000_000
self.max_height: int = 1_000_000
self.last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
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.fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
self._current_width = 200 # initial window size, always without scaling
self._current_height = 200
self._min_width: int = 0
self._min_height: int = 0
self._max_width: int = 1_000_000
self._max_height: int = 1_000_000
self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
if "bg" in kwargs:
self.fg_color = kwargs["bg"]
del kwargs["bg"]
elif "background" in kwargs:
self.fg_color = kwargs["background"]
del kwargs["background"]
self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self.set_appearance_mode, self)
super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
# set bg color of tkinter.Toplevel
super().configure(bg=self._apply_appearance_mode(self._fg_color))
# set title of tkinter.Toplevel
super().title("CTkToplevel")
# self.geometry(f"{self.current_width}x{self.current_height}")
# 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"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self.after(200, self._windows_set_titlebar_icon)
self.bind('<Configure>', self.update_dimensions_event)
# set titlebar color (Windows only)
if sys.platform.startswith("win"):
self._windows_set_titlebar_color(self._get_appearance_mode())
def update_dimensions_event(self, event=None):
detected_width = self.winfo_width() # detect current window size
detected_height = self.winfo_height()
if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling):
self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event
self.current_height = round(detected_height / self.window_scaling) # current_width and current_height are independent of the scale
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self.window_scaling = new_window_scaling
# force new dimensions on window by using min, max, and geometry
super().minsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
super().maxsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
super().geometry(
f"{self.apply_window_scaling(self.current_width)}x" + f"{self.apply_window_scaling(self.current_height)}")
# set new scaled min and max with 400ms delay (otherwise it won't work for some reason)
self.after(400, self.set_scaled_min_max)
def set_scaled_min_max(self):
if self.min_width is not None or self.min_height is not None:
super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
if self.max_width is not None or self.max_height is not None:
super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
def apply_geometry_scaling(self, geometry_string):
numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers
if len(numbers) == 2:
return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\
f"{self.apply_window_scaling(numbers[1]):.0f}"
elif len(numbers) == 4:
return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\
f"{self.apply_window_scaling(numbers[1]):.0f}+" +\
f"{self.apply_window_scaling(numbers[2]):.0f}+" +\
f"{self.apply_window_scaling(numbers[3]):.0f}"
else:
return geometry_string
def apply_window_scaling(self, value):
if isinstance(value, (int, float)):
return int(value * self.window_scaling)
else:
return value
def geometry(self, geometry_string):
super().geometry(self.apply_geometry_scaling(geometry_string))
# update width and height attributes
numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers
self.current_width = max(self.min_width, min(numbers[0], self.max_width)) # bound value between min and max
self.current_height = max(self.min_height, min(numbers[1], self.max_height))
self.bind('<Configure>', self._update_dimensions_event)
self.bind('<FocusIn>', self._focus_in_event)
def destroy(self):
AppearanceModeTracker.remove(self.set_appearance_mode)
ScalingTracker.remove_window(self.set_scaling, self)
self.disable_macos_dark_title_bar()
super().destroy()
self._disable_macos_dark_title_bar()
def resizable(self, *args, **kwargs):
super().resizable(*args, **kwargs)
self.last_resizable_args = (args, kwargs)
# call destroy methods of super classes
tkinter.Toplevel.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
def _focus_in_event(self, event):
# sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
if sys.platform == "darwin":
self.lift()
def _update_dimensions_event(self, event=None):
if not self._block_update_dimensions_event:
detected_width = self.winfo_width() # detect current window size
detected_height = self.winfo_height()
if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height):
self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event
self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale
def _set_scaling(self, new_widget_scaling, new_window_scaling):
super()._set_scaling(new_widget_scaling, new_window_scaling)
# Force new dimensions on window by using min, max, and geometry. Without min, max it won't work.
super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}")
# set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window)
self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11)
def block_update_dimensions_event(self):
self._block_update_dimensions_event = False
def unblock_update_dimensions_event(self):
self._block_update_dimensions_event = False
def _set_scaled_min_max(self):
if self._min_width is not None or self._min_height is not None:
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
if self._max_width is not None or self._max_height is not None:
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def geometry(self, geometry_string: str = None):
if geometry_string is not None:
super().geometry(self._apply_geometry_scaling(geometry_string))
# update width and height attributes
width, height, x, y = self._parse_geometry_string(geometry_string)
if width is not None and height is not None:
self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max
self._current_height = max(self._min_height, min(height, self._max_height))
else:
return self._reverse_geometry_scaling(super().geometry())
def withdraw(self):
if self._windows_set_titlebar_color_called:
self._withdraw_called_after_windows_set_titlebar_color = True
super().withdraw()
def iconify(self):
if self._windows_set_titlebar_color_called:
self._iconify_called_after_windows_set_titlebar_color = True
super().iconify()
def resizable(self, width: bool = None, height: bool = None):
current_resizable_values = super().resizable(width, height)
self._last_resizable_args = ([], {"width": width, "height": height})
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self.after(10, lambda: self._windows_set_titlebar_color(self._get_appearance_mode()))
return current_resizable_values
def minsize(self, width=None, height=None):
self.min_width = width
self.min_height = height
if self.current_width < width: self.current_width = width
if self.current_height < height: self.current_height = height
super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
self._min_width = width
self._min_height = height
if self._current_width < width:
self._current_width = width
if self._current_height < height:
self._current_height = height
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
def maxsize(self, width=None, height=None):
self.max_width = width
self.max_height = height
if self.current_width > width: self.current_width = width
if self.current_height > height: self.current_height = height
super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
self._max_width = width
self._max_height = height
if self._current_width > width:
self._current_width = width
if self._current_height > height:
self._current_height = height
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def config(self, *args, **kwargs):
self.configure(*args, **kwargs)
def configure(self, *args, **kwargs):
bg_changed = False
if "bg" in kwargs:
self.fg_color = kwargs["bg"]
bg_changed = True
kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
elif "background" in kwargs:
self.fg_color = kwargs["background"]
bg_changed = True
kwargs["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
elif "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
del kwargs["fg_color"]
bg_changed = True
elif len(args) > 0 and type(args[0]) == dict:
if "bg" in args[0]:
self.fg_color=args[0]["bg"]
bg_changed = True
args[0]["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
elif "background" in args[0]:
self.fg_color=args[0]["background"]
bg_changed = True
args[0]["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
if bg_changed:
from ..widgets.widget_base_class import CTkBaseClass
def configure(self, **kwargs):
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
super().configure(bg=self._apply_appearance_mode(self._fg_color))
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self.fg_color)
try:
child.configure(bg_color=self._fg_color)
except Exception:
pass
super().configure(*args, **kwargs)
super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
check_kwargs_empty(kwargs)
@staticmethod
def enable_macos_dark_title_bar():
if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
def cget(self, attribute_name: str) -> any:
if attribute_name == "fg_color":
return self._fg_color
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
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
@staticmethod
def disable_macos_dark_title_bar():
if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
@classmethod
def _disable_macos_dark_title_bar(cls):
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
# This command reverts the dark-mode setting for all programs.
def windows_set_titlebar_color(self, color_mode: str):
def _windows_set_titlebar_color(self, color_mode: str):
"""
Set the titlebar color of the window to light or dark theme on Microsoft Windows.
@ -206,8 +238,10 @@ class CTkToplevel(tkinter.Toplevel):
https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
"""
if sys.platform.startswith("win") and not Settings.deactivate_windows_window_header_manipulation:
if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation:
self._state_before_windows_set_titlebar_color = self.state()
self.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()
@ -235,18 +269,39 @@ class CTkToplevel(tkinter.Toplevel):
except Exception as err:
print(err)
self.deiconify()
self._windows_set_titlebar_color_called = True
self.after(5, self._revert_withdraw_after_windows_set_titlebar_color)
def set_appearance_mode(self, mode_string):
if mode_string.lower() == "dark":
self.appearance_mode = 1
elif mode_string.lower() == "light":
self.appearance_mode = 0
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:
if self._withdraw_called_after_windows_set_titlebar_color:
pass # leave it withdrawed
elif self._iconify_called_after_windows_set_titlebar_color:
super().iconify()
else:
if self._state_before_windows_set_titlebar_color == "normal":
self.deiconify()
elif self._state_before_windows_set_titlebar_color == "iconic":
self.iconify()
elif self._state_before_windows_set_titlebar_color == "zoomed":
self.state("zoomed")
else:
self.state(self._state_before_windows_set_titlebar_color) # other states
self._windows_set_titlebar_color_called = False
self._withdraw_called_after_windows_set_titlebar_color = False
self._iconify_called_after_windows_set_titlebar_color = False
def _set_appearance_mode(self, mode_string):
super()._set_appearance_mode(mode_string)
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self._windows_set_titlebar_color(mode_string)
super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
super().configure(bg=self._apply_appearance_mode(self._fg_color))

View File

@ -0,0 +1,16 @@
from .ctk_button import CTkButton
from .ctk_checkbox import CTkCheckBox
from .ctk_combobox import CTkComboBox
from .ctk_entry import CTkEntry
from .ctk_frame import CTkFrame
from .ctk_label import CTkLabel
from .ctk_optionmenu import CTkOptionMenu
from .ctk_progressbar import CTkProgressBar
from .ctk_radiobutton import CTkRadioButton
from .ctk_scrollbar import CTkScrollbar
from .ctk_segmented_button import CTkSegmentedButton
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

@ -0,0 +1,4 @@
from .appearance_mode_base_class import CTkAppearanceModeBaseClass
from .appearance_mode_tracker import AppearanceModeTracker
AppearanceModeTracker.init_appearance_mode()

View File

@ -0,0 +1,61 @@
from typing import Union, Tuple, List
from .appearance_mode_tracker import AppearanceModeTracker
class CTkAppearanceModeBaseClass:
"""
Super-class that manages the appearance mode. Methods:
- 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() to convert tuple color
"""
def __init__(self):
AppearanceModeTracker.add(self._set_appearance_mode, self)
self.__appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
def destroy(self):
AppearanceModeTracker.remove(self._set_appearance_mode)
def _set_appearance_mode(self, mode_string: str):
""" can be overridden but super method must be called at the beginning """
if mode_string.lower() == "dark":
self.__appearance_mode = 1
elif mode_string.lower() == "light":
self.__appearance_mode = 0
def _get_appearance_mode(self) -> str:
""" get appearance mode as a string, 'light' or 'dark' """
if self.__appearance_mode == 0:
return "light"
else:
return "dark"
def _apply_appearance_mode(self, color: Union[str, Tuple[str, str], List[str]]) -> str:
"""
color can be either a single hex color string or a color name or it can be a
tuple color with (light_color, dark_color). The functions returns
always a single color string
"""
if isinstance(color, (tuple, list)):
return color[self.__appearance_mode]
else:
return color
@staticmethod
def _check_color_type(color: any, transparency: bool = False):
if color is None:
raise ValueError(f"color is None, for transparency set color='transparent'")
elif isinstance(color, (tuple, list)) and (color[0] == "transparent" or color[1] == "transparent"):
raise ValueError(f"transparency is not allowed in tuple color {color}, use 'transparent'")
elif color == "transparent" and transparency is False:
raise ValueError(f"transparency is not allowed for this attribute")
elif isinstance(color, str):
return color
elif isinstance(color, (tuple, list)) and len(color) == 2 and isinstance(color[0], str) and isinstance(color[1], str):
return color
else:
raise ValueError(f"color {color} must be string ('transparent' or 'color-name' or 'hex-color') or tuple of two strings, not {type(color)}")

View File

@ -21,7 +21,7 @@ class AppearanceModeTracker:
callback_list = []
app_list = []
update_loop_running = False
update_loop_interval = 500 # milliseconds
update_loop_interval = 30 # milliseconds
appearance_mode_set_by = "system"
appearance_mode = 0 # Light (standard)
@ -45,12 +45,15 @@ class AppearanceModeTracker:
cls.app_list.append(app)
if not cls.update_loop_running:
app.after(500, cls.update)
app.after(cls.update_loop_interval, cls.update)
cls.update_loop_running = True
@classmethod
def remove(cls, callback: Callable):
cls.callback_list.remove(callback)
try:
cls.callback_list.remove(callback)
except ValueError:
return
@staticmethod
def detect_appearance_mode() -> int:

View File

@ -0,0 +1,12 @@
import sys
from .ctk_canvas import CTkCanvas
from .draw_engine import DrawEngine
CTkCanvas.init_font_character_mapping()
# determine draw method based on current platform
if sys.platform == "darwin":
DrawEngine.preferred_drawing_method = "polygon_shapes"
else:
DrawEngine.preferred_drawing_method = "font_shapes"

View File

@ -4,12 +4,32 @@ from typing import Union, Tuple
class CTkCanvas(tkinter.Canvas):
"""
Canvas with additional functionality to draw antialiased circles on Windows/Linux.
Call .init_font_character_mapping() at program start to load the correct character
dictionary according to the operating system. Characters (circle sizes) are optimised
to look best for rendering CustomTkinter shapes on the different operating systems.
- .create_aa_circle() creates antialiased circle and returns int identifier.
- .coords() is modified to support the aa-circle shapes correctly like you would expect.
- .itemconfig() is also modified to support aa-cricle shapes.
The aa-circles are created by choosing a character from the custom created and loaded
font 'CustomTkinter_shapes_font'. It contains circle shapes with different sizes filling
either the whole character space or just pert of it (characters A to R). Circles with a smaller
radius need a smaller circle character to look correct when rendered on the canvas.
For an optimal result, the draw-engine creates two aa-circles on top of each other, while
one is rotated by 90 degrees. This helps to make the circle look more symetric, which is
not can be a problem when using only a single circle character.
"""
radius_to_char_fine: dict = None # dict to map radius to font circle character
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.aa_circle_canvas_ids = set()
self._aa_circle_canvas_ids = set()
@classmethod
def init_font_character_mapping(cls):
@ -21,7 +41,7 @@ class CTkCanvas(tkinter.Canvas):
radius_to_char_fine_windows_10 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C',
11: 'C', 10: 'C',
9: 'D', 8: 'D', 7: 'D', 6: 'F', 5: 'D', 4: 'G', 3: 'G', 2: 'H', 1: 'H',
9: 'D', 8: 'D', 7: 'D', 6: 'C', 5: 'D', 4: 'G', 3: 'G', 2: 'H', 1: 'H',
0: 'A'}
radius_to_char_fine_windows_11 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C',
@ -29,15 +49,22 @@ class CTkCanvas(tkinter.Canvas):
9: 'E', 8: 'F', 7: 'C', 6: 'I', 5: 'E', 4: 'G', 3: 'P', 2: 'R', 1: 'R',
0: 'A'}
radius_to_char_fine_linux = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'F', 12: 'C',
11: 'F', 10: 'C',
9: 'D', 8: 'G', 7: 'D', 6: 'F', 5: 'D', 4: 'G', 3: 'M', 2: 'H', 1: 'H',
0: 'A'}
if sys.platform.startswith("win"):
if sys.getwindowsversion().build > 20000: # Windows 11
cls.radius_to_char_fine = radius_to_char_fine_windows_11
else: # < Windows 11
cls.radius_to_char_fine = radius_to_char_fine_windows_10
else: # macOS and Linux
elif sys.platform.startswith("linux"): # Optimized on Kali Linux
cls.radius_to_char_fine = radius_to_char_fine_linux
else:
cls.radius_to_char_fine = radius_to_char_fine_windows_10
def get_char_from_radius(self, radius: int) -> str:
def _get_char_from_radius(self, radius: int) -> str:
if radius >= 20:
return "A"
else:
@ -46,10 +73,10 @@ class CTkCanvas(tkinter.Canvas):
def create_aa_circle(self, x_pos: int, y_pos: int, radius: int, angle: int = 0, fill: str = "white",
tags: Union[str, Tuple[str, ...]] = "", anchor: str = tkinter.CENTER) -> int:
# create a circle with a font element
circle_1 = self.create_text(x_pos, y_pos, text=self.get_char_from_radius(radius), anchor=anchor, fill=fill,
circle_1 = self.create_text(x_pos, y_pos, text=self._get_char_from_radius(radius), anchor=anchor, fill=fill,
font=("CustomTkinter_shapes_font", -radius * 2), tags=tags, angle=angle)
self.addtag_withtag("ctk_aa_circle_font_element", circle_1)
self.aa_circle_canvas_ids.add(circle_1)
self._aa_circle_canvas_ids.add(circle_1)
return circle_1
@ -60,13 +87,13 @@ class CTkCanvas(tkinter.Canvas):
super().coords(coords_id, *args[:2])
if len(args) == 3:
super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self.get_char_from_radius(args[2]))
super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self._get_char_from_radius(args[2]))
elif type(tag_or_id) == int and tag_or_id in self.aa_circle_canvas_ids:
elif type(tag_or_id) == int and tag_or_id in self._aa_circle_canvas_ids:
super().coords(tag_or_id, *args[:2])
if len(args) == 3:
super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self.get_char_from_radius(args[2]))
super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self._get_char_from_radius(args[2]))
else:
super().coords(tag_or_id, *args)
@ -77,14 +104,14 @@ class CTkCanvas(tkinter.Canvas):
del kwargs_except_outline["outline"]
if type(tag_or_id) == int:
if tag_or_id in self.aa_circle_canvas_ids:
if tag_or_id in self._aa_circle_canvas_ids:
super().itemconfigure(tag_or_id, *args, **kwargs_except_outline)
else:
super().itemconfigure(tag_or_id, *args, **kwargs)
else:
configure_ids = self.find_withtag(tag_or_id)
for configure_id in configure_ids:
if configure_id in self.aa_circle_canvas_ids:
if configure_id in self._aa_circle_canvas_ids:
super().itemconfigure(configure_id, *args, **kwargs_except_outline)
else:
super().itemconfigure(configure_id, *args, **kwargs)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
from .dropdown_menu import DropdownMenu
from .ctk_base_class import CTkBaseClass

View File

@ -0,0 +1,326 @@
import sys
import warnings
import tkinter
import tkinter.ttk as ttk
from typing import Union, Callable, Tuple
try:
from typing import TypedDict
except ImportError:
from typing_extensions import TypedDict
from .... import windows # import windows for isinstance checks
from ..theme import ThemeManager
from ..font import CTkFont
from ..image import CTkImage
from ..appearance_mode import CTkAppearanceModeBaseClass
from ..scaling import CTkScalingBaseClass
from ..utility import pop_from_dict_by_set, check_kwargs_empty
class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
""" Base class of every CTk widget, handles the dimensions, bg_color,
appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """
# attributes that are passed to and managed by the tkinter frame only:
_valid_tk_frame_attributes: set = {"cursor"}
_cursor_manipulation_enabled: bool = True
def __init__(self,
master: any,
width: int = 0,
height: int = 0,
bg_color: Union[str, Tuple[str, str]] = "transparent",
**kwargs):
# call init methods of super classes
tkinter.Frame.__init__(self, master=master, width=width, height=height, **pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="widget")
# check if kwargs is empty, if not raise error for unsupported arguments
check_kwargs_empty(kwargs, raise_error=True)
# dimensions independent of scaling
self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget
self._current_height = height # _current_width and _current_height are independent of the scale
self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height
self._desired_height = height
# set width and height of tkinter.Frame
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
# save latest geometry function and kwargs
class GeometryCallDict(TypedDict):
function: Callable
kwargs: dict
self._last_geometry_manager_call: Union[GeometryCallDict, None] = None
# background color
self._bg_color: Union[str, Tuple[str, str]] = self._detect_color_of_master() if bg_color == "transparent" else self._check_color_type(bg_color, transparency=True)
# set bg color of tkinter.Frame
super().configure(bg=self._apply_appearance_mode(self._bg_color))
# add configure callback to tkinter.Frame
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, CTkAppearanceModeBaseClass)):
master_old_configure = self.master.config
def new_configure(*args, **kwargs):
if "bg" in kwargs:
self.configure(bg_color=kwargs["bg"])
elif "background" in kwargs:
self.configure(bg_color=kwargs["background"])
# args[0] is dict when attribute gets changed by widget[<attribute>] syntax
elif len(args) > 0 and type(args[0]) == dict:
if "bg" in args[0]:
self.configure(bg_color=args[0]["bg"])
elif "background" in args[0]:
self.configure(bg_color=args[0]["background"])
master_old_configure(*args, **kwargs)
self.master.config = new_configure
self.master.configure = new_configure
def destroy(self):
""" Destroy this and all descendants widgets. """
# call destroy methods of super classes
tkinter.Frame.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
def _draw(self, no_color_updates: bool = False):
""" can be overridden but super method must be called """
if no_color_updates is False:
# Configuring color of tkinter.Frame not necessary at the moment?
# Causes flickering on Windows and Linux for segmented button for some reason!
# super().configure(bg=self._apply_appearance_mode(self._bg_color))
pass
def config(self, *args, **kwargs):
raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.")
def configure(self, require_redraw=False, **kwargs):
""" basic configure with bg_color, width, height support, calls configure of tkinter.Frame, updates in the end """
if "width" in kwargs:
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self._set_dimensions(height=kwargs.pop("height"))
if "bg_color" in kwargs:
new_bg_color = self._check_color_type(kwargs.pop("bg_color"), transparency=True)
if new_bg_color == "transparent":
self._bg_color = self._detect_color_of_master()
else:
self._bg_color = self._check_color_type(new_bg_color)
require_redraw = True
super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes)) # configure tkinter.Frame
# if there are still items in the kwargs dict, raise ValueError
check_kwargs_empty(kwargs, raise_error=True)
if require_redraw:
self._draw()
def cget(self, attribute_name: str):
""" basic cget with bg_color, width, height support, calls cget of tkinter.Frame """
if attribute_name == "bg_color":
return self._bg_color
elif attribute_name == "width":
return self._desired_width
elif attribute_name == "height":
return self._desired_height
elif attribute_name in self._valid_tk_frame_attributes:
return super().cget(attribute_name) # cget of tkinter.Frame
else:
raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.")
def _check_font_type(self, font: any):
""" check font type when passed to widget """
if isinstance(font, CTkFont):
return font
elif type(font) == tuple and len(font) == 1:
warnings.warn(f"{type(self).__name__} Warning: font {font} given without size, will be extended with default text size of current theme\n")
return font[0], ThemeManager.theme["text"]["size"]
elif type(font) == tuple and 2 <= len(font) <= 6:
return font
else:
raise ValueError(f"Wrong font type {type(font)}\n" +
f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 to 6 or an instance of CTkFont.\n" +
f"\nUsage example:\n" +
f"font=customtkinter.CTkFont(family='<name>', size=<size in px>)\n" +
f"font=('<name>', <size in px>)\n")
def _check_image_type(self, image: any):
""" check image type when passed to widget """
if image is None:
return image
elif isinstance(image, CTkImage):
return image
else:
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):
# only redraw if dimensions changed (for performance), independent of scaling
if round(self._current_width) != round(self._reverse_widget_scaling(event.width)) or round(self._current_height) != round(self._reverse_widget_scaling(event.height)):
self._current_width = self._reverse_widget_scaling(event.width) # adjust current size according to new size given by event
self._current_height = self._reverse_widget_scaling(event.height) # _current_width and _current_height are independent of the scale
self._draw(no_color_updates=True) # faster drawing without color changes
def _detect_color_of_master(self, master_widget=None) -> Union[str, Tuple[str, str]]:
""" detect foreground color of master widget for bg_color and transparent color """
if master_widget is None:
master_widget = self.master
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"):
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
try:
ttk_style = ttk.Style()
return ttk_style.lookup(master_widget.winfo_class(), 'background')
except Exception:
return "#FFFFFF", "#000000"
else: # master is normal tkinter widget
try:
return master_widget.cget("bg") # try to get bg color by .cget() method
except Exception:
return "#FFFFFF", "#000000"
def _set_appearance_mode(self, mode_string):
super()._set_appearance_mode(mode_string)
self._draw()
super().update_idletasks()
def _set_scaling(self, new_widget_scaling, new_window_scaling):
super()._set_scaling(new_widget_scaling, new_window_scaling)
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
if self._last_geometry_manager_call is not None:
self._last_geometry_manager_call["function"](**self._apply_argument_scaling(self._last_geometry_manager_call["kwargs"]))
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
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
def bind(self, sequence=None, command=None, add=None):
raise NotImplementedError
def unbind(self, sequence=None, funcid=None):
raise NotImplementedError
def unbind_all(self, sequence):
raise AttributeError("'unbind_all' is not allowed, because it would delete necessary internal callbacks for all widgets")
def bind_all(self, sequence=None, func=None, add=None):
raise AttributeError("'bind_all' is not allowed, could result in undefined behavior")
def place(self, **kwargs):
"""
Place a widget in the parent widget. Use as options:
in=master - master relative to which the widget is placed
in_=master - see 'in' option description
x=amount - locate anchor of this widget at position x of master
y=amount - locate anchor of this widget at position y of master
relx=amount - locate anchor of this widget between 0.0 and 1.0 relative to width of master (1.0 is right edge)
rely=amount - locate anchor of this widget between 0.0 and 1.0 relative to height of master (1.0 is bottom edge)
anchor=NSEW (or subset) - position anchor according to given direction
width=amount - width of this widget in pixel
height=amount - height of this widget in pixel
relwidth=amount - width of this widget between 0.0 and 1.0 relative to width of master (1.0 is the same width as the master)
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))
def place_forget(self):
""" Unmap this widget. """
self._last_geometry_manager_call = None
return super().place_forget()
def pack(self, **kwargs):
"""
Pack a widget in the parent widget. Use as options:
after=widget - pack it after you have packed widget
anchor=NSEW (or subset) - position widget according to given direction
before=widget - pack it before you will pack widget
expand=bool - expand widget if parent size grows
fill=NONE or X or Y or BOTH - fill widget if widget grows
in=master - use master to contain this widget
in_=master - see 'in' option description
ipadx=amount - add internal padding in x direction
ipady=amount - add internal padding in y direction
padx=amount - add padding in x direction
pady=amount - add padding in y direction
side=TOP or BOTTOM or LEFT or RIGHT - where to add this widget.
"""
self._last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs}
return super().pack(**self._apply_argument_scaling(kwargs))
def pack_forget(self):
""" Unmap this widget and do not use it for the packing order. """
self._last_geometry_manager_call = None
return super().pack_forget()
def grid(self, **kwargs):
"""
Position a widget in the parent widget in a grid. Use as options:
column=number - use cell identified with given column (starting with 0)
columnspan=number - this widget will span several columns
in=master - use master to contain this widget
in_=master - see 'in' option description
ipadx=amount - add internal padding in x direction
ipady=amount - add internal padding in y direction
padx=amount - add padding in x direction
pady=amount - add padding in y direction
row=number - use cell identified with given row (starting with 0)
rowspan=number - this widget will span several rows
sticky=NSEW - if cell is larger on which sides will this widget stick to the cell boundary
"""
self._last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs}
return super().grid(**self._apply_argument_scaling(kwargs))
def grid_forget(self):
""" Unmap this widget. """
self._last_geometry_manager_call = None
return super().grid_forget()

View File

@ -0,0 +1,198 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, List, Optional
from ..theme import ThemeManager
from ..font import CTkFont
from ..appearance_mode import CTkAppearanceModeBaseClass
from ..scaling import CTkScalingBaseClass
class DropdownMenu(tkinter.Menu, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
def __init__(self, *args,
min_character_width: int = 18,
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
hover_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
font: Optional[Union[tuple, CTkFont]] = None,
command: Union[Callable, None] = None,
values: Optional[List[str]] = None,
**kwargs):
# call init methods of super classes
tkinter.Menu.__init__(self, *args, **kwargs)
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="widget")
self._min_character_width = min_character_width
self._fg_color = ThemeManager.theme["DropdownMenu"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._hover_color = ThemeManager.theme["DropdownMenu"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
self._text_color = ThemeManager.theme["DropdownMenu"]["text_color"] if text_color is None else self._check_color_type(text_color)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._configure_menu_for_platforms()
self._values = values
self._command = command
self._add_menu_commands()
def destroy(self):
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
# call destroy methods of super classes
tkinter.Menu.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling """
super().configure(font=self._apply_font_scaling(self._font))
def _configure_menu_for_platforms(self):
""" apply platform specific appearance attributes, configure all colors """
if sys.platform == "darwin":
super().configure(tearoff=False,
font=self._apply_font_scaling(self._font))
elif sys.platform.startswith("win"):
super().configure(tearoff=False,
relief="flat",
activebackground=self._apply_appearance_mode(self._hover_color),
borderwidth=self._apply_widget_scaling(4),
activeborderwidth=self._apply_widget_scaling(4),
bg=self._apply_appearance_mode(self._fg_color),
fg=self._apply_appearance_mode(self._text_color),
activeforeground=self._apply_appearance_mode(self._text_color),
font=self._apply_font_scaling(self._font),
cursor="hand2")
else:
super().configure(tearoff=False,
relief="flat",
activebackground=self._apply_appearance_mode(self._hover_color),
borderwidth=0,
activeborderwidth=0,
bg=self._apply_appearance_mode(self._fg_color),
fg=self._apply_appearance_mode(self._text_color),
activeforeground=self._apply_appearance_mode(self._text_color),
font=self._apply_font_scaling(self._font))
def _add_menu_commands(self):
""" delete existing menu labels and createe new labels with command according to values list """
self.delete(0, "end") # delete all old commands
if sys.platform.startswith("linux"):
for value in self._values:
self.add_command(label=" " + value.ljust(self._min_character_width) + " ",
command=lambda v=value: self._button_callback(v),
compound="left")
else:
for value in self._values:
self.add_command(label=value.ljust(self._min_character_width),
command=lambda v=value: self._button_callback(v),
compound="left")
def _button_callback(self, value):
if self._command is not None:
self._command(value)
def open(self, x: Union[int, float], y: Union[int, float]):
if sys.platform == "darwin":
y += self._apply_widget_scaling(8)
else:
y += self._apply_widget_scaling(3)
if sys.platform == "darwin" or sys.platform.startswith("win"):
self.post(int(x), int(y))
else: # Linux
self.tk_popup(int(x), int(y))
def configure(self, **kwargs):
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
super().configure(bg=self._apply_appearance_mode(self._fg_color))
if "hover_color" in kwargs:
self._hover_color = self._check_color_type(kwargs.pop("hover_color"))
super().configure(activebackground=self._apply_appearance_mode(self._hover_color))
if "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
super().configure(fg=self._apply_appearance_mode(self._text_color))
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
if "command" in kwargs:
self._command = kwargs.pop("command")
if "values" in kwargs:
self._values = kwargs.pop("values")
self._add_menu_commands()
super().configure(**kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "min_character_width":
return self._min_character_width
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "hover_color":
return self._hover_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "font":
return self._font
elif attribute_name == "command":
return self._command
elif attribute_name == "values":
return self._values
else:
return super().cget(attribute_name)
@staticmethod
def _check_font_type(font: any):
if isinstance(font, CTkFont):
return font
elif type(font) == tuple and len(font) == 1:
sys.stderr.write(f"Warning: font {font} given without size, will be extended with default text size of current theme\n")
return font[0], ThemeManager.theme["text"]["size"]
elif type(font) == tuple and 2 <= len(font) <= 3:
return font
else:
raise ValueError(f"Wrong font type {type(font)} for font '{font}'\n" +
f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 or 3 or an instance of CTkFont.\n" +
f"\nUsage example:\n" +
f"font=customtkinter.CTkFont(family='<name>', size=<size in px>)\n" +
f"font=('<name>', <size in px>)\n")
def _set_scaling(self, new_widget_scaling, new_window_scaling):
super()._set_scaling(new_widget_scaling, new_window_scaling)
self._configure_menu_for_platforms()
def _set_appearance_mode(self, mode_string):
""" colors won't update on appearance mode change when dropdown is open, because it's not necessary """
super()._set_appearance_mode(mode_string)
self._configure_menu_for_platforms()

View File

@ -0,0 +1,593 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
from .image import CTkImage
class CTkButton(CTkBaseClass):
"""
Button with rounded corners, border, hover effect, image support, click command and textvariable.
For detailed information check out the documentation.
"""
_image_label_spacing: int = 6
def __init__(self,
master: any,
width: int = 140,
height: int = 28,
corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
border_spacing: int = 2,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
hover_color: Optional[Union[str, Tuple[str, str]]] = None,
border_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,
background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None,
round_width_to_even_numbers: bool = True,
round_height_to_even_numbers: bool = True,
text: str = "CTkButton",
font: Optional[Union[tuple, CTkFont]] = None,
textvariable: Union[tkinter.Variable, None] = None,
image: Union[CTkImage, "ImageTk.PhotoImage", None] = None,
state: str = "normal",
hover: bool = True,
command: Union[Callable[[], None], None] = None,
compound: str = "left",
anchor: str = "center",
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# shape
self._corner_radius: int = ThemeManager.theme["CTkButton"]["corner_radius"] if corner_radius is None else corner_radius
self._corner_radius = min(self._corner_radius, round(self._current_height / 2))
self._border_width: int = ThemeManager.theme["CTkButton"]["border_width"] if border_width is None else border_width
self._border_spacing: int = border_spacing
# color
self._fg_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
self._hover_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
self._border_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._text_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
# rendering options
self._background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = background_corner_colors # rendering options for DrawEngine
self._round_width_to_even_numbers: bool = round_width_to_even_numbers # rendering options for DrawEngine
self._round_height_to_even_numbers: bool = round_height_to_even_numbers # rendering options for DrawEngine
# text, font
self._text = text
self._text_label: Union[tkinter.Label, None] = None
self._textvariable: tkinter.Variable = textvariable
self._font: Union[tuple, CTkFont] = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# image
self._image = self._check_image_type(image)
self._image_label: Union[tkinter.Label, None] = None
if isinstance(self._image, CTkImage):
self._image.add_configure_callback(self._update_image)
# other
self._state: str = state
self._hover: bool = hover
self._command: Callable = command
self._compound: str = compound
self._anchor: str = anchor
self._click_animation_running: bool = False
# canvas and draw engine
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew")
self._draw_engine = DrawEngine(self._canvas)
self._draw_engine.set_round_to_even_numbers(self._round_width_to_even_numbers, self._round_height_to_even_numbers) # rendering options
# configure cursor and initial draw
self._create_bindings()
self._set_cursor()
self._draw()
def _create_bindings(self, sequence: Optional[str] = None):
""" set necessary bindings for functionality of widget, will overwrite other bindings """
if sequence is None or sequence == "<Enter>":
self._canvas.bind("<Enter>", self._on_enter)
if self._text_label is not None:
self._text_label.bind("<Enter>", self._on_enter)
if self._image_label is not None:
self._image_label.bind("<Enter>", self._on_enter)
if sequence is None or sequence == "<Leave>":
self._canvas.bind("<Leave>", self._on_leave)
if self._text_label is not None:
self._text_label.bind("<Leave>", self._on_leave)
if self._image_label is not None:
self._image_label.bind("<Leave>", self._on_leave)
if sequence is None or sequence == "<Button-1>":
self._canvas.bind("<Button-1>", self._clicked)
if self._text_label is not None:
self._text_label.bind("<Button-1>", self._clicked)
if self._image_label is not None:
self._image_label.bind("<Button-1>", self._clicked)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self._create_grid()
if self._text_label is not None:
self._text_label.configure(font=self._apply_font_scaling(self._font))
self._update_image()
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw(no_color_updates=True)
def _set_appearance_mode(self, mode_string):
super()._set_appearance_mode(mode_string)
self._update_image()
def _set_dimensions(self, width: int = None, height: int = None):
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._draw()
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
if self._text_label is not None:
self._text_label.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._canvas.grid_forget()
self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew")
def _update_image(self):
if self._image_label is not None:
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):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
if self._background_corner_colors is not None:
self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height))
self._canvas.itemconfig("background_corner_top_left", fill=self._apply_appearance_mode(self._background_corner_colors[0]))
self._canvas.itemconfig("background_corner_top_right", fill=self._apply_appearance_mode(self._background_corner_colors[1]))
self._canvas.itemconfig("background_corner_bottom_right", fill=self._apply_appearance_mode(self._background_corner_colors[2]))
self._canvas.itemconfig("background_corner_bottom_left", fill=self._apply_appearance_mode(self._background_corner_colors[3]))
else:
self._canvas.delete("background_parts")
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 no_color_updates is False or requires_recoloring:
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
# set color for the button border parts (outline)
self._canvas.itemconfig("border_parts",
outline=self._apply_appearance_mode(self._border_color),
fill=self._apply_appearance_mode(self._border_color))
# set color for inner button parts
if self._fg_color == "transparent":
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(self._bg_color),
fill=self._apply_appearance_mode(self._bg_color))
else:
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(self._fg_color),
fill=self._apply_appearance_mode(self._fg_color))
# create text label if text given
if self._text is not None and self._text != "":
if self._text_label is None:
self._text_label = tkinter.Label(master=self,
font=self._apply_font_scaling(self._font),
text=self._text,
padx=0,
pady=0,
borderwidth=1,
textvariable=self._textvariable)
self._create_grid()
self._text_label.bind("<Enter>", self._on_enter)
self._text_label.bind("<Leave>", self._on_leave)
self._text_label.bind("<Button-1>", self._clicked)
self._text_label.bind("<Button-1>", self._clicked)
if no_color_updates is False:
# set text_label fg color (text color)
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
if self._state == tkinter.DISABLED:
self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
else:
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
if self._apply_appearance_mode(self._fg_color) == "transparent":
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
else:
self._text_label.configure(bg=self._apply_appearance_mode(self._fg_color))
else:
# delete text_label if no text given
if self._text_label is not None:
self._text_label.destroy()
self._text_label = None
self._create_grid()
# create image label if image given
if self._image is not None:
if self._image_label is None:
self._image_label = tkinter.Label(master=self)
self._update_image() # set image
self._create_grid()
self._image_label.bind("<Enter>", self._on_enter)
self._image_label.bind("<Leave>", self._on_leave)
self._image_label.bind("<Button-1>", self._clicked)
self._image_label.bind("<Button-1>", self._clicked)
if no_color_updates is False:
# set image_label bg color (background color of label)
if self._apply_appearance_mode(self._fg_color) == "transparent":
self._image_label.configure(bg=self._apply_appearance_mode(self._bg_color))
else:
self._image_label.configure(bg=self._apply_appearance_mode(self._fg_color))
else:
# delete text_label if no text given
if self._image_label is not None:
self._image_label.destroy()
self._image_label = None
self._create_grid()
def _create_grid(self):
""" configure grid system (5x5) """
# Outer rows and columns have weight of 1000 to overpower the rows and columns of the label and image with weight 1.
# Rows and columns of image and label need weight of 1 to collapse in case of missing space on the button,
# so image and label need sticky option to stick together in the center, and therefore outer rows and columns
# need weight of 100 in case of other anchor than center.
n_padding_weight, s_padding_weight, e_padding_weight, w_padding_weight = 1000, 1000, 1000, 1000
if self._anchor != "center":
if "n" in self._anchor:
n_padding_weight, s_padding_weight = 0, 1000
if "s" in self._anchor:
n_padding_weight, s_padding_weight = 1000, 0
if "e" in self._anchor:
e_padding_weight, w_padding_weight = 1000, 0
if "w" in self._anchor:
e_padding_weight, w_padding_weight = 0, 1000
scaled_minsize_rows = self._apply_widget_scaling(max(self._border_width + 1, self._border_spacing))
scaled_minsize_columns = self._apply_widget_scaling(max(self._corner_radius, self._border_width + 1, self._border_spacing))
self.grid_rowconfigure(0, weight=n_padding_weight, minsize=scaled_minsize_rows)
self.grid_rowconfigure(4, weight=s_padding_weight, minsize=scaled_minsize_rows)
self.grid_columnconfigure(0, weight=e_padding_weight, minsize=scaled_minsize_columns)
self.grid_columnconfigure(4, weight=w_padding_weight, minsize=scaled_minsize_columns)
if self._compound in ("right", "left"):
self.grid_rowconfigure(2, weight=1)
if self._image_label is not None and self._text_label is not None:
self.grid_columnconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._image_label_spacing))
else:
self.grid_columnconfigure(2, weight=0)
self.grid_rowconfigure((1, 3), weight=0)
self.grid_columnconfigure((1, 3), weight=1)
else:
self.grid_columnconfigure(2, weight=1)
if self._image_label is not None and self._text_label is not None:
self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._image_label_spacing))
else:
self.grid_rowconfigure(2, weight=0)
self.grid_columnconfigure((1, 3), weight=0)
self.grid_rowconfigure((1, 3), weight=1)
if self._compound == "right":
if self._image_label is not None:
self._image_label.grid(row=2, column=3, sticky="w")
if self._text_label is not None:
self._text_label.grid(row=2, column=1, sticky="e")
elif self._compound == "left":
if self._image_label is not None:
self._image_label.grid(row=2, column=1, sticky="e")
if self._text_label is not None:
self._text_label.grid(row=2, column=3, sticky="w")
elif self._compound == "top":
if self._image_label is not None:
self._image_label.grid(row=1, column=2, sticky="s")
if self._text_label is not None:
self._text_label.grid(row=3, column=2, sticky="n")
elif self._compound == "bottom":
if self._image_label is not None:
self._image_label.grid(row=3, column=2, sticky="n")
if self._text_label is not None:
self._text_label.grid(row=1, column=2, sticky="s")
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
self._create_grid()
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
self._create_grid()
require_redraw = True
if "border_spacing" in kwargs:
self._border_spacing = kwargs.pop("border_spacing")
self._create_grid()
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
if "hover_color" in kwargs:
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 "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 "background_corner_colors" in kwargs:
self._background_corner_colors = kwargs.pop("background_corner_colors")
require_redraw = True
if "text" in kwargs:
self._text = kwargs.pop("text")
if self._text_label is None:
require_redraw = True # text_label will be created in .draw()
else:
self._text_label.configure(text=self._text)
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
if "textvariable" in kwargs:
self._textvariable = kwargs.pop("textvariable")
if self._text_label is not None:
self._text_label.configure(textvariable=self._textvariable)
if "image" in kwargs:
if isinstance(self._image, CTkImage):
self._image.remove_configure_callback(self._update_image)
self._image = self._check_image_type(kwargs.pop("image"))
if isinstance(self._image, CTkImage):
self._image.add_configure_callback(self._update_image)
self._update_image()
if "state" in kwargs:
self._state = kwargs.pop("state")
self._set_cursor()
require_redraw = True
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "command" in kwargs:
self._command = kwargs.pop("command")
self._set_cursor()
if "compound" in kwargs:
self._compound = kwargs.pop("compound")
require_redraw = True
if "anchor" in kwargs:
self._anchor = kwargs.pop("anchor")
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "border_spacing":
return self._border_spacing
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "hover_color":
return self._hover_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text_color_disabled":
return self._text_color_disabled
elif attribute_name == "background_corner_colors":
return self._background_corner_colors
elif attribute_name == "text":
return self._text
elif attribute_name == "font":
return self._font
elif attribute_name == "textvariable":
return self._textvariable
elif attribute_name == "image":
return self._image
elif attribute_name == "state":
return self._state
elif attribute_name == "hover":
return self._hover
elif attribute_name == "command":
return self._command
elif attribute_name == "compound":
return self._compound
elif attribute_name == "anchor":
return self._anchor
else:
return super().cget(attribute_name)
def _set_cursor(self):
if self._cursor_manipulation_enabled:
if self._state == tkinter.DISABLED:
if sys.platform == "darwin" and self._command is not None:
self.configure(cursor="arrow")
elif sys.platform.startswith("win") and self._command is not None:
self.configure(cursor="arrow")
elif self._state == tkinter.NORMAL:
if sys.platform == "darwin" and self._command is not None:
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and self._command is not None:
self.configure(cursor="hand2")
def _on_enter(self, event=None):
if self._hover is True and self._state == "normal":
if self._hover_color is None:
inner_parts_color = self._fg_color
else:
inner_parts_color = self._hover_color
# set color of inner button parts to hover color
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(inner_parts_color),
fill=self._apply_appearance_mode(inner_parts_color))
# set text_label bg color to button hover color
if self._text_label is not None:
self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
# set image_label bg color to button hover color
if self._image_label is not None:
self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
def _on_leave(self, event=None):
self._click_animation_running = False
if self._fg_color == "transparent":
inner_parts_color = self._bg_color
else:
inner_parts_color = self._fg_color
# set color of inner button parts
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(inner_parts_color),
fill=self._apply_appearance_mode(inner_parts_color))
# set text_label bg color (label color)
if self._text_label is not None:
self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
# set image_label bg color (image bg color)
if self._image_label is not None:
self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
def _click_animation(self):
if self._click_animation_running:
self._on_enter()
def _clicked(self, event=None):
if self._state != tkinter.DISABLED:
# click animation: change color with .on_leave() and back to normal after 100ms with click_animation()
self._on_leave()
self._click_animation_running = True
self.after(100, self._click_animation)
if self._command is not None:
self._command()
def invoke(self):
""" calls command function if button is not disabled """
if self._state != tkinter.DISABLED:
if self._command is not None:
return self._command()
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
""" called on the tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._canvas.bind(sequence, command, add=True)
if self._text_label is not None:
self._text_label.bind(sequence, command, add=True)
if self._image_label is not None:
self._image_label.bind(sequence, command, add=True)
def unbind(self, sequence: str = None, funcid: str = None):
""" called on the tkinter.Label and tkinter.Canvas """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._canvas.unbind(sequence, None)
if self._text_label is not None:
self._text_label.unbind(sequence, None)
if self._image_label is not None:
self._image_label.unbind(sequence, None)
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
def focus(self):
return self._text_label.focus()
def focus_set(self):
return self._text_label.focus_set()
def focus_force(self):
return self._text_label.focus_force()

View File

@ -0,0 +1,461 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
class CTkCheckBox(CTkBaseClass):
"""
Checkbox with rounded corners, border, variable support and hover effect.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 100,
height: int = 24,
checkbox_width: int = 24,
checkbox_height: int = 24,
corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
hover_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Optional[Union[str, Tuple[str, str]]] = None,
checkmark_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 = "CTkCheckBox",
font: Optional[Union[tuple, CTkFont]] = None,
textvariable: Union[tkinter.Variable, None] = None,
state: str = tkinter.NORMAL,
hover: bool = True,
command: Union[Callable[[], None], None] = None,
onvalue: Union[int, str] = 1,
offvalue: Union[int, str] = 0,
variable: Union[tkinter.Variable, None] = None,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# dimensions
self._checkbox_width = checkbox_width
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)
# 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
# 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)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# callback and hover functionality
self._command = command
self._state = state
self._hover = hover
self._check_state = False
self._onvalue = onvalue
self._offvalue = offvalue
self._variable: tkinter.Variable = variable
self._variable_callback_blocked = False
self._textvariable: tkinter.Variable = textvariable
self._variable_callback_name = None
# configure grid system (1x3)
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,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._checkbox_width),
height=self._apply_widget_scaling(self._checkbox_height))
self._canvas.grid(row=0, column=0, sticky="e")
self._draw_engine = DrawEngine(self._canvas)
self._text_label = tkinter.Label(master=self,
bd=0,
padx=0,
pady=0,
text=self._text,
justify=tkinter.LEFT,
font=self._apply_font_scaling(self._font),
textvariable=self._textvariable)
self._text_label.grid(row=0, column=2, sticky="w")
self._text_label["anchor"] = "w"
# register variable callback and set state according to variable
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._onvalue else False
self._create_bindings()
self._set_cursor()
self._draw()
def _create_bindings(self, sequence: Optional[str] = None):
""" set necessary bindings for functionality of widget, will overwrite other bindings """
if sequence is None or sequence == "<Enter>":
self._canvas.bind("<Enter>", self._on_enter)
self._text_label.bind("<Enter>", self._on_enter)
if sequence is None or sequence == "<Leave>":
self._canvas.bind("<Leave>", self._on_leave)
self._text_label.bind("<Leave>", self._on_leave)
if sequence is None or sequence == "<Button-1>":
self._canvas.bind("<Button-1>", self.toggle)
self._text_label.bind("<Button-1>", self.toggle)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self._text_label.configure(font=self._apply_font_scaling(self._font))
self._canvas.delete("checkmark")
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width),
height=self._apply_widget_scaling(self._checkbox_height))
self._draw(no_color_updates=True)
def _set_dimensions(self, width: int = None, height: int = None):
super()._set_dimensions(width, height)
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
if self._text_label is not None:
self._text_label.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._bg_canvas.grid_forget()
self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
def destroy(self):
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
requires_recoloring_1 = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._checkbox_width),
self._apply_widget_scaling(self._checkbox_height),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width))
if self._check_state is True:
requires_recoloring_2 = self._draw_engine.draw_checkmark(self._apply_widget_scaling(self._checkbox_width),
self._apply_widget_scaling(self._checkbox_height),
self._apply_widget_scaling(self._checkbox_height * 0.58))
else:
requires_recoloring_2 = False
self._canvas.delete("checkmark")
if no_color_updates is False or requires_recoloring_1 or requires_recoloring_2:
self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
if self._check_state is True:
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(self._fg_color),
fill=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("border_parts",
outline=self._apply_appearance_mode(self._fg_color),
fill=self._apply_appearance_mode(self._fg_color))
if "create_line" in self._canvas.gettags("checkmark"):
self._canvas.itemconfig("checkmark", fill=self._apply_appearance_mode(self._checkmark_color))
else:
self._canvas.itemconfig("checkmark", fill=self._apply_appearance_mode(self._checkmark_color))
else:
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(self._bg_color),
fill=self._apply_appearance_mode(self._bg_color))
self._canvas.itemconfig("border_parts",
outline=self._apply_appearance_mode(self._border_color),
fill=self._apply_appearance_mode(self._border_color))
if self._state == tkinter.DISABLED:
self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
else:
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "checkbox_width" in kwargs:
self._checkbox_width = kwargs.pop("checkbox_width")
self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width))
require_redraw = True
if "checkbox_height" in kwargs:
self._checkbox_height = kwargs.pop("checkbox_height")
self._canvas.configure(height=self._apply_widget_scaling(self._checkbox_height))
require_redraw = True
if "text" in kwargs:
self._text = kwargs.pop("text")
self._text_label.configure(text=self._text)
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
if "state" in kwargs:
self._state = kwargs.pop("state")
self._set_cursor()
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
require_redraw = True
if "hover_color" in kwargs:
self._hover_color = self._check_color_type(kwargs.pop("hover_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"))
require_redraw = True
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "command" in kwargs:
self._command = kwargs.pop("command")
if "textvariable" in kwargs:
self._textvariable = kwargs.pop("textvariable")
self._text_label.configure(textvariable=self._textvariable)
if "variable" in kwargs:
if self._variable is not None and self._variable != "":
self._variable.trace_remove("write", self._variable_callback_name) # remove old variable callback
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._onvalue else False
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "checkbox_width":
return self._checkbox_width
elif attribute_name == "checkbox_height":
return self._checkbox_height
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "hover_color":
return self._hover_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "checkmark_color":
return self._checkmark_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
elif attribute_name == "font":
return self._font
elif attribute_name == "textvariable":
return self._textvariable
elif attribute_name == "state":
return self._state
elif attribute_name == "hover":
return self._hover
elif attribute_name == "onvalue":
return self._onvalue
elif attribute_name == "offvalue":
return self._offvalue
elif attribute_name == "variable":
return self._variable
else:
return super().cget(attribute_name)
def _set_cursor(self):
if self._cursor_manipulation_enabled:
if self._state == tkinter.DISABLED:
if sys.platform == "darwin":
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif sys.platform.startswith("win"):
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif self._state == tkinter.NORMAL:
if sys.platform == "darwin":
self._canvas.configure(cursor="pointinghand")
if self._text_label is not None:
self._text_label.configure(cursor="pointinghand")
elif sys.platform.startswith("win"):
self._canvas.configure(cursor="hand2")
if self._text_label is not None:
self._text_label.configure(cursor="hand2")
def _on_enter(self, event=0):
if self._hover is True and self._state == tkinter.NORMAL:
if self._check_state is True:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._hover_color),
outline=self._apply_appearance_mode(self._hover_color))
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._hover_color),
outline=self._apply_appearance_mode(self._hover_color))
else:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._hover_color),
outline=self._apply_appearance_mode(self._hover_color))
def _on_leave(self, event=0):
if self._check_state is True:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
else:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=self._apply_appearance_mode(self._bg_color))
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._border_color),
outline=self._apply_appearance_mode(self._border_color))
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
if self._variable.get() == self._onvalue:
self.select(from_variable_callback=True)
elif self._variable.get() == self._offvalue:
self.deselect(from_variable_callback=True)
def toggle(self, event=0):
if self._state == tkinter.NORMAL:
if self._check_state is True:
self._check_state = False
self._draw()
else:
self._check_state = True
self._draw()
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
self._variable_callback_blocked = False
if self._command is not None:
self._command()
def select(self, from_variable_callback=False):
self._check_state = True
self._draw()
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._onvalue)
self._variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
self._check_state = False
self._draw()
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._offvalue)
self._variable_callback_blocked = False
def get(self) -> Union[int, str]:
return self._onvalue if self._check_state is True else self._offvalue
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
""" called on the tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._canvas.bind(sequence, command, add=True)
self._text_label.bind(sequence, command, add=True)
def unbind(self, sequence: str = None, funcid: str = None):
""" called on the tkinter.Label and tkinter.Canvas """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._canvas.unbind(sequence, None)
self._text_label.unbind(sequence, None)
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
def focus(self):
return self._text_label.focus()
def focus_set(self):
return self._text_label.focus_set()
def focus_force(self):
return self._text_label.focus_force()

View File

@ -0,0 +1,424 @@
import tkinter
import sys
import copy
from typing import Union, Tuple, Callable, List, Optional
from .core_widget_classes import DropdownMenu
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
class CTkComboBox(CTkBaseClass):
"""
Combobox with dropdown menu, rounded corners, border, variable support.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 140,
height: int = 28,
corner_radius: Optional[int] = None,
border_width: Optional[int] = 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,
button_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_text_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,
font: Optional[Union[tuple, CTkFont]] = None,
dropdown_font: Optional[Union[tuple, CTkFont]] = None,
values: Optional[List[str]] = None,
state: str = tkinter.NORMAL,
hover: bool = True,
variable: Union[tkinter.Variable, None] = None,
command: Union[Callable[[str], None], None] = None,
justify: str = "left",
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# shape
self._corner_radius = ThemeManager.theme["CTkComboBox"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkComboBox"]["border_width"] if border_width is None else border_width
# color
self._fg_color = ThemeManager.theme["CTkComboBox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._border_color = ThemeManager.theme["CTkComboBox"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._button_color = ThemeManager.theme["CTkComboBox"]["button_color"] if button_color is None else self._check_color_type(button_color)
self._button_hover_color = ThemeManager.theme["CTkComboBox"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
self._text_color = ThemeManager.theme["CTkComboBox"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkComboBox"]["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)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# callback and hover functionality
self._command = command
self._variable = variable
self._state = state
self._hover = hover
if values is None:
self._values = ["CTkComboBox"]
else:
self._values = values
self._dropdown_menu = DropdownMenu(master=self,
values=self._values,
command=self._dropdown_callback,
fg_color=dropdown_fg_color,
hover_color=dropdown_hover_color,
text_color=dropdown_text_color,
font=dropdown_font)
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self.draw_engine = DrawEngine(self._canvas)
self._entry = tkinter.Entry(master=self,
state=self._state,
width=1,
bd=0,
justify=justify,
highlightthickness=0,
font=self._apply_font_scaling(self._font))
self._create_grid()
self._create_bindings()
self._draw() # initial draw
if self._variable is not None:
self._entry.configure(textvariable=self._variable)
# insert default value
if self._variable is None:
if len(self._values) > 0:
self._entry.insert(0, self._values[0])
else:
self._entry.insert(0, "CTkComboBox")
def _create_bindings(self, sequence: Optional[str] = None):
""" set necessary bindings for functionality of widget, will overwrite other bindings """
if sequence is None:
self._canvas.tag_bind("right_parts", "<Enter>", self._on_enter)
self._canvas.tag_bind("dropdown_arrow", "<Enter>", self._on_enter)
self._canvas.tag_bind("right_parts", "<Leave>", self._on_leave)
self._canvas.tag_bind("dropdown_arrow", "<Leave>", self._on_leave)
self._canvas.tag_bind("right_parts", "<Button-1>", self._clicked)
self._canvas.tag_bind("dropdown_arrow", "<Button-1>", self._clicked)
def _create_grid(self):
self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
left_section_width = self._current_width - self._current_height
self._entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew",
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)),
max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))),
pady=self._apply_widget_scaling(self._border_width))
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
# change entry font size and grid padding
self._entry.configure(font=self._apply_font_scaling(self._font))
self._create_grid()
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw(no_color_updates=True)
def _set_dimensions(self, width: int = None, height: int = None):
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._draw()
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
self._entry.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._canvas.grid_forget()
self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
def destroy(self):
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
left_section_width = self._current_width - self._current_height
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(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),
self._apply_widget_scaling(left_section_width))
requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self._apply_widget_scaling(self._current_width - (self._current_height / 2)),
self._apply_widget_scaling(self._current_height / 2),
self._apply_widget_scaling(self._current_height / 3))
if no_color_updates is False or requires_recoloring or requires_recoloring_2:
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
self._canvas.itemconfig("inner_parts_left",
outline=self._apply_appearance_mode(self._fg_color),
fill=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("border_parts_left",
outline=self._apply_appearance_mode(self._border_color),
fill=self._apply_appearance_mode(self._border_color))
self._canvas.itemconfig("inner_parts_right",
outline=self._apply_appearance_mode(self._button_color),
fill=self._apply_appearance_mode(self._button_color))
self._canvas.itemconfig("border_parts_right",
outline=self._apply_appearance_mode(self._button_color),
fill=self._apply_appearance_mode(self._button_color))
self._entry.configure(bg=self._apply_appearance_mode(self._fg_color),
fg=self._apply_appearance_mode(self._text_color),
readonlybackground=self._apply_appearance_mode(self._fg_color),
disabledbackground=self._apply_appearance_mode(self._fg_color),
disabledforeground=self._apply_appearance_mode(self._text_color_disabled),
highlightcolor=self._apply_appearance_mode(self._fg_color),
insertbackground=self._apply_appearance_mode(self._text_color))
if self._state == tkinter.DISABLED:
self._canvas.itemconfig("dropdown_arrow",
fill=self._apply_appearance_mode(self._text_color_disabled))
else:
self._canvas.itemconfig("dropdown_arrow",
fill=self._apply_appearance_mode(self._text_color))
def _open_dropdown_menu(self):
self._dropdown_menu.open(self.winfo_rootx(),
self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0))
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
self._create_grid()
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"))
require_redraw = True
if "button_color" in kwargs:
self._button_color = self._check_color_type(kwargs.pop("button_color"))
require_redraw = True
if "button_hover_color" in kwargs:
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
require_redraw = True
if "dropdown_fg_color" in kwargs:
self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_fg_color"))
if "dropdown_hover_color" in kwargs:
self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
if "dropdown_text_color" in kwargs:
self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
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 "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
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 "state" in kwargs:
self._state = kwargs.pop("state")
self._entry.configure(state=self._state)
require_redraw = True
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "variable" in kwargs:
self._variable = kwargs.pop("variable")
self._entry.configure(textvariable=self._variable)
if "command" in kwargs:
self._command = kwargs.pop("command")
if "justify" in kwargs:
self._entry.configure(justify=kwargs.pop("justify"))
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "button_color":
return self._button_color
elif attribute_name == "button_hover_color":
return self._button_hover_color
elif attribute_name == "dropdown_fg_color":
return self._dropdown_menu.cget("fg_color")
elif attribute_name == "dropdown_hover_color":
return self._dropdown_menu.cget("hover_color")
elif attribute_name == "dropdown_text_color":
return self._dropdown_menu.cget("text_color")
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text_color_disabled":
return self._text_color_disabled
elif attribute_name == "font":
return self._font
elif attribute_name == "dropdown_font":
return self._dropdown_menu.cget("font")
elif attribute_name == "values":
return copy.copy(self._values)
elif attribute_name == "state":
return self._state
elif attribute_name == "hover":
return self._hover
elif attribute_name == "variable":
return self._variable
elif attribute_name == "command":
return self._command
elif attribute_name == "justify":
return self._entry.cget("justify")
else:
return super().cget(attribute_name)
def _on_enter(self, event=0):
if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0:
if sys.platform == "darwin" and len(self._values) > 0 and self._cursor_manipulation_enabled:
self._canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and len(self._values) > 0 and self._cursor_manipulation_enabled:
self._canvas.configure(cursor="hand2")
# set color of inner button parts to hover color
self._canvas.itemconfig("inner_parts_right",
outline=self._apply_appearance_mode(self._button_hover_color),
fill=self._apply_appearance_mode(self._button_hover_color))
self._canvas.itemconfig("border_parts_right",
outline=self._apply_appearance_mode(self._button_hover_color),
fill=self._apply_appearance_mode(self._button_hover_color))
def _on_leave(self, event=0):
if sys.platform == "darwin" and len(self._values) > 0 and self._cursor_manipulation_enabled:
self._canvas.configure(cursor="arrow")
elif sys.platform.startswith("win") and len(self._values) > 0 and self._cursor_manipulation_enabled:
self._canvas.configure(cursor="arrow")
# set color of inner button parts
self._canvas.itemconfig("inner_parts_right",
outline=self._apply_appearance_mode(self._button_color),
fill=self._apply_appearance_mode(self._button_color))
self._canvas.itemconfig("border_parts_right",
outline=self._apply_appearance_mode(self._button_color),
fill=self._apply_appearance_mode(self._button_color))
def _dropdown_callback(self, value: str):
if self._state == "readonly":
self._entry.configure(state="normal")
self._entry.delete(0, tkinter.END)
self._entry.insert(0, value)
self._entry.configure(state="readonly")
else:
self._entry.delete(0, tkinter.END)
self._entry.insert(0, value)
if self._command is not None:
self._command(value)
def set(self, value: str):
if self._state == "readonly":
self._entry.configure(state="normal")
self._entry.delete(0, tkinter.END)
self._entry.insert(0, value)
self._entry.configure(state="readonly")
else:
self._entry.delete(0, tkinter.END)
self._entry.insert(0, value)
def get(self) -> str:
return self._entry.get()
def _clicked(self, event=None):
if self._state is not tkinter.DISABLED and len(self._values) > 0:
self._open_dropdown_menu()
def bind(self, sequence=None, command=None, add=True):
""" called on the tkinter.Entry """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._entry.bind(sequence, command, add=True)
def unbind(self, sequence=None, funcid=None):
""" called on the tkinter.Entry """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._entry.unbind(sequence, None) # unbind all callbacks for sequence
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
def focus(self):
return self._entry.focus()
def focus_set(self):
return self._entry.focus_set()
def focus_force(self):
return self._entry.focus_force()

View File

@ -0,0 +1,384 @@
import tkinter
from typing import Union, Tuple, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
from .utility import pop_from_dict_by_set, check_kwargs_empty
class CTkEntry(CTkBaseClass):
"""
Entry with rounded corners, border, textvariable support, focus and placeholder.
For detailed information check out the documentation.
"""
_minimum_x_padding = 6 # minimum padding between tkinter entry and frame border
# attributes that are passed to and managed by the tkinter entry only:
_valid_tk_entry_attributes = {"exportselection", "insertborderwidth", "insertofftime",
"insertontime", "insertwidth", "justify", "selectborderwidth",
"show", "takefocus", "validate", "validatecommand", "xscrollcommand"}
def __init__(self,
master: any,
width: int = 140,
height: int = 28,
corner_radius: Optional[int] = None,
border_width: Optional[int] = 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,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
placeholder_text_color: Optional[Union[str, Tuple[str, str]]] = None,
textvariable: Union[tkinter.Variable, None] = None,
placeholder_text: Union[str, None] = None,
font: Optional[Union[tuple, CTkFont]] = None,
state: str = tkinter.NORMAL,
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height)
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
# color
self._fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
self._text_color = ThemeManager.theme["CTkEntry"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._placeholder_text_color = ThemeManager.theme["CTkEntry"]["placeholder_text_color"] if placeholder_text_color is None else self._check_color_type(placeholder_text_color)
self._border_color = ThemeManager.theme["CTkEntry"]["border_color"] if border_color is None else self._check_color_type(border_color)
# shape
self._corner_radius = ThemeManager.theme["CTkEntry"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkEntry"]["border_width"] if border_width is None else border_width
# text and state
self._is_focused: bool = True
self._placeholder_text = placeholder_text
self._placeholder_text_active = False
self._pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back
self._textvariable = textvariable
self._state = state
self._textvariable_callback_name: str = ""
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
if not (self._textvariable is None or self._textvariable == ""):
self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._draw_engine = DrawEngine(self._canvas)
self._entry = tkinter.Entry(master=self,
bd=0,
width=1,
highlightthickness=0,
font=self._apply_font_scaling(self._font),
state=self._state,
textvariable=self._textvariable,
**pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes))
check_kwargs_empty(kwargs, raise_error=True)
self._create_grid()
self._activate_placeholder()
self._create_bindings()
self._draw()
def _create_bindings(self, sequence: Optional[str] = None):
""" set necessary bindings for functionality of widget, will overwrite other bindings """
if sequence is None or sequence == "<FocusIn>":
self._entry.bind("<FocusIn>", self._entry_focus_in)
if sequence is None or sequence == "<FocusOut>":
self._entry.bind("<FocusOut>", self._entry_focus_out)
def _create_grid(self):
self._canvas.grid(column=0, row=0, sticky="nswe")
if self._corner_radius >= self._minimum_x_padding:
self._entry.grid(column=0, row=0, sticky="nswe",
padx=min(self._apply_widget_scaling(self._corner_radius), round(self._apply_widget_scaling(self._current_height/2))),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1)))
else:
self._entry.grid(column=0, row=0, sticky="nswe",
padx=self._apply_widget_scaling(self._minimum_x_padding),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1)))
def _textvariable_callback(self, var_name, index, mode):
if self._textvariable.get() == "":
self._activate_placeholder()
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self._entry.configure(font=self._apply_font_scaling(self._font))
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height))
self._create_grid()
self._draw(no_color_updates=True)
def _set_dimensions(self, width=None, height=None):
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._draw(no_color_updates=True)
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
self._entry.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._canvas.grid_forget()
self._canvas.grid(column=0, row=0, sticky="nswe")
def destroy(self):
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
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),
outline=self._apply_appearance_mode(self._bg_color))
self._entry.configure(bg=self._apply_appearance_mode(self._bg_color),
disabledbackground=self._apply_appearance_mode(self._bg_color),
readonlybackground=self._apply_appearance_mode(self._bg_color),
highlightcolor=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))
self._entry.configure(bg=self._apply_appearance_mode(self._fg_color),
disabledbackground=self._apply_appearance_mode(self._fg_color),
readonlybackground=self._apply_appearance_mode(self._fg_color),
highlightcolor=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))
if self._placeholder_text_active:
self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color),
disabledforeground=self._apply_appearance_mode(self._placeholder_text_color),
insertbackground=self._apply_appearance_mode(self._placeholder_text_color))
else:
self._entry.config(fg=self._apply_appearance_mode(self._text_color),
disabledforeground=self._apply_appearance_mode(self._text_color),
insertbackground=self._apply_appearance_mode(self._text_color))
def configure(self, require_redraw=False, **kwargs):
if "state" in kwargs:
self._state = kwargs.pop("state")
self._entry.configure(state=self._state)
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
require_redraw = True
if "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
if "placeholder_text_color" in kwargs:
self._placeholder_text_color = self._check_color_type(kwargs.pop("placeholder_text_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
self._create_grid()
require_redraw = True
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
self._create_grid()
require_redraw = True
if "placeholder_text" in kwargs:
self._placeholder_text = kwargs.pop("placeholder_text")
if self._placeholder_text_active:
self._entry.delete(0, tkinter.END)
self._entry.insert(0, self._placeholder_text)
else:
self._activate_placeholder()
if "textvariable" in kwargs:
self._textvariable = kwargs.pop("textvariable")
self._entry.configure(textvariable=self._textvariable)
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
if "show" in kwargs:
if self._placeholder_text_active:
self._pre_placeholder_arguments["show"] = kwargs.pop("show") # remember show argument for when placeholder gets deactivated
else:
self._entry.configure(show=kwargs.pop("show"))
self._entry.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes)) # configure Tkinter.Entry
super().configure(require_redraw=require_redraw, **kwargs) # configure CTkBaseClass
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "placeholder_text_color":
return self._placeholder_text_color
elif attribute_name == "textvariable":
return self._textvariable
elif attribute_name == "placeholder_text":
return self._placeholder_text
elif attribute_name == "font":
return self._font
elif attribute_name == "state":
return self._state
elif attribute_name in self._valid_tk_entry_attributes:
return self._entry.cget(attribute_name) # cget of tkinter.Entry
else:
return super().cget(attribute_name) # cget of CTkBaseClass
def bind(self, sequence=None, command=None, add=True):
""" called on the tkinter.Entry """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._entry.bind(sequence, command, add=True)
def unbind(self, sequence=None, funcid=None):
""" called on the tkinter.Entry """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._entry.unbind(sequence, None) # unbind all callbacks for sequence
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
def _activate_placeholder(self):
if self._entry.get() == "" and self._placeholder_text is not None and (self._textvariable is None or self._textvariable == ""):
self._placeholder_text_active = True
self._pre_placeholder_arguments = {"show": self._entry.cget("show")}
self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color),
disabledforeground=self._apply_appearance_mode(self._placeholder_text_color),
show="")
self._entry.delete(0, tkinter.END)
self._entry.insert(0, self._placeholder_text)
def _deactivate_placeholder(self):
if self._placeholder_text_active and self._entry.cget("state") != "readonly":
self._placeholder_text_active = False
self._entry.config(fg=self._apply_appearance_mode(self._text_color),
disabledforeground=self._apply_appearance_mode(self._text_color),)
self._entry.delete(0, tkinter.END)
for argument, value in self._pre_placeholder_arguments.items():
self._entry[argument] = value
def _entry_focus_out(self, event=None):
self._activate_placeholder()
self._is_focused = False
def _entry_focus_in(self, event=None):
self._deactivate_placeholder()
self._is_focused = True
def delete(self, first_index, last_index=None):
self._entry.delete(first_index, last_index)
if not self._is_focused and self._entry.get() == "":
self._activate_placeholder()
def insert(self, index, string):
self._deactivate_placeholder()
return self._entry.insert(index, string)
def get(self):
if self._placeholder_text_active:
return ""
else:
return self._entry.get()
def focus(self):
self._entry.focus()
def focus_set(self):
self._entry.focus_set()
def focus_force(self):
self._entry.focus_force()
def index(self, index):
return self._entry.index(index)
def icursor(self, index):
return self._entry.icursor(index)
def select_adjust(self, index):
return self._entry.select_adjust(index)
def select_from(self, index):
return self._entry.icursor(index)
def select_clear(self):
return self._entry.select_clear()
def select_present(self):
return self._entry.select_present()
def select_range(self, start_index, end_index):
return self._entry.select_range(start_index, end_index)
def select_to(self, index):
return self._entry.select_to(index)
def xview(self, index):
return self._entry.xview(index)
def xview_moveto(self, f):
return self._entry.xview_moveto(f)
def xview_scroll(self, number, what):
return self._entry.xview_scroll(number, what)

View File

@ -0,0 +1,196 @@
from typing import Union, Tuple, List, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
class CTkFrame(CTkBaseClass):
"""
Frame with rounded corners and border.
Default foreground colors are set according to theme.
To make the frame transparent set fg_color=None.
For detailed information check out the documentation.
"""
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,
background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None,
overwrite_preferred_drawing_method: Union[str, None] = None,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self._border_color = ThemeManager.theme["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color)
# determine fg_color of frame
if fg_color is None:
if isinstance(self.master, CTkFrame):
if self.master._fg_color == ThemeManager.theme["CTkFrame"]["fg_color"]:
self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"]
else:
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
else:
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
else:
self._fg_color = self._check_color_type(fg_color, transparency=True)
self._background_corner_colors = background_corner_colors # rendering options for DrawEngine
# 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._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._canvas.place(x=0, y=0, relwidth=1, relheight=1)
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
self._draw_engine = DrawEngine(self._canvas)
self._overwrite_preferred_drawing_method = overwrite_preferred_drawing_method
self._draw(no_color_updates=True)
def winfo_children(self) -> List[any]:
"""
winfo_children of CTkFrame without self.canvas widget,
because it's not a child but part of the CTkFrame itself
"""
child_widgets = super().winfo_children()
try:
child_widgets.remove(self._canvas)
return child_widgets
except ValueError:
return child_widgets
def _set_scaling(self, *args, **kwargs):
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._draw()
def _set_dimensions(self, width=None, height=None):
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._draw()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
if not self._canvas.winfo_exists():
return
if self._background_corner_colors is not None:
self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height))
self._canvas.itemconfig("background_corner_top_left", fill=self._apply_appearance_mode(self._background_corner_colors[0]))
self._canvas.itemconfig("background_corner_top_right", fill=self._apply_appearance_mode(self._background_corner_colors[1]))
self._canvas.itemconfig("background_corner_bottom_right", fill=self._apply_appearance_mode(self._background_corner_colors[2]))
self._canvas.itemconfig("background_corner_bottom_left", fill=self._apply_appearance_mode(self._background_corner_colors[3]))
else:
self._canvas.delete("background_parts")
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),
overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method)
if no_color_updates is False or requires_recoloring:
if self._fg_color == "transparent":
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=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))
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))
# self._canvas.tag_lower("inner_parts") # maybe unnecessary, I don't know ???
# self._canvas.tag_lower("border_parts")
def configure(self, require_redraw=False, **kwargs):
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
# check if CTk widgets are children of the frame and change their bg_color to new frame fg_color
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self._fg_color)
if "bg_color" in kwargs:
# pass bg_color change to children if fg_color is "transparent"
if self._fg_color == "transparent":
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self._fg_color)
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
if "background_corner_colors" in kwargs:
self._background_corner_colors = kwargs.pop("background_corner_colors")
require_redraw = True
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "background_corner_colors":
return self._background_corner_colors
else:
return super().cget(attribute_name)
def bind(self, sequence=None, command=None, add=True):
""" called on the tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._canvas.bind(sequence, command, add=True)
def unbind(self, sequence=None, funcid=None):
""" called on the tkinter.Canvas """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._canvas.unbind(sequence, None)

View File

@ -0,0 +1,274 @@
import tkinter
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
from .image import CTkImage
from .utility import pop_from_dict_by_set, check_kwargs_empty
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:
_valid_tk_label_attributes = {"cursor", "justify", "padx", "pady",
"textvariable", "state", "takefocus", "underline"}
def __init__(self,
master: any,
width: int = 0,
height: int = 28,
corner_radius: Optional[int] = None,
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: str = "CTkLabel",
font: Optional[Union[tuple, CTkFont]] = None,
image: Union[CTkImage, None] = None,
compound: str = "center",
anchor: str = "center", # label anchor: center, n, e, s, w
wraplength: int = 0,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height)
# color
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)
# shape
self._corner_radius = ThemeManager.theme["CTkLabel"]["corner_radius"] if corner_radius is None else corner_radius
# text
self._anchor = anchor
self._text = text
self._wraplength = wraplength
# image
self._image = self._check_image_type(image)
self._compound = compound
if isinstance(self._image, CTkImage):
self._image.add_configure_callback(self._update_image)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.grid(row=0, column=0, sticky="nswe")
self._draw_engine = DrawEngine(self._canvas)
self._label = tkinter.Label(master=self,
highlightthickness=0,
padx=0,
pady=0,
borderwidth=0,
anchor=self._anchor,
compound=self._compound,
wraplength=self._apply_widget_scaling(self._wraplength),
text=self._text,
font=self._apply_font_scaling(self._font))
self._label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes))
check_kwargs_empty(kwargs, raise_error=True)
self._create_grid()
self._update_image()
self._draw()
def _set_scaling(self, *args, **kwargs):
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._label.configure(font=self._apply_font_scaling(self._font))
self._label.configure(wraplength=self._apply_widget_scaling(self._wraplength))
self._create_grid()
self._update_image()
self._draw(no_color_updates=True)
def _set_appearance_mode(self, mode_string):
super()._set_appearance_mode(mode_string)
self._update_image()
def _set_dimensions(self, width=None, height=None):
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._create_grid()
self._draw()
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
self._label.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._canvas.grid_forget()
self._canvas.grid(row=0, column=0, sticky="nswe")
def _update_image(self):
if isinstance(self._image, CTkImage):
self._label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(),
self._get_appearance_mode()))
elif self._image is not None:
self._label.configure(image=self._image)
def destroy(self):
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _create_grid(self):
""" configure grid system (1x1) """
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
self._label.grid(row=0, column=0, sticky=text_label_grid_sticky,
padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height / 2))))
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
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),
0)
if no_color_updates is False or requires_recoloring:
if self._apply_appearance_mode(self._fg_color) == "transparent":
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=self._apply_appearance_mode(self._bg_color))
self._label.configure(fg=self._apply_appearance_mode(self._text_color),
bg=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))
self._label.configure(fg=self._apply_appearance_mode(self._text_color),
bg=self._apply_appearance_mode(self._fg_color))
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
self._create_grid()
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
if "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
if "text" in kwargs:
self._text = kwargs.pop("text")
self._label.configure(text=self._text)
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
if "image" in kwargs:
if isinstance(self._image, CTkImage):
self._image.remove_configure_callback(self._update_image)
self._image = self._check_image_type(kwargs.pop("image"))
if isinstance(self._image, CTkImage):
self._image.add_configure_callback(self._update_image)
self._update_image()
if "compound" in kwargs:
self._compound = kwargs.pop("compound")
self._label.configure(compound=self._compound)
if "anchor" in kwargs:
self._anchor = kwargs.pop("anchor")
self._label.configure(anchor=self._anchor)
self._create_grid()
if "wraplength" in kwargs:
self._wraplength = kwargs.pop("wraplength")
self._label.configure(wraplength=self._apply_widget_scaling(self._wraplength))
self._label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes)) # configure tkinter.Label
super().configure(require_redraw=require_redraw, **kwargs) # configure CTkBaseClass
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text":
return self._text
elif attribute_name == "font":
return self._font
elif attribute_name == "image":
return self._image
elif attribute_name == "compound":
return self._compound
elif attribute_name == "anchor":
return self._anchor
elif attribute_name == "wraplength":
return self._wraplength
elif attribute_name in self._valid_tk_label_attributes:
return self._label.cget(attribute_name) # cget of tkinter.Label
else:
return super().cget(attribute_name) # cget of CTkBaseClass
def bind(self, sequence: str = None, command: Callable = None, add: str = True):
""" called on the tkinter.Label and tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._canvas.bind(sequence, command, add=True)
self._label.bind(sequence, command, add=True)
def unbind(self, sequence: str = None, funcid: Optional[str] = None):
""" called on the tkinter.Label and tkinter.Canvas """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._canvas.unbind(sequence, None)
self._label.unbind(sequence, None)
def focus(self):
return self._label.focus()
def focus_set(self):
return self._label.focus_set()
def focus_force(self):
return self._label.focus_force()

View File

@ -0,0 +1,422 @@
import tkinter
import copy
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .core_widget_classes import DropdownMenu
from .font import CTkFont
class CTkOptionMenu(CTkBaseClass):
"""
Optionmenu with rounded corners, dropdown menu, variable support, command.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 140,
height: int = 28,
corner_radius: Optional[Union[int]] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
button_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_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,
dropdown_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_text_color: Optional[Union[str, Tuple[str, str]]] = None,
font: Optional[Union[tuple, CTkFont]] = None,
dropdown_font: Optional[Union[tuple, CTkFont]] = None,
values: Optional[list] = None,
variable: Union[tkinter.Variable, None] = None,
state: str = tkinter.NORMAL,
hover: bool = True,
command: Union[Callable[[str], None], None] = None,
dynamic_resizing: bool = True,
anchor: str = "w",
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color variables
self._fg_color = ThemeManager.theme["CTkOptionMenu"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._button_color = ThemeManager.theme["CTkOptionMenu"]["button_color"] if button_color is None else self._check_color_type(button_color)
self._button_hover_color = ThemeManager.theme["CTkOptionMenu"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
# shape
self._corner_radius = ThemeManager.theme["CTkOptionMenu"]["corner_radius"] if corner_radius is None else corner_radius
# text and font
self._text_color = ThemeManager.theme["CTkOptionMenu"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkOptionMenu"]["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)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# callback and hover functionality
self._command = command
self._variable = variable
self._variable_callback_blocked: bool = False
self._variable_callback_name: Union[str, None] = None
self._state = state
self._hover = hover
self._dynamic_resizing = dynamic_resizing
if values is None:
self._values = ["CTkOptionMenu"]
else:
self._values = values
if len(self._values) > 0:
self._current_value = self._values[0]
else:
self._current_value = "CTkOptionMenu"
self._dropdown_menu = DropdownMenu(master=self,
values=self._values,
command=self._dropdown_callback,
fg_color=dropdown_fg_color,
hover_color=dropdown_hover_color,
text_color=dropdown_text_color,
font=dropdown_font)
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw_engine = DrawEngine(self._canvas)
self._text_label = tkinter.Label(master=self,
font=self._apply_font_scaling(self._font),
anchor=anchor,
padx=0,
pady=0,
borderwidth=1,
text=self._current_value)
if self._cursor_manipulation_enabled:
if sys.platform == "darwin":
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win"):
self.configure(cursor="hand2")
self._create_grid()
if not self._dynamic_resizing:
self.grid_propagate(0)
self._create_bindings()
self._draw() # initial draw
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._current_value = self._variable.get()
self._text_label.configure(text=self._current_value)
def _create_bindings(self, sequence: Optional[str] = None):
""" set necessary bindings for functionality of widget, will overwrite other bindings """
if sequence is None or sequence == "<Enter>":
self._canvas.bind("<Enter>", self._on_enter)
self._text_label.bind("<Enter>", self._on_enter)
if sequence is None or sequence == "<Leave>":
self._canvas.bind("<Leave>", self._on_leave)
self._text_label.bind("<Leave>", self._on_leave)
if sequence is None or sequence == "<Button-1>":
self._canvas.bind("<Button-1>", self._clicked)
self._text_label.bind("<Button-1>", self._clicked)
def _create_grid(self):
self._canvas.grid(row=0, column=0, sticky="nsew")
left_section_width = self._current_width - self._current_height
self._text_label.grid(row=0, column=0, sticky="ew",
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)),
max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))))
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
# change label font size and grid padding
self._text_label.configure(font=self._apply_font_scaling(self._font))
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._create_grid()
self._draw(no_color_updates=True)
def _set_dimensions(self, width: int = None, height: int = None):
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._draw()
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
self._text_label.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._canvas.grid_forget()
self._canvas.grid(row=0, column=0, sticky="nsew")
def destroy(self):
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
left_section_width = self._current_width - self._current_height
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border_vertical_split(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._corner_radius),
0,
self._apply_widget_scaling(left_section_width))
requires_recoloring_2 = self._draw_engine.draw_dropdown_arrow(self._apply_widget_scaling(self._current_width - (self._current_height / 2)),
self._apply_widget_scaling(self._current_height / 2),
self._apply_widget_scaling(self._current_height / 3))
if no_color_updates is False or requires_recoloring or requires_recoloring_2:
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
self._canvas.itemconfig("inner_parts_left",
outline=self._apply_appearance_mode(self._fg_color),
fill=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("inner_parts_right",
outline=self._apply_appearance_mode(self._button_color),
fill=self._apply_appearance_mode(self._button_color))
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
if self._state == tkinter.DISABLED:
self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
self._canvas.itemconfig("dropdown_arrow",
fill=self._apply_appearance_mode(self._text_color_disabled))
else:
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
self._canvas.itemconfig("dropdown_arrow",
fill=self._apply_appearance_mode(self._text_color))
self._text_label.configure(bg=self._apply_appearance_mode(self._fg_color))
self._canvas.update_idletasks()
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
self._create_grid()
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
require_redraw = True
if "button_color" in kwargs:
self._button_color = self._check_color_type(kwargs.pop("button_color"))
require_redraw = True
if "button_hover_color" in kwargs:
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
require_redraw = True
if "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
if "dropdown_fg_color" in kwargs:
self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_fg_color"))
if "dropdown_hover_color" in kwargs:
self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
if "dropdown_text_color" in kwargs:
self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
if "command" in kwargs:
self._command = kwargs.pop("command")
if "variable" in kwargs:
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._current_value = self._variable.get()
self._text_label.configure(text=self._current_value)
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 "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "state" in kwargs:
self._state = kwargs.pop("state")
require_redraw = True
if "dynamic_resizing" in kwargs:
self._dynamic_resizing = kwargs.pop("dynamic_resizing")
if not self._dynamic_resizing:
self.grid_propagate(0)
else:
self.grid_propagate(1)
if "anchor" in kwargs:
self._text_label.configure(anchor=kwargs.pop("anchor"))
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "button_color":
return self._button_color
elif attribute_name == "button_hover_color":
return self._button_hover_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text_color_disabled":
return self._text_color_disabled
elif attribute_name == "dropdown_fg_color":
return self._dropdown_menu.cget("fg_color")
elif attribute_name == "dropdown_hover_color":
return self._dropdown_menu.cget("hover_color")
elif attribute_name == "dropdown_text_color":
return self._dropdown_menu.cget("text_color")
elif attribute_name == "font":
return self._font
elif attribute_name == "dropdown_font":
return self._dropdown_menu.cget("font")
elif attribute_name == "values":
return copy.copy(self._values)
elif attribute_name == "variable":
return self._variable
elif attribute_name == "state":
return self._state
elif attribute_name == "hover":
return self._hover
elif attribute_name == "command":
return self._command
elif attribute_name == "dynamic_resizing":
return self._dynamic_resizing
elif attribute_name == "anchor":
return self._text_label.cget("anchor")
else:
return super().cget(attribute_name)
def _open_dropdown_menu(self):
self._dropdown_menu.open(self.winfo_rootx(),
self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0))
def _on_enter(self, event=0):
if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0:
# set color of inner button parts to hover color
self._canvas.itemconfig("inner_parts_right",
outline=self._apply_appearance_mode(self._button_hover_color),
fill=self._apply_appearance_mode(self._button_hover_color))
def _on_leave(self, event=0):
# set color of inner button parts
self._canvas.itemconfig("inner_parts_right",
outline=self._apply_appearance_mode(self._button_color),
fill=self._apply_appearance_mode(self._button_color))
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self._current_value = self._variable.get()
self._text_label.configure(text=self._current_value)
def _dropdown_callback(self, value: str):
self._current_value = value
self._text_label.configure(text=self._current_value)
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(self._current_value)
self._variable_callback_blocked = False
if self._command is not None:
self._command(self._current_value)
def set(self, value: str):
self._current_value = value
self._text_label.configure(text=self._current_value)
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(self._current_value)
self._variable_callback_blocked = False
def get(self) -> str:
return self._current_value
def _clicked(self, event=0):
if self._state is not tkinter.DISABLED and len(self._values) > 0:
self._open_dropdown_menu()
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
""" called on the tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._canvas.bind(sequence, command, add=True)
self._text_label.bind(sequence, command, add=True)
def unbind(self, sequence: str = None, funcid: str = None):
""" called on the tkinter.Label and tkinter.Canvas """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._canvas.unbind(sequence, None)
self._text_label.unbind(sequence, None)
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
def focus(self):
return self._text_label.focus()
def focus_set(self):
return self._text_label.focus_set()
def focus_force(self):
return self._text_label.focus_force()

View File

@ -0,0 +1,312 @@
import tkinter
import math
from typing import Union, Tuple, Optional, Callable
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
class CTkProgressBar(CTkBaseClass):
"""
Progressbar with rounded corners, border, variable support,
indeterminate mode, vertical orientation.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: Optional[int] = None,
height: Optional[int] = None,
corner_radius: Optional[int] = None,
border_width: Optional[int] = 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,
progress_color: Optional[Union[str, Tuple[str, str]]] = None,
variable: Union[tkinter.Variable, None] = None,
orientation: str = "horizontal",
mode: Literal["determinate", "indeterminate"] = "determinate",
determinate_speed: float = 1,
indeterminate_speed: float = 1,
**kwargs):
# set default dimensions according to orientation
if width is None:
if orientation.lower() == "vertical":
width = 8
else:
width = 200
if height is None:
if orientation.lower() == "vertical":
height = 200
else:
height = 8
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self._border_color = ThemeManager.theme["CTkProgressBar"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._fg_color = ThemeManager.theme["CTkProgressBar"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._progress_color = ThemeManager.theme["CTkProgressBar"]["progress_color"] if progress_color is None else self._check_color_type(progress_color)
# control variable
self._variable = variable
self._variable_callback_blocked = False
self._variable_callback_name = None
self._loop_after_id = None
# shape
self._corner_radius = ThemeManager.theme["CTkProgressBar"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkProgressBar"]["border_width"] if border_width is None else border_width
self._determinate_value: float = 0.5 # range 0-1
self._determinate_speed = determinate_speed # range 0-1
self._indeterminate_value: float = 0 # range 0-inf
self._indeterminate_width: float = 0.4 # range 0-1
self._indeterminate_speed = indeterminate_speed # range 0-1 to travel in 50ms
self._loop_running: bool = False
self._orientation = orientation
self._mode = mode # "determinate" or "indeterminate"
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe")
self._draw_engine = DrawEngine(self._canvas)
self._draw() # initial draw
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._variable_callback_blocked = True
self.set(self._variable.get(), from_variable_callback=True)
self._variable_callback_blocked = False
def _set_scaling(self, *args, **kwargs):
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._draw(no_color_updates=True)
def _set_dimensions(self, width=None, height=None):
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._draw()
def destroy(self):
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
if self._orientation.lower() == "horizontal":
orientation = "w"
elif self._orientation.lower() == "vertical":
orientation = "s"
else:
orientation = "w"
if self._mode == "determinate":
requires_recoloring = self._draw_engine.draw_rounded_progress_bar_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),
0,
self._determinate_value,
orientation)
else: # indeterminate mode
progress_value = (math.sin(self._indeterminate_value * math.pi / 40) + 1) / 2
progress_value_1 = min(1.0, progress_value + (self._indeterminate_width / 2))
progress_value_2 = max(0.0, progress_value - (self._indeterminate_width / 2))
requires_recoloring = self._draw_engine.draw_rounded_progress_bar_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),
progress_value_1,
progress_value_2,
orientation)
if no_color_updates is False or requires_recoloring:
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_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),
outline=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("progress_parts",
fill=self._apply_appearance_mode(self._progress_color),
outline=self._apply_appearance_mode(self._progress_color))
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
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"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
if "progress_color" in kwargs:
self._progress_color = self._check_color_type(kwargs.pop("progress_color"))
require_redraw = True
if "variable" in kwargs:
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.set(self._variable.get(), from_variable_callback=True)
else:
self._variable = None
if "mode" in kwargs:
self._mode = kwargs.pop("mode")
require_redraw = True
if "determinate_speed" in kwargs:
self._determinate_speed = kwargs.pop("determinate_speed")
if "indeterminate_speed" in kwargs:
self._indeterminate_speed = kwargs.pop("indeterminate_speed")
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "progress_color":
return self._progress_color
elif attribute_name == "variable":
return self._variable
elif attribute_name == "orientation":
return self._orientation
elif attribute_name == "mode":
return self._mode
elif attribute_name == "determinate_speed":
return self._determinate_speed
elif attribute_name == "indeterminate_speed":
return self._indeterminate_speed
else:
return super().cget(attribute_name)
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self.set(self._variable.get(), from_variable_callback=True)
def set(self, value, from_variable_callback=False):
""" set determinate value """
self._determinate_value = value
if self._determinate_value > 1:
self._determinate_value = 1
elif self._determinate_value < 0:
self._determinate_value = 0
self._draw(no_color_updates=True)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(round(self._determinate_value) if isinstance(self._variable, tkinter.IntVar) else self._determinate_value)
self._variable_callback_blocked = False
def get(self) -> float:
""" get determinate value """
return self._determinate_value
def start(self):
""" start automatic mode """
if not self._loop_running:
self._loop_running = True
self._internal_loop()
def stop(self):
""" stop automatic mode """
if self._loop_after_id is not None:
self.after_cancel(self._loop_after_id)
self._loop_running = False
def _internal_loop(self):
if self._loop_running:
if self._mode == "determinate":
self._determinate_value += self._determinate_speed / 50
if self._determinate_value > 1:
self._determinate_value -= 1
self._draw()
self._loop_after_id = self.after(20, self._internal_loop)
else:
self._indeterminate_value += self._indeterminate_speed
self._draw()
self._loop_after_id = self.after(20, self._internal_loop)
def step(self):
""" increase progress """
if self._mode == "determinate":
self._determinate_value += self._determinate_speed / 50
if self._determinate_value > 1:
self._determinate_value -= 1
self._draw()
else:
self._indeterminate_value += self._indeterminate_speed
self._draw()
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
""" called on the tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._canvas.bind(sequence, command, add=True)
def unbind(self, sequence: str = None, funcid: str = None):
""" called on the tkinter.Label and tkinter.Canvas """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._canvas.unbind(sequence, None)
def focus(self):
return self._canvas.focus()
def focus_set(self):
return self._canvas.focus_set()
def focus_force(self):
return self._canvas.focus_force()

View File

@ -0,0 +1,430 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
class CTkRadioButton(CTkBaseClass):
"""
Radiobutton with rounded corners, border, label, variable support, command.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 100,
height: int = 22,
radiobutton_width: int = 22,
radiobutton_height: int = 22,
corner_radius: Optional[int] = None,
border_width_unchecked: Optional[int] = None,
border_width_checked: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
hover_color: Optional[Union[str, Tuple[str, str]]] = None,
border_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 = "CTkRadioButton",
font: Optional[Union[tuple, CTkFont]] = None,
textvariable: Union[tkinter.Variable, None] = None,
variable: Union[tkinter.Variable, None] = None,
value: Union[int, str] = 0,
state: str = tkinter.NORMAL,
hover: bool = True,
command: Union[Callable, None] = None,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# dimensions
self._radiobutton_width = radiobutton_width
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)
# 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
# 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)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# callback and control variables
self._command = command
self._state = state
self._hover = hover
self._check_state: bool = False
self._value = value
self._variable: tkinter.Variable = variable
self._variable_callback_blocked: bool = False
self._textvariable = textvariable
self._variable_callback_name: Union[str, None] = None
# configure grid system (3x1)
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,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._radiobutton_width),
height=self._apply_widget_scaling(self._radiobutton_height))
self._canvas.grid(row=0, column=0)
self._draw_engine = DrawEngine(self._canvas)
self._text_label = tkinter.Label(master=self,
bd=0,
padx=0,
pady=0,
text=self._text,
justify=tkinter.LEFT,
font=self._apply_font_scaling(self._font),
textvariable=self._textvariable)
self._text_label.grid(row=0, column=2, sticky="w")
self._text_label["anchor"] = "w"
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._value else False
self._create_bindings()
self._set_cursor()
self._draw()
def _create_bindings(self, sequence: Optional[str] = None):
""" set necessary bindings for functionality of widget, will overwrite other bindings """
if sequence is None or sequence == "<Enter>":
self._canvas.bind("<Enter>", self._on_enter)
self._text_label.bind("<Enter>", self._on_enter)
if sequence is None or sequence == "<Leave>":
self._canvas.bind("<Leave>", self._on_leave)
self._text_label.bind("<Leave>", self._on_leave)
if sequence is None or sequence == "<Button-1>":
self._canvas.bind("<Button-1>", self.invoke)
self._text_label.bind("<Button-1>", self.invoke)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self._text_label.configure(font=self._apply_font_scaling(self._font))
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.configure(width=self._apply_widget_scaling(self._radiobutton_width),
height=self._apply_widget_scaling(self._radiobutton_height))
self._draw(no_color_updates=True)
def _set_dimensions(self, width: int = None, height: int = None):
super()._set_dimensions(width, height)
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
self._text_label.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._bg_canvas.grid_forget()
self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
def destroy(self):
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
if self._check_state is True:
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._radiobutton_width),
self._apply_widget_scaling(self._radiobutton_height),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width_checked))
else:
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._radiobutton_width),
self._apply_widget_scaling(self._radiobutton_height),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width_unchecked))
if no_color_updates is False or requires_recoloring:
self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
if self._check_state is False:
self._canvas.itemconfig("border_parts",
outline=self._apply_appearance_mode(self._border_color),
fill=self._apply_appearance_mode(self._border_color))
else:
self._canvas.itemconfig("border_parts",
outline=self._apply_appearance_mode(self._fg_color),
fill=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(self._bg_color),
fill=self._apply_appearance_mode(self._bg_color))
if self._state == tkinter.DISABLED:
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color_disabled))
else:
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "border_width_unchecked" in kwargs:
self._border_width_unchecked = kwargs.pop("border_width_unchecked")
require_redraw = True
if "border_width_checked" in kwargs:
self._border_width_checked = kwargs.pop("border_width_checked")
require_redraw = True
if "radiobutton_width" in kwargs:
self._radiobutton_width = kwargs.pop("radiobutton_width")
self._canvas.configure(width=self._apply_widget_scaling(self._radiobutton_width))
require_redraw = True
if "radiobutton_height" in kwargs:
self._radiobutton_height = kwargs.pop("radiobutton_height")
self._canvas.configure(height=self._apply_widget_scaling(self._radiobutton_height))
require_redraw = True
if "text" in kwargs:
self._text = kwargs.pop("text")
self._text_label.configure(text=self._text)
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
if "state" in kwargs:
self._state = kwargs.pop("state")
self._set_cursor()
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
require_redraw = True
if "hover_color" in kwargs:
self._hover_color = self._check_color_type(kwargs.pop("hover_color"))
require_redraw = 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 "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "command" in kwargs:
self._command = kwargs.pop("command")
if "textvariable" in kwargs:
self._textvariable = kwargs.pop("textvariable")
self._text_label.configure(textvariable=self._textvariable)
if "variable" in kwargs:
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._value else False
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width_unchecked":
return self._border_width_unchecked
elif attribute_name == "border_width_checked":
return self._border_width_checked
elif attribute_name == "radiobutton_width":
return self._radiobutton_width
elif attribute_name == "radiobutton_height":
return self._radiobutton_height
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "hover_color":
return self._hover_color
elif attribute_name == "border_color":
return self._border_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
elif attribute_name == "font":
return self._font
elif attribute_name == "textvariable":
return self._textvariable
elif attribute_name == "variable":
return self._variable
elif attribute_name == "value":
return self._value
elif attribute_name == "state":
return self._state
elif attribute_name == "hover":
return self._hover
elif attribute_name == "command":
return self._command
else:
return super().cget(attribute_name)
def _set_cursor(self):
if self._cursor_manipulation_enabled:
if self._state == tkinter.DISABLED:
if sys.platform == "darwin":
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif sys.platform.startswith("win"):
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif self._state == tkinter.NORMAL:
if sys.platform == "darwin":
self._canvas.configure(cursor="pointinghand")
if self._text_label is not None:
self._text_label.configure(cursor="pointinghand")
elif sys.platform.startswith("win"):
self._canvas.configure(cursor="hand2")
if self._text_label is not None:
self._text_label.configure(cursor="hand2")
def _on_enter(self, event=0):
if self._hover is True and self._state == tkinter.NORMAL:
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._hover_color),
outline=self._apply_appearance_mode(self._hover_color))
def _on_leave(self, event=0):
if self._check_state is True:
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
else:
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._border_color),
outline=self._apply_appearance_mode(self._border_color))
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
if self._variable.get() == self._value:
self.select(from_variable_callback=True)
else:
self.deselect(from_variable_callback=True)
def invoke(self, event=0):
if self._state == tkinter.NORMAL:
if self._check_state is False:
self._check_state = True
self.select()
if self._command is not None:
self._command()
def select(self, from_variable_callback=False):
self._check_state = True
self._draw()
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._value)
self._variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
self._check_state = False
self._draw()
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set("")
self._variable_callback_blocked = False
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
""" called on the tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._canvas.bind(sequence, command, add=True)
self._text_label.bind(sequence, command, add=True)
def unbind(self, sequence: str = None, funcid: str = None):
""" called on the tkinter.Label and tkinter.Canvas """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._canvas.unbind(sequence, None)
self._text_label.unbind(sequence, None)
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
def focus(self):
return self._text_label.focus()
def focus_set(self):
return self._text_label.focus_set()
def focus_force(self):
return self._text_label.focus_force()

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

@ -0,0 +1,281 @@
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
class CTkScrollbar(CTkBaseClass):
"""
Scrollbar with rounded corners, configurable spacing.
Connect to scrollable widget by passing .set() method and set command attribute.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: Optional[Union[int, str]] = None,
height: Optional[Union[int, str]] = None,
corner_radius: Optional[int] = None,
border_spacing: Optional[int] = None,
minimum_pixel_length: int = 20,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
button_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
hover: bool = True,
command: Union[Callable, None] = None,
orientation: str = "vertical",
**kwargs):
# set default dimensions according to orientation
if width is None:
if orientation.lower() == "vertical":
width = 16
else:
width = 200
if height is None:
if orientation.lower() == "horizontal":
height = 16
else:
height = 200
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self._fg_color = ThemeManager.theme["CTkScrollbar"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
self._button_color = ThemeManager.theme["CTkScrollbar"]["button_color"] if button_color is None else self._check_color_type(button_color)
self._button_hover_color = ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
# shape
self._corner_radius = ThemeManager.theme["CTkScrollbar"]["corner_radius"] if corner_radius is None else corner_radius
self._border_spacing = ThemeManager.theme["CTkScrollbar"]["border_spacing"] if border_spacing is None else border_spacing
self._hover = hover
self._hover_state: bool = False
self._command = command
self._orientation = orientation
self._start_value: float = 0 # 0 to 1
self._end_value: float = 1 # 0 to 1
self._minimum_pixel_length = minimum_pixel_length
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._canvas.place(x=0, y=0, relwidth=1, relheight=1)
self._draw_engine = DrawEngine(self._canvas)
self._create_bindings()
self._draw()
def _create_bindings(self, sequence: Optional[str] = None):
""" set necessary bindings for functionality of widget, will overwrite other bindings """
if sequence is None:
self._canvas.tag_bind("border_parts", "<Button-1>", self._clicked)
if sequence is None or sequence == "<Enter>":
self._canvas.bind("<Enter>", self._on_enter)
if sequence is None or sequence == "<Leave>":
self._canvas.bind("<Leave>", self._on_leave)
if sequence is None or sequence == "<B1-Motion>":
self._canvas.bind("<B1-Motion>", self._clicked)
if sequence is None or sequence == "<MouseWheel>":
self._canvas.bind("<MouseWheel>", self._mouse_scroll_event)
def _set_scaling(self, *args, **kwargs):
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._draw(no_color_updates=True)
def _set_dimensions(self, width=None, height=None):
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._draw(no_color_updates=True)
def _get_scrollbar_values_for_minimum_pixel_size(self):
# correct scrollbar float values if scrollbar is too small
if self._orientation == "vertical":
scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_height
if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_height != 0:
# calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_height)
corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor
corrected_start_value = self._start_value - self._start_value * interval_extend_factor
return corrected_start_value, corrected_end_value
else:
return self._start_value, self._end_value
else:
scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_width
if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_width != 0:
# calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_width)
corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor
corrected_start_value = self._start_value - self._start_value * interval_extend_factor
return corrected_start_value, corrected_end_value
else:
return self._start_value, self._end_value
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
corrected_start_value, corrected_end_value = self._get_scrollbar_values_for_minimum_pixel_size()
requires_recoloring = self._draw_engine.draw_rounded_scrollbar(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_spacing),
corrected_start_value,
corrected_end_value,
self._orientation)
if no_color_updates is False or requires_recoloring:
if self._hover_state is True:
self._canvas.itemconfig("scrollbar_parts",
fill=self._apply_appearance_mode(self._button_hover_color),
outline=self._apply_appearance_mode(self._button_hover_color))
else:
self._canvas.itemconfig("scrollbar_parts",
fill=self._apply_appearance_mode(self._button_color),
outline=self._apply_appearance_mode(self._button_color))
if self._fg_color == "transparent":
self._canvas.configure(bg=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.configure(bg=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
self._canvas.update_idletasks()
def configure(self, require_redraw=False, **kwargs):
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
if "button_color" in kwargs:
self._button_color = self._check_color_type(kwargs.pop("button_color"))
require_redraw = True
if "button_hover_color" in kwargs:
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
require_redraw = True
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "command" in kwargs:
self._command = kwargs.pop("command")
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "border_spacing" in kwargs:
self._border_spacing = kwargs.pop("border_spacing")
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_spacing":
return self._border_spacing
elif attribute_name == "minimum_pixel_length":
return self._minimum_pixel_length
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "scrollbar_color":
return self._button_color
elif attribute_name == "scrollbar_hover_color":
return self._button_hover_color
elif attribute_name == "hover":
return self._hover
elif attribute_name == "command":
return self._command
elif attribute_name == "orientation":
return self._orientation
else:
return super().cget(attribute_name)
def _on_enter(self, event=0):
if self._hover is True:
self._hover_state = True
self._canvas.itemconfig("scrollbar_parts",
outline=self._apply_appearance_mode(self._button_hover_color),
fill=self._apply_appearance_mode(self._button_hover_color))
def _on_leave(self, event=0):
self._hover_state = False
self._canvas.itemconfig("scrollbar_parts",
outline=self._apply_appearance_mode(self._button_color),
fill=self._apply_appearance_mode(self._button_color))
def _clicked(self, event):
if self._orientation == "vertical":
value = self._reverse_widget_scaling(((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing)))
else:
value = self._reverse_widget_scaling(((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing)))
current_scrollbar_length = self._end_value - self._start_value
value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2)))
self._start_value = value - (current_scrollbar_length / 2)
self._end_value = value + (current_scrollbar_length / 2)
self._draw()
if self._command is not None:
self._command('moveto', self._start_value)
def _mouse_scroll_event(self, event=None):
if self._command is not None:
if sys.platform.startswith("win"):
self._command('scroll', -int(event.delta/40), 'units')
else:
self._command('scroll', -event.delta, 'units')
def set(self, start_value: float, end_value: float):
self._start_value = float(start_value)
self._end_value = float(end_value)
self._draw()
def get(self):
return self._start_value, self._end_value
def bind(self, sequence=None, command=None, add=True):
""" called on the tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._canvas.bind(sequence, command, add=True)
def unbind(self, sequence=None, funcid=None):
""" called on the tkinter.Canvas, restores internal callbacks """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._canvas.unbind(sequence, None) # unbind all callbacks for sequence
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
def focus(self):
return self._canvas.focus()
def focus_set(self):
return self._canvas.focus_set()
def focus_force(self):
return self._canvas.focus_force()

View File

@ -0,0 +1,421 @@
import tkinter
import copy
from typing import Union, Tuple, List, Dict, Callable, Optional
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
from .theme import ThemeManager
from .font import CTkFont
from .ctk_button import CTkButton
from .ctk_frame import CTkFrame
class CTkSegmentedButton(CTkFrame):
"""
Segmented button with corner radius, border width, variable support.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 140,
height: int = 28,
corner_radius: Optional[int] = None,
border_width: int = 3,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
selected_color: Optional[Union[str, Tuple[str, str]]] = None,
selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
unselected_color: Optional[Union[str, Tuple[str, str]]] = None,
unselected_hover_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,
background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None,
font: Optional[Union[tuple, CTkFont]] = None,
values: Optional[list] = None,
variable: Union[tkinter.Variable, None] = None,
dynamic_resizing: bool = True,
command: Union[Callable[[str], None], None] = None,
state: str = "normal",
**kwargs):
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
self._sb_fg_color = ThemeManager.theme["CTkSegmentedButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._sb_selected_color = ThemeManager.theme["CTkSegmentedButton"]["selected_color"] if selected_color is None else self._check_color_type(selected_color)
self._sb_selected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["selected_hover_color"] if selected_hover_color is None else self._check_color_type(selected_hover_color)
self._sb_unselected_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_color"] if unselected_color is None else self._check_color_type(unselected_color)
self._sb_unselected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_hover_color"] if unselected_hover_color is None else self._check_color_type(unselected_hover_color)
self._sb_text_color = ThemeManager.theme["CTkSegmentedButton"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._sb_text_color_disabled = ThemeManager.theme["CTkSegmentedButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
self._sb_corner_radius = ThemeManager.theme["CTkSegmentedButton"]["corner_radius"] if corner_radius is None else corner_radius
self._sb_border_width = ThemeManager.theme["CTkSegmentedButton"]["border_width"] if border_width is None else border_width
self._background_corner_colors = background_corner_colors # rendering options for DrawEngine
self._command: Callable[[str], None] = command
self._font = CTkFont() if font is None else font
self._state = state
self._buttons_dict: Dict[str, CTkButton] = {} # mapped from value to button object
if values is None:
self._value_list: List[str] = ["CTkSegmentedButton"]
else:
self._value_list: List[str] = values # Values ordered like buttons rendered on widget
self._dynamic_resizing = dynamic_resizing
if not self._dynamic_resizing:
self.grid_propagate(False)
self._check_unique_values(self._value_list)
self._current_value: str = ""
if len(self._value_list) > 0:
self._create_buttons_from_values()
self._create_button_grid()
self._variable = variable
self._variable_callback_blocked: bool = False
self._variable_callback_name: Union[str, None] = None
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.set(self._variable.get(), from_variable_callback=True)
super().configure(corner_radius=self._sb_corner_radius, fg_color="transparent")
def destroy(self):
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
def _set_dimensions(self, width: int = None, height: int = None):
super()._set_dimensions(width, height)
for button in self._buttons_dict.values():
button.configure(height=height)
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self.set(self._variable.get(), from_variable_callback=True)
def _get_index_by_value(self, value: str):
for index, value_from_list in enumerate(self._value_list):
if value_from_list == value:
return index
raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
def _configure_button_corners_for_index(self, index: int):
if index == 0 and len(self._value_list) == 1:
if self._background_corner_colors is None:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
else:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=self._background_corner_colors)
elif index == 0:
if self._background_corner_colors is None:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._sb_fg_color, self._sb_fg_color, self._bg_color))
else:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._background_corner_colors[0], self._sb_fg_color, self._sb_fg_color, self._background_corner_colors[3]))
elif index == len(self._value_list) - 1:
if self._background_corner_colors is None:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._bg_color, self._bg_color, self._sb_fg_color))
else:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._background_corner_colors[1], self._background_corner_colors[2], self._sb_fg_color))
else:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._sb_fg_color, self._sb_fg_color, self._sb_fg_color))
def _unselect_button_by_value(self, value: str):
if value in self._buttons_dict:
self._buttons_dict[value].configure(fg_color=self._sb_unselected_color,
hover_color=self._sb_unselected_hover_color)
def _select_button_by_value(self, value: str):
if self._current_value is not None and self._current_value != "":
self._unselect_button_by_value(self._current_value)
self._current_value = value
self._buttons_dict[value].configure(fg_color=self._sb_selected_color,
hover_color=self._sb_selected_hover_color)
def _create_button(self, index: int, value: str) -> CTkButton:
new_button = CTkButton(self,
width=0,
height=self._current_height,
corner_radius=self._sb_corner_radius,
border_width=self._sb_border_width,
fg_color=self._sb_unselected_color,
border_color=self._sb_fg_color,
hover_color=self._sb_unselected_hover_color,
text_color=self._sb_text_color,
text_color_disabled=self._sb_text_color_disabled,
text=value,
font=self._font,
state=self._state,
command=lambda v=value: self.set(v, from_button_callback=True),
background_corner_colors=None,
round_width_to_even_numbers=False,
round_height_to_even_numbers=False) # DrawEngine rendering option (so that theres no gap between buttons)
return new_button
@staticmethod
def _check_unique_values(values: List[str]):
""" raises exception if values are not unique """
if len(values) != len(set(values)):
raise ValueError("CTkSegmentedButton values are not unique")
def _create_button_grid(self):
# remove minsize from every grid cell in the first row
number_of_columns, _ = self.grid_size()
for n in range(number_of_columns):
self.grid_columnconfigure(n, weight=1, minsize=0)
self.grid_rowconfigure(0, weight=1)
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")
def _create_buttons_from_values(self):
assert len(self._buttons_dict) == 0
assert len(self._value_list) > 0
for index, value in enumerate(self._value_list):
self._buttons_dict[value] = self._create_button(index, value)
self._configure_button_corners_for_index(index)
def configure(self, **kwargs):
if "bg_color" in kwargs:
super().configure(bg_color=kwargs.pop("bg_color"))
if len(self._buttons_dict) > 0:
self._configure_button_corners_for_index(0)
if len(self._buttons_dict) > 1:
max_index = len(self._buttons_dict) - 1
self._configure_button_corners_for_index(max_index)
if "fg_color" in kwargs:
self._sb_fg_color = self._check_color_type(kwargs.pop("fg_color"))
for index, button in enumerate(self._buttons_dict.values()):
button.configure(border_color=self._sb_fg_color)
self._configure_button_corners_for_index(index)
if "selected_color" in kwargs:
self._sb_selected_color = self._check_color_type(kwargs.pop("selected_color"))
if self._current_value in self._buttons_dict:
self._buttons_dict[self._current_value].configure(fg_color=self._sb_selected_color)
if "selected_hover_color" in kwargs:
self._sb_selected_hover_color = self._check_color_type(kwargs.pop("selected_hover_color"))
if self._current_value in self._buttons_dict:
self._buttons_dict[self._current_value].configure(hover_color=self._sb_selected_hover_color)
if "unselected_color" in kwargs:
self._sb_unselected_color = self._check_color_type(kwargs.pop("unselected_color"))
for value, button in self._buttons_dict.items():
if value != self._current_value:
button.configure(fg_color=self._sb_unselected_color)
if "unselected_hover_color" in kwargs:
self._sb_unselected_hover_color = self._check_color_type(kwargs.pop("unselected_hover_color"))
for value, button in self._buttons_dict.items():
if value != self._current_value:
button.configure(hover_color=self._sb_unselected_hover_color)
if "text_color" in kwargs:
self._sb_text_color = self._check_color_type(kwargs.pop("text_color"))
for button in self._buttons_dict.values():
button.configure(text_color=self._sb_text_color)
if "text_color_disabled" in kwargs:
self._sb_text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
for button in self._buttons_dict.values():
button.configure(text_color_disabled=self._sb_text_color_disabled)
if "background_corner_colors" in kwargs:
self._background_corner_colors = kwargs.pop("background_corner_colors")
for i in range(len(self._buttons_dict)):
self._configure_button_corners_for_index(i)
if "font" in kwargs:
self._font = kwargs.pop("font")
for button in self._buttons_dict.values():
button.configure(font=self._font)
if "values" in kwargs:
for button in self._buttons_dict.values():
button.destroy()
self._buttons_dict.clear()
self._value_list = kwargs.pop("values")
self._check_unique_values(self._value_list)
if len(self._value_list) > 0:
self._create_buttons_from_values()
self._create_button_grid()
if self._current_value in self._value_list:
self._select_button_by_value(self._current_value)
if "variable" in kwargs:
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.set(self._variable.get(), from_variable_callback=True)
else:
self._variable = None
if "dynamic_resizing" in kwargs:
self._dynamic_resizing = kwargs.pop("dynamic_resizing")
if not self._dynamic_resizing:
self.grid_propagate(False)
else:
self.grid_propagate(True)
if "command" in kwargs:
self._command = kwargs.pop("command")
if "state" in kwargs:
self._state = kwargs.pop("state")
for button in self._buttons_dict.values():
button.configure(state=self._state)
super().configure(**kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._sb_corner_radius
elif attribute_name == "border_width":
return self._sb_border_width
elif attribute_name == "fg_color":
return self._sb_fg_color
elif attribute_name == "selected_color":
return self._sb_selected_color
elif attribute_name == "selected_hover_color":
return self._sb_selected_hover_color
elif attribute_name == "unselected_color":
return self._sb_unselected_color
elif attribute_name == "unselected_hover_color":
return self._sb_unselected_hover_color
elif attribute_name == "text_color":
return self._sb_text_color
elif attribute_name == "text_color_disabled":
return self._sb_text_color_disabled
elif attribute_name == "font":
return self._font
elif attribute_name == "values":
return copy.copy(self._value_list)
elif attribute_name == "variable":
return self._variable
elif attribute_name == "dynamic_resizing":
return self._dynamic_resizing
elif attribute_name == "command":
return self._command
else:
return super().cget(attribute_name)
def set(self, value: str, from_variable_callback: bool = False, from_button_callback: bool = False):
if value == self._current_value:
return
elif value in self._buttons_dict:
self._select_button_by_value(value)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(value)
self._variable_callback_blocked = False
else:
if self._current_value in self._buttons_dict:
self._unselect_button_by_value(self._current_value)
self._current_value = value
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(value)
self._variable_callback_blocked = False
if from_button_callback:
if self._command is not None:
self._command(self._current_value)
def get(self) -> str:
return self._current_value
def insert(self, index: int, value: str):
if value not in self._buttons_dict:
if value != "":
self._value_list.insert(index, value)
self._buttons_dict[value] = self._create_button(index, value)
self._configure_button_corners_for_index(index)
if index > 0:
self._configure_button_corners_for_index(index - 1)
if index < len(self._buttons_dict) - 1:
self._configure_button_corners_for_index(index + 1)
self._create_button_grid()
if value == self._current_value:
self._select_button_by_value(self._current_value)
else:
raise ValueError(f"CTkSegmentedButton can not insert value ''")
else:
raise ValueError(f"CTkSegmentedButton can not insert value '{value}', already part of the values")
def move(self, new_index: int, value: str):
if 0 <= new_index < len(self._value_list):
if value in self._buttons_dict:
self.delete(value)
self.insert(new_index, value)
else:
raise ValueError(f"CTkSegmentedButton has no value named '{value}'")
else:
raise ValueError(f"CTkSegmentedButton new_index {new_index} not in range of value list with len {len(self._value_list)}")
def delete(self, value: str):
if value in self._buttons_dict:
self._buttons_dict[value].destroy()
self._buttons_dict.pop(value)
index_to_remove = self._get_index_by_value(value)
self._value_list.pop(index_to_remove)
# removed index was outer right element
if index_to_remove == len(self._buttons_dict) and len(self._buttons_dict) > 0:
self._configure_button_corners_for_index(index_to_remove - 1)
# removed index was outer left element
if index_to_remove == 0 and len(self._buttons_dict) > 0:
self._configure_button_corners_for_index(0)
#if index_to_remove <= len(self._buttons_dict) - 1:
# self._configure_button_corners_for_index(index_to_remove)
self._create_button_grid()
else:
raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
def bind(self, sequence=None, command=None, add=None):
raise NotImplementedError
def unbind(self, sequence=None, funcid=None):
raise NotImplementedError

View File

@ -0,0 +1,397 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
class CTkSlider(CTkBaseClass):
"""
Slider with rounded corners, border, number of steps, variable support, vertical orientation.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: Optional[int] = None,
height: Optional[int] = None,
corner_radius: Optional[int] = None,
button_corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
button_length: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Union[str, Tuple[str, str]] = "transparent",
progress_color: Optional[Union[str, Tuple[str, str]]] = None,
button_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
from_: int = 0,
to: int = 1,
state: str = "normal",
number_of_steps: Union[int, None] = None,
hover: bool = True,
command: Union[Callable[[float], None], None] = None,
variable: Union[tkinter.Variable, None] = None,
orientation: str = "horizontal",
**kwargs):
# set default dimensions according to orientation
if width is None:
if orientation.lower() == "vertical":
width = 16
else:
width = 200
if height is None:
if orientation.lower() == "vertical":
height = 200
else:
height = 16
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self._border_color = self._check_color_type(border_color, transparency=True)
self._fg_color = ThemeManager.theme["CTkSlider"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._progress_color = ThemeManager.theme["CTkSlider"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True)
self._button_color = ThemeManager.theme["CTkSlider"]["button_color"] if button_color is None else self._check_color_type(button_color)
self._button_hover_color = ThemeManager.theme["CTkSlider"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
# shape
self._corner_radius = ThemeManager.theme["CTkSlider"]["corner_radius"] if corner_radius is None else corner_radius
self._button_corner_radius = ThemeManager.theme["CTkSlider"]["button_corner_radius"] if button_corner_radius is None else button_corner_radius
self._border_width = ThemeManager.theme["CTkSlider"]["border_width"] if border_width is None else border_width
self._button_length = ThemeManager.theme["CTkSlider"]["button_length"] if button_length is None else button_length
self._value: float = 0.5 # initial value of slider in percent
self._orientation = orientation
self._hover_state: bool = False
self._hover = hover
self._from_ = from_
self._to = to
self._number_of_steps = number_of_steps
self._output_value = self._from_ + (self._value * (self._to - self._from_))
if self._corner_radius < self._button_corner_radius:
self._corner_radius = self._button_corner_radius
# callback and control variables
self._command = command
self._variable: tkinter.Variable = variable
self._variable_callback_blocked: bool = False
self._variable_callback_name: Union[bool, None] = None
self._state = state
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe")
self._draw_engine = DrawEngine(self._canvas)
self._create_bindings()
self._set_cursor()
self._draw() # initial draw
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._variable_callback_blocked = True
self.set(self._variable.get(), from_variable_callback=True)
self._variable_callback_blocked = False
def _create_bindings(self, sequence: Optional[str] = None):
""" set necessary bindings for functionality of widget, will overwrite other bindings """
if sequence is None or sequence == "<Enter>":
self._canvas.bind("<Enter>", self._on_enter)
if sequence is None or sequence == "<Leave>":
self._canvas.bind("<Leave>", self._on_leave)
if sequence is None or sequence == "<Button-1>":
self._canvas.bind("<Button-1>", self._clicked)
if sequence is None or sequence == "<B1-Motion>":
self._canvas.bind("<B1-Motion>", self._clicked)
def _set_scaling(self, *args, **kwargs):
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._draw(no_color_updates=True)
def _set_dimensions(self, width=None, height=None):
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._draw()
def destroy(self):
# remove variable_callback from variable callbacks if variable exists
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
def _set_cursor(self):
if self._state == "normal" and self._cursor_manipulation_enabled:
if sys.platform == "darwin":
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win"):
self.configure(cursor="hand2")
elif self._state == "disabled" and self._cursor_manipulation_enabled:
if sys.platform == "darwin":
self.configure(cursor="arrow")
elif sys.platform.startswith("win"):
self.configure(cursor="arrow")
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
if self._orientation.lower() == "horizontal":
orientation = "w"
elif self._orientation.lower() == "vertical":
orientation = "s"
else:
orientation = "w"
requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(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),
self._apply_widget_scaling(self._button_length),
self._apply_widget_scaling(self._button_corner_radius),
self._value, orientation)
if no_color_updates is False or requires_recoloring:
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),
outline=self._apply_appearance_mode(self._bg_color))
else:
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),
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),
outline=self._apply_appearance_mode(self._fg_color))
else:
self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._progress_color),
outline=self._apply_appearance_mode(self._progress_color))
if self._hover_state is True:
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_hover_color),
outline=self._apply_appearance_mode(self._button_hover_color))
else:
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_color),
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()
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
require_redraw = True
if "progress_color" in kwargs:
self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True)
require_redraw = True
if "button_color" in kwargs:
self._button_color = self._check_color_type(kwargs.pop("button_color"))
require_redraw = True
if "button_hover_color" in kwargs:
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 "number_of_steps" in kwargs:
self._number_of_steps = kwargs.pop("number_of_steps")
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "command" in kwargs:
self._command = kwargs.pop("command")
if "variable" in kwargs:
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.set(self._variable.get(), from_variable_callback=True)
else:
self._variable = None
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "button_corner_radius":
return self._button_corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "button_length":
return self._button_length
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "progress_color":
return self._progress_color
elif attribute_name == "button_color":
return self._button_color
elif attribute_name == "button_hover_color":
return self._button_hover_color
elif attribute_name == "from_":
return self._from_
elif attribute_name == "to":
return self._to
elif attribute_name == "state":
return self._state
elif attribute_name == "number_of_steps":
return self._number_of_steps
elif attribute_name == "hover":
return self._hover
elif attribute_name == "command":
return self._command
elif attribute_name == "variable":
return self._variable
elif attribute_name == "orientation":
return self._orientation
else:
return super().cget(attribute_name)
def _clicked(self, event=None):
if self._state == "normal":
if self._orientation.lower() == "horizontal":
self._value = self._reverse_widget_scaling(event.x / self._current_width)
else:
self._value = 1 - self._reverse_widget_scaling(event.y / self._current_height)
if self._value > 1:
self._value = 1
if self._value < 0:
self._value = 0
self._output_value = self._round_to_step_size(self._from_ + (self._value * (self._to - self._from_)))
self._value = (self._output_value - self._from_) / (self._to - self._from_)
self._draw(no_color_updates=False)
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
self._variable_callback_blocked = False
if self._command is not None:
self._command(self._output_value)
def _on_enter(self, event=0):
if self._hover is True and self._state == "normal":
self._hover_state = True
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_hover_color),
outline=self._apply_appearance_mode(self._button_hover_color))
def _on_leave(self, event=0):
self._hover_state = False
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_color),
outline=self._apply_appearance_mode(self._button_color))
def _round_to_step_size(self, value) -> float:
if self._number_of_steps is not None:
step_size = (self._to - self._from_) / self._number_of_steps
value = self._to - (round((self._to - value) / step_size) * step_size)
return value
else:
return value
def get(self) -> float:
return self._output_value
def set(self, output_value, from_variable_callback=False):
if self._from_ < self._to:
if output_value > self._to:
output_value = self._to
elif output_value < self._from_:
output_value = self._from_
else:
if output_value < self._to:
output_value = self._to
elif output_value > self._from_:
output_value = self._from_
self._output_value = self._round_to_step_size(output_value)
self._value = (self._output_value - self._from_) / (self._to - self._from_)
self._draw(no_color_updates=False)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
self._variable_callback_blocked = False
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self.set(self._variable.get(), from_variable_callback=True)
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
""" called on the tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._canvas.bind(sequence, command, add=True)
def unbind(self, sequence: str = None, funcid: str = None):
""" called on the tkinter.Label and tkinter.Canvas """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._canvas.unbind(sequence, None)
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
def focus(self):
return self._canvas.focus()
def focus_set(self):
return self._canvas.focus_set()
def focus_force(self):
return self._canvas.focus_force()

View File

@ -0,0 +1,483 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
class CTkSwitch(CTkBaseClass):
"""
Switch with rounded corners, border, label, command, variable support.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 100,
height: int = 24,
switch_width: int = 36,
switch_height: int = 18,
corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
button_length: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Union[str, Tuple[str, str]] = "transparent",
progress_color: Optional[Union[str, Tuple[str, str]]] = None,
button_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_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 = "CTkSwitch",
font: Optional[Union[tuple, CTkFont]] = None,
textvariable: Union[tkinter.Variable, None] = None,
onvalue: Union[int, str] = 1,
offvalue: Union[int, str] = 0,
variable: Union[tkinter.Variable, None] = None,
hover: bool = True,
command: Union[Callable, None] = None,
state: str = tkinter.NORMAL,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# dimensions
self._switch_width = switch_width
self._switch_height = switch_height
# 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._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)
self._text_color = ThemeManager.theme["CTkSwitch"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkSwitch"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
# text
self._text = text
self._text_label = None
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# shape
self._corner_radius = ThemeManager.theme["CTkSwitch"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkSwitch"]["border_width"] if border_width is None else border_width
self._button_length = ThemeManager.theme["CTkSwitch"]["button_length"] if button_length is None else button_length
self._hover_state: bool = False
self._check_state: bool = False # True if switch is activated
self._hover = hover
self._state = state
self._onvalue = onvalue
self._offvalue = offvalue
# callback and control variables
self._command = command
self._variable = variable
self._variable_callback_blocked = False
self._variable_callback_name = None
self._textvariable = textvariable
# configure grid system (3x1)
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,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._switch_width),
height=self._apply_widget_scaling(self._switch_height))
self._canvas.grid(row=0, column=0, sticky="")
self._draw_engine = DrawEngine(self._canvas)
self._text_label = tkinter.Label(master=self,
bd=0,
padx=0,
pady=0,
text=self._text,
justify=tkinter.LEFT,
font=self._apply_font_scaling(self._font),
textvariable=self._textvariable)
self._text_label.grid(row=0, column=2, sticky="w")
self._text_label["anchor"] = "w"
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._onvalue else False
self._create_bindings()
self._set_cursor()
self._draw() # initial draw
def _create_bindings(self, sequence: Optional[str] = None):
""" set necessary bindings for functionality of widget, will overwrite other bindings """
if sequence is None or sequence == "<Enter>":
self._canvas.bind("<Enter>", self._on_enter)
self._text_label.bind("<Enter>", self._on_enter)
if sequence is None or sequence == "<Leave>":
self._canvas.bind("<Leave>", self._on_leave)
self._text_label.bind("<Leave>", self._on_leave)
if sequence is None or sequence == "<Button-1>":
self._canvas.bind("<Button-1>", self.toggle)
self._text_label.bind("<Button-1>", self.toggle)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self._text_label.configure(font=self._apply_font_scaling(self._font))
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.configure(width=self._apply_widget_scaling(self._switch_width),
height=self._apply_widget_scaling(self._switch_height))
self._draw(no_color_updates=True)
def _set_dimensions(self, width: int = None, height: int = None):
super()._set_dimensions(width, height)
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
self._text_label.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._bg_canvas.grid_forget()
self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
def destroy(self):
# remove variable_callback from variable callbacks if variable exists
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _set_cursor(self):
if self._cursor_manipulation_enabled:
if self._state == tkinter.DISABLED:
if sys.platform == "darwin":
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif sys.platform.startswith("win"):
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif self._state == tkinter.NORMAL:
if sys.platform == "darwin":
self._canvas.configure(cursor="pointinghand")
if self._text_label is not None:
self._text_label.configure(cursor="pointinghand")
elif sys.platform.startswith("win"):
self._canvas.configure(cursor="hand2")
if self._text_label is not None:
self._text_label.configure(cursor="hand2")
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
if self._check_state is True:
requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._switch_width),
self._apply_widget_scaling(self._switch_height),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width),
self._apply_widget_scaling(self._button_length),
self._apply_widget_scaling(self._corner_radius),
1, "w")
else:
requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._switch_width),
self._apply_widget_scaling(self._switch_height),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width),
self._apply_widget_scaling(self._button_length),
self._apply_widget_scaling(self._corner_radius),
0, "w")
if no_color_updates is False or requires_recoloring:
self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
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),
outline=self._apply_appearance_mode(self._bg_color))
else:
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),
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),
outline=self._apply_appearance_mode(self._fg_color))
else:
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),
outline=self._apply_appearance_mode(self._button_color))
if self._state == tkinter.DISABLED:
self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
else:
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("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 "switch_width" in kwargs:
self._switch_width = kwargs.pop("switch_width")
self._canvas.configure(width=self._apply_widget_scaling(self._switch_width))
require_redraw = True
if "switch_height" in kwargs:
self._switch_height = kwargs.pop("switch_height")
self._canvas.configure(height=self._apply_widget_scaling(self._switch_height))
require_redraw = True
if "text" in kwargs:
self._text = kwargs.pop("text")
self._text_label.configure(text=self._text)
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
if "state" in kwargs:
self._state = kwargs.pop("state")
self._set_cursor()
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
if "button_color" in kwargs:
self._button_color = self._check_color_type(kwargs.pop("button_color"))
require_redraw = True
if "button_hover_color" in kwargs:
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
require_redraw = 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:
self._hover = kwargs.pop("hover")
if "command" in kwargs:
self._command = kwargs.pop("command")
if "textvariable" in kwargs:
self._textvariable = kwargs.pop("textvariable")
self._text_label.configure(textvariable=self._textvariable)
if "variable" in kwargs:
if self._variable is not None and self._variable != "":
self._variable.trace_remove("write", self._variable_callback_name)
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._onvalue else False
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "button_length":
return self._button_length
elif attribute_name == "switch_width":
return self._switch_width
elif attribute_name == "switch_height":
return self._switch_height
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "progress_color":
return self._progress_color
elif attribute_name == "button_color":
return self._button_color
elif attribute_name == "button_hover_color":
return self._button_hover_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
elif attribute_name == "font":
return self._font
elif attribute_name == "textvariable":
return self._textvariable
elif attribute_name == "onvalue":
return self._onvalue
elif attribute_name == "offvalue":
return self._offvalue
elif attribute_name == "variable":
return self._variable
elif attribute_name == "hover":
return self._hover
elif attribute_name == "command":
return self._command
elif attribute_name == "state":
return self._state
else:
return super().cget(attribute_name)
def toggle(self, event=None):
if self._state is not tkinter.DISABLED:
if self._check_state is True:
self._check_state = False
else:
self._check_state = True
self._draw(no_color_updates=True)
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
self._variable_callback_blocked = False
if self._command is not None:
self._command()
def select(self, from_variable_callback=False):
if self._state is not tkinter.DISABLED or from_variable_callback:
self._check_state = True
self._draw(no_color_updates=True)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._onvalue)
self._variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
if self._state is not tkinter.DISABLED or from_variable_callback:
self._check_state = False
self._draw(no_color_updates=True)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._offvalue)
self._variable_callback_blocked = False
def get(self) -> Union[int, str]:
return self._onvalue if self._check_state is True else self._offvalue
def _on_enter(self, event=0):
if self._hover is True and self._state == "normal":
self._hover_state = True
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_hover_color),
outline=self._apply_appearance_mode(self._button_hover_color))
def _on_leave(self, event=0):
self._hover_state = False
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_color),
outline=self._apply_appearance_mode(self._button_color))
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
if self._variable.get() == self._onvalue:
self.select(from_variable_callback=True)
elif self._variable.get() == self._offvalue:
self.deselect(from_variable_callback=True)
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
""" called on the tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._canvas.bind(sequence, command, add=True)
self._text_label.bind(sequence, command, add=True)
def unbind(self, sequence: str = None, funcid: str = None):
""" called on the tkinter.Label and tkinter.Canvas """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._canvas.unbind(sequence, None)
self._text_label.unbind(sequence, None)
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
def focus(self):
return self._text_label.focus()
def focus_set(self):
return self._text_label.focus_set()
def focus_force(self):
return self._text_label.focus_force()

View File

@ -0,0 +1,370 @@
import tkinter
from typing import Union, Tuple, Dict, List, Callable, Optional
from .theme import ThemeManager
from .ctk_frame import CTkFrame
from .core_rendering import CTkCanvas
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .ctk_segmented_button import CTkSegmentedButton
class CTkTabview(CTkBaseClass):
"""
Tabview...
For detailed information check out the documentation.
"""
_top_spacing: int = 10 # px on top of the buttons
_top_button_overhang: int = 8 # px
_button_height: int = 26
_segmented_button_border_width: int = 3
def __init__(self,
master: any,
width: int = 300,
height: int = 250,
corner_radius: Optional[int] = None,
border_width: Optional[int] = 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,
segmented_button_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_selected_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_unselected_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_unselected_hover_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,
command: Union[Callable, None] = None,
state: str = "normal",
**kwargs):
# transfer some functionality to CTkFrame
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self._border_color = ThemeManager.theme["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color)
# determine fg_color of frame
if fg_color is None:
if isinstance(self.master, (CTkFrame, CTkTabview)):
if self.master.cget("fg_color") == ThemeManager.theme["CTkFrame"]["fg_color"]:
self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"]
else:
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
else:
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
else:
self._fg_color = self._check_color_type(fg_color, transparency=True)
# 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._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))
self._draw_engine = DrawEngine(self._canvas)
self._segmented_button = CTkSegmentedButton(self,
values=[],
height=self._button_height,
fg_color=segmented_button_fg_color,
selected_color=segmented_button_selected_color,
selected_hover_color=segmented_button_selected_hover_color,
unselected_color=segmented_button_unselected_color,
unselected_hover_color=segmented_button_unselected_hover_color,
text_color=text_color,
text_color_disabled=text_color_disabled,
corner_radius=corner_radius,
border_width=self._segmented_button_border_width,
command=self._segmented_button_callback,
state=state)
self._configure_segmented_button_background_corners()
self._configure_grid()
self._set_grid_canvas()
self._tab_dict: Dict[str, CTkFrame] = {}
self._name_list: List[str] = [] # list of unique tab names in order of tabs
self._current_name: str = ""
self._command = command
self._draw()
def _segmented_button_callback(self, selected_name):
self._set_grid_tab_by_name(selected_name)
self._tab_dict[self._current_name].grid_forget()
self._current_name = selected_name
if self._command is not None:
self._command()
def winfo_children(self) -> List[any]:
"""
winfo_children of CTkTabview without canvas and segmented button widgets,
because it's not a child but part of the CTkTabview itself
"""
child_widgets = super().winfo_children()
try:
child_widgets.remove(self._canvas)
child_widgets.remove(self._segmented_button)
return child_widgets
except ValueError:
return child_widgets
def _set_scaling(self, *args, **kwargs):
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))
self._configure_grid()
self._draw(no_color_updates=True)
def _set_dimensions(self, width=None, height=None):
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))
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:
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)
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)
self.grid_columnconfigure(0, weight=1)
def _set_grid_canvas(self):
self._canvas.grid(row=2, 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")
def _set_grid_tab_by_name(self, name: str):
""" 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)))
def _grid_forget_all_tabs(self):
for frame in self._tab_dict.values():
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)
return new_tab
def _draw(self, no_color_updates: bool = False):
super()._draw(no_color_updates)
if not self._canvas.winfo_exists():
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._corner_radius),
self._apply_widget_scaling(self._border_width))
if no_color_updates is False or requires_recoloring:
if self._fg_color == "transparent":
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=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))
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
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
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)
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
if "segmented_button_fg_color" in kwargs:
self._segmented_button.configure(fg_color=kwargs.pop("segmented_button_fg_color"))
if "segmented_button_selected_color" in kwargs:
self._segmented_button.configure(selected_color=kwargs.pop("segmented_button_selected_color"))
if "segmented_button_selected_hover_color" in kwargs:
self._segmented_button.configure(selected_hover_color=kwargs.pop("segmented_button_selected_hover_color"))
if "segmented_button_unselected_color" in kwargs:
self._segmented_button.configure(unselected_color=kwargs.pop("segmented_button_unselected_color"))
if "segmented_button_unselected_hover_color" in kwargs:
self._segmented_button.configure(unselected_hover_color=kwargs.pop("segmented_button_unselected_hover_color"))
if "text_color" in kwargs:
self._segmented_button.configure(text_color=kwargs.pop("text_color"))
if "text_color_disabled" in kwargs:
self._segmented_button.configure(text_color_disabled=kwargs.pop("text_color_disabled"))
if "command" in kwargs:
self._command = kwargs.pop("command")
if "state" in kwargs:
self._segmented_button.configure(state=kwargs.pop("state"))
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str):
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "segmented_button_fg_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "segmented_button_selected_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "segmented_button_selected_hover_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "segmented_button_unselected_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "segmented_button_unselected_hover_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "text_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "text_color_disabled":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "command":
return self._command
elif attribute_name == "state":
return self._segmented_button.cget(attribute_name)
else:
return super().cget(attribute_name)
def tab(self, name: str) -> CTkFrame:
""" returns reference to the tab with given name """
if name in self._tab_dict:
return self._tab_dict[name]
else:
raise ValueError(f"CTkTabview has no tab named '{name}'")
def insert(self, index: int, name: str) -> CTkFrame:
""" creates new tab with given name at position index """
if name not in self._tab_dict:
# if no tab exists, set grid for segmented button
if len(self._tab_dict) == 0:
self._set_grid_segmented_button()
self._name_list.insert(index, 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)
return self._tab_dict[name]
else:
raise ValueError(f"CTkTabview already has tab named '{name}'")
def add(self, name: str) -> CTkFrame:
""" appends new tab with given name """
return self.insert(len(self._tab_dict), name)
def move(self, new_index: int, name: str):
if 0 <= new_index < len(self._name_list):
if name in self._tab_dict:
self._segmented_button.move(new_index, name)
else:
raise ValueError(f"CTkTabview has no name '{name}'")
else:
raise ValueError(f"CTkTabview new_index {new_index} not in range of name list with len {len(self._name_list)}")
def delete(self, name: str):
""" delete tab by name """
if name in self._tab_dict:
self._name_list.remove(name)
self._tab_dict[name].grid_forget()
self._tab_dict.pop(name)
self._segmented_button.delete(name)
# set current_name to '' and remove segmented button if no tab is left
if len(self._name_list) == 0:
self._current_name = ""
self._segmented_button.grid_forget()
# if only one tab left, select this tab
elif len(self._name_list) == 1:
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)
# more tabs are left
else:
# if current_name is deleted tab, select first tab at position 0
if self._current_name == name:
self.set(self._name_list[0])
else:
raise ValueError(f"CTkTabview has no tab named '{name}'")
def set(self, name: str):
""" select tab by name """
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)
else:
raise ValueError(f"CTkTabview has no tab named '{name}'")
def get(self) -> str:
""" returns name of selected tab, returns empty string if no tab selected """
return self._current_name

View File

@ -0,0 +1,500 @@
import tkinter
from typing import Union, Tuple, Optional, Callable
from .core_rendering import CTkCanvas
from .ctk_scrollbar import CTkScrollbar
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
from .utility import pop_from_dict_by_set, check_kwargs_empty
class CTkTextbox(CTkBaseClass):
"""
Textbox with x and y scrollbars, rounded corners, and all text features of tkinter.Text widget.
Scrollbars only appear when they are needed. Text is wrapped on line end by default,
set wrap='none' to disable automatic line wrapping.
For detailed information check out the documentation.
Detailed methods and parameters of the underlaying tkinter.Text widget can be found here:
https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/text.html
(most of them are implemented here too)
"""
_scrollbar_update_time = 200 # interval in ms, to check if scrollbars are needed
# attributes that are passed to and managed by the tkinter textbox only:
_valid_tk_text_attributes = {"autoseparators", "cursor", "exportselection",
"insertborderwidth", "insertofftime", "insertontime", "insertwidth",
"maxundo", "padx", "pady", "selectborderwidth", "spacing1",
"spacing2", "spacing3", "state", "tabs", "takefocus", "undo", "wrap",
"xscrollcommand", "yscrollcommand"}
def __init__(self,
master: any,
width: int = 200,
height: int = 200,
corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
border_spacing: int = 3,
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,
text_color: Optional[Union[str, str]] = None,
scrollbar_button_color: Optional[Union[str, Tuple[str, str]]] = None,
scrollbar_button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
font: Optional[Union[tuple, CTkFont]] = None,
activate_scrollbars: bool = True,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height)
# color
self._fg_color = ThemeManager.theme["CTkTextbox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
self._border_color = ThemeManager.theme["CTkTextbox"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._text_color = ThemeManager.theme["CTkTextbox"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._scrollbar_button_color = ThemeManager.theme["CTkTextbox"]["scrollbar_button_color"] if scrollbar_button_color is None else self._check_color_type(scrollbar_button_color)
self._scrollbar_button_hover_color = ThemeManager.theme["CTkTextbox"]["scrollbar_button_hover_color"] if scrollbar_button_hover_color is None else self._check_color_type(scrollbar_button_hover_color)
# shape
self._corner_radius = ThemeManager.theme["CTkTextbox"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkTextbox"]["border_width"] if border_width is None else border_width
self._border_spacing = border_spacing
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
self._draw_engine = DrawEngine(self._canvas)
self._textbox = tkinter.Text(self,
fg=self._apply_appearance_mode(self._text_color),
width=0,
height=0,
font=self._apply_font_scaling(self._font),
highlightthickness=0,
relief="flat",
insertbackground=self._apply_appearance_mode(self._text_color),
**pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes))
check_kwargs_empty(kwargs, raise_error=True)
# scrollbars
self._scrollbars_activated = activate_scrollbars
self._hide_x_scrollbar = True
self._hide_y_scrollbar = True
self._y_scrollbar = CTkScrollbar(self,
width=8,
height=0,
border_spacing=0,
fg_color=self._fg_color,
button_color=self._scrollbar_button_color,
button_hover_color=self._scrollbar_button_hover_color,
orientation="vertical",
command=self._textbox.yview)
self._textbox.configure(yscrollcommand=self._y_scrollbar.set)
self._x_scrollbar = CTkScrollbar(self,
height=8,
width=0,
border_spacing=0,
fg_color=self._fg_color,
button_color=self._scrollbar_button_color,
button_hover_color=self._scrollbar_button_hover_color,
orientation="horizontal",
command=self._textbox.xview)
self._textbox.configure(xscrollcommand=self._x_scrollbar.set)
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, 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):
# configure 2x2 grid
self.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)))
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)))
if re_grid_textbox:
self._textbox.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew",
padx=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0),
pady=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0))
if re_grid_x_scrollbar:
if not self._hide_x_scrollbar and self._scrollbars_activated:
self._x_scrollbar.grid(row=1, column=0, rowspan=1, columnspan=1, sticky="ewn",
pady=(3, self._border_spacing + self._border_width),
padx=(max(self._corner_radius, self._border_width + self._border_spacing), 0)) # scrollbar grid method without scaling
else:
self._x_scrollbar.grid_forget()
if re_grid_y_scrollbar:
if not self._hide_y_scrollbar and self._scrollbars_activated:
self._y_scrollbar.grid(row=0, column=1, rowspan=1, columnspan=1, sticky="nsw",
padx=(3, self._border_spacing + self._border_width),
pady=(max(self._corner_radius, self._border_width + self._border_spacing), 0)) # scrollbar grid method without scaling
else:
self._y_scrollbar.grid_forget()
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:
if self._textbox.xview() != (0.0, 1.0) and not self._x_scrollbar.winfo_ismapped(): # x scrollbar needed
self._hide_x_scrollbar = False
self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True)
elif self._textbox.xview() == (0.0, 1.0) and self._x_scrollbar.winfo_ismapped(): # x scrollbar not needed
self._hide_x_scrollbar = True
self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True)
if self._textbox.yview() != (0.0, 1.0) and not self._y_scrollbar.winfo_ismapped(): # y scrollbar needed
self._hide_y_scrollbar = False
self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
elif self._textbox.yview() == (0.0, 1.0) and self._y_scrollbar.winfo_ismapped(): # y scrollbar not needed
self._hide_y_scrollbar = True
self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
else:
self._hide_x_scrollbar = False
self._hide_x_scrollbar = False
self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
if self._textbox.winfo_exists() and continue_loop is True:
self.after(self._scrollbar_update_time, lambda: self._check_if_scrollbars_needed(continue_loop=True))
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self._textbox.configure(font=self._apply_font_scaling(self._font))
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
self._draw(no_color_updates=True)
def _set_dimensions(self, width=None, height=None):
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._draw()
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
self._textbox.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._canvas.grid_forget()
self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
def destroy(self):
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
if not self._canvas.winfo_exists():
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._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width))
if no_color_updates is False or requires_recoloring:
if self._fg_color == "transparent":
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=self._apply_appearance_mode(self._bg_color))
self._textbox.configure(fg=self._apply_appearance_mode(self._text_color),
bg=self._apply_appearance_mode(self._bg_color),
insertbackground=self._apply_appearance_mode(self._text_color))
self._x_scrollbar.configure(fg_color=self._bg_color, button_color=self._scrollbar_button_color,
button_hover_color=self._scrollbar_button_hover_color)
self._y_scrollbar.configure(fg_color=self._bg_color, button_color=self._scrollbar_button_color,
button_hover_color=self._scrollbar_button_hover_color)
else:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
self._textbox.configure(fg=self._apply_appearance_mode(self._text_color),
bg=self._apply_appearance_mode(self._fg_color),
insertbackground=self._apply_appearance_mode(self._text_color))
self._x_scrollbar.configure(fg_color=self._fg_color, button_color=self._scrollbar_button_color,
button_hover_color=self._scrollbar_button_hover_color)
self._y_scrollbar.configure(fg_color=self._fg_color, button_color=self._scrollbar_button_color,
button_hover_color=self._scrollbar_button_hover_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))
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
def configure(self, require_redraw=False, **kwargs):
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
# check if CTk widgets are children of the frame and change their _bg_color to new frame fg_color
for child in self.winfo_children():
if isinstance(child, CTkBaseClass) and hasattr(child, "_fg_color"):
child.configure(bg_color=self._fg_color)
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
if "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
if "scrollbar_button_color" in kwargs:
self._scrollbar_button_color = self._check_color_type(kwargs.pop("scrollbar_button_color"))
self._x_scrollbar.configure(button_color=self._scrollbar_button_color)
self._y_scrollbar.configure(button_color=self._scrollbar_button_color)
if "scrollbar_button_hover_color" in kwargs:
self._scrollbar_button_hover_color = self._check_color_type(kwargs.pop("scrollbar_button_hover_color"))
self._x_scrollbar.configure(button_hover_color=self._scrollbar_button_hover_color)
self._y_scrollbar.configure(button_hover_color=self._scrollbar_button_hover_color)
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
require_redraw = True
if "border_spacing" in kwargs:
self._border_spacing = kwargs.pop("border_spacing")
self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
require_redraw = True
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
self._textbox.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes))
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "border_spacing":
return self._border_spacing
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "font":
return self._font
else:
return super().cget(attribute_name)
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
""" called on the tkinter.Canvas """
if not (add == "+" or add is True):
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
self._textbox.bind(sequence, command, add=True)
def unbind(self, sequence: str = None, funcid: str = None):
""" called on the tkinter.Label and tkinter.Canvas """
if funcid is not None:
raise ValueError("'funcid' argument can only be None, because there is a bug in" +
" tkinter and its not clear whether the internal callbacks will be unbinded or not")
self._textbox.unbind(sequence, None)
def focus(self):
return self._textbox.focus()
def focus_set(self):
return self._textbox.focus_set()
def focus_force(self):
return self._textbox.focus_force()
def insert(self, index, text, tags=None):
return self._textbox.insert(index, text, tags)
def get(self, index1, index2=None):
return self._textbox.get(index1, index2)
def bbox(self, index):
return self._textbox.bbox(index)
def compare(self, index, op, index2):
return self._textbox.compare(index, op, index2)
def delete(self, index1, index2=None):
return self._textbox.delete(index1, index2)
def dlineinfo(self, index):
return self._textbox.dlineinfo(index)
def edit_modified(self, arg=None):
return self._textbox.edit_modified(arg)
def edit_redo(self):
self._check_if_scrollbars_needed()
return self._textbox.edit_redo()
def edit_reset(self):
return self._textbox.edit_reset()
def edit_separator(self):
return self._textbox.edit_separator()
def edit_undo(self):
self._check_if_scrollbars_needed()
return self._textbox.edit_undo()
def image_create(self, index, **kwargs):
raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")
def image_cget(self, index, option):
raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")
def image_configure(self, index):
raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")
def image_names(self):
raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")
def index(self, i):
return self._textbox.index(i)
def mark_gravity(self, mark, gravity=None):
return self._textbox.mark_gravity(mark, gravity)
def mark_names(self):
return self._textbox.mark_names()
def mark_next(self, index):
return self._textbox.mark_next(index)
def mark_previous(self, index):
return self._textbox.mark_previous(index)
def mark_set(self, mark, index):
return self._textbox.mark_set(mark, index)
def mark_unset(self, mark):
return self._textbox.mark_unset(mark)
def scan_dragto(self, x, y):
return self._textbox.scan_dragto(x, y)
def scan_mark(self, x, y):
return self._textbox.scan_mark(x, y)
def search(self, pattern, index, *args, **kwargs):
return self._textbox.search(pattern, index, *args, **kwargs)
def see(self, index):
return self._textbox.see(index)
def tag_add(self, tagName, index1, index2=None):
return self._textbox.tag_add(tagName, index1, index2)
def tag_bind(self, tagName, sequence, func, add=None):
return self._textbox.tag_bind(tagName, sequence, func, add)
def tag_cget(self, tagName, option):
return self._textbox.tag_cget(tagName, option)
def tag_config(self, tagName, **kwargs):
if "font" in kwargs:
raise AttributeError("'font' option forbidden, because would be incompatible with scaling")
return self._textbox.tag_config(tagName, **kwargs)
def tag_delete(self, *tagName):
return self._textbox.tag_delete(*tagName)
def tag_lower(self, tagName, belowThis=None):
return self._textbox.tag_lower(tagName, belowThis)
def tag_names(self, index=None):
return self._textbox.tag_names(index)
def tag_nextrange(self, tagName, index1, index2=None):
return self._textbox.tag_nextrange(tagName, index1, index2)
def tag_prevrange(self, tagName, index1, index2=None):
return self._textbox.tag_prevrange(tagName, index1, index2)
def tag_raise(self, tagName, aboveThis=None):
return self._textbox.tag_raise(tagName, aboveThis)
def tag_ranges(self, tagName):
return self._textbox.tag_ranges(tagName)
def tag_remove(self, tagName, index1, index2=None):
return self._textbox.tag_remove(tagName, index1, index2)
def tag_unbind(self, tagName, sequence, funcid=None):
return self._textbox.tag_unbind(tagName, sequence, funcid)
def window_cget(self, index, option):
raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")
def window_configure(self, index, option):
raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")
def window_create(self, index, **kwargs):
raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")
def window_names(self):
raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")
def xview(self, *args):
return self._textbox.xview(*args)
def xview_moveto(self, fraction):
return self._textbox.xview_moveto(fraction)
def xview_scroll(self, n, what):
return self._textbox.xview_scroll(n, what)
def yview(self, *args):
return self._textbox.yview(*args)
def yview_moveto(self, fraction):
return self._textbox.yview_moveto(fraction)
def yview_scroll(self, n, what):
return self._textbox.yview_scroll(n, what)

View File

@ -0,0 +1,24 @@
import os
import sys
from .ctk_font import CTkFont
from .font_manager import FontManager
# import DrawEngine to set preferred_drawing_method if loading shapes font fails
from ..core_rendering import DrawEngine
FontManager.init_font_manager()
# load Roboto fonts (used on Windows/Linux)
customtkinter_directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf"))
FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "Roboto", "Roboto-Medium.ttf"))
# load font necessary for rendering the widgets (used on Windows/Linux)
if FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "CustomTkinter_shapes_font.otf")) is False:
# change draw method if font loading failed
if DrawEngine.preferred_drawing_method == "font_shapes":
sys.stderr.write("customtkinter.windows.widgets.font warning: " +
"Preferred drawing method 'font_shapes' can not be used because the font file could not be loaded.\n" +
"Using 'circle_shapes' instead. The rendering quality will be bad!\n")
DrawEngine.preferred_drawing_method = "circle_shapes"

View File

@ -0,0 +1,91 @@
from tkinter.font import Font
import copy
from typing import List, Callable, Tuple, Optional
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
from ..theme import ThemeManager
class CTkFont(Font):
"""
Font object with size in pixel, independent of scaling.
To get scaled tuple representation use create_scaled_tuple() method.
family The font family name as a string.
size The font height as an integer in pixel.
weight 'bold' for boldface, 'normal' for regular weight.
slant 'italic' for italic, 'roman' for unslanted.
underline 1 for underlined text, 0 for normal.
overstrike 1 for overstruck text, 0 for normal.
Tkinter Font: https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/fonts.html
"""
def __init__(self,
family: Optional[str] = None,
size: Optional[int] = None,
weight: Literal["normal", "bold"] = None,
slant: Literal["italic", "roman"] = "roman",
underline: bool = False,
overstrike: bool = False):
self._size_configure_callback_list: List[Callable] = []
self._size = ThemeManager.theme["CTkFont"]["size"] if size is None else size
super().__init__(family=ThemeManager.theme["CTkFont"]["family"] if family is None else family,
size=-abs(self._size),
weight=ThemeManager.theme["CTkFont"]["weight"] if weight is None else weight,
slant=slant,
underline=underline,
overstrike=overstrike)
self._family = super().cget("family")
self._tuple_style_string = f"{super().cget('weight')} {slant} {'underline' if underline else ''} {'overstrike' if overstrike else ''}"
def add_size_configure_callback(self, callback: Callable):
""" add function, that gets called when font got configured """
self._size_configure_callback_list.append(callback)
def remove_size_configure_callback(self, callback: Callable):
""" remove function, that gets called when font got configured """
self._size_configure_callback_list.remove(callback)
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)"""
return self._family, round(-abs(self._size) * font_scaling), self._tuple_style_string
def config(self, *args, **kwargs):
raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.")
def configure(self, **kwargs):
if "size" in kwargs:
self._size = kwargs.pop("size")
super().configure(size=-abs(self._size))
if "family" in kwargs:
super().configure(family=kwargs.pop("family"))
self._family = super().cget("family")
super().configure(**kwargs)
# update style string for create_scaled_tuple() method
self._tuple_style_string = f"{super().cget('weight')} {super().cget('slant')} {'underline' if super().cget('underline') else ''} {'overstrike' if super().cget('overstrike') else ''}"
# call all functions registered with add_size_configure_callback()
for callback in self._size_configure_callback_list:
callback()
def cget(self, attribute_name: str) -> any:
if attribute_name == "size":
return self._size
if attribute_name == "family":
return self._family
else:
return super().cget(attribute_name)
def copy(self) -> "CTkFont":
return copy.deepcopy(self)

View File

@ -6,13 +6,15 @@ from typing import Union
class FontManager:
linux_font_path = "~/.fonts/"
@classmethod
def init_font_manager(cls):
# Linux
if sys.platform.startswith("linux"):
try:
if not os.path.isdir(os.path.expanduser('~/.fonts/')):
os.mkdir(os.path.expanduser('~/.fonts/'))
if not os.path.isdir(os.path.expanduser(cls.linux_font_path)):
os.mkdir(os.path.expanduser(cls.linux_font_path))
return True
except Exception as err:
sys.stderr.write("FontManager error: " + str(err) + "\n")
@ -42,7 +44,7 @@ class FontManager:
flags = (FR_PRIVATE if private else 0) | (FR_NOT_ENUM if not enumerable else 0)
num_fonts_added = add_font_resource_ex(byref(path_buffer), flags, 0)
return bool(num_fonts_added)
return bool(min(num_fonts_added, 1))
@classmethod
def load_font(cls, font_path: str) -> bool:
@ -51,9 +53,9 @@ class FontManager:
return cls.windows_load_font(font_path, private=True, enumerable=False)
# Linux
elif sys.platform.startswith("win"):
elif sys.platform.startswith("linux"):
try:
shutil.copy(font_path, os.path.expanduser("~/.fonts/"))
shutil.copy(font_path, os.path.expanduser(cls.linux_font_path))
return True
except Exception as err:
sys.stderr.write("FontManager error: " + str(err) + "\n")

View File

@ -0,0 +1 @@
from .ctk_image import CTkImage

View File

@ -0,0 +1,122 @@
from typing import Tuple, Dict, Callable, List
try:
from PIL import Image, ImageTk
except ImportError:
pass
class CTkImage:
"""
Class to store one or two PIl.Image.Image objects and display size independent of scaling:
light_image: PIL.Image.Image for light mode
dark_image: PIL.Image.Image for dark mode
size: tuple (<width>, <height>) with display size for both images
One of the two images can be None and will be replaced by the other image.
"""
_checked_PIL_import = False
def __init__(self,
light_image: "Image.Image" = None,
dark_image: "Image.Image" = None,
size: Tuple[int, int] = (20, 20)):
if not self._checked_PIL_import:
self._check_pil_import()
self._light_image = light_image
self._dark_image = dark_image
self._check_images()
self._size = size
self._configure_callback_list: List[Callable] = []
self._scaled_light_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {}
self._scaled_dark_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {}
@classmethod
def _check_pil_import(cls):
try:
_, _ = Image, ImageTk
except NameError:
raise ImportError("PIL.Image and PIL.ImageTk couldn't be imported")
def add_configure_callback(self, callback: Callable):
""" add function, that gets called when image got configured """
self._configure_callback_list.append(callback)
def remove_configure_callback(self, callback: Callable):
""" remove function, that gets called when image got configured """
self._configure_callback_list.remove(callback)
def configure(self, **kwargs):
if "light_image" in kwargs:
self._light_image = kwargs.pop("light_image")
self._scaled_light_photo_images = {}
self._check_images()
if "dark_image" in kwargs:
self._dark_image = kwargs.pop("dark_image")
self._scaled_dark_photo_images = {}
self._check_images()
if "size" in kwargs:
self._size = kwargs.pop("size")
# call all functions registered with add_configure_callback()
for callback in self._configure_callback_list:
callback()
def cget(self, attribute_name: str) -> any:
if attribute_name == "light_image":
return self._light_image
if attribute_name == "dark_image":
return self._dark_image
if attribute_name == "size":
return self._size
def _check_images(self):
# check types
if self._light_image is not None and not isinstance(self._light_image, Image.Image):
raise ValueError(f"CTkImage: light_image must be instance if PIL.Image.Image, not {type(self._light_image)}")
if self._dark_image is not None and not isinstance(self._dark_image, Image.Image):
raise ValueError(f"CTkImage: dark_image must be instance if PIL.Image.Image, not {type(self._dark_image)}")
# check values
if self._light_image is None and self._dark_image is None:
raise ValueError("CTkImage: No image given, light_image is None and dark_image is None.")
# check sizes
if self._light_image is not None and self._dark_image is not None and self._light_image.size != self._dark_image.size:
raise ValueError(f"CTkImage: light_image size {self._light_image.size} must be the same as dark_image size {self._dark_image.size}.")
def _get_scaled_size(self, widget_scaling: float) -> Tuple[int, int]:
return round(self._size[0] * widget_scaling), round(self._size[1] * widget_scaling)
def _get_scaled_light_photo_image(self, scaled_size: Tuple[int, int]) -> "ImageTk.PhotoImage":
if scaled_size in self._scaled_light_photo_images:
return self._scaled_light_photo_images[scaled_size]
else:
self._scaled_light_photo_images[scaled_size] = ImageTk.PhotoImage(self._light_image.resize(scaled_size))
return self._scaled_light_photo_images[scaled_size]
def _get_scaled_dark_photo_image(self, scaled_size: Tuple[int, int]) -> "ImageTk.PhotoImage":
if scaled_size in self._scaled_dark_photo_images:
return self._scaled_dark_photo_images[scaled_size]
else:
self._scaled_dark_photo_images[scaled_size] = ImageTk.PhotoImage(self._dark_image.resize(scaled_size))
return self._scaled_dark_photo_images[scaled_size]
def create_scaled_photo_image(self, widget_scaling: float, appearance_mode: str) -> "ImageTk.PhotoImage":
scaled_size = self._get_scaled_size(widget_scaling)
if appearance_mode == "light" and self._light_image is not None:
return self._get_scaled_light_photo_image(scaled_size)
elif appearance_mode == "light" and self._light_image is None:
return self._get_scaled_dark_photo_image(scaled_size)
elif appearance_mode == "dark" and self._dark_image is not None:
return self._get_scaled_dark_photo_image(scaled_size)
elif appearance_mode == "dark" and self._dark_image is None:
return self._get_scaled_light_photo_image(scaled_size)

View File

@ -0,0 +1,7 @@
import sys
from .scaling_base_class import CTkScalingBaseClass
from .scaling_tracker import ScalingTracker
if sys.platform.startswith("win") and sys.getwindowsversion().build < 9000: # No automatic scaling on Windows < 8.1
ScalingTracker.deactivate_automatic_dpi_awareness = True

View File

@ -0,0 +1,159 @@
from typing import Union, Tuple
import copy
import re
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
from .scaling_tracker import ScalingTracker
from ..font import CTkFont
class CTkScalingBaseClass:
"""
Super-class that manages the scaling values and callbacks.
Works for widgets and windows, type must be set in init method with
scaling_type attribute. Methods:
- _set_scaling() abstractmethod, gets called when scaling changes, must be overridden
- destroy() must be called when sub-class is destroyed
- _apply_widget_scaling()
- _reverse_widget_scaling()
- _apply_window_scaling()
- _reverse_window_scaling()
- _apply_font_scaling()
- _apply_argument_scaling()
- _apply_geometry_scaling()
- _reverse_geometry_scaling()
- _parse_geometry_string()
"""
def __init__(self, scaling_type: Literal["widget", "window"] = "widget"):
self.__scaling_type = scaling_type
if self.__scaling_type == "widget":
ScalingTracker.add_widget(self._set_scaling, self) # add callback for automatic scaling changes
self.__widget_scaling = ScalingTracker.get_widget_scaling(self)
elif self.__scaling_type == "window":
ScalingTracker.activate_high_dpi_awareness() # make process DPI aware
ScalingTracker.add_window(self._set_scaling, self) # add callback for automatic scaling changes
self.__window_scaling = ScalingTracker.get_window_scaling(self)
def destroy(self):
if self.__scaling_type == "widget":
ScalingTracker.remove_widget(self._set_scaling, self)
elif self.__scaling_type == "window":
ScalingTracker.remove_window(self._set_scaling, self)
def _set_scaling(self, new_widget_scaling, new_window_scaling):
""" can be overridden, but super method must be called at the beginning """
self.__widget_scaling = new_widget_scaling
self.__window_scaling = new_window_scaling
def _get_widget_scaling(self) -> float:
return self.__widget_scaling
def _get_window_scaling(self) -> float:
return self.__window_scaling
def _apply_widget_scaling(self, value: Union[int, float]) -> Union[float]:
assert self.__scaling_type == "widget"
return value * self.__widget_scaling
def _reverse_widget_scaling(self, value: Union[int, float]) -> Union[float]:
assert self.__scaling_type == "widget"
return value / self.__widget_scaling
def _apply_window_scaling(self, value: Union[int, float]) -> int:
assert self.__scaling_type == "window"
return int(value * self.__window_scaling)
def _reverse_window_scaling(self, scaled_value: Union[int, float]) -> int:
assert self.__scaling_type == "window"
return int(scaled_value / self.__window_scaling)
def _apply_font_scaling(self, font: Union[Tuple, CTkFont]) -> tuple:
""" Takes CTkFont object and returns tuple font with scaled size, has to be called again for every change of font object """
assert self.__scaling_type == "widget"
if type(font) == tuple:
if len(font) == 1:
return font
elif len(font) == 2:
return font[0], -abs(round(font[1] * self.__widget_scaling))
elif 3 <= len(font) <= 6:
return font[0], -abs(round(font[1] * self.__widget_scaling)), font[2:]
else:
raise ValueError(f"Can not scale font {font}. font needs to be tuple of len 1, 2 or 3")
elif isinstance(font, CTkFont):
return font.create_scaled_tuple(self.__widget_scaling)
else:
raise ValueError(f"Can not scale font '{font}' of type {type(font)}. font needs to be tuple or instance of CTkFont")
def _apply_argument_scaling(self, kwargs: dict) -> dict:
assert self.__scaling_type == "widget"
scaled_kwargs = copy.copy(kwargs)
# scale padding values
if "pady" in scaled_kwargs:
if isinstance(scaled_kwargs["pady"], (int, float)):
scaled_kwargs["pady"] = self._apply_widget_scaling(scaled_kwargs["pady"])
elif isinstance(scaled_kwargs["pady"], tuple):
scaled_kwargs["pady"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["pady"]])
if "padx" in kwargs:
if isinstance(scaled_kwargs["padx"], (int, float)):
scaled_kwargs["padx"] = self._apply_widget_scaling(scaled_kwargs["padx"])
elif isinstance(scaled_kwargs["padx"], tuple):
scaled_kwargs["padx"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["padx"]])
# scaled x, y values for place geometry manager
if "x" in scaled_kwargs:
scaled_kwargs["x"] = self._apply_widget_scaling(scaled_kwargs["x"])
if "y" in scaled_kwargs:
scaled_kwargs["y"] = self._apply_widget_scaling(scaled_kwargs["y"])
return scaled_kwargs
@staticmethod
def _parse_geometry_string(geometry_string: str) -> tuple:
# index: 1 2 3 4 5 6
# regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
width = int(result.group(2)) if result.group(2) is not None else None
height = int(result.group(3)) if result.group(3) is not None else None
x = int(result.group(5)) if result.group(5) is not None else None
y = int(result.group(6)) if result.group(6) is not None else None
return width, height, x, y
def _apply_geometry_scaling(self, geometry_string: str) -> str:
assert self.__scaling_type == "window"
width, height, x, y = self._parse_geometry_string(geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}+{x}+{y}"
def _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
assert self.__scaling_type == "window"
width, height, x, y = self._parse_geometry_string(scaled_geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}+{x}+{y}"

View File

@ -11,21 +11,16 @@ class ScalingTracker:
widget_scaling = 1 # user values which multiply to detected window scaling factor
window_scaling = 1
spacing_scaling = 1
update_loop_running = False
update_loop_interval = 150 # milliseconds
update_loop_interval = 100 # ms
loop_pause_after_new_scaling = 1500 # ms
@classmethod
def get_widget_scaling(cls, widget) -> float:
window_root = cls.get_window_root_of_widget(widget)
return cls.window_dpi_scaling_dict[window_root] * cls.widget_scaling
@classmethod
def get_spacing_scaling(cls, widget) -> float:
window_root = cls.get_window_root_of_widget(widget)
return cls.window_dpi_scaling_dict[window_root] * cls.spacing_scaling
@classmethod
def get_window_scaling(cls, window) -> float:
window_root = cls.get_window_root_of_widget(window)
@ -36,11 +31,6 @@ class ScalingTracker:
cls.widget_scaling = max(widget_scaling_factor, 0.4)
cls.update_scaling_callbacks_all()
@classmethod
def set_spacing_scaling(cls, spacing_scaling_factor: float):
cls.spacing_scaling = max(spacing_scaling_factor, 0.4)
cls.update_scaling_callbacks_all()
@classmethod
def set_window_scaling(cls, window_scaling_factor: float):
cls.window_scaling = max(window_scaling_factor, 0.4)
@ -62,11 +52,9 @@ class ScalingTracker:
for set_scaling_callback in callback_list:
if not cls.deactivate_automatic_dpi_awareness:
set_scaling_callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling,
cls.window_dpi_scaling_dict[window] * cls.spacing_scaling,
cls.window_dpi_scaling_dict[window] * cls.window_scaling)
else:
set_scaling_callback(cls.widget_scaling,
cls.spacing_scaling,
cls.window_scaling)
@classmethod
@ -74,11 +62,9 @@ class ScalingTracker:
for set_scaling_callback in cls.window_widgets_dict[window]:
if not cls.deactivate_automatic_dpi_awareness:
set_scaling_callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling,
cls.window_dpi_scaling_dict[window] * cls.spacing_scaling,
cls.window_dpi_scaling_dict[window] * cls.window_scaling)
else:
set_scaling_callback(cls.widget_scaling,
cls.spacing_scaling,
cls.window_scaling)
@classmethod
@ -124,7 +110,7 @@ class ScalingTracker:
@classmethod
def activate_high_dpi_awareness(cls):
""" make process DPI aware, customtkinter elemets will get scaled automatically,
""" make process DPI aware, customtkinter elements will get scaled automatically,
only gets activated when CTk object is created """
if not cls.deactivate_automatic_dpi_awareness:
@ -132,45 +118,87 @@ class ScalingTracker:
pass # high DPI scaling works automatically on macOS
elif sys.platform.startswith("win"):
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(2)
# Microsoft Docs: https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness
import ctypes
# Values for SetProcessDpiAwareness and SetProcessDpiAwarenessContext:
# internal enum PROCESS_DPI_AWARENESS
# {
# Process_DPI_Unaware = 0,
# Process_System_DPI_Aware = 1,
# Process_Per_Monitor_DPI_Aware = 2
# }
#
# internal enum DPI_AWARENESS_CONTEXT
# {
# DPI_AWARENESS_CONTEXT_UNAWARE = 16,
# DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = 17,
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = 18,
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = 34
# }
# ctypes.windll.user32.SetProcessDpiAwarenessContext(34) # Non client area scaling at runtime (titlebar)
# does not work with resizable(False, False), window starts growing on monitor with different scaling (weird tkinter bug...)
# ctypes.windll.user32.EnableNonClientDpiScaling(hwnd) does not work for some reason (tested on Windows 11)
# It's too bad, that these Windows API methods don't work properly with tkinter. But I tested days with multiple monitor setups,
# and I don't think there is anything left to do. So this is the best option at the moment:
ctypes.windll.shcore.SetProcessDpiAwareness(2) # Titlebar does not scale at runtime
else:
pass # DPI awareness on Linux not implemented
@classmethod
def get_window_dpi_scaling(cls, window) -> float:
if sys.platform == "darwin":
return 1 # scaling works automatically on macOS
if not cls.deactivate_automatic_dpi_awareness:
if sys.platform == "darwin":
return 1 # scaling works automatically on macOS
elif sys.platform.startswith("win"):
from ctypes import windll, pointer, wintypes
elif sys.platform.startswith("win"):
from ctypes import windll, pointer, wintypes
DPI100pc = 96 # DPI 96 is 100% scaling
DPI_type = 0 # MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2
window_hwnd = wintypes.HWND(window.winfo_id())
monitor_handle = windll.user32.MonitorFromWindow(window_hwnd, wintypes.DWORD(2)) # MONITOR_DEFAULTTONEAREST = 2
x_dpi, y_dpi = wintypes.UINT(), wintypes.UINT()
windll.shcore.GetDpiForMonitor(monitor_handle, DPI_type, pointer(x_dpi), pointer(y_dpi))
return (x_dpi.value + y_dpi.value) / (2 * DPI100pc)
DPI100pc = 96 # DPI 96 is 100% scaling
DPI_type = 0 # MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2
window_hwnd = wintypes.HWND(window.winfo_id())
monitor_handle = windll.user32.MonitorFromWindow(window_hwnd, wintypes.DWORD(2)) # MONITOR_DEFAULTTONEAREST = 2
x_dpi, y_dpi = wintypes.UINT(), wintypes.UINT()
windll.shcore.GetDpiForMonitor(monitor_handle, DPI_type, pointer(x_dpi), pointer(y_dpi))
return (x_dpi.value + y_dpi.value) / (2 * DPI100pc)
else:
return 1 # DPI awareness on Linux not implemented
else:
return 1 # DPI awareness on Linux not implemented
return 1
@classmethod
def check_dpi_scaling(cls):
new_scaling_detected = False
# check for every window if scaling value changed
for window in cls.window_widgets_dict:
current_dpi_scaling_value = cls.get_window_dpi_scaling(window)
if current_dpi_scaling_value != cls.window_dpi_scaling_dict[window]:
cls.window_dpi_scaling_dict[window] = current_dpi_scaling_value
cls.update_scaling_callbacks_for_window(window)
if window.winfo_exists() and not window.state() == "iconic":
current_dpi_scaling_value = cls.get_window_dpi_scaling(window)
if current_dpi_scaling_value != cls.window_dpi_scaling_dict[window]:
cls.window_dpi_scaling_dict[window] = current_dpi_scaling_value
if sys.platform.startswith("win"):
window.attributes("-alpha", 0.15)
window.block_update_dimensions_event()
cls.update_scaling_callbacks_for_window(window)
window.unblock_update_dimensions_event()
if sys.platform.startswith("win"):
window.attributes("-alpha", 1)
new_scaling_detected = True
# find an existing tkinter object for the next call of .after()
for app in cls.window_widgets_dict.keys():
try:
app.after(cls.update_loop_interval, cls.check_dpi_scaling)
if new_scaling_detected:
app.after(cls.loop_pause_after_new_scaling, cls.check_dpi_scaling)
else:
app.after(cls.update_loop_interval, cls.check_dpi_scaling)
return
except Exception:
continue

View File

@ -0,0 +1,9 @@
from .theme_manager import ThemeManager
# load default blue theme
try:
ThemeManager.load_theme("blue")
except FileNotFoundError as err:
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

@ -0,0 +1,49 @@
import sys
import os
import pathlib
import json
from typing import List, Union
class ThemeManager:
theme: dict = {} # contains all the theme data
_built_in_themes: List[str] = ["blue", "green", "dark-blue", "sweetkind"]
_currently_loaded_theme: Union[str, None] = None
@classmethod
def load_theme(cls, theme_name_or_path: str):
script_directory = os.path.dirname(os.path.abspath(__file__))
if theme_name_or_path in cls._built_in_themes:
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:
cls.theme = json.load(f)
# store theme path for saving
cls._currently_loaded_theme = theme_name_or_path
# filter theme values for platform
for key in cls.theme.keys():
# check if values for key differ on platforms
if "macOS" in cls.theme[key].keys():
if sys.platform == "darwin":
cls.theme[key] = cls.theme[key]["macOS"]
elif sys.platform.startswith("win"):
cls.theme[key] = cls.theme[key]["Windows"]
else:
cls.theme[key] = cls.theme[key]["Linux"]
@classmethod
def save_theme(cls):
if cls._currently_loaded_theme is not None:
if cls._currently_loaded_theme not in cls._built_in_themes:
with open(cls._currently_loaded_theme, "r") as f:
json.dump(cls.theme, f, indent=2)
else:
raise ValueError(f"cannot modify builtin theme '{cls._currently_loaded_theme}'")
else:
raise ValueError(f"cannot save theme, no theme is loaded")

View File

@ -0,0 +1 @@
from .utility_functions import pop_from_dict_by_set, check_kwargs_empty

View File

@ -0,0 +1,22 @@
def pop_from_dict_by_set(dictionary: dict, valid_keys: set) -> dict:
""" remove and create new dict with key value pairs of dictionary, where key is in valid_keys """
new_dictionary = {}
for key in list(dictionary.keys()):
if key in valid_keys:
new_dictionary[key] = dictionary.pop(key)
return new_dictionary
def check_kwargs_empty(kwargs_dict, raise_error=False) -> bool:
""" returns True if kwargs are empty, False otherwise, raises error if not empty """
if len(kwargs_dict) > 0:
if raise_error:
raise ValueError(f"{list(kwargs_dict.keys())} are not supported arguments. Look at the documentation for supported arguments.")
else:
return True
else:
return False

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 MiB

View File

@ -7,199 +7,158 @@ customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "gre
class App(customtkinter.CTk):
WIDTH = 780
HEIGHT = 520
def __init__(self):
super().__init__()
# configure window
self.title("CustomTkinter complex_example.py")
self.geometry(f"{App.WIDTH}x{App.HEIGHT}")
self.geometry(f"{1100}x{580}")
self.protocol("WM_DELETE_WINDOW", self.on_closing) # call .on_closing() when app gets closed
# ============ create two frames ============
# configure grid layout (2x1)
# configure grid layout (4x4)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure((2, 3), weight=0)
self.grid_rowconfigure((0, 1, 2), weight=1)
self.frame_left = customtkinter.CTkFrame(master=self,
width=180,
corner_radius=0)
self.frame_left.grid(row=0, column=0, sticky="nswe")
# create sidebar frame with widgets
self.sidebar_frame = customtkinter.CTkFrame(self, width=140, corner_radius=0)
self.sidebar_frame.grid(row=0, column=0, rowspan=4, sticky="nsew")
self.sidebar_frame.grid_rowconfigure(4, weight=1)
self.logo_label = customtkinter.CTkLabel(self.sidebar_frame, text="CustomTkinter", font=customtkinter.CTkFont(size=20, weight="bold"))
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
self.sidebar_button_1 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_event)
self.sidebar_button_1.grid(row=1, column=0, padx=20, pady=10)
self.sidebar_button_2 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_event)
self.sidebar_button_2.grid(row=2, column=0, padx=20, pady=10)
self.sidebar_button_3 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_event)
self.sidebar_button_3.grid(row=3, column=0, padx=20, pady=10)
self.appearance_mode_label = customtkinter.CTkLabel(self.sidebar_frame, text="Appearance Mode:", anchor="w")
self.appearance_mode_label.grid(row=5, column=0, padx=20, pady=(10, 0))
self.appearance_mode_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["Light", "Dark", "System"],
command=self.change_appearance_mode_event)
self.appearance_mode_optionemenu.grid(row=6, column=0, padx=20, pady=(10, 10))
self.scaling_label = customtkinter.CTkLabel(self.sidebar_frame, text="UI Scaling:", anchor="w")
self.scaling_label.grid(row=7, column=0, padx=20, pady=(10, 0))
self.scaling_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["80%", "90%", "100%", "110%", "120%"],
command=self.change_scaling_event)
self.scaling_optionemenu.grid(row=8, column=0, padx=20, pady=(10, 20))
self.frame_right = customtkinter.CTkFrame(master=self)
self.frame_right.grid(row=0, column=1, sticky="nswe", padx=20, pady=20)
# create main entry and button
self.entry = customtkinter.CTkEntry(self, placeholder_text="CTkEntry")
self.entry.grid(row=3, column=1, columnspan=2, padx=(20, 0), pady=(20, 20), sticky="nsew")
# ============ frame_left ============
self.main_button_1 = customtkinter.CTkButton(master=self, fg_color="transparent", border_width=2, text_color=("gray10", "#DCE4EE"))
self.main_button_1.grid(row=3, column=3, padx=(20, 20), pady=(20, 20), sticky="nsew")
# configure grid layout (1x11)
self.frame_left.grid_rowconfigure(0, minsize=10) # empty row with minsize as spacing
self.frame_left.grid_rowconfigure(5, weight=1) # empty row as spacing
self.frame_left.grid_rowconfigure(8, minsize=20) # empty row with minsize as spacing
self.frame_left.grid_rowconfigure(11, minsize=10) # empty row with minsize as spacing
# create textbox
self.textbox = customtkinter.CTkTextbox(self, width=250)
self.textbox.grid(row=0, column=1, padx=(20, 0), pady=(20, 0), sticky="nsew")
self.label_1 = customtkinter.CTkLabel(master=self.frame_left,
text="CustomTkinter",
text_font=("Roboto Medium", -16)) # font name and size in px
self.label_1.grid(row=1, column=0, pady=10, padx=10)
# create tabview
self.tabview = customtkinter.CTkTabview(self, width=250)
self.tabview.grid(row=0, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
self.tabview.add("CTkTabview")
self.tabview.add("Tab 2")
self.tabview.add("Tab 3")
self.tabview.tab("CTkTabview").grid_columnconfigure(0, weight=1) # configure grid of individual tabs
self.tabview.tab("Tab 2").grid_columnconfigure(0, weight=1)
self.button_1 = customtkinter.CTkButton(master=self.frame_left,
text="CTkButton 1",
fg_color=("gray75", "gray30"), # <- custom tuple-color
command=self.button_event)
self.button_1.grid(row=2, column=0, pady=10, padx=20)
self.button_2 = customtkinter.CTkButton(master=self.frame_left,
text="CTkButton 2",
fg_color=("gray75", "gray30"), # <- custom tuple-color
command=self.button_event)
self.button_2.grid(row=3, column=0, pady=10, padx=20)
self.button_3 = customtkinter.CTkButton(master=self.frame_left,
text="CTkButton 3",
fg_color=("gray75", "gray30"), # <- custom tuple-color
command=self.button_event)
self.button_3.grid(row=4, column=0, pady=10, padx=20)
self.switch_1 = customtkinter.CTkSwitch(master=self.frame_left)
self.switch_1.grid(row=9, column=0, pady=10, padx=20, sticky="w")
self.switch_2 = customtkinter.CTkSwitch(master=self.frame_left,
text="Dark Mode",
command=self.change_mode)
self.switch_2.grid(row=10, column=0, pady=10, padx=20, sticky="w")
# ============ frame_right ============
# configure grid layout (3x7)
self.frame_right.rowconfigure((0, 1, 2, 3), weight=1)
self.frame_right.rowconfigure(7, weight=10)
self.frame_right.columnconfigure((0, 1), weight=1)
self.frame_right.columnconfigure(2, weight=0)
self.frame_info = customtkinter.CTkFrame(master=self.frame_right)
self.frame_info.grid(row=0, column=0, columnspan=2, rowspan=4, pady=20, padx=20, sticky="nsew")
# ============ frame_info ============
# configure grid layout (1x1)
self.frame_info.rowconfigure(0, weight=1)
self.frame_info.columnconfigure(0, weight=1)
self.label_info_1 = customtkinter.CTkLabel(master=self.frame_info,
text="CTkLabel: Lorem ipsum dolor sit,\n" +
"amet consetetur sadipscing elitr,\n" +
"sed diam nonumy eirmod tempor" ,
height=100,
fg_color=("white", "gray38"), # <- custom tuple-color
justify=tkinter.LEFT)
self.label_info_1.grid(column=0, row=0, sticky="nwe", padx=15, pady=15)
self.progressbar = customtkinter.CTkProgressBar(master=self.frame_info)
self.progressbar.grid(row=1, column=0, sticky="ew", padx=15, pady=15)
# ============ frame_right ============
self.optionmenu_1 = customtkinter.CTkOptionMenu(self.tabview.tab("CTkTabview"), dynamic_resizing=False,
values=["Value 1", "Value 2", "Value Long Long Long"])
self.optionmenu_1.grid(row=0, column=0, padx=20, pady=(20, 10))
self.combobox_1 = customtkinter.CTkComboBox(self.tabview.tab("CTkTabview"),
values=["Value 1", "Value 2", "Value Long....."])
self.combobox_1.grid(row=1, column=0, padx=20, pady=(10, 10))
self.string_input_button = customtkinter.CTkButton(self.tabview.tab("CTkTabview"), text="Open CTkInputDialog",
command=self.open_input_dialog_event)
self.string_input_button.grid(row=2, column=0, padx=20, pady=(10, 10))
self.label_tab_2 = customtkinter.CTkLabel(self.tabview.tab("Tab 2"), text="CTkLabel on Tab 2")
self.label_tab_2.grid(row=0, column=0, padx=20, pady=20)
# create radiobutton frame
self.radiobutton_frame = customtkinter.CTkFrame(self)
self.radiobutton_frame.grid(row=0, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew")
self.radio_var = tkinter.IntVar(value=0)
self.label_radio_group = customtkinter.CTkLabel(master=self.frame_right,
text="CTkRadioButton Group:")
self.label_radio_group.grid(row=0, column=2, columnspan=1, pady=20, padx=10, sticky="")
self.radio_button_1 = customtkinter.CTkRadioButton(master=self.frame_right,
variable=self.radio_var,
value=0)
self.label_radio_group = customtkinter.CTkLabel(master=self.radiobutton_frame, text="CTkRadioButton Group:")
self.label_radio_group.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="")
self.radio_button_1 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=0)
self.radio_button_1.grid(row=1, column=2, pady=10, padx=20, sticky="n")
self.radio_button_2 = customtkinter.CTkRadioButton(master=self.frame_right,
variable=self.radio_var,
value=1)
self.radio_button_2 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=1)
self.radio_button_2.grid(row=2, column=2, pady=10, padx=20, sticky="n")
self.radio_button_3 = customtkinter.CTkRadioButton(master=self.frame_right,
variable=self.radio_var,
value=2)
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")
self.slider_1 = customtkinter.CTkSlider(master=self.frame_right,
from_=0,
to=1,
number_of_steps=3,
command=self.progressbar.set)
self.slider_1.grid(row=4, column=0, columnspan=2, pady=10, padx=20, sticky="we")
# create slider and progressbar frame
self.slider_progressbar_frame = customtkinter.CTkFrame(self, fg_color="transparent")
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)
self.seg_button_1.grid(row=0, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
self.progressbar_1 = customtkinter.CTkProgressBar(self.slider_progressbar_frame)
self.progressbar_1.grid(row=1, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
self.progressbar_2 = customtkinter.CTkProgressBar(self.slider_progressbar_frame)
self.progressbar_2.grid(row=2, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
self.slider_1 = customtkinter.CTkSlider(self.slider_progressbar_frame, from_=0, to=1, number_of_steps=4)
self.slider_1.grid(row=3, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
self.slider_2 = customtkinter.CTkSlider(self.slider_progressbar_frame, orientation="vertical")
self.slider_2.grid(row=0, column=1, rowspan=5, padx=(10, 10), pady=(10, 10), sticky="ns")
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")
self.slider_2 = customtkinter.CTkSlider(master=self.frame_right,
command=self.progressbar.set)
self.slider_2.grid(row=5, column=0, columnspan=2, pady=10, padx=20, sticky="we")
# 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)
self.slider_button_1 = customtkinter.CTkButton(master=self.frame_right,
height=25,
text="CTkButton",
command=self.button_event)
self.slider_button_1.grid(row=4, column=2, columnspan=1, pady=10, padx=20, sticky="we")
self.slider_button_2 = customtkinter.CTkButton(master=self.frame_right,
height=25,
text="CTkButton",
command=self.button_event)
self.slider_button_2.grid(row=5, column=2, columnspan=1, pady=10, padx=20, sticky="we")
self.checkbox_button_1 = customtkinter.CTkButton(master=self.frame_right,
height=25,
text="CTkButton",
border_width=3, # <- custom border_width
fg_color=None, # <- no fg_color
command=self.button_event)
self.checkbox_button_1.grid(row=6, column=2, columnspan=1, pady=10, padx=20, sticky="we")
self.check_box_1 = customtkinter.CTkCheckBox(master=self.frame_right,
text="CTkCheckBox")
self.check_box_1.grid(row=6, column=0, pady=10, padx=20, sticky="w")
self.check_box_2 = customtkinter.CTkCheckBox(master=self.frame_right,
text="CTkCheckBox")
self.check_box_2.grid(row=6, column=1, pady=10, padx=20, sticky="w")
self.entry = customtkinter.CTkEntry(master=self.frame_right,
width=120,
placeholder_text="CTkEntry")
self.entry.grid(row=8, column=0, columnspan=2, pady=20, padx=20, sticky="we")
self.button_5 = customtkinter.CTkButton(master=self.frame_right,
text="CTkButton",
command=self.button_event)
self.button_5.grid(row=8, column=2, columnspan=1, pady=20, padx=20, sticky="we")
# 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.radio_button_1.select()
self.switch_2.select()
self.slider_1.set(0.2)
self.slider_2.set(0.7)
self.progressbar.set(0.5)
self.slider_button_1.configure(state=tkinter.DISABLED, text="Disabled Button")
self.radio_button_3.configure(state=tkinter.DISABLED)
self.check_box_1.configure(state=tkinter.DISABLED, text="CheckBox disabled")
self.check_box_2.select()
self.sidebar_button_3.configure(state="disabled", text="Disabled CTkButton")
self.checkbox_3.configure(state="disabled")
self.checkbox_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%")
self.optionmenu_1.set("CTkOptionmenu")
self.combobox_1.set("CTkComboBox")
self.slider_1.configure(command=self.progressbar_2.set)
self.slider_2.configure(command=self.progressbar_3.set)
self.progressbar_1.configure(mode="indeterminnate")
self.progressbar_1.start()
self.textbox.insert("0.0", "CTkTextbox\n\n" + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20)
self.seg_button_1.configure(values=["CTkSegmentedButton", "Value 2", "Value 3"])
self.seg_button_1.set("Value 2")
def button_event(self):
print("Button pressed")
def open_input_dialog_event(self):
dialog = customtkinter.CTkInputDialog(text="Type in a number:", title="CTkInputDialog")
print("CTkInputDialog:", dialog.get_input())
def change_mode(self):
if self.switch_2.get() == 1:
customtkinter.set_appearance_mode("dark")
else:
customtkinter.set_appearance_mode("light")
def change_appearance_mode_event(self, new_appearance_mode: str):
customtkinter.set_appearance_mode(new_appearance_mode)
def on_closing(self, event=0):
self.destroy()
def change_scaling_event(self, new_scaling: str):
new_scaling_float = int(new_scaling.replace("%", "")) / 100
customtkinter.set_widget_scaling(new_scaling_float)
def start(self):
self.mainloop()
def sidebar_button_event(self):
print("sidebar_button click")
if __name__ == "__main__":
app = App()
app.start()
app.mainloop()

View File

@ -1,68 +1,61 @@
import tkinter
import tkinter.messagebox
import customtkinter
from PIL import Image, ImageTk
from PIL import Image
import os
customtkinter.set_appearance_mode("Dark") # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
PATH = os.path.dirname(os.path.realpath(__file__))
customtkinter.set_appearance_mode("dark")
class App(customtkinter.CTk):
APP_NAME = "CustomTkinter example_background_image.py"
WIDTH = 900
HEIGHT = 600
width = 900
height = 600
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title(App.APP_NAME)
self.geometry(f"{App.WIDTH}x{App.HEIGHT}")
self.minsize(App.WIDTH, App.HEIGHT)
self.maxsize(App.WIDTH, App.HEIGHT)
self.title("CustomTkinter example_background_image.py")
self.geometry(f"{self.width}x{self.height}")
self.resizable(False, False)
self.protocol("WM_DELETE_WINDOW", self.on_closing)
# load and create background image
current_path = os.path.dirname(os.path.realpath(__file__))
self.bg_image = customtkinter.CTkImage(Image.open(current_path + "/test_images/bg_gradient.jpg"),
size=(self.width, self.height))
self.bg_image_label = customtkinter.CTkLabel(self, image=self.bg_image)
self.bg_image_label.grid(row=0, column=0)
# load image with PIL and convert to PhotoImage
image = Image.open(PATH + "/test_images/bg_gradient.jpg").resize((self.WIDTH, self.HEIGHT))
self.bg_image = ImageTk.PhotoImage(image)
# create login frame
self.login_frame = customtkinter.CTkFrame(self, corner_radius=0)
self.login_frame.grid(row=0, column=0, sticky="ns")
self.login_label = customtkinter.CTkLabel(self.login_frame, text="CustomTkinter\nLogin Page",
font=customtkinter.CTkFont(size=20, weight="bold"))
self.login_label.grid(row=0, column=0, padx=30, pady=(150, 15))
self.username_entry = customtkinter.CTkEntry(self.login_frame, width=200, placeholder_text="username")
self.username_entry.grid(row=1, column=0, padx=30, pady=(15, 15))
self.password_entry = customtkinter.CTkEntry(self.login_frame, width=200, show="*", placeholder_text="password")
self.password_entry.grid(row=2, column=0, padx=30, pady=(0, 15))
self.login_button = customtkinter.CTkButton(self.login_frame, text="Login", command=self.login_event, width=200)
self.login_button.grid(row=3, column=0, padx=30, pady=(15, 15))
self.image_label = tkinter.Label(master=self, image=self.bg_image)
self.image_label.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
# create main frame
self.main_frame = customtkinter.CTkFrame(self, corner_radius=0)
self.main_frame.grid_columnconfigure(0, weight=1)
self.main_label = customtkinter.CTkLabel(self.main_frame, text="CustomTkinter\nMain Page",
font=customtkinter.CTkFont(size=20, weight="bold"))
self.main_label.grid(row=0, column=0, padx=30, pady=(30, 15))
self.back_button = customtkinter.CTkButton(self.main_frame, text="Back", command=self.back_event, width=200)
self.back_button.grid(row=1, column=0, padx=30, pady=(15, 15))
self.frame = customtkinter.CTkFrame(master=self,
width=300,
height=App.HEIGHT,
corner_radius=0)
self.frame.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
def login_event(self):
print("Login pressed - username:", self.username_entry.get(), "password:", self.password_entry.get())
self.label_1 = customtkinter.CTkLabel(master=self.frame, width=200, height=60,
fg_color=("gray70", "gray25"), text="CustomTkinter\ninterface example")
self.label_1.place(relx=0.5, rely=0.3, anchor=tkinter.CENTER)
self.login_frame.grid_forget() # remove login frame
self.main_frame.grid(row=0, column=0, sticky="nsew", padx=100) # show main frame
self.entry_1 = customtkinter.CTkEntry(master=self.frame, corner_radius=6, width=200, placeholder_text="username")
self.entry_1.place(relx=0.5, rely=0.52, anchor=tkinter.CENTER)
self.entry_2 = customtkinter.CTkEntry(master=self.frame, corner_radius=6, width=200, show="*", placeholder_text="password")
self.entry_2.place(relx=0.5, rely=0.6, anchor=tkinter.CENTER)
self.button_2 = customtkinter.CTkButton(master=self.frame, text="Login",
corner_radius=6, command=self.button_event, width=200)
self.button_2.place(relx=0.5, rely=0.7, anchor=tkinter.CENTER)
def button_event(self):
print("Login pressed - username:", self.entry_1.get(), "password:", self.entry_2.get())
def on_closing(self, event=0):
self.destroy()
def start(self):
self.mainloop()
def back_event(self):
self.main_frame.grid_forget() # remove main frame
self.login_frame.grid(row=0, column=0, sticky="ns") # show login frame
if __name__ == "__main__":
app = App()
app.start()
app.mainloop()

View File

@ -1,64 +0,0 @@
import tkinter
import customtkinter
from PIL import Image, ImageTk # <- import PIL for the images
import os
PATH = os.path.dirname(os.path.realpath(__file__))
customtkinter.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
app = customtkinter.CTk() # create CTk window like you do with the Tk window (you can also use normal tkinter.Tk window)
app.geometry("450x260")
app.title("CustomTkinter example_button_images.py")
def button_function():
print("button pressed")
# load images as PhotoImage
image_size = 20
settings_image = ImageTk.PhotoImage(Image.open(PATH + "/test_images/settings.png").resize((image_size, image_size)))
bell_image = ImageTk.PhotoImage(Image.open(PATH + "/test_images/bell.png").resize((image_size, image_size)))
add_folder_image = ImageTk.PhotoImage(Image.open(PATH + "/test_images/add-folder.png").resize((image_size, image_size), Image.ANTIALIAS))
add_list_image = ImageTk.PhotoImage(Image.open(PATH + "/test_images/add-list.png").resize((image_size, image_size), Image.ANTIALIAS))
add_user_image = ImageTk.PhotoImage(Image.open(PATH + "/test_images/add-user.png").resize((image_size, image_size), Image.ANTIALIAS))
chat_image = ImageTk.PhotoImage(Image.open(PATH + "/test_images/chat.png").resize((image_size, image_size), Image.ANTIALIAS))
home_image = ImageTk.PhotoImage(Image.open(PATH + "/test_images/home.png").resize((image_size, image_size), Image.ANTIALIAS))
app.grid_rowconfigure(0, weight=1)
app.grid_columnconfigure(0, weight=1, minsize=200)
frame_1 = customtkinter.CTkFrame(master=app, width=250, height=240, corner_radius=15)
frame_1.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
frame_1.grid_columnconfigure(0, weight=1)
frame_1.grid_columnconfigure(1, weight=1)
frame_1.grid_rowconfigure(0, minsize=10) # add empty row for spacing
button_1 = customtkinter.CTkButton(master=frame_1, image=add_folder_image, text="Add Folder", width=190, height=40,
compound="right", command=button_function)
button_1.grid(row=1, column=0, columnspan=2, padx=20, pady=10, sticky="ew")
button_2 = customtkinter.CTkButton(master=frame_1, image=add_list_image, text="Add Item", width=190, height=40,
compound="right", fg_color="#D35B58", hover_color="#C77C78",
command=button_function)
button_2.grid(row=2, column=0, columnspan=2, padx=20, pady=10, sticky="ew")
button_3 = customtkinter.CTkButton(master=frame_1, image=chat_image, text="", width=50, height=50,
corner_radius=10, fg_color="gray40", hover_color="gray25", command=button_function)
button_3.grid(row=3, column=0, columnspan=1, padx=20, pady=10, sticky="w")
button_4 = customtkinter.CTkButton(master=frame_1, image=home_image, text="", width=50, height=50,
corner_radius=10, fg_color="gray40", hover_color="gray25", command=button_function)
button_4.grid(row=3, column=1, columnspan=1, padx=20, pady=10, sticky="e")
button_5 = customtkinter.CTkButton(master=app, image=add_user_image, text="Add User", width=130, height=70, border_width=3,
corner_radius=10, compound="bottom", border_color="#D35B58", fg_color=("gray84", "gray25"), hover_color="#C77C78",
command=button_function)
button_5.grid(row=0, column=1, padx=20, pady=20)
app.mainloop()

118
examples/image_example.py Normal file
View File

@ -0,0 +1,118 @@
import customtkinter
import os
from PIL import Image
class App(customtkinter.CTk):
def __init__(self):
super().__init__()
self.title("image_example.py")
self.geometry("700x450")
# set grid layout 1x2
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
# load images with light and dark mode image
image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_images")
self.logo_image = customtkinter.CTkImage(Image.open(os.path.join(image_path, "CustomTkinter_logo_single.png")), size=(26, 26))
self.large_test_image = customtkinter.CTkImage(Image.open(os.path.join(image_path, "large_test_image.png")), size=(500, 150))
self.image_icon_image = customtkinter.CTkImage(Image.open(os.path.join(image_path, "image_icon_light.png")), size=(20, 20))
self.home_image = customtkinter.CTkImage(light_image=Image.open(os.path.join(image_path, "home_dark.png")),
dark_image=Image.open(os.path.join(image_path, "home_light.png")), size=(20, 20))
self.chat_image = customtkinter.CTkImage(light_image=Image.open(os.path.join(image_path, "chat_dark.png")),
dark_image=Image.open(os.path.join(image_path, "chat_light.png")), size=(20, 20))
self.add_user_image = customtkinter.CTkImage(light_image=Image.open(os.path.join(image_path, "add_user_dark.png")),
dark_image=Image.open(os.path.join(image_path, "add_user_light.png")), size=(20, 20))
# create navigation frame
self.navigation_frame = customtkinter.CTkFrame(self, corner_radius=0)
self.navigation_frame.grid(row=0, column=0, sticky="nsew")
self.navigation_frame.grid_rowconfigure(4, weight=1)
self.navigation_frame_label = customtkinter.CTkLabel(self.navigation_frame, text=" Image Example", image=self.logo_image,
compound="left", font=customtkinter.CTkFont(size=15, weight="bold"))
self.navigation_frame_label.grid(row=0, column=0, padx=20, pady=20)
self.home_button = customtkinter.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Home",
fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"),
image=self.home_image, anchor="w", command=self.home_button_event)
self.home_button.grid(row=1, column=0, sticky="ew")
self.frame_2_button = customtkinter.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Frame 2",
fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"),
image=self.chat_image, anchor="w", command=self.frame_2_button_event)
self.frame_2_button.grid(row=2, column=0, sticky="ew")
self.frame_3_button = customtkinter.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Frame 3",
fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"),
image=self.add_user_image, anchor="w", command=self.frame_3_button_event)
self.frame_3_button.grid(row=3, column=0, sticky="ew")
self.appearance_mode_menu = customtkinter.CTkOptionMenu(self.navigation_frame, values=["Light", "Dark", "System"],
command=self.change_appearance_mode_event)
self.appearance_mode_menu.grid(row=6, column=0, padx=20, pady=20, sticky="s")
# create home frame
self.home_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent")
self.home_frame.grid_columnconfigure(0, weight=1)
self.home_frame_large_image_label = customtkinter.CTkLabel(self.home_frame, text="", image=self.large_test_image)
self.home_frame_large_image_label.grid(row=0, column=0, padx=20, pady=10)
self.home_frame_button_1 = customtkinter.CTkButton(self.home_frame, text="", image=self.image_icon_image)
self.home_frame_button_1.grid(row=1, column=0, padx=20, pady=10)
self.home_frame_button_2 = customtkinter.CTkButton(self.home_frame, text="CTkButton", image=self.image_icon_image, compound="right")
self.home_frame_button_2.grid(row=2, column=0, padx=20, pady=10)
self.home_frame_button_3 = customtkinter.CTkButton(self.home_frame, text="CTkButton", image=self.image_icon_image, compound="top")
self.home_frame_button_3.grid(row=3, column=0, padx=20, pady=10)
self.home_frame_button_4 = customtkinter.CTkButton(self.home_frame, text="CTkButton", image=self.image_icon_image, compound="bottom", anchor="w")
self.home_frame_button_4.grid(row=4, column=0, padx=20, pady=10)
# create second frame
self.second_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent")
# create third frame
self.third_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent")
# select default frame
self.select_frame_by_name("home")
def select_frame_by_name(self, name):
# set button color for selected button
self.home_button.configure(fg_color=("gray75", "gray25") if name == "home" else "transparent")
self.frame_2_button.configure(fg_color=("gray75", "gray25") if name == "frame_2" else "transparent")
self.frame_3_button.configure(fg_color=("gray75", "gray25") if name == "frame_3" else "transparent")
# show selected frame
if name == "home":
self.home_frame.grid(row=0, column=1, sticky="nsew")
else:
self.home_frame.grid_forget()
if name == "frame_2":
self.second_frame.grid(row=0, column=1, sticky="nsew")
else:
self.second_frame.grid_forget()
if name == "frame_3":
self.third_frame.grid(row=0, column=1, sticky="nsew")
else:
self.third_frame.grid_forget()
def home_button_event(self):
self.select_frame_by_name("home")
def frame_2_button_event(self):
self.select_frame_by_name("frame_2")
def frame_3_button_event(self):
self.select_frame_by_name("frame_3")
def change_appearance_mode_event(self, new_appearance_mode):
customtkinter.set_appearance_mode(new_appearance_mode)
if __name__ == "__main__":
app = App()
app.mainloop()

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

@ -1,61 +1,71 @@
import tkinter
import customtkinter
customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
app = customtkinter.CTk() # create CTk window like you do with the Tk window
app.geometry("400x480")
app = customtkinter.CTk()
app.geometry("400x780")
app.title("CustomTkinter simple_example.py")
def button_function():
print("Button click", label_1.text_label.cget("text"))
def button_callback():
print("Button click", combobox_1.get())
def slider_function(value):
def slider_callback(value):
progressbar_1.set(value)
def check_box_function():
print("checkbox_1:", checkbox_1.get())
y_padding = 13
frame_1 = customtkinter.CTkFrame(master=app, corner_radius=15)
frame_1 = customtkinter.CTkFrame(master=app)
frame_1.pack(pady=20, padx=60, fill="both", expand=True)
label_1 = customtkinter.CTkLabel(master=frame_1, justify=tkinter.LEFT)
label_1.pack(pady=y_padding, padx=10)
label_1 = customtkinter.CTkLabel(master=frame_1, justify=customtkinter.LEFT)
label_1.pack(pady=10, padx=10)
progressbar_1 = customtkinter.CTkProgressBar(master=frame_1)
progressbar_1.pack(pady=y_padding, padx=10)
progressbar_1.pack(pady=10, padx=10)
button_1 = customtkinter.CTkButton(master=frame_1, corner_radius=8, command=button_function)
button_1.pack(pady=y_padding, padx=10)
button_1 = customtkinter.CTkButton(master=frame_1, command=button_callback)
button_1.pack(pady=10, padx=10)
slider_1 = customtkinter.CTkSlider(master=frame_1, command=slider_function, from_=0, to=1)
slider_1.pack(pady=y_padding, padx=10)
slider_1 = customtkinter.CTkSlider(master=frame_1, command=slider_callback, from_=0, to=1)
slider_1.pack(pady=10, padx=10)
slider_1.set(0.5)
entry_1 = customtkinter.CTkEntry(master=frame_1, placeholder_text="CTkEntry")
entry_1.pack(pady=y_padding, padx=10)
entry_1.pack(pady=10, padx=10)
checkbox_1 = customtkinter.CTkCheckBox(master=frame_1, command=check_box_function)
checkbox_1.pack(pady=y_padding, padx=10)
optionmenu_1 = customtkinter.CTkOptionMenu(frame_1, values=["Option 1", "Option 2", "Option 42 long long long..."])
optionmenu_1.pack(pady=10, padx=10)
optionmenu_1.set("CTkOptionMenu")
radiobutton_var = tkinter.IntVar(value=1)
combobox_1 = customtkinter.CTkComboBox(frame_1, values=["Option 1", "Option 2", "Option 42 long long long..."])
combobox_1.pack(pady=10, padx=10)
combobox_1.set("CTkComboBox")
checkbox_1 = customtkinter.CTkCheckBox(master=frame_1)
checkbox_1.pack(pady=10, padx=10)
radiobutton_var = customtkinter.IntVar(value=1)
radiobutton_1 = customtkinter.CTkRadioButton(master=frame_1, variable=radiobutton_var, value=1)
radiobutton_1.pack(pady=y_padding, padx=10)
radiobutton_1.pack(pady=10, padx=10)
radiobutton_2 = customtkinter.CTkRadioButton(master=frame_1, variable=radiobutton_var, value=2)
radiobutton_2.pack(pady=y_padding, padx=10)
s_var = tkinter.StringVar(value="on")
radiobutton_2.pack(pady=10, padx=10)
switch_1 = customtkinter.CTkSwitch(master=frame_1)
switch_1.pack(pady=y_padding, padx=10)
switch_1.pack(pady=10, padx=10)
text_1 = customtkinter.CTkTextbox(master=frame_1, width=200, height=70)
text_1.pack(pady=10, padx=10)
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.pack(pady=10, padx=10)
tabview_1.add("CTkTabview")
tabview_1.add("Tab 2")
app.mainloop()

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Some files were not shown because too many files have changed in this diff Show More