├── ttkbootstrap ├── dialogs │ ├── file.py │ ├── __init__.py │ ├── input.py │ ├── message.py │ └── calendar.py ├── style │ ├── __init__.py │ ├── publisher.py │ ├── style.py │ ├── bootstyle.py │ ├── utility.py │ └── colors.py ├── themes │ ├── __init__.py │ ├── user.py │ └── standard.py ├── constants.py ├── widgets │ ├── __init__.py │ ├── date_entry.py │ ├── floodgauge.py │ └── meter.py ├── __init__.py └── __main__.py ├── del.ico ├── edit.ico ├── exit.ico ├── help.ico ├── home.ico ├── favicon.ico ├── github.ico ├── update.ico ├── update.txt ├── recovery.ico ├── .gitignore ├── hook-apscheduler.py ├── introduction.md ├── cx_setup.py ├── README.md ├── CountBoard.iss ├── TkHtmlView.py ├── CustomWindow.py ├── WindowEffect.py └── Tile.py /ttkbootstrap/dialogs/file.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ttkbootstrap/dialogs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ttkbootstrap/dialogs/input.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ttkbootstrap/dialogs/message.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ttkbootstrap/style/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ttkbootstrap/themes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ttkbootstrap/themes/user.py: -------------------------------------------------------------------------------- 1 | USER_THEMES={} -------------------------------------------------------------------------------- /del.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaoyongxian666/CountBoard/HEAD/del.ico -------------------------------------------------------------------------------- /edit.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaoyongxian666/CountBoard/HEAD/edit.ico -------------------------------------------------------------------------------- /exit.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaoyongxian666/CountBoard/HEAD/exit.ico -------------------------------------------------------------------------------- /help.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaoyongxian666/CountBoard/HEAD/help.ico -------------------------------------------------------------------------------- /home.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaoyongxian666/CountBoard/HEAD/home.ico -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaoyongxian666/CountBoard/HEAD/favicon.ico -------------------------------------------------------------------------------- /github.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaoyongxian666/CountBoard/HEAD/github.ico -------------------------------------------------------------------------------- /update.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaoyongxian666/CountBoard/HEAD/update.ico -------------------------------------------------------------------------------- /update.txt: -------------------------------------------------------------------------------- 1 | {"version":"1.3.0.2","url":"https://gaoyongxian.lanzouo.com/iBR5lx63n2h"} -------------------------------------------------------------------------------- /recovery.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaoyongxian666/CountBoard/HEAD/recovery.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _doc 2 | *.egg 3 | *.egg-info 4 | dist 5 | build 6 | .idea 7 | *.pyc 8 | pack 9 | *.sqlite 10 | .venv 11 | logs 12 | pack32 13 | *.txt -------------------------------------------------------------------------------- /ttkbootstrap/constants.py: -------------------------------------------------------------------------------- 1 | DEFAULT = 'default' 2 | DEFAULT_THEME = 'litera' 3 | TTK_CLAM = 'clam' 4 | TTK_ALT = 'alt' 5 | TTK_DEFAULT = 'default' 6 | 7 | # bootstyle colors 8 | PRIMARY = 'primary' 9 | SECONDARY = 'secondary' 10 | SUCCESS = 'success' 11 | DANGER = 'danger' 12 | WARNING = 'warning' 13 | INFO = 'info' 14 | LIGHT = 'light' 15 | DARK = 'dark' -------------------------------------------------------------------------------- /hook-apscheduler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """ 3 | @Project :CountBoard 4 | @File :hooks-apscheduler.py 5 | @Author :Gao yongxian 6 | @Date :2021/12/2 13:02 7 | @contact: g1695698547@163.com 8 | """ 9 | """apscheduler 3.6.3""" 10 | # hooks-a.py 不能被检索到是? 11 | 12 | from PyInstaller.utils.hooks import collect_submodules, copy_metadata, collect_all 13 | 14 | datas = copy_metadata('apscheduler', recursive=True) 15 | hiddenimports = collect_submodules('apscheduler') 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /ttkbootstrap/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from .meter import Meter 2 | from .floodgauge import Floodgauge 3 | from .date_entry import DateEntry 4 | 5 | from tkinter.ttk import Button, Checkbutton, Combobox 6 | from tkinter.ttk import Entry, Frame, Label 7 | from tkinter.ttk import Labelframe, LabelFrame, Menubutton 8 | from tkinter.ttk import Notebook, OptionMenu, PanedWindow 9 | from tkinter.ttk import Panedwindow, Progressbar, Radiobutton 10 | from tkinter.ttk import Scale, Scrollbar, Separator 11 | from tkinter.ttk import Sizegrip, Spinbox, Treeview 12 | 13 | -------------------------------------------------------------------------------- /introduction.md: -------------------------------------------------------------------------------- 1 | CountBoard 是一个基于Tkinter开源的桌面日程倒计时应用。 2 | 3 | ## 基本功能 4 | 5 | * 磁贴主题 6 | * Acrylic:亚克力效果。 7 | * Aero:毛玻璃效果。 8 | * 修改功能 9 | * 双击日程可修改或者删除。 10 | * 右键可以新建,删除,修改。 11 | * 提醒功能 12 | * 定时提醒:每天固定时间进行提醒。 13 | * 间隔提醒:每隔多少时间进行提醒。 14 | * 计时模式 15 | * 普通模式:24小时以内算做一天。 16 | * 紧迫模式:24小时以内算做零天。 17 | * 磁贴主题 18 | * 嵌入桌面:绑定到桌面。 19 | * 独立窗体:独立的窗体,可以设置置顶等。 20 | 21 | 22 | ## 更新日志 23 | * V1.3 24 | * 2021-11-30:修改bug,增加桌面模式,提醒功能,优化代码等 25 | * V1.2 26 | * 2021-11-10:增加自动调整大小,自动贴边,开机自启等功能 27 | * V1.1 28 | * 2021-11-04:修改外观,实现了亚克力效果和磨玻璃效果 29 | * V1.0 30 | * 2021-10-16:完成基本功能 31 | -------------------------------------------------------------------------------- /cx_setup.py: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/marcelotduarte/cx_Freeze/blob/main/cx_Freeze/samples/Tkinter/setup.py 2 | # 3 | # A simple setup script to create an executable using Tkinter. This also 4 | # demonstrates the method for creating a Windows executable that does not have 5 | # an associated console. 6 | 7 | import sys 8 | from cx_Freeze import setup, Executable 9 | 10 | from pathlib import Path 11 | icon_path = Path('favicon.ico') 12 | 13 | base = None 14 | if sys.platform == "win32": 15 | base = "Win32GUI" 16 | 17 | executables = [Executable("Main_window.py", base=base)] 18 | 19 | options = { 20 | 'build_exe': { 21 | 'include_files': [ 22 | icon_path 23 | ], # additional plugins needed by qt at runtime 24 | 'zip_include_packages': [ 25 | 'encodings', 26 | 'markdown2', 27 | 'PIL', 28 | 'sqlitedict', 29 | 'tkhtmlview' 30 | ], # reduce size of packages that are used 31 | 'excludes': [ 32 | 'unittest', 33 | 'pydoc', 34 | 'pdb' 35 | ] 36 | } 37 | } 38 | 39 | setup( 40 | name="CountBoard", 41 | version="1.1.0.2", 42 | description="CountBoard 是一个基于 Tkinter 简单的,开源的桌面日程倒计时应用。", 43 | executables=executables, 44 | options=options 45 | ) 46 | 47 | # Use cx-freeze package to freeze application. 48 | # 49 | # pip install cx-freeze 50 | # Use python cx_setup.py bdist_msi command to create windows msi install file. -------------------------------------------------------------------------------- /ttkbootstrap/style/publisher.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from collections import namedtuple 3 | from typing import List 4 | 5 | Subscriber = namedtuple('Subscriber', ['name', 'func', 'channel']) 6 | 7 | 8 | class Channel(Enum): 9 | """A grouping for Publisher subscribers. Indicates whether the 10 | widget is a legacy `STD` tk widget or a styled `TTK` widget. 11 | """ 12 | STD = 1 13 | TTK = 2 14 | 15 | 16 | class Publisher: 17 | 18 | __subscribers = {} 19 | 20 | @staticmethod 21 | def subscribe(name, func, channel): 22 | """Subscribe to an event. 23 | 24 | Parameters 25 | ---------- 26 | name : str 27 | The widget's tkinter/tcl name. 28 | 29 | func : Callable 30 | A function to call when passing a message. 31 | 32 | channel : Channel 33 | Indicates the channel grouping the subscribers. 34 | """ 35 | subs = Publisher.__subscribers 36 | subs[name] = Subscriber(name, func, channel) 37 | 38 | @staticmethod 39 | def unsubscribe(name): 40 | """Subscribe to an event. 41 | 42 | Parameters 43 | ---------- 44 | name : str 45 | The widget's tkinter/tcl name. 46 | 47 | func : Callable 48 | A function to call when passing a message. 49 | """ 50 | subs = Publisher.__subscribers 51 | try: 52 | del(subs[name]) 53 | except: 54 | pass 55 | 56 | def get_subscribers(channel): 57 | """Return a list of subscribers 58 | 59 | Returns 60 | ------- 61 | dict_items 62 | List of key-value tuples 63 | """ 64 | subs = Publisher.__subscribers.values() 65 | channel_subs = [s for s in subs if s.channel == channel] 66 | return channel_subs 67 | 68 | def publish_message(channel, *args): 69 | """Publish a message to all subscribers""" 70 | subs: List[Subscriber] = Publisher.get_subscribers(channel) 71 | for sub in subs: 72 | sub.func(*args) 73 | 74 | @staticmethod 75 | def clear_subscribers(): 76 | Publisher.__subscribers.clear() 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CountBoard 是一个基于Tkinter开源的桌面日程倒计时应用。 2 | 3 | 4 | ## 基本功能 5 | 6 | 7 | * 磁贴主题 8 | * Acrylic:亚克力效果。 9 | * Aero:毛玻璃效果。 10 | * 修改功能 11 | * 双击日程可修改或者删除。 12 | * 右键可以新建,删除,修改。 13 | * 提醒功能 14 | * 定时提醒:每天固定时间进行提醒。 15 | * 间隔提醒:每隔多少时间进行提醒。 16 | * 计时模式 17 | * 普通模式:24小时以内算做一天。 18 | * 紧迫模式:24小时以内算做零天。 19 | * 磁贴主题 20 | * 嵌入桌面:绑定到桌面。 21 | * 独立窗体:独立的窗体,可以设置置顶等。 22 | 23 | ## 预览图 24 | 25 | ![预览图](https://pic.imgdb.cn/item/61a889432ab3f51d9190ca1b.pngg) 26 | 27 | ![预览图](https://pic.imgdb.cn/item/61a876552ab3f51d9183e286.png) 28 | 29 | ![预览图](https://pic.imgdb.cn/item/61a876552ab3f51d9183e294.png) 30 | 31 | ![预览图](https://pic.imgdb.cn/item/61a876552ab3f51d9183e2a0.png) 32 | 33 | ![预览图](https://pic.imgdb.cn/item/61a876552ab3f51d9183e2a6.png) 34 | 35 | ![预览图](https://pic.imgdb.cn/item/61a876ae2ab3f51d9184183f.png) 36 | 37 | 38 | 39 | ## 其他说明 40 | * 美化包: [ttkbootstrap](https://github.com/israel-dryer/ttkbootstrap) 41 | * 托盘图标: [pywin10](https://github.com/Gaoyongxian666/pywin10) 42 | * 数据存储: [sqlitedict](https://github.com/Gaoyongxian666/pywin10) 43 | 44 | ## 下载地址 45 | ### 2021-12-02更新 1.3.0.2 46 | * [安装包](https://gaoyongxian.lanzouo.com/iBR5lx63n2h) 47 | * [便携版](https://gaoyongxian.lanzouo.com/ixsSax63kgd) 48 | * [32位版](https://gaoyongxian.lanzouo.com/iTwuNx63l7a) 49 | 50 | ## 贡献者(欢迎PR) 51 | * [rtrobin](https://github.com/rtrobin) 52 | * Use cx-freeze package to freeze application. 53 | 54 | ## 更新日志 55 | * V1.3 56 | * 2021-11-30:修改日期不更新bug,增加桌面模式,提醒功能,优化代码等 57 | * V1.2 58 | * 2021-11-10:增加自动调整大小,自动贴边,开机自启等功能 59 | * V1.1 60 | * 2021-11-04:修改外观,实现了亚克力效果和磨玻璃效果 61 | * V1.0 62 | * 2021-10-16:完成基本功能 63 | 64 | ## 如何打包 65 | 1. 使用`pyinstaller`进行打包`pip install pyinstaller` 66 | 2. 下载项目到本地,在项目目录下新建一个`pack`文件夹(用来存放由`pyinstaller`生成文件) 67 | 3. 在`pack`文件夹下新建一个`hooks`文件夹,将`hook-apscheduler.py`复制过去. 68 | 4. 打开`cmd`,`cd`到`pack`文件夹下,使用命令 `pyinstaller -F -i "C:\Users\Gao yongxian\PycharmProjects\CountBoard\favicon.ico" "C:\Users\Gao yongxian\PycharmProjects\CountBoard\CountBoard.py" -w --additional-hooks-dir=./hooks` (注意修改你自己的路径) 69 | 5. 命令说明:`-F`是生成单`exe`文件,你会在`dist`文件夹下看到你的`exe`文件.`-i`是指定窗口的图标.`-w`是指不带命令行.`--additional-hooks-dir`是指定你自己的hooks文件 70 | 6. 因为项目中使用了`apscheduler`,而`pyinstaller`在打包`apscheduler`时不能自动生成元数据,所以只能自己指定了. -------------------------------------------------------------------------------- /ttkbootstrap/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Why does this project exist? 3 | ============================ 4 | The purpose of this project is create a set of beautifully designed 5 | and easy to apply styles for your tkinter applications. Ttk can be 6 | very time-consuming to style if you are just a casual user. This 7 | project takes the pain out of getting a modern look and feel so 8 | that you can focus on designing your application. This project was 9 | created to harness the power of ttk's (and thus Python's) existing 10 | built-in theme engine to create modern and professional-looking 11 | user interfaces which are inspired by, and in many cases, 12 | whole-sale rip-off's of the themes found on Bootswatch_. Even 13 | better, you have the abilty to 14 | :ref:`create and use your own custom themes ` 15 | using TTK Creator. 16 | 17 | A bootstrap approach to style 18 | ============================= 19 | Many people are familiar with bootstrap for web developement. It 20 | comes pre-packaged with built-in css style classes that provide a 21 | professional and consistent api for quick development. I took a 22 | similar approach with this project by pre-defining styles for 23 | nearly all ttk widgets. This makes is very easy to apply the 24 | theme colors to various widgets by using style declarations. If 25 | you want a button in the `secondary` theme color, simply apply the 26 | **secondary.TButton** style to the button. Want a blue outlined 27 | button? Apply the **info.Outline.TButton** style to the button. 28 | 29 | What about the old tkinter widgets? 30 | =================================== 31 | Some of the ttk widgets utilize existing tkinter widgets. For 32 | example: there is a tkinter popdown list in the ``ttk.Combobox`` 33 | and a legacy tkinter widget inside the ``ttk.OptionMenu``. To 34 | make sure these widgets didn't stick out like a sore thumb, I 35 | created a ``StyleTK`` class to apply the same color and style to 36 | these legacy widgets. While these legacy widgets are not 37 | necessarily intended to be used (and will probably not look as 38 | nice as the ttk versions when they exist), they are available if 39 | needed, and shouldn't look completely out-of-place in your 40 | ttkbootstrap themed application. 41 | :ref:`Check out this example ` to 42 | see for yourself. 43 | 44 | .. _Bootswatch: https://bootswatch.com/ 45 | 46 | """ 47 | from ttkbootstrap.style.style import Style 48 | from ttkbootstrap.style import bootstyle 49 | from ttkbootstrap.widgets import * 50 | bootstyle.setup_ttkbootstap_api() 51 | -------------------------------------------------------------------------------- /CountBoard.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "CountBoard" 5 | #define MyAppVersion "1.3.0.2" 6 | #define MyAppPublisher "Gaoyongxian" 7 | #define MyAppURL "https://github.com/Gaoyongxian666/CountBoard" 8 | #define MyAppExeName "CountBoard.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{6DA65E00-FAAE-4286-B277-6D6F99519AD0} 14 | AppName={#MyAppName} 15 | AppVersion={#MyAppVersion} 16 | ;AppVerName={#MyAppName} {#MyAppVersion} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName={userdocs}/{#MyAppName}/{#MyAppVersion} 22 | DisableProgramGroupPage=yes 23 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 24 | ;PrivilegesRequired=lowest 25 | OutputDir=C:\Users\Gao yongxian\Desktop 26 | OutputBaseFilename=CountBoard_{#MyAppVersion} 27 | SetupIconFile=C:\Users\Gao yongxian\PycharmProjects\CountBoard\favicon.ico 28 | Compression=lzma 29 | SolidCompression=yes 30 | WizardStyle=modern 31 | UninstallDisplayIcon={app}\{#MyAppExeName} 32 | 33 | [Code] 34 | function InitializeSetup(): boolean; 35 | var 36 | ResultStr: String; 37 | ResultCode: Integer; 38 | begin 39 | if RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{6DA65E00-FAAE-4286-B277-6D6F99519AD0}_is1', 'UninstallString', ResultStr) then 40 | begin 41 | ResultStr := RemoveQuotes(ResultStr); 42 | Exec(ResultStr, '/silent', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); 43 | end; 44 | result := true; 45 | end; 46 | 47 | [Languages] 48 | Name: "english"; MessagesFile: "compiler:Default.isl" 49 | Name: "cn"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" 50 | 51 | [Tasks] 52 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 53 | 54 | [Files] 55 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion 56 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\cacert.pem"; DestDir: "{app}"; Flags: ignoreversion 57 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\favicon.ico"; DestDir: "{app}"; Flags: ignoreversion 58 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\introduction.md"; DestDir: "{app}"; Flags: ignoreversion 59 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\del.ico"; DestDir: "{app}"; Flags: ignoreversion 60 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\home.ico"; DestDir: "{app}"; Flags: ignoreversion 61 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\edit.ico"; DestDir: "{app}"; Flags: ignoreversion 62 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\exit.ico"; DestDir: "{app}"; Flags: ignoreversion 63 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\github.ico"; DestDir: "{app}"; Flags: ignoreversion 64 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\help.ico"; DestDir: "{app}"; Flags: ignoreversion 65 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\recovery.ico"; DestDir: "{app}"; Flags: ignoreversion 66 | Source: "C:\Users\Gao yongxian\PycharmProjects\CountBoard\pack\CountBoard\update.ico"; DestDir: "{app}"; Flags: ignoreversion 67 | 68 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 69 | 70 | [Icons] 71 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 72 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 73 | 74 | [Run] 75 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 76 | 77 | -------------------------------------------------------------------------------- /TkHtmlView.py: -------------------------------------------------------------------------------- 1 | """ 2 | tkinter HTML text widgets 3 | """ 4 | import sys 5 | import tkinter as tk 6 | 7 | import ttkbootstrap 8 | from tkhtmlview import html_parser 9 | 10 | VERSION = "0.1.0.post1" 11 | 12 | 13 | class _ScrolledText(tk.Text): 14 | # ---------------------------------------------------------------------------------------------- 15 | def __init__(self, master=None, **kw): 16 | self.frame = tk.Frame(master) 17 | 18 | self.vbar = ttkbootstrap.Scrollbar(self.frame, bootstyle='round') 19 | kw.update({'yscrollcommand': self.vbar.set}) 20 | self.vbar.pack(side=tk.RIGHT, fill=tk.Y) 21 | self.vbar['command'] = self.yview 22 | 23 | tk.Text.__init__(self, self.frame, **kw) 24 | self.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 25 | 26 | text_meths = vars(tk.Text).keys() 27 | methods = vars(tk.Pack).keys() | vars(tk.Grid).keys() | vars(tk.Place).keys() 28 | methods = methods.difference(text_meths) 29 | 30 | for m in methods: 31 | if m[0] != '_' and m != 'config' and m != 'configure': 32 | setattr(self, m, getattr(self.frame, m)) 33 | 34 | def __str__(self): 35 | return str(self.frame) 36 | 37 | 38 | class HTMLScrolledText(_ScrolledText): 39 | # ---------------------------------------------------------------------------------------------- 40 | """ 41 | HTML scrolled text widget 42 | """ 43 | 44 | def __init__(self, *args, html=None, **kwargs): 45 | # ------------------------------------------------------------------------------------------ 46 | super().__init__(*args, **kwargs) 47 | self._w_init(kwargs) 48 | self.html_parser = html_parser.HTMLTextParser() 49 | if isinstance(html, str): 50 | self.set_html(html) 51 | 52 | def _w_init(self, kwargs): 53 | # ------------------------------------------------------------------------------------------ 54 | if not 'wrap' in kwargs.keys(): 55 | self.config(wrap='word') 56 | if not 'background' in kwargs.keys(): 57 | if sys.platform.startswith('win'): 58 | self.config(background='SystemWindow') 59 | else: 60 | self.config(background='white') 61 | 62 | def fit_height(self): 63 | # ------------------------------------------------------------------------------------------ 64 | """ 65 | Fit widget height to wrapped lines 66 | """ 67 | for h in range(1, 4): 68 | self.config(height=h) 69 | self.master.update() 70 | if self.yview()[1] >= 1: 71 | break 72 | else: 73 | self.config(height=0.5 + 3 / self.yview()[1]) 74 | 75 | def set_html(self, html, strip=True): 76 | # ------------------------------------------------------------------------------------------ 77 | """ 78 | Set HTML widget text. If strip is enabled (default) it ignores spaces and new lines. 79 | 80 | """ 81 | prev_state = self.cget('state') 82 | self.config(state=tk.NORMAL) 83 | self.delete('1.0', tk.END) 84 | self.tag_delete(self.tag_names) 85 | self.html_parser.w_set_html(self, html, strip=strip) 86 | self.config(state=prev_state) 87 | 88 | 89 | class HTMLText(HTMLScrolledText): 90 | # ---------------------------------------------------------------------------------------------- 91 | """ 92 | HTML text widget 93 | """ 94 | 95 | def _w_init(self, kwargs): 96 | # ------------------------------------------------------------------------------------------ 97 | super()._w_init(kwargs) 98 | self.vbar.pack_forget() 99 | 100 | def fit_height(self): 101 | # ------------------------------------------------------------------------------------------ 102 | super().fit_height() 103 | # self.master.update() 104 | self.vbar.pack_forget() 105 | 106 | 107 | class TkHtmlView_noscrollbar(HTMLText): 108 | # ---------------------------------------------------------------------------------------------- 109 | """ 110 | HTML label widget 111 | """ 112 | 113 | def _w_init(self, kwargs): 114 | # ------------------------------------------------------------------------------------------ 115 | super()._w_init(kwargs) 116 | if not 'background' in kwargs.keys(): 117 | if sys.platform.startswith('win'): 118 | self.config(background='SystemButtonFace') 119 | else: 120 | self.config(background='#d9d9d9') 121 | 122 | if not 'borderwidth' in kwargs.keys(): 123 | self.config(borderwidth=0) 124 | 125 | if not 'padx' in kwargs.keys(): 126 | self.config(padx=3) 127 | 128 | def set_html(self, *args, **kwargs): 129 | # ------------------------------------------------------------------------------------------ 130 | super().set_html(*args, **kwargs) 131 | self.config(state=tk.DISABLED) 132 | 133 | 134 | class TkHtmlView(HTMLScrolledText): 135 | # ---------------------------------------------------------------------------------------------- 136 | """ 137 | HTML label widget 138 | """ 139 | 140 | def _w_init(self, kwargs): 141 | # ------------------------------------------------------------------------------------------ 142 | super()._w_init(kwargs) 143 | if not 'background' in kwargs.keys(): 144 | if sys.platform.startswith('win'): 145 | self.config(background='SystemButtonFace') 146 | else: 147 | self.config(background='#d9d9d9') 148 | 149 | if not 'borderwidth' in kwargs.keys(): 150 | self.config(borderwidth=0) 151 | 152 | if not 'padx' in kwargs.keys(): 153 | self.config(padx=3) 154 | 155 | def set_html(self, *args, **kwargs): 156 | # ------------------------------------------------------------------------------------------ 157 | super().set_html(*args, **kwargs) 158 | self.config(state=tk.DISABLED) 159 | -------------------------------------------------------------------------------- /ttkbootstrap/widgets/date_entry.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from ttkbootstrap.dialogs.calendar import ask_date 4 | from datetime import datetime 5 | 6 | class DateEntry(ttk.Frame): 7 | 8 | def __init__( 9 | self, 10 | master=None, 11 | dateformat=r"%Y-%m-%d", 12 | firstweekday=6, 13 | startdate=None, 14 | bootstyle='', 15 | **kwargs 16 | ): 17 | """A date entry widget combines the `Combobox` and a `Button` 18 | with a callback attached to the `ask_date` function. 19 | 20 | When pressed, a date chooser popup is displayed. The returned 21 | value is inserted into the combobox. 22 | 23 | The date chooser popup will use the date in the combobox as the 24 | date of focus if it is in the format specified by the 25 | `dateformat` parameter. By default, this format is "%Y-%m-%d". 26 | 27 | The bootstyle api may be used to change the style of the widget. 28 | The available colors include -> primary, secondary, success, 29 | info, warning, danger, light, dark. 30 | 31 | The starting weekday on the date chooser popup can be changed 32 | with the `firstweekday` parameter. By default this value is 33 | `6`, which represents "Sunday". 34 | 35 | The `Entry` and `Button` widgets are accessible from the 36 | `DateEntry.Entry` and `DateEntry.Button` properties. 37 | 38 | Parameters 39 | ---------- 40 | master : Widget, optional 41 | The parent wiget. 42 | 43 | dateformat : str, optional 44 | The format string used to render the text in the entry 45 | widget. Default = "%Y-%m-%d. For more information on 46 | acceptable formats, see https://strftime.org/ 47 | 48 | firstweekday : int, optional 49 | Specifies the first day of the week. 0=Monday, 1=Tuesday, 50 | etc... Default = 6 (Sunday). 51 | 52 | startdate : datetime, optional 53 | The date that is in focus when the widget is displayed. By 54 | default, the current date. 55 | 56 | bootstyle : str 57 | A style keyword used to set the focus color of the entry 58 | and the background color of the date button. Available 59 | options include -> primary, secondary, success, info, 60 | warning, danger, dark, light. 61 | 62 | **kwargs : Dict[str, Any] 63 | Other keyword arguments passed to the frame containing the 64 | entry and date button. 65 | """ 66 | self._dateformat = dateformat 67 | self._firstweekday = firstweekday 68 | 69 | self._startdate = startdate or datetime.today() 70 | self._bootstyle = bootstyle 71 | super().__init__(master, **kwargs) 72 | 73 | # add visual components 74 | entry_kwargs = {'bootstyle': self._bootstyle} 75 | if 'width' in kwargs: 76 | entry_kwargs['width'] = kwargs.pop('width') 77 | 78 | self.entry = ttk.Entry(self, **entry_kwargs) 79 | self.entry.pack(side=tk.LEFT, fill=tk.X, expand=tk.YES) 80 | 81 | self.button = ttk.Button( 82 | master=self, 83 | command=self.on_date_ask, 84 | bootstyle=f'{self._bootstyle}-date' 85 | ) 86 | self.button.pack(side=tk.LEFT) 87 | 88 | # starting value 89 | self.entry.insert(tk.END, self._startdate.strftime(self._dateformat)) 90 | 91 | def __getitem__(self, key: str): 92 | return self.configure(cnf=key) 93 | 94 | def __setitem__(self, key: str, value): 95 | self.configure(cnf=None, **{key: value}) 96 | 97 | def _configure_set(self, **kwargs): 98 | """Override configure method to allow for setting custom 99 | DateEntry parameters""" 100 | 101 | if 'state' in kwargs: 102 | state = kwargs.pop('state') 103 | if state in ['readonly', 'invalid']: 104 | self.entry.configure(state=state) 105 | elif state == 'disabled': 106 | self.entry.configure(state=state) 107 | self.button.configure(state=state) 108 | else: 109 | kwargs[state] = state 110 | if 'dateformat' in kwargs: 111 | self._dateformat = kwargs.pop('dateformat') 112 | if 'firstweekday' in kwargs: 113 | self._firstweekday = kwargs.pop('firstweekday') 114 | if 'startdate' in kwargs: 115 | self._startdate = kwargs.pop('startdate') 116 | if 'bootstyle' in kwargs: 117 | self._bootstyle = kwargs.pop('bootstyle') 118 | self.entry.configure(bootstyle=self._bootstyle) 119 | self.button.configure(bootstyle=[self._bootstyle, 'date']) 120 | if 'width' in kwargs: 121 | width = kwargs.pop('width') 122 | self.entry.configure(width=width) 123 | 124 | super(ttk.Frame, self).configure(**kwargs) 125 | 126 | def _configure_get(self, cnf): 127 | """Override the configure get method""" 128 | if cnf == 'state': 129 | entrystate = self.entry.cget('state') 130 | buttonstate = self.button.cget('state') 131 | return {'Entry': entrystate, 'Button': buttonstate} 132 | if cnf == 'dateformat': 133 | return self._dateformat 134 | if cnf == 'firstweekday': 135 | return self._firstweekday 136 | if cnf == 'startdate': 137 | return self._startdate 138 | if cnf == 'bootstyle': 139 | return self._bootstyle 140 | else: 141 | return super(ttk.Frame, self).configure(cnf=cnf) 142 | 143 | def configure(self, cnf=None, **kwargs): 144 | if cnf is not None: 145 | return self._configure_get(cnf) 146 | else: 147 | return self._configure_set(**kwargs) 148 | 149 | def on_date_ask(self): 150 | """Callback for pushing the date button""" 151 | _val = self.entry.get() 152 | try: 153 | self._startdate = datetime.strptime(_val, self._dateformat) 154 | except Exception as e: 155 | print("Date entry text does not match", self._dateformat) 156 | self._startdate = datetime.today() 157 | self.entry.delete(first=0, last=tk.END) 158 | self.entry.insert(tk.END, self._startdate.strftime(self._dateformat)) 159 | 160 | old_date = datetime.strptime(_val or self._startdate, self._dateformat) 161 | 162 | # get the new date and insert into the entry 163 | new_date = ask_date( 164 | parent=self.entry, 165 | startdate=old_date, 166 | firstweekday=self._firstweekday, 167 | bootstyle=self._bootstyle 168 | ) 169 | self.entry.delete(first=0, last=tk.END) 170 | self.entry.insert(tk.END, new_date.strftime(self._dateformat)) 171 | self.entry.focus_force() 172 | -------------------------------------------------------------------------------- /ttkbootstrap/widgets/floodgauge.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter.ttk import Progressbar 3 | 4 | DETERMINATE = 'determinate' 5 | HORIZONTAL = tk.HORIZONTAL 6 | PRIMARY = 'primary' 7 | 8 | 9 | class Floodgauge(Progressbar): 10 | 11 | def __init__( 12 | self, 13 | master=None, 14 | cursor=None, 15 | font=None, 16 | length=None, 17 | maximum=100, 18 | mode=DETERMINATE, 19 | orient=HORIZONTAL, 20 | bootstyle=PRIMARY, 21 | takefocus=False, 22 | text=None, 23 | value=0, 24 | mask=None, 25 | **kwargs 26 | ): 27 | """A widget that shows the status of a long-running operation 28 | with an optional text indicator. 29 | 30 | Similar to the `ttk.Progressbar`, this widget can operate in 31 | two modes. *determinate* mode shows the amount completed 32 | relative to the total amount of work to be done, and 33 | *indeterminate* mode provides an animated display to let the 34 | user know that something is happening. 35 | 36 | Variable are generated automatically for this widget and can be 37 | linked to other widgets by referencing them via the 38 | `textvariable` and `variable` attributes. 39 | 40 | Parameters 41 | ---------- 42 | master : Widget 43 | Parent widget 44 | 45 | cursor : str 46 | The cursor that will appear when the mouse is over the 47 | progress bar. 48 | 49 | font Union[Font, str] 50 | The font to use for the progress bar label. 51 | 52 | length : int 53 | Specifies the length of the long axis of the progress bar 54 | (width if orient = horizontal, height if if vertical); 55 | Default = 300. 56 | 57 | maximum : float 58 | A floating point number specifying the maximum `value`. 59 | Default = 100. 60 | 61 | mode : { determinate, indeterminate } 62 | Use `indeterminate` if you cannot accurately measure the 63 | relative progress of the underlying process. In this mode, 64 | a rectangle bounces back and forth between the ends of the 65 | widget once you use the `Floodgauge.start()` method. 66 | Otherwise, use `determinate` if the relative progress can be 67 | calculated in advance. Default = 'determinate'. 68 | 69 | orient : { horizontal, vertical } 70 | Specifies the orientation of the widget. 71 | 72 | bootstyle : str 73 | The style used to render the widget. Options include 74 | primary, secondary, success, info, warning, danger, light, 75 | dark. 76 | 77 | takefocus : bool 78 | This widget is not included in focus traversal by default. 79 | To add the widget to focus traversal, use 80 | `takefocus=True`. 81 | 82 | text : str 83 | A string of text to be displayed in the Floodgauge label. 84 | This is assigned to the attribute `Floodgauge.textvariable` 85 | 86 | value : float 87 | The current value of the progressbar. In `determinate` 88 | mode, this represents the amount of work completed. In 89 | `indeterminate` mode, it is interpreted modulo `maximum`; 90 | that is, the progress bar completes one "cycle" when the 91 | `value` increases by `maximum`. 92 | 93 | mask : str 94 | A string format that can be used to update the Floodgauge 95 | label every time the value is updated. For example, the 96 | string "{}% Storage Used" with a widget value of 45 would 97 | show "45% Storage Used" on the Floodgauge label. If a 98 | mask is set, then the `text` option is ignored. 99 | 100 | **kwargs : Dict[str, Any] 101 | Other configuration options from the option database. 102 | """ 103 | # progress bar value variable 104 | self.variable = tk.IntVar(value=value) 105 | self.textvariable = tk.StringVar(value=text) 106 | self._bootstyle = bootstyle 107 | self._font = font or 'helvetica 10' 108 | self._mask = mask 109 | self._traceid = None 110 | 111 | super().__init__( 112 | master=master, 113 | class_='Floodgauge', 114 | cursor=cursor, 115 | length=length, 116 | maximum=maximum, 117 | mode=mode, 118 | orient=orient, 119 | bootstyle=bootstyle, 120 | takefocus=takefocus, 121 | variable=self.variable, 122 | **kwargs 123 | ) 124 | self._set_widget_text(self.textvariable.get()) 125 | self.bind('<>', self._on_theme_change) 126 | self.bind('<>', self._on_theme_change) 127 | 128 | if self._mask is not None: 129 | self._set_mask() 130 | 131 | def _set_widget_text(self, *_): 132 | ttkstyle = self.cget('style') 133 | if self._mask is None: 134 | text = self.textvariable.get() 135 | else: 136 | value = self.variable.get() 137 | text = self._mask.format(value) 138 | self.tk.call("ttk::style", "configure", ttkstyle, '-text', text) 139 | self.tk.call("ttk::style", "configure", ttkstyle, '-font', self._font) 140 | 141 | def _set_mask(self): 142 | if self._traceid is None: 143 | self._traceid = self.variable.trace_add( 144 | 'write', self._set_widget_text) 145 | 146 | def _unset_mask(self): 147 | if self._traceid is not None: 148 | self.variable.trace_remove('write', self._traceid) 149 | self._traceid = None 150 | 151 | def _on_theme_change(self, *_): 152 | text = self.textvariable.get() 153 | self._set_widget_text(text) 154 | 155 | def _configure_get(self, cnf): 156 | if cnf == 'value': 157 | return self.variable.get() 158 | if cnf == 'text': 159 | return self.textvariable.get() 160 | if cnf == 'bootstyle': 161 | return self._bootstyle 162 | if cnf == 'mask': 163 | return self._mask 164 | if cnf == 'font': 165 | return self._font 166 | else: 167 | return super(Progressbar, self).configure(cnf=cnf) 168 | 169 | def _configure_set(self, **kwargs): 170 | if 'value' in kwargs: 171 | self.variable.set(kwargs.pop('value')) 172 | if 'text' in kwargs: 173 | self.textvariable.set(kwargs.pop('text')) 174 | if 'bootstyle' in kwargs: 175 | self._bootstyle = kwargs.get('bootstyle') 176 | if 'mask' in kwargs: 177 | self._mask = kwargs.pop('mask') 178 | if 'font' in kwargs: 179 | self._font = kwargs.pop('font') 180 | else: 181 | super(Progressbar, self).configure(cnf=None, **kwargs) 182 | 183 | def __getitem__(self, key: str): 184 | return self._configure_get(cnf=key) 185 | 186 | def __setitem__(self, key: str, value): 187 | self._configure_set(**{key: value}) 188 | 189 | def configure(self, cnf=None, **kwargs): 190 | if cnf is not None: 191 | return self._configure_get(cnf) 192 | else: 193 | self._configure_set(**kwargs) 194 | -------------------------------------------------------------------------------- /ttkbootstrap/style/style.py: -------------------------------------------------------------------------------- 1 | from tkinter import TclError, ttk 2 | from typing import Callable 3 | from ttkbootstrap.constants import * 4 | from ttkbootstrap.themes.standard import STANDARD_THEMES 5 | from ttkbootstrap.themes.user import USER_THEMES 6 | from ttkbootstrap.style.style_builder import StyleBuilderTTK, ThemeDefinition 7 | from ttkbootstrap.style.publisher import Publisher, Channel 8 | from ttkbootstrap.style import utility as util 9 | 10 | 11 | class StyleManager(ttk.Style): 12 | """A class for setting the application style. 13 | 14 | Sets the theme of the `tkinter.Tk` instance and supports all 15 | ttkbootstrap and ttk themes provided. This class is meant to be a 16 | drop-in replacement for `ttk.Style` and inherits all of it's 17 | methods and properties.For more details on the `ttk.Style` class, 18 | see the python documentation. 19 | """ 20 | instance = None 21 | 22 | def __init__(self, theme=DEFAULT_THEME, **kwargs): 23 | """ 24 | Parameters 25 | ---------- 26 | theme : str 27 | The name of the theme to use at runtime; default="flatly" 28 | 29 | highdpi : bool 30 | Enables high dpi awareness for Windows. 31 | 32 | scaling : float 33 | Controls the pixel density used to draw the widgets. The 34 | default scaling of 1.0 is 72ppi. 35 | """ 36 | self._theme_objects = {} 37 | self._theme_definitions = {} 38 | self._style_registry = set() # all styles used 39 | self._theme_styles = {} # styles used in theme 40 | self._theme_names = set() 41 | self._load_themes() 42 | super().__init__(**kwargs) 43 | 44 | StyleManager.instance = self 45 | self.theme_use(theme) 46 | 47 | @staticmethod 48 | def get_builder(): 49 | style: StyleManager = Style() 50 | theme_name = style.theme.name 51 | return style._theme_objects[theme_name] 52 | 53 | @staticmethod 54 | def get_builder_tk(): 55 | builder = StyleManager.get_builder() 56 | return builder.builder_tk 57 | 58 | @property 59 | def colors(self): 60 | """The theme colors""" 61 | theme = self.theme.name 62 | if theme in list(self._theme_names): 63 | definition = self._theme_definitions.get(theme) 64 | if not definition: 65 | return [] #TODO refactor this 66 | else: 67 | return definition.colors 68 | else: 69 | return [] # TODO refactor this 70 | 71 | def _load_themes(self): 72 | """Load all ttkbootstrap defined themes""" 73 | # create a theme definition object for each theme, this will be 74 | # used to generate the theme in tkinter along with any assets 75 | # at run-time 76 | if USER_THEMES: 77 | STANDARD_THEMES.update(USER_THEMES) 78 | theme_settings = {"themes": STANDARD_THEMES} 79 | for name, definition in theme_settings["themes"].items(): 80 | self.register_theme( 81 | ThemeDefinition( 82 | name=name, 83 | themetype=definition["type"], 84 | colors=definition["colors"], 85 | ) 86 | ) 87 | 88 | def theme_names(self): 89 | """Return a list of all ttkbootstrap themes""" 90 | return list(self._theme_definitions.keys()) 91 | 92 | def register_ttkstyle(self, ttkstyle): 93 | """Register that a style has been created""" 94 | self._style_registry.add(ttkstyle) 95 | theme = self.theme.name 96 | self._theme_styles[theme].add(ttkstyle) 97 | 98 | def register_theme(self, definition): 99 | """Registers a theme definition for use by the ``Style`` class. 100 | 101 | This makes the definition and name available at run-time so 102 | that the assets and styles can be created. 103 | 104 | Parameters 105 | ---------- 106 | definition : ThemeDefinition 107 | An instance of the ``ThemeDefinition`` class 108 | """ 109 | theme = definition.name 110 | self._theme_names.add(theme) 111 | self._theme_definitions[theme] = definition 112 | self._theme_styles[theme] = set() 113 | 114 | def theme_use(self, themename=None): 115 | """Changes the theme used in rendering the application widgets. 116 | 117 | If themename is None, returns the theme in use, otherwise, set 118 | the current theme to themename, refreshes all widgets and emits 119 | a ``<>`` event. 120 | 121 | Only use this method if you are changing the theme *during* 122 | runtime. Otherwise, pass the theme name into the Style 123 | constructor to instantiate the style with a theme. 124 | 125 | Parameters 126 | ---------- 127 | themename : str 128 | The name of the theme to apply when creating new widgets 129 | """ 130 | if not themename: 131 | # return current theme 132 | return super().theme_use() 133 | 134 | # change to an existing theme 135 | existing_themes = super().theme_names() 136 | if themename in existing_themes: 137 | self.theme = self._theme_definitions.get(themename) 138 | super().theme_use(themename) 139 | self.create_ttk_styles_on_theme_change() 140 | Publisher.publish_message(Channel.STD) 141 | # setup a new theme 142 | elif themename in self._theme_names: 143 | self.theme = self._theme_definitions.get(themename) 144 | self._theme_objects[themename] = StyleBuilderTTK(self) 145 | self.create_ttk_styles_on_theme_change() 146 | Publisher.publish_message(Channel.STD) 147 | else: 148 | raise TclError(themename, "is not a valid theme.") 149 | 150 | 151 | def exists(self, ttkstyle: str): 152 | """Return True if style exists else False""" 153 | theme_styles = self._theme_styles.get(self.theme.name) 154 | exists_in_theme = ttkstyle in theme_styles 155 | exists_in_registry = ttkstyle in self._style_registry 156 | return exists_in_theme and exists_in_registry 157 | 158 | def create_ttk_styles_on_theme_change(self): 159 | """Create existing styles when the theme changes""" 160 | for ttkstyle in self._style_registry: 161 | if not self.exists(ttkstyle): 162 | color = util.ttkstyle_widget_color(ttkstyle) 163 | method_name = util.ttkstyle_method_name(string=ttkstyle) 164 | builder: StyleBuilderTTK = self.get_builder() 165 | method: Callable = builder.name_to_method(method_name) 166 | method(builder, color) 167 | 168 | def set_window_scaling(self, scaling): 169 | """Scale widget dpi""" 170 | self.master.tk.call('tk', 'scaling', scaling) 171 | 172 | def get_window_scaling(self): 173 | return self.master.tk.call('tk', 'scaling') 174 | 175 | def enable_high_dpi_awareness(self): 176 | """Enable high dpi awareness. Currently for Windows Only.""" 177 | root = self.master 178 | if root._windowingsystem == 'win32': 179 | from ctypes import windll 180 | windll.user32.SetProcessDPIAware() 181 | 182 | def Style(theme=DEFAULT_THEME, **kwargs): 183 | """Returns a singleton instance of the `BootStyle` class. 184 | 185 | Parameters 186 | ---------- 187 | theme : str 188 | The name of the theme. 189 | 190 | Examples 191 | -------- 192 | Return an instance of the BootStyle class 193 | >>> style = Style() 194 | 195 | Return instance with defined theme 196 | >>> style = Style(theme='superhero') 197 | """ 198 | if StyleManager.instance is None: 199 | StyleManager(theme, **kwargs) 200 | 201 | elif theme != DEFAULT_THEME: 202 | StyleManager.instance.theme_use(theme) 203 | 204 | return StyleManager.instance -------------------------------------------------------------------------------- /ttkbootstrap/style/bootstyle.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | import ttkbootstrap.style.utility as util 4 | from ttkbootstrap.style.style import Style 5 | from ttkbootstrap.style.style_builder import StyleBuilderTK, StyleBuilderTTK 6 | from ttkbootstrap.style.publisher import Publisher, Channel 7 | 8 | TTK_WIDGETS = ( 9 | ttk.Button, 10 | ttk.Checkbutton, 11 | ttk.Combobox, 12 | ttk.Entry, 13 | ttk.Frame, 14 | ttk.Labelframe, 15 | ttk.Label, 16 | ttk.Menubutton, 17 | ttk.Notebook, 18 | ttk.Panedwindow, 19 | ttk.Progressbar, 20 | ttk.Radiobutton, 21 | ttk.Scale, 22 | ttk.Scrollbar, 23 | ttk.Separator, 24 | ttk.Sizegrip, 25 | ttk.Spinbox, 26 | ttk.Treeview, 27 | ttk.OptionMenu, 28 | ) 29 | 30 | TK_WIDGETS = ( 31 | tk.Tk, 32 | tk.Toplevel, 33 | tk.Button, 34 | tk.Label, 35 | tk.Text, 36 | tk.Frame, 37 | tk.Checkbutton, 38 | tk.Radiobutton, 39 | tk.Entry, 40 | tk.Scale, 41 | tk.Spinbox, 42 | tk.Listbox, 43 | tk.Menu, 44 | tk.Menubutton, 45 | tk.LabelFrame, 46 | tk.Canvas 47 | ) 48 | 49 | 50 | def override_ttk_widget_constructor(func): 51 | """Override widget constructors with bootstyle api options""" 52 | 53 | def __init__(self, *args, **kwargs): 54 | 55 | # capture bootstyle and style arguments 56 | if 'bootstyle' in kwargs: 57 | bootstyle = kwargs.pop('bootstyle') 58 | else: 59 | bootstyle = '' 60 | 61 | if 'style' in kwargs: 62 | style = kwargs.pop('style') or '' 63 | else: 64 | style = '' 65 | 66 | # instantiate the widget 67 | func(self, *args, **kwargs) 68 | 69 | # must be called AFTER instantiation in order to use winfo_class 70 | # in the `get_ttkstyle_name` method 71 | 72 | if style: 73 | ttkstyle = update_ttk_widget_style(self, style, **kwargs) 74 | self.configure(style=ttkstyle) 75 | elif bootstyle: 76 | ttkstyle = update_ttk_widget_style(self, bootstyle, **kwargs) 77 | self.configure(style=ttkstyle) 78 | else: 79 | ttkstyle = update_ttk_widget_style(self, 'default', **kwargs) 80 | self.configure(style=ttkstyle) 81 | 82 | return __init__ 83 | 84 | 85 | def override_ttk_widget_configure(func): 86 | 87 | def configure(self, cnf=None, **kwargs): 88 | # get configuration 89 | if cnf == 'bootstyle': 90 | return self.cget('style') 91 | elif cnf is not None: 92 | return self.cget(cnf) 93 | 94 | # set configuration 95 | if 'bootstyle' in kwargs: 96 | bootstyle = kwargs.pop('bootstyle') 97 | else: 98 | bootstyle = '' 99 | 100 | if 'style' in kwargs: 101 | style = kwargs.get('style') 102 | ttkstyle = update_ttk_widget_style(self, style, **kwargs) 103 | elif bootstyle: 104 | ttkstyle = update_ttk_widget_style(self, bootstyle, **kwargs) 105 | kwargs.update(style=ttkstyle) 106 | 107 | # update widget configuration 108 | func(self, **kwargs) 109 | 110 | return configure 111 | 112 | 113 | def update_ttk_widget_style(widget: ttk.Widget, style_string: str=None, **kwargs): 114 | """Update the ttk style or create if not existing. 115 | 116 | Parameters 117 | ---------- 118 | widget: ttk.Widget 119 | The widget instance being updated. 120 | 121 | style_string : str 122 | The style string to evalulate. May be the `style`, `ttkstyle` 123 | or `bootstyle` argument depending on the context and scenario. 124 | 125 | **kwargs: Dict[str, Any] 126 | Other keyword arguments. 127 | 128 | Returns 129 | ------- 130 | ttkstyle : str 131 | The ttkstyle or empty string if there is none. 132 | """ 133 | style = Style() 134 | 135 | # get existing widget style if not provided 136 | if style_string is None: 137 | style_string = widget.cget('style') 138 | 139 | # do nothing if the style has not been set 140 | if not style_string: 141 | return '' 142 | 143 | # build style if not existing (example: theme changed) 144 | ttkstyle = util.ttkstyle_name(widget, style_string, **kwargs) 145 | if not style.exists(ttkstyle): 146 | widget_color = util.ttkstyle_widget_color(ttkstyle) 147 | method_name = util.ttkstyle_method_name(widget, ttkstyle) 148 | builder: StyleBuilderTTK = style.get_builder() 149 | builder_method = builder.name_to_method(method_name) 150 | builder_method(builder, widget_color) 151 | 152 | # subscribe popdown style to theme changes 153 | if widget.winfo_class() == 'TCombobox': 154 | builder: StyleBuilderTTK = style.get_builder() 155 | Publisher.subscribe( 156 | name=widget._name, 157 | func=lambda w=widget: builder.update_combobox_popdown_style(w), 158 | channel=Channel.STD 159 | ) 160 | builder.update_combobox_popdown_style(widget) 161 | 162 | return ttkstyle 163 | 164 | 165 | def setup_ttkbootstap_api(): 166 | """Setup ttkbootstrap for use with tkinter and ttk""" 167 | 168 | # TTK WIDGETS 169 | for widget in TTK_WIDGETS: 170 | # override widget constructor 171 | _init = override_ttk_widget_constructor(widget.__init__) 172 | widget.__init__ = _init 173 | 174 | # override configure method 175 | _configure = override_ttk_widget_configure(widget.configure) 176 | widget.configure = _configure 177 | 178 | # override get and set methods 179 | def __setitem(self, key, val): return _configure(self, **{key: val}) 180 | def __getitem(self, key): return _configure(self, cnf=key) 181 | if widget.__name__ != 'OptionMenu': # this has it's own override 182 | widget.__setitem__ = __setitem 183 | widget.__getitem__ = __getitem 184 | 185 | # TK WIDGETS 186 | for widget in TK_WIDGETS: 187 | 188 | # override widget constructor 189 | _init = override_tk_widget_constructor(widget.__init__) 190 | widget.__init__ = _init 191 | 192 | # override widget destroy method (quit for tk.Tk) 193 | widget.destroy = override_widget_destroy_method 194 | 195 | def update_tk_widget_style(widget): 196 | """Lookup the widget name and call the appropriate update 197 | method 198 | 199 | Parameters 200 | ---------- 201 | widget : object 202 | The tcl/tk name given by `tk.Widget.winfo_name()` 203 | """ 204 | try: 205 | style = Style() 206 | method_name = util.tkupdate_method_name(widget) 207 | builder = style.get_builder_tk() 208 | builder_method = getattr(StyleBuilderTK, method_name) 209 | builder_method(builder, widget) 210 | except: 211 | """Must pass here to prevent a failure when the user calls 212 | the `Style`method BEFORE an instance of `Tk` is instantiated. 213 | This will defer the update of the `Tk` background until the end 214 | of the `BootStyle` object instantiation (created by the `Style` 215 | method)""" 216 | pass 217 | 218 | 219 | def override_tk_widget_constructor(func): 220 | """Override widget constructors to apply default style for tk 221 | widgets 222 | """ 223 | 224 | def __init__wrapper(self, *args, **kwargs): 225 | 226 | # instantiate the widget 227 | func(self, *args, **kwargs) 228 | 229 | Publisher.subscribe( 230 | name=str(self), 231 | func=lambda w=self: update_tk_widget_style(w), 232 | channel=Channel.STD 233 | ) 234 | update_tk_widget_style(self) 235 | 236 | return __init__wrapper 237 | 238 | 239 | def override_widget_destroy_method(self): 240 | """Unsubscribe widget from publication and destroy""" 241 | if isinstance(self, tk.Widget): 242 | Publisher.unsubscribe(self._name) 243 | super(tk.Widget, self).destroy() 244 | elif isinstance(self, tk.Tk): 245 | Publisher.clear_subscribers() 246 | super(tk.Tk, self).quit() 247 | elif isinstance(self, tk.Toplevel): 248 | Publisher.clear_subscribers() 249 | super(tk.Toplevel, self).destroy() 250 | -------------------------------------------------------------------------------- /CustomWindow.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """ 3 | @Project :CountBoard 4 | @File :CustomWindow.py 5 | @Author :Gao yongxian 6 | @Date :2021/11/8 11:00 7 | @contact: g1695698547@163.com 8 | """ 9 | import os 10 | import sys 11 | import traceback 12 | from win32api import GetMonitorInfo, MonitorFromPoint 13 | 14 | 15 | class CustomWindow(): 16 | """ 17 | 自定义模板(用来窗口初始化): self.root要在继承之前使用,tk.Toplevel() 或 tk.TK() 18 | """ 19 | 20 | def __init__(self, *args, **kwargs): 21 | """初始化""" 22 | super().__init__() 23 | 24 | # 布局初始化 25 | self.__init2__(**kwargs) 26 | 27 | def __init2__(self, **kwargs): 28 | """布局初始化""" 29 | # 传参,变量初始化 30 | try: 31 | # 先隐藏窗口,加载完成再显示窗口 32 | self.root.withdraw() 33 | except: 34 | print("self.root要在继承之前使用,tk.Toplevel() 或 tk.TK()") 35 | try: 36 | self.overrideredirect = kwargs["overrideredirect"] 37 | except: 38 | self.overrideredirect = 0 39 | try: 40 | self.width = kwargs["width"] 41 | except: 42 | self.width = 300 43 | try: 44 | self.height = kwargs["height"] 45 | except: 46 | self.height = 300 47 | try: 48 | self.title = kwargs["title"] 49 | except: 50 | self.title = "无标题" 51 | try: 52 | self.position = kwargs["position"] 53 | except: 54 | self.position = "center" 55 | try: 56 | self.icon = kwargs["icon"] 57 | except: 58 | self.icon = os.path.join(os.path.dirname(sys.argv[0]),"favicon.ico") 59 | try: 60 | self.topmost = kwargs["topmost"] 61 | except: 62 | self.topmost = 1 63 | try: 64 | self.alpha = kwargs["alpha"] 65 | except: 66 | self.alpha = 1 67 | try: 68 | self.pre_window = kwargs["pre_window"] 69 | except: 70 | self.pre_window = None 71 | try: 72 | self._geometry = kwargs["_geometry"] 73 | except: 74 | self._geometry = "(300x300+300+300)" 75 | try: 76 | self._auto_margin = kwargs["_auto_margin"] 77 | except: 78 | self._auto_margin = False 79 | try: 80 | self.exe_dir_path = kwargs["exe_dir_path"] 81 | except: 82 | self.exe_dir_path = os.path.dirname(sys.argv[0]) 83 | try: 84 | self.offset = kwargs["offset"] 85 | except: 86 | self.offset = 0 87 | try: 88 | self.show = kwargs["show"] 89 | except: 90 | self.show = 1 91 | 92 | # 窗体基本设置 93 | self.root.title(self.title) 94 | self.root.iconbitmap(self.icon) 95 | self.root.wm_attributes('-alpha', self.alpha) 96 | self.root.wm_attributes('-topmost', self.topmost) 97 | # 无边框窗体:此项只有最后设置才可以,否则无法起效 98 | self.root.overrideredirect(self.overrideredirect) 99 | 100 | # 使用win32获取工作区宽高 101 | self.work_width = GetMonitorInfo(MonitorFromPoint((0, 0))).get('Work')[2] 102 | self.work_heigh = GetMonitorInfo(MonitorFromPoint((0, 0))).get('Work')[3] 103 | 104 | # 屏幕宽高 105 | self.win_width = self.root.winfo_screenwidth() 106 | self.win_heigth = self.root.winfo_screenheight() 107 | 108 | # 设置窗体位置 109 | if self.position == "custom": 110 | self.width = self._geometry[0] 111 | self.height = self._geometry[1] 112 | self.set_position(self.position, self.pre_window, self._geometry) 113 | 114 | # 设置贴边 115 | self.set_auto_margin(self._auto_margin) 116 | 117 | # 先隐藏窗体,加载完成,最后再显示窗体 118 | if self.show: 119 | self.root.deiconify() 120 | 121 | def modify_offset(self, offset): 122 | """外部调用,修改贴边距离""" 123 | self.offset = offset 124 | 125 | def modify_auto_margin(self, _auto_margin): 126 | """外部调用,修改是否贴边""" 127 | self._auto_margin = _auto_margin 128 | 129 | def set_position(self, position="center", pre_window=None, _geometry: str = ""): 130 | """ 131 | 设置窗体的位置,可以在外部调用 132 | 133 | Args: 134 | position: 窗体位置 135 | pre_window: 上一个窗体对象 136 | _geometry: 自定义位置 137 | """ 138 | if position == "center": 139 | # 中心位置,要传入宽高 140 | width_adjust = (self.work_width - self.width) / 2 141 | higth_adjust = (self.work_heigh - self.height) / 2 142 | self.root.geometry("%dx%d+%d+%d" % (self.width, self.height, width_adjust, higth_adjust)) 143 | elif position == "follow": 144 | # 跟随位置,要传入高 145 | # 跟随上一个窗体,在上一个窗体正下方,且宽度一致 146 | self.pre_window_root = pre_window.root 147 | self.width = pre_window.width 148 | self.pre_window_root_x, self.pre_window_root_y, self.pre_window_root_w, self.pre_window_root_h = \ 149 | self.pre_window_root.winfo_x(), \ 150 | self.pre_window_root.winfo_y(), \ 151 | self.pre_window_root.winfo_width(), \ 152 | self.pre_window_root.winfo_height() 153 | self.width_adjust = self.pre_window_root_x 154 | self.height_adjust = self.pre_window_root_y + self.pre_window_root_h + 40 155 | self.root.geometry("%dx%d+%d+%d" % (self.width, self.height, self.width_adjust, self.height_adjust)) 156 | elif position == "custom": 157 | # 自定义位置,要传入_geometry 158 | self.root.geometry("%dx%d+%d+%d" % _geometry) 159 | 160 | def exit(self): 161 | """退出窗体""" 162 | # 退出消息循环 163 | self.root.quit() 164 | # 销毁窗口 165 | self.root.destroy() 166 | 167 | def set_auto_margin(self, flag): 168 | """设置自动贴边:有边框的窗体会有像素偏差,无边框正常""" 169 | self.root_x, self.root_y, self.abs_x, self.abs_y = 0, 0, 0, 0 170 | 171 | if flag == True: 172 | self.root.bind('', self._on_move) 173 | self.root.bind('', self._on_tap) 174 | self.root.bind('', self._on_release) 175 | else: 176 | self.root.unbind('') 177 | self.root.unbind('') 178 | self.root.unbind('') 179 | 180 | def _on_move(self, event): 181 | """移动""" 182 | offset_x = event.x_root - self.root_x 183 | offset_y = event.y_root - self.root_y 184 | 185 | x_adjust = self.abs_x + offset_x 186 | y_adjust = self.abs_y + offset_y 187 | 188 | geo_str = "%dx%d+%d+%d" % (self.width, self.height, 189 | x_adjust, y_adjust) 190 | self.root.geometry(geo_str) 191 | 192 | def _on_tap(self, event): 193 | """鼠标左键按下:更新窗口信息--1.鼠标位置,2.窗口大小""" 194 | self.root_x, self.root_y = event.x_root, event.y_root 195 | self.abs_x, self.abs_y = self.root.winfo_x(), self.root.winfo_y() 196 | self.width = self.root.winfo_width() 197 | self.height = self.root.winfo_height() 198 | 199 | def _on_release(self, event, **kwargs): 200 | """鼠标左键弹起""" 201 | offset_x = event.x_root - self.root_x 202 | offset_y = event.y_root - self.root_y 203 | 204 | if self._auto_margin: 205 | if self.width + self.abs_x + offset_x > self.work_width: 206 | x_adjust = self.work_width - self.width - self.offset 207 | elif self.abs_x + offset_x < 0: 208 | x_adjust = 0 + self.offset 209 | else: 210 | x_adjust = self.abs_x + offset_x 211 | 212 | if self.height + self.abs_y + offset_y > self.work_heigh: 213 | y_adjust = self.work_heigh - self.height - self.offset 214 | elif self.abs_y + offset_y < 0: 215 | y_adjust = 0 + self.offset 216 | else: 217 | y_adjust = self.abs_y + offset_y 218 | else: 219 | y_adjust = self.abs_y + offset_y 220 | x_adjust = self.abs_x + offset_x 221 | 222 | geo_str = "%dx%d+%d+%d" % (self.width, self.height, x_adjust, y_adjust) 223 | 224 | try: 225 | mysetting_dict = kwargs["mysetting_dict"] 226 | mysetting_dict["tile_geometry"] = [(self.width, self.height, x_adjust, y_adjust)] 227 | print("写入数据库成功") 228 | 229 | except: 230 | print("写入数据库失败") 231 | print(traceback.format_exc()) 232 | 233 | self.root.geometry(geo_str) 234 | -------------------------------------------------------------------------------- /ttkbootstrap/style/utility.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | COLORS = [ 4 | 'primary', 5 | 'secondary', 6 | 'success', 7 | 'info', 8 | 'warning', 9 | 'danger', 10 | 'light', 11 | 'dark' 12 | ] 13 | 14 | ORIENTS = [ 15 | 'horizontal', 16 | 'vertical' 17 | ] 18 | 19 | TYPES = [ 20 | 'outline', 21 | 'link', 22 | 'inverse', 23 | 'round', 24 | 'square', 25 | 'striped', 26 | 'focus', 27 | 'input', 28 | 'date', 29 | 'metersubtxt', 30 | 'meter', 31 | ] 32 | 33 | CLASSES = [ 34 | 'button', 35 | 'progressbar', 36 | 'checkbutton', 37 | 'combobox', 38 | 'entry', 39 | 'labelframe', 40 | 'label', 41 | 'frame', 42 | 'floodgauge', 43 | 'sizegrip', 44 | 'optionmenu', 45 | 'menubutton', 46 | 'menu', 47 | 'notebook', 48 | 'panedwindow', 49 | 'radiobutton', 50 | 'separator', 51 | 'scrollbar', 52 | 'spinbox', 53 | 'scale', 54 | 'text', 55 | 'toolbutton', 56 | 'treeview', 57 | 'toggle', 58 | 'tk', 59 | 'calendar', 60 | 'listbox', 61 | 'canvas', 62 | 'toplevel' 63 | ] 64 | 65 | # regex patterns 66 | COLOR_PATTERN = re.compile('|'.join(COLORS)) 67 | ORIENT_PATTERN = re.compile('|'.join(ORIENTS)) 68 | CLASS_PATTERN = re.compile('|'.join(CLASSES)) 69 | TYPE_PATTERN = re.compile('|'.join(TYPES)) 70 | 71 | # ttkstyle namebuilder helper methods 72 | 73 | def ttkstyle_widget_class(widget=None, string=''): 74 | """Find and return the widget class""" 75 | # find widget class from string pattern 76 | match = re.search(CLASS_PATTERN, string.lower()) 77 | if match is not None: 78 | widget_class = match.group(0) 79 | return widget_class 80 | 81 | # find widget class from tkinter/tcl method 82 | if widget is None: 83 | return '' 84 | _class = widget.winfo_class() 85 | match = re.search(CLASS_PATTERN, _class.lower()) 86 | if match is not None: 87 | widget_class = match.group(0) 88 | return widget_class 89 | else: 90 | return '' 91 | 92 | def ttkstyle_widget_type(string): 93 | """Find and return the widget type""" 94 | match = re.search(TYPE_PATTERN, string.lower()) 95 | if match is None: 96 | return '' 97 | else: 98 | widget_type = match.group(0) 99 | return widget_type 100 | 101 | def ttkstyle_widget_orient(widget=None, string='', **kwargs): 102 | """Find and return widget orient, or default orient for widget if 103 | a widget with orientation. 104 | """ 105 | # string method (priority) 106 | match = re.search(ORIENT_PATTERN, string) 107 | widget_orient = '' 108 | 109 | if match is not None: 110 | widget_orient = match.group(0) 111 | return widget_orient 112 | 113 | # orient from kwargs 114 | if 'orient' in kwargs: 115 | _orient = kwargs.pop('orient') 116 | if _orient == 'h': 117 | widget_orient = 'horizontal' 118 | elif _orient == 'v': 119 | widget_orient = 'vertical' 120 | else: 121 | widget_orient = _orient 122 | return widget_orient 123 | 124 | # orient from settings 125 | if widget is None: 126 | return widget_orient 127 | try: 128 | widget_orient = str(widget.cget('orient')) 129 | except: 130 | pass 131 | 132 | return widget_orient 133 | 134 | def ttkstyle_widget_color(string): 135 | """Find and return widget color""" 136 | _color = re.search(COLOR_PATTERN, string.lower()) 137 | if _color is None: 138 | return '' 139 | else: 140 | widget_color = _color.group(0) 141 | return widget_color 142 | 143 | def ttkstyle_name(widget=None, string='', **kwargs): 144 | """Parse a string to build and return a ttkstyle name.""" 145 | style_string = ''.join(string).lower() 146 | widget_color = ttkstyle_widget_color(style_string) 147 | widget_type = ttkstyle_widget_type(style_string) 148 | widget_orient = ttkstyle_widget_orient(widget, style_string, **kwargs) 149 | widget_class = ttkstyle_widget_class(widget, style_string) 150 | 151 | if widget_color: 152 | widget_color = f'{widget_color}.' 153 | 154 | if widget_type: 155 | widget_type = f'{widget_type.title()}.' 156 | 157 | if widget_orient: 158 | widget_orient = f'{widget_orient.title()}.' 159 | 160 | if widget_class.startswith('t'): 161 | widget_class = widget_class.title() 162 | else: 163 | widget_class = f'T{widget_class.title()}' 164 | 165 | ttkstyle = f'{widget_color}{widget_type}{widget_orient}{widget_class}' 166 | return ttkstyle 167 | 168 | def ttkstyle_method_name(widget=None, string=''): 169 | """Parse a string to build and return the name of the 170 | `StyleBuilderTTK` method that creates the ttk style for the target 171 | widget. 172 | """ 173 | style_string = ''.join(string).lower() 174 | widget_type = ttkstyle_widget_type(style_string) 175 | widget_class = ttkstyle_widget_class(widget, style_string) 176 | 177 | if widget_type: 178 | widget_type = f'_{widget_type}' 179 | 180 | if widget_class: 181 | widget_class = f'_{widget_class}' 182 | 183 | if not widget_type and not widget_class: 184 | return '' 185 | else: 186 | method_name = f'create{widget_type}{widget_class}_style' 187 | return method_name 188 | 189 | def tkupdate_method_name(widget): 190 | """Lookup the tkinter style update method from the widget class""" 191 | widget_class = ttkstyle_widget_class(widget) 192 | 193 | if widget_class: 194 | widget_class = f'_{widget_class}' 195 | 196 | method_name = f'update{widget_class}_style' 197 | return method_name 198 | 199 | def enable_high_dpi_awareness(**kwargs): 200 | """Enable high dpi awareness. 201 | 202 | WINDOWS 203 | ------- 204 | Call the method BEFORE creating the `Tk` object. No parameters 205 | required. 206 | 207 | LINUX 208 | ----- 209 | Call the method AFTER creating the `Tk` object. `root` and 210 | `scaling` are required keyword arguments (described below). 211 | A number between 1.6 and 2.0 is usually suffient to scale 212 | for high-dpi screen. 213 | 214 | Other Parameters 215 | ---------------- 216 | root : Tk 217 | The root widget 218 | 219 | scaling : float 220 | Sets and queries the current scaling factor used by Tk to 221 | convert between physical units (for example, points, inches, or 222 | millimeters) and pixels. The number argument is a floating 223 | point number that specifies the number of pixels per point on 224 | window's display. If the window argument is omitted, it defaults 225 | to the main window. If the number argument is omitted, the 226 | current value of the scaling factor is returned. 227 | 228 | A “point” is a unit of measurement equal to 1/72 inch. A scaling 229 | factor of 1.0 corresponds to 1 pixel per point, which is 230 | equivalent to a standard 72 dpi monitor. A scaling factor of 231 | 1.25 would mean 1.25 pixels per point, which is the setting for 232 | a 90 dpi monitor; setting the scaling factor to 1.25 on a 72 dpi 233 | monitor would cause everything in the application to be displayed 234 | 1.25 times as large as normal. The initial value for the scaling 235 | factor is set when the application starts, based on properties 236 | of the installed monitor, but it can be changed at any time. 237 | Measurements made after the scaling factor is changed will use 238 | the new scaling factor, but it is undefined whether existing 239 | widgets will resize themselves dynamically to accommodate the 240 | new scaling factor. 241 | 242 | """ 243 | try: 244 | from ctypes import windll 245 | windll.user32.SetProcessDPIAware() 246 | except: 247 | pass 248 | 249 | try: 250 | root = kwargs['root'] 251 | root.tk.call('tk', 'scaling', kwargs['scaling']) 252 | except: 253 | pass 254 | 255 | 256 | def scale_size(widget, size): 257 | """Scale the size based on the scaling factor of ttk 258 | 259 | size : Union[int, List, Tuple] 260 | A single integer or an iterable of integers 261 | """ 262 | BASELINE = 1.33398982438864281 263 | scaling = widget.tk.call('tk', 'scaling') 264 | factor = scaling / BASELINE 265 | 266 | if isinstance(size, int): 267 | return int(size * factor) 268 | elif isinstance(size, tuple) or isinstance(size, list): 269 | return [int(x * factor) for x in size] -------------------------------------------------------------------------------- /ttkbootstrap/style/colors.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | from ttkbootstrap.constants import * 3 | 4 | class Colors: 5 | """A class that contains the theme colors as well as several 6 | helper methods for manipulating colors. 7 | """ 8 | 9 | def __init__( 10 | self, 11 | primary, 12 | secondary, 13 | success, 14 | info, 15 | warning, 16 | danger, 17 | light, 18 | dark, 19 | bg, 20 | fg, 21 | selectbg, 22 | selectfg, 23 | border, 24 | inputfg, 25 | inputbg, 26 | ): 27 | """This class is attached to the ``Style`` object at run-time 28 | for the selected theme, and so is available to use with 29 | ``Style.colors``. The colors can be accessed via dot notation 30 | or get method: 31 | 32 | .. code-block:: python 33 | 34 | # dot-notation 35 | Colors.primary 36 | 37 | # get method 38 | Colors.get('primary') 39 | 40 | This class is an iterator, so you can iterate over the main 41 | style color labels (primary, secondary, success, info, warning, 42 | danger): 43 | 44 | .. code-block:: python 45 | 46 | for color_label in Colors: 47 | color = Colors.get(color_label) 48 | print(color_label, color) 49 | 50 | If, for some reason, you need to iterate over all theme color 51 | labels, then you can use the ``Colors.label_iter`` method. This 52 | will include all theme colors. 53 | 54 | .. code-block:: python 55 | 56 | for color_label in Colors.label_iter(): 57 | color = Colors.get(color_label) 58 | print(color_label, color) 59 | 60 | Parameters 61 | ---------- 62 | primary : str 63 | The primary theme color; used by default for all widgets. 64 | 65 | secondary : str 66 | An accent color; commonly of a `grey` hue. 67 | 68 | success : str 69 | An accent color; commonly of a `green` hue. 70 | 71 | info : str 72 | An accent color; commonly of a `blue` hue. 73 | 74 | warning : str 75 | An accent color; commonly of an `orange` hue. 76 | 77 | danger : str 78 | An accent color; commonly of a `red` hue. 79 | 80 | light : str 81 | An accent color. 82 | 83 | dark : str 84 | An accent color. 85 | 86 | bg : str 87 | Background color. 88 | 89 | fg : str 90 | Default text color. 91 | 92 | selectfg : str 93 | The color of selected text. 94 | 95 | selectbg : str 96 | The background color of selected text. 97 | 98 | border : str 99 | The color used for widget borders. 100 | 101 | inputfg : str 102 | The text color for input widgets. 103 | 104 | inputbg : str 105 | The text background color for input widgets. 106 | """ 107 | self.primary = primary 108 | self.secondary = secondary 109 | self.success = success 110 | self.info = info 111 | self.warning = warning 112 | self.danger = danger 113 | self.light = light 114 | self.dark = dark 115 | self.bg = bg 116 | self.fg = fg 117 | self.selectbg = selectbg 118 | self.selectfg = selectfg 119 | self.border = border 120 | self.inputfg = inputfg 121 | self.inputbg = inputbg 122 | 123 | def rgb_to_hsv(r, g, b): 124 | return colorsys.rgb_to_hsv(r, g, b) 125 | 126 | def get_foreground(self, color_label: str): 127 | """Return the appropriate foreground color for the specified 128 | color_label. 129 | 130 | Parameters 131 | ---------- 132 | color_label : str 133 | A color label corresponding to a class property 134 | """ 135 | if color_label == LIGHT: 136 | return self.dark 137 | elif color_label == DARK: 138 | return self.light 139 | else: 140 | return self.selectfg 141 | 142 | def get(self, color_label): 143 | """Lookup a color property 144 | 145 | Parameters 146 | ---------- 147 | color_label : str 148 | A color label corresponding to a class propery 149 | 150 | Returns 151 | ------- 152 | str 153 | A hexadecimal color value. 154 | """ 155 | return self.__dict__.get(color_label) 156 | 157 | def set(self, color_label, color_value): 158 | """Set a color property 159 | 160 | Parameters 161 | ---------- 162 | color_label : str 163 | The name of the color to be set (key) 164 | 165 | color_value : str 166 | A hexadecimal color value 167 | """ 168 | self.__dict__[color_label] = color_value 169 | 170 | def __iter__(self): 171 | return iter( 172 | ["primary", "secondary", "success", "info", "warning", "danger", 173 | "light", "dark"] 174 | ) 175 | 176 | def __repr__(self): 177 | out = tuple(zip(self.__dict__.keys(), self.__dict__.values())) 178 | return str(out) 179 | 180 | @staticmethod 181 | def label_iter(): 182 | """Iterate over all color label properties in the Color class 183 | 184 | Returns 185 | ------- 186 | iter 187 | An iterator representing the name of the color properties 188 | """ 189 | return iter( 190 | [ 191 | "primary", 192 | "secondary", 193 | "success", 194 | "info", 195 | "warning", 196 | "danger", 197 | "light", 198 | "dark", 199 | "bg", 200 | "fg", 201 | "selectbg", 202 | "selectfg", 203 | "border", 204 | "inputfg", 205 | "inputbg", 206 | ] 207 | ) 208 | 209 | @staticmethod 210 | def hex_to_rgb(color): 211 | """Convert hexadecimal color to rgb color value 212 | 213 | Parameters 214 | ---------- 215 | color : str 216 | A hexadecimal color value 217 | 218 | Returns 219 | ------- 220 | tuple[int, int, int] 221 | An rgb color value. 222 | """ 223 | if len(color) == 4: 224 | # 3 digit hexadecimal colors 225 | r = round(int(color[1], 16) / 255, 2) 226 | g = round(int(color[2], 16) / 255, 2) 227 | b = round(int(color[3], 16) / 255, 2) 228 | else: 229 | # 6 digit hexadecimal colors 230 | r = round(int(color[1:3], 16) / 255, 2) 231 | g = round(int(color[3:5], 16) / 255, 2) 232 | b = round(int(color[5:], 16) / 255, 2) 233 | return r, g, b 234 | 235 | @staticmethod 236 | def rgb_to_hex(r, g, b): 237 | """Convert rgb to hexadecimal color value 238 | 239 | Parameters 240 | ---------- 241 | r : int 242 | red 243 | 244 | g : int 245 | green 246 | 247 | b : int 248 | blue 249 | 250 | Returns: 251 | str: a hexadecimal colorl value 252 | """ 253 | r_ = int(r * 255) 254 | g_ = int(g * 255) 255 | b_ = int(b * 255) 256 | return "#{:02x}{:02x}{:02x}".format(r_, g_, b_) 257 | 258 | @staticmethod 259 | def update_hsv(color, hd=0, sd=0, vd=0): 260 | """Modify the hue, saturation, and/or value of a given hex 261 | color value. 262 | 263 | Parameters 264 | ---------- 265 | color : str 266 | A hexadecimal color value to adjust. 267 | 268 | hd : float 269 | % change in hue 270 | 271 | sd : float 272 | % change in saturation 273 | 274 | vd : float 275 | % change in value 276 | 277 | Returns 278 | ------- 279 | str 280 | The resulting hexadecimal color value 281 | """ 282 | r, g, b = Colors.hex_to_rgb(color) 283 | h, s, v = colorsys.rgb_to_hsv(r, g, b) 284 | 285 | # hue 286 | if h * (1 + hd) > 1: 287 | h = 1 288 | elif h * (1 + hd) < 0: 289 | h = 0 290 | else: 291 | h *= 1 + hd 292 | 293 | # saturation 294 | if s * (1 + sd) > 1: 295 | s = 1 296 | elif s * (1 + sd) < 0: 297 | s = 0 298 | else: 299 | s *= 1 + sd 300 | 301 | # value 302 | if v * (1 + vd) > 1: 303 | v = 0.95 304 | elif v * (1 + vd) < 0.05: 305 | v = 0.05 306 | else: 307 | v *= 1 + vd 308 | 309 | r, g, b = colorsys.hsv_to_rgb(h, s, v) 310 | return Colors.rgb_to_hex(r, g, b) -------------------------------------------------------------------------------- /ttkbootstrap/themes/standard.py: -------------------------------------------------------------------------------- 1 | STANDARD_THEMES = { 2 | "cosmo": { 3 | "type": "light", 4 | "colors": { 5 | "primary": "#2780e3", 6 | "secondary": "#7E8081", 7 | "success": "#3fb618", 8 | "info": "#9954bb", 9 | "warning": "#ff7518", 10 | "danger": "#ff0039", 11 | "light":"#F8F9FA", 12 | "dark": "#373A3C", 13 | "bg": "#ffffff", 14 | "fg": "#373a3c", 15 | "selectbg": "#7e8081", 16 | "selectfg": "#ffffff", 17 | "border": "#ced4da", 18 | "inputfg": "#373a3c", 19 | "inputbg": "#fdfdfe" 20 | } 21 | }, 22 | "flatly": { 23 | "type": "light", 24 | "colors": { 25 | "primary": "#2c3e50", 26 | "secondary": "#95a5a6", 27 | "success": "#18bc9c", 28 | "info": "#3498db", 29 | "warning": "#f39c12", 30 | "danger": "#e74c3c", 31 | "light": "#ECF0F1", 32 | "dark": "#7B8A8B", 33 | "bg": "#ffffff", 34 | "fg": "#212529", 35 | "selectbg": "#95a5a6", 36 | "selectfg": "#ffffff", 37 | "border": "#ced4da", 38 | "inputfg": "#212529", 39 | "inputbg": "#ffffff" 40 | }, 41 | }, 42 | "litera": { 43 | "type": "light", 44 | "colors": { 45 | "primary": "#4582ec", 46 | "secondary": "#adb5bd", 47 | "success": "#02b875", 48 | "info": "#17a2b8", 49 | "warning": "#f0ad4e", 50 | "danger": "#d9534f", 51 | "light": "#F8F9FA", 52 | "dark": "#343A40", 53 | "bg": "#ffffff", 54 | "fg": "#343a40", 55 | "selectbg": "#adb5bd", 56 | "selectfg": "#ffffff", 57 | "border": "#e5e5e5", 58 | "inputfg": "#343a40", 59 | "inputbg": "#fff" 60 | }, 61 | }, 62 | "minty": { 63 | "type": "light", 64 | "colors": { 65 | "primary": "#78c2ad", 66 | "secondary": "#f3969a", 67 | "success": "#56cc9d", 68 | "info": "#6cc3d5", 69 | "warning": "#ffce67", 70 | "danger": "#ff7851", 71 | "light": "#F8F9FA", 72 | "dark": "#343A40", 73 | "bg": "#ffffff", 74 | "fg": "#5a5a5a", 75 | "selectbg": "#f3969a", 76 | "selectfg": "#ffffff", 77 | "border": "#ced4da", 78 | "inputfg": "#696969", 79 | "inputbg": "#fff" 80 | }, 81 | }, 82 | "lumen": { 83 | "type": "light", 84 | "colors": { 85 | "primary": "#158cba", 86 | "secondary": "#919191", 87 | "success": "#28b62c", 88 | "info": "#75caeb", 89 | "warning": "#ff851b", 90 | "danger": "#ff4136", 91 | "light": "#F6F6F6", 92 | "dark": "#555555", 93 | "bg": "#ffffff", 94 | "fg": "#555555", 95 | "selectbg": "#919191", 96 | "selectfg": "#ffffff", 97 | "border": "#ced4da", 98 | "inputfg": "#555555", 99 | "inputbg": "#fff" 100 | }, 101 | }, 102 | "sandstone": { 103 | "type": "light", 104 | "colors": { 105 | "primary": "#325D88", 106 | "secondary": "#8e8c84", 107 | "success": "#93c54b", 108 | "info": "#29abe0", 109 | "warning": "#f47c3c", 110 | "danger": "#d9534f", 111 | "light": "#F8F5F0", 112 | "dark": "#3E3F3A", 113 | "bg": "#ffffff", 114 | "fg": "#3e3f3a", 115 | "selectbg": "#8e8c84", 116 | "selectfg": "#ffffff", 117 | "border": "#ced4da", 118 | "inputfg": "#6E6D69", 119 | "inputbg": "#fff" 120 | }, 121 | }, 122 | "yeti": { 123 | "type": "light", 124 | "colors": { 125 | "primary": "#008cba", 126 | "secondary": "#707070", 127 | "success": "#43ac6a", 128 | "info": "#5bc0de", 129 | "warning": "#e99002", 130 | "danger": "#f04124", 131 | "light": "#EEEEEE", 132 | "dark": "#222222", 133 | "bg": "#ffffff", 134 | "fg": "#222222", 135 | "selectbg": "#707070", 136 | "selectfg": "#ffffff", 137 | "border": "#cccccc", 138 | "inputfg": "#222222", 139 | "inputbg": "#fff" 140 | } 141 | }, 142 | "pulse": { 143 | "type": "light", 144 | "colors": { 145 | "primary": "#593196", 146 | "secondary": "#69676E", 147 | "success": "#13b955", 148 | "info": "#009cdc", 149 | "warning": "#efa31d", 150 | "danger": "#fc3939", 151 | "light": "#F9F8FC", 152 | "dark": "#17141F", 153 | "bg": "#ffffff", 154 | "fg": "#444444", 155 | "selectbg": "#69676e", 156 | "selectfg": "#ffffff", 157 | "border": "#cbc8d0", 158 | "inputfg": "#444444", 159 | "inputbg": "#fdfdfe" 160 | }, 161 | }, 162 | "united": { 163 | "type": "light", 164 | "colors": { 165 | "primary": "#e95420", 166 | "secondary": "#aea79f", 167 | "success": "#38b44a", 168 | "info": "#17a2b8", 169 | "warning": "#efb73e", 170 | "danger": "#df382c", 171 | "light": "#E9ECEF", 172 | "dark": "#772953", 173 | "bg": "#ffffff", 174 | "fg": "#333333", 175 | "selectbg": "#aea79f", 176 | "selectfg": "#ffffff", 177 | "border": "#ced4da", 178 | "inputfg": "#333333", 179 | "inputbg": "#fff" 180 | }, 181 | }, 182 | "morph": { 183 | "type": "light", 184 | "colors": { 185 | "primary": "#378DFC", 186 | "secondary": "#aaaaaa", 187 | "success": "#43cc29", 188 | "info": "#5B62F4", 189 | "warning": "#FFC107", 190 | "danger": "#E52527", 191 | "light": "#F0F5FA", 192 | "dark": "#212529", 193 | "bg": "#D9E3F1", 194 | "fg": "#7B8AB8", 195 | "selectbg": "#aaaaaa", 196 | "selectfg": "#FBFDFF", 197 | "border": "#B9C7DA", 198 | "inputfg": "#7F8EBA", 199 | "inputbg": "#F0F5FA" 200 | }, 201 | }, 202 | "journal": { 203 | "type": "light", 204 | "colors": { 205 | "primary": "#eb6864", 206 | "secondary": "#aaaaaa", 207 | "success": "#22b24c", 208 | "info": "#336699", 209 | "warning": "#f5e625", 210 | "danger": "#f57a00", 211 | "light": "#F8F9FA", 212 | "dark": "#222222", 213 | "bg": "#ffffff", 214 | "fg": "#222222", 215 | "selectbg": "#aaaaaa", 216 | "selectfg": "#ffffff", 217 | "border": "#ced4da", 218 | "inputfg": "#565656", 219 | "inputbg": "#fff" 220 | }, 221 | }, 222 | "darkly": { 223 | "type": "dark", 224 | "colors": { 225 | "primary": "#375a7f", 226 | "secondary": "#444444", 227 | "success": "#00bc8c", 228 | "info": "#3498db", 229 | "warning": "#f39c12", 230 | "danger": "#e74c3c", 231 | "light": "#ADB5BD", 232 | "dark": "#303030", 233 | "bg": "#222222", 234 | "fg": "#ffffff", 235 | "selectbg": "#444444", 236 | "selectfg": "#ffffff", 237 | "border": "#222222", 238 | "inputfg": "#ffffff", 239 | "inputbg": "#444444" 240 | }, 241 | }, 242 | "superhero": { 243 | "type": "dark", 244 | "colors": { 245 | "primary": "#4c9be8", 246 | "secondary": "#4e5d6c", 247 | "success": "#5cb85c", 248 | "info": "#5bc0de", 249 | "warning": "#f0ad4e", 250 | "danger": "#d9534f", 251 | "light": "#ABB6C2", 252 | "dark": "#20374C", 253 | "bg": "#2b3e50", 254 | "fg": "#ffffff", 255 | "selectbg": "#4e5d6c", 256 | "selectfg": "#ffffff", 257 | "border": "#222222", 258 | "inputfg": "#ebebeb", 259 | "inputbg": "#4e5d6c" 260 | }, 261 | }, 262 | "solar": { 263 | "type": "dark", 264 | "colors": { 265 | "primary": "#bc951a", 266 | "secondary": "#94a2a4", 267 | "success": "#44aca4", 268 | "info": "#3f98d7", 269 | "warning": "#d05e2f", 270 | "danger": "#d95092", 271 | "light": "#FDF6E3", 272 | "dark": "#073642", 273 | "bg": "#002B36", 274 | "fg": "#ffffff", 275 | "selectbg": "#94a2a4", 276 | "selectfg": "#ffffff", 277 | "border": "#00252e", 278 | "inputfg": "#A9BDBD", 279 | "inputbg": "#073642" 280 | }, 281 | }, 282 | "cyborg": { 283 | "type": "dark", 284 | "colors": { 285 | "primary": "#2a9fd6", 286 | "secondary": "#555555", 287 | "success": "#77b300", 288 | "info": "#9933cc", 289 | "warning": "#ff8800", 290 | "danger": "#cc0000", 291 | "light": "#ADAFAE", 292 | "dark": "#222222", 293 | "bg": "#060606", 294 | "fg": "#ffffff", 295 | "selectbg": "#555555", 296 | "selectfg": "#ffffff", 297 | "border": "#060606", 298 | "inputfg": "#ffffff", 299 | "inputbg": "#282828" 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /ttkbootstrap/__main__.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import ttkbootstrap as ttk 3 | from ttkbootstrap.style import utility 4 | 5 | utility.enable_high_dpi_awareness() 6 | 7 | def setup_demo(master): 8 | 9 | ZEN = """Beautiful is better than ugly. 10 | Explicit is better than implicit. 11 | Simple is better than complex. 12 | Complex is better than complicated. 13 | Flat is better than nested. 14 | Sparse is better than dense. 15 | Readability counts. 16 | Special cases aren't special enough to break the rules. 17 | Although practicality beats purity. 18 | Errors should never pass silently. 19 | Unless explicitly silenced. 20 | In the face of ambiguity, refuse the temptation to guess. 21 | There should be one-- and preferably only one --obvious way to do it. 22 | Although that way may not be obvious at first unless you're Dutch. 23 | Now is better than never. 24 | Although never is often better than *right* now. 25 | If the implementation is hard to explain, it's a bad idea. 26 | If the implementation is easy to explain, it may be a good idea. 27 | Namespaces are one honking great idea -- let's do more of those!""" 28 | 29 | root = ttk.Frame(master, padding=10) 30 | style = ttk.Style() 31 | theme_names = style.theme_names() 32 | 33 | theme_selection = ttk.Frame(root, padding=(10, 10, 10, 0)) 34 | theme_selection.pack(fill=tk.X, expand=tk.YES) 35 | 36 | theme_selected = ttk.Label(theme_selection, text="litera", font="-size 24 -weight bold") 37 | theme_selected.pack(side=tk.LEFT) 38 | 39 | lbl = ttk.Label(theme_selection, text="Select a theme:") 40 | theme_cbo = ttk.Combobox( 41 | master=theme_selection, 42 | text=style.theme.name, 43 | values=theme_names, 44 | ) 45 | theme_cbo.pack(padx=10, side=tk.RIGHT) 46 | theme_cbo.current(theme_names.index(style.theme.name)) 47 | lbl.pack(side=tk.RIGHT) 48 | 49 | ttk.Separator(root).pack(fill=tk.X, pady=10, padx=10) 50 | 51 | def change_theme(e): 52 | t = cbo.get() 53 | style.theme_use(t) 54 | theme_selected.configure(text=t) 55 | theme_cbo.selection_clear() 56 | default.focus_set() 57 | 58 | theme_cbo.bind('<>', change_theme) 59 | 60 | lframe = ttk.Frame(root, padding=5) 61 | lframe.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES) 62 | 63 | rframe = ttk.Frame(root, padding=5) 64 | rframe.pack(side=tk.RIGHT, fill=tk.BOTH, expand=tk.YES) 65 | 66 | color_group = ttk.Labelframe( 67 | master=lframe, 68 | text="Theme color options", 69 | padding=10 70 | ) 71 | color_group.pack(fill=tk.X, side=tk.TOP) 72 | 73 | for color in style.colors: 74 | cb = ttk.Button(color_group, text=color, bootstyle=color) 75 | cb.pack(side=tk.LEFT, expand=tk.YES, padx=5, fill=tk.X) 76 | 77 | rb_group = ttk.Labelframe( 78 | lframe, text="Checkbuttons & radiobuttons", padding=10) 79 | rb_group.pack(fill=tk.X, pady=10, side=tk.TOP) 80 | 81 | check1 = ttk.Checkbutton(rb_group, text="selected") 82 | check1.pack(side=tk.LEFT, expand=tk.YES, padx=5) 83 | check1.invoke() 84 | check2 = ttk.Checkbutton(rb_group, text="deselected") 85 | check2.pack(side=tk.LEFT, expand=tk.YES, padx=5) 86 | check3 = ttk.Checkbutton(rb_group, text="disabled", state=tk.DISABLED) 87 | check3.pack(side=tk.LEFT, expand=tk.YES, padx=5) 88 | 89 | radio1 = ttk.Radiobutton(rb_group, text="selected", value=1) 90 | radio1.pack(side=tk.LEFT, expand=tk.YES, padx=5) 91 | radio1.invoke() 92 | radio2 = ttk.Radiobutton(rb_group, text="deselected", value=2) 93 | radio2.pack(side=tk.LEFT, expand=tk.YES, padx=5) 94 | radio3 = ttk.Radiobutton(rb_group, text="disabled", 95 | value=3, state=tk.DISABLED) 96 | radio3.pack(side=tk.LEFT, expand=tk.YES, padx=5) 97 | 98 | ttframe = ttk.Frame(lframe) 99 | ttframe.pack(pady=5, fill=tk.X, side=tk.TOP) 100 | 101 | table_data = [ 102 | ('South Island, New Zealand', 1), 103 | ('Paris', 2), 104 | ('Bora Bora', 3), 105 | ('Maui', 4), 106 | ('Tahiti', 5) 107 | ] 108 | 109 | tv = ttk.Treeview( 110 | master=ttframe, 111 | columns=[0, 1], 112 | show='headings', 113 | height=5 114 | ) 115 | for row in table_data: 116 | tv.insert('', tk.END, values=row) 117 | 118 | tv.selection_set('I001') 119 | tv.heading(0, text='City') 120 | tv.heading(1, text='Rank') 121 | tv.column(0, width=300) 122 | tv.column(1, width=70, anchor=tk.CENTER) 123 | tv.pack(side=tk.LEFT, anchor=tk.NE, fill=tk.X) 124 | 125 | # # notebook with table and text tabs 126 | nb = ttk.Notebook(ttframe) 127 | nb.pack( 128 | side=tk.LEFT, 129 | padx=(10, 0), 130 | expand=tk.YES, 131 | fill=tk.BOTH 132 | ) 133 | nb_text = "This is a notebook tab.\nYou can put any widget you want here." 134 | nb.add(ttk.Label(nb, text=nb_text), text="Tab 1", sticky=tk.NW) 135 | nb.add( 136 | child=ttk.Label(nb, text="A notebook tab."), 137 | text="Tab 2", 138 | sticky=tk.NW 139 | ) 140 | nb.add(ttk.Frame(nb), text='Tab 3') 141 | nb.add(ttk.Frame(nb), text='Tab 4') 142 | nb.add(ttk.Frame(nb), text='Tab 5') 143 | 144 | # text widget 145 | txt = tk.Text( 146 | master=lframe, 147 | height=5, 148 | width=50, 149 | wrap='none' 150 | ) 151 | txt.insert(tk.END, ZEN) 152 | txt.pack( 153 | side=tk.LEFT, 154 | anchor=tk.NW, 155 | pady=5, 156 | fill=tk.BOTH, 157 | expand=tk.YES 158 | ) 159 | lframe_inner = ttk.Frame(lframe) 160 | lframe_inner.pack( 161 | fill=tk.BOTH, 162 | expand=tk.YES, 163 | padx=10 164 | ) 165 | s1 = ttk.Scale( 166 | master=lframe_inner, 167 | orient=tk.HORIZONTAL, 168 | value=75, 169 | from_=100, 170 | to=0 171 | ) 172 | s1.pack(fill=tk.X, pady=5, expand=tk.YES) 173 | 174 | ttk.Progressbar( 175 | master=lframe_inner, 176 | orient=tk.HORIZONTAL, 177 | value=50, 178 | ).pack(fill=tk.X, pady=5, expand=tk.YES) 179 | 180 | ttk.Progressbar( 181 | master=lframe_inner, 182 | orient=tk.HORIZONTAL, 183 | value=75, 184 | bootstyle='success-striped' 185 | ).pack(fill=tk.X, pady=5, expand=tk.YES) 186 | 187 | m = ttk.Meter( 188 | master=lframe_inner, 189 | metersize=150, 190 | amountused=45, 191 | subtext='meter widget', 192 | bootstyle='info', 193 | interactive=True 194 | ) 195 | m.pack(pady=10) 196 | 197 | sb = ttk.Scrollbar( 198 | master=lframe_inner, 199 | orient=tk.HORIZONTAL, 200 | ) 201 | sb.set(0.1, 0.9) 202 | sb.pack(fill=tk.X, pady=5, expand=tk.YES) 203 | 204 | sb = ttk.Scrollbar( 205 | master=lframe_inner, 206 | orient=tk.HORIZONTAL, 207 | bootstyle='danger-round' 208 | ) 209 | sb.set(0.1, 0.9) 210 | sb.pack(fill=tk.X, pady=5, expand=tk.YES) 211 | 212 | btn_group = ttk.Labelframe( 213 | master=rframe, 214 | text="Buttons", 215 | padding=(10, 5) 216 | ) 217 | btn_group.pack(fill=tk.X) 218 | 219 | menu = tk.Menu(root) 220 | for i, t in enumerate(style.theme_names()): 221 | menu.add_radiobutton(label=t, value=i) 222 | 223 | default = ttk.Button( 224 | master=btn_group, 225 | text="solid button" 226 | ) 227 | default.pack(fill=tk.X, pady=5) 228 | default.focus_set() 229 | 230 | mb = ttk.Menubutton( 231 | master=btn_group, 232 | text="solid menubutton", 233 | bootstyle="secondary", menu=menu 234 | ) 235 | mb.pack(fill=tk.X, pady=5) 236 | 237 | cb = ttk.Checkbutton( 238 | master=btn_group, 239 | text="solid toolbutton", 240 | bootstyle="success-toolbutton", 241 | ) 242 | cb.invoke() 243 | cb.pack(fill=tk.X, pady=5) 244 | 245 | ob = ttk.Button( 246 | master=btn_group, 247 | text="outline button", 248 | bootstyle='info-outline' 249 | ) 250 | ob.pack(fill=tk.X, pady=5) 251 | 252 | mb = ttk.Menubutton( 253 | master=btn_group, 254 | text="outline menubutton", 255 | bootstyle="warning-outline", 256 | menu=menu 257 | ) 258 | mb.pack(fill=tk.X, pady=5) 259 | 260 | cb = ttk.Checkbutton( 261 | master=btn_group, 262 | text="outline toolbutton", 263 | bootstyle="success-outline-toolbutton" 264 | ) 265 | cb.pack(fill=tk.X, pady=5) 266 | 267 | lb = ttk.Button( 268 | master=btn_group, 269 | text="link button", 270 | bootstyle='link' 271 | ) 272 | lb.pack(fill=tk.X, pady=5) 273 | 274 | cb1 = ttk.Checkbutton( 275 | master=btn_group, 276 | text="rounded toggle", 277 | bootstyle="success-round-toggle", 278 | ) 279 | cb1.invoke() 280 | cb1.pack(fill=tk.X, pady=5) 281 | 282 | cb2 = ttk.Checkbutton( 283 | master=btn_group, 284 | text="squared toggle", 285 | bootstyle="square-toggle" 286 | ) 287 | cb2.pack(fill=tk.X, pady=5) 288 | cb2.invoke() 289 | 290 | input_group = ttk.Labelframe( 291 | master=rframe, 292 | text="Other input widgets", 293 | padding=10 294 | ) 295 | input_group.pack( 296 | fill=tk.BOTH, 297 | pady=(10, 5), 298 | expand=tk.YES 299 | ) 300 | entry = ttk.Entry(input_group) 301 | entry.pack(fill=tk.X) 302 | entry.insert(tk.END, "entry widget") 303 | 304 | password = ttk.Entry( 305 | master=input_group, 306 | show="•" 307 | ) 308 | password.pack(fill=tk.X, pady=5) 309 | password.insert(tk.END, "password") 310 | 311 | spinbox = ttk.Spinbox( 312 | master=input_group, 313 | from_=0, 314 | to=100 315 | ) 316 | spinbox.pack(fill=tk.X) 317 | spinbox.set(45) 318 | 319 | cbo = ttk.Combobox( 320 | master=input_group, 321 | text=style.theme.name, 322 | values=theme_names, 323 | exportselection=False 324 | ) 325 | cbo.pack(fill=tk.X, pady=5) 326 | cbo.current(theme_names.index(style.theme.name)) 327 | 328 | de = ttk.DateEntry(input_group) 329 | de.pack(fill=tk.X) 330 | 331 | return root 332 | 333 | if __name__ == '__main__': 334 | 335 | root = tk.Tk() 336 | 337 | bagel = setup_demo(root) 338 | bagel.pack(fill=tk.BOTH, expand=tk.YES) 339 | 340 | root.mainloop() 341 | -------------------------------------------------------------------------------- /WindowEffect.py: -------------------------------------------------------------------------------- 1 | from ctypes import POINTER, c_bool, c_int, pointer, sizeof, WinDLL, byref,Structure 2 | from ctypes.wintypes import DWORD, HWND, ULONG, POINT, RECT, UINT, LONG, LPCVOID 3 | import win32api, win32gui 4 | from win32.lib import win32con 5 | from enum import Enum 6 | 7 | 8 | class WINDOWCOMPOSITIONATTRIB(Enum): 9 | WCA_UNDEFINED = 0, 10 | WCA_NCRENDERING_ENABLED = 1, 11 | WCA_NCRENDERING_POLICY = 2, 12 | WCA_TRANSITIONS_FORCEDISABLED = 3, 13 | WCA_ALLOW_NCPAINT = 4, 14 | WCA_CAPTION_BUTTON_BOUNDS = 5, 15 | WCA_NONCLIENT_RTL_LAYOUT = 6, 16 | WCA_FORCE_ICONIC_REPRESENTATION = 7, 17 | WCA_EXTENDED_FRAME_BOUNDS = 8, 18 | WCA_HAS_ICONIC_BITMAP = 9, 19 | WCA_THEME_ATTRIBUTES = 10, 20 | WCA_NCRENDERING_EXILED = 11, 21 | WCA_NCADORNMENTINFO = 12, 22 | WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, 23 | WCA_VIDEO_OVERLAY_ACTIVE = 14, 24 | WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, 25 | WCA_DISALLOW_PEEK = 16, 26 | WCA_CLOAK = 17, 27 | WCA_CLOAKED = 18, 28 | WCA_ACCENT_POLICY = 19, 29 | WCA_FREEZE_REPRESENTATION = 20, 30 | WCA_EVER_UNCLOAKED = 21, 31 | WCA_VISUAL_OWNER = 22, 32 | WCA_LAST = 23 33 | 34 | 35 | class ACCENT_STATE(Enum): 36 | """ Client area status enumeration class """ 37 | ACCENT_DISABLED = 0, 38 | ACCENT_ENABLE_GRADIENT = 1, 39 | ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, 40 | ACCENT_ENABLE_BLURBEHIND = 3, # Aero effect 41 | ACCENT_ENABLE_ACRYLICBLURBEHIND = 4, # Acrylic effect 42 | ACCENT_INVALID_STATE = 5, 43 | 44 | 45 | class ACCENT_POLICY(Structure): 46 | """ Specific attributes of client area """ 47 | 48 | _fields_ = [ 49 | ("AccentState", DWORD), 50 | ("AccentFlags", DWORD), 51 | ("GradientColor", DWORD), 52 | ("AnimationId", DWORD), 53 | ] 54 | 55 | 56 | class WINDOWCOMPOSITIONATTRIBDATA(Structure): 57 | _fields_ = [ 58 | ("Attribute", DWORD), 59 | # Pointer() receives any ctypes type and returns a pointer type 60 | ("Data", POINTER(ACCENT_POLICY)), 61 | ("SizeOfData", ULONG), 62 | ] 63 | 64 | 65 | class DWMNCRENDERINGPOLICY(Enum): 66 | DWMNCRP_USEWINDOWSTYLE = 0 67 | DWMNCRP_DISABLED = 1 68 | DWMNCRP_ENABLED = 2 69 | DWMNCRP_LAS = 3 70 | 71 | 72 | class DWMWINDOWATTRIBUTE(Enum): 73 | DWMWA_NCRENDERING_ENABLED = 1 74 | DWMWA_NCRENDERING_POLICY = 2 75 | DWMWA_TRANSITIONS_FORCEDISABLED = 3 76 | DWMWA_ALLOW_NCPAINT = 4 77 | DWMWA_CAPTION_BUTTON_BOUNDS = 5 78 | DWMWA_NONCLIENT_RTL_LAYOUT = 6 79 | DWMWA_FORCE_ICONIC_REPRESENTATION = 7 80 | DWMWA_FLIP3D_POLICY = 8 81 | DWMWA_EXTENDED_FRAME_BOUNDS = 9 82 | DWMWA_HAS_ICONIC_BITMAP = 10 83 | DWMWA_DISALLOW_PEEK = 11 84 | DWMWA_EXCLUDED_FROM_PEEK = 12 85 | DWMWA_CLOAK = 13 86 | DWMWA_CLOAKED = 14 87 | DWMWA_FREEZE_REPRESENTATION = 25 88 | DWMWA_LAST = 16 89 | 90 | 91 | class MARGINS(Structure): 92 | _fields_ = [ 93 | ("cxLeftWidth", c_int), 94 | ("cxRightWidth", c_int), 95 | ("cyTopHeight", c_int), 96 | ("cyBottomHeight", c_int), 97 | ] 98 | 99 | 100 | class MINMAXINFO(Structure): 101 | _fields_ = [ 102 | ("ptReserved", POINT), 103 | ("ptMaxSize", POINT), 104 | ("ptMaxPosition", POINT), 105 | ("ptMinTrackSize", POINT), 106 | ("ptMaxTrackSize", POINT), 107 | ] 108 | 109 | 110 | class PWINDOWPOS(Structure): 111 | _fields_ = [ 112 | ('hWnd', HWND), 113 | ('hwndInsertAfter', HWND), 114 | ('x', c_int), 115 | ('y', c_int), 116 | ('cx', c_int), 117 | ('cy', c_int), 118 | ('flags', UINT) 119 | ] 120 | 121 | 122 | class NCCALCSIZE_PARAMS(Structure): 123 | _fields_ = [ 124 | ('rgrc', RECT*3), 125 | ('lppos', POINTER(PWINDOWPOS)) 126 | ] 127 | 128 | 129 | 130 | class WindowEffect: 131 | """ A class that calls Windows API to realize window effect """ 132 | 133 | def __init__(self): 134 | # 获取句柄的两种方式(另一种是通过win32获取) 135 | # Declare the function signature of the API 136 | self.user32 = WinDLL("user32") 137 | self.dwmapi = WinDLL("dwmapi") 138 | self.SetWindowCompositionAttribute = self.user32.SetWindowCompositionAttribute 139 | self.DwmExtendFrameIntoClientArea = self.dwmapi.DwmExtendFrameIntoClientArea 140 | self.DwmSetWindowAttribute = self.dwmapi.DwmSetWindowAttribute 141 | self.SetWindowCompositionAttribute.restype = c_bool 142 | self.DwmExtendFrameIntoClientArea.restype = LONG 143 | self.DwmSetWindowAttribute.restype = LONG 144 | self.SetWindowCompositionAttribute.argtypes = [ 145 | c_int, 146 | POINTER(WINDOWCOMPOSITIONATTRIBDATA), 147 | ] 148 | self.DwmSetWindowAttribute.argtypes = [c_int, DWORD, LPCVOID, DWORD] 149 | self.DwmExtendFrameIntoClientArea.argtypes = [c_int, POINTER(MARGINS)] 150 | 151 | # Initialize structure 152 | # 客户端的四组属性 153 | self.accentPolicy = ACCENT_POLICY() 154 | self.winCompAttrData = WINDOWCOMPOSITIONATTRIBDATA() 155 | 156 | self.winCompAttrData.Attribute = WINDOWCOMPOSITIONATTRIB.WCA_ACCENT_POLICY.value[0] 157 | self.winCompAttrData.SizeOfData = sizeof(self.accentPolicy) 158 | self.winCompAttrData.Data = pointer(self.accentPolicy) 159 | 160 | def setAcrylicEffect(self, hWnd, setAcrylicEffect,gradientColor: str = "01010100", isEnableShadow: bool = True, 161 | animationId: int = 0): 162 | """ Add the acrylic effect to the window 163 | 164 | Parameters 165 | ---------- 166 | hWnd: int or `sip.voidptr` 167 | Window handle 168 | 169 | gradientColor: str 170 | Hexadecimal acrylic mixed color, corresponding to four RGBA channels 171 | 172 | isEnableShadow: bool 173 | Enable window shadows 174 | 175 | animationId: int 176 | Turn on matte animation 177 | """ 178 | # Acrylic mixed color 179 | if setAcrylicEffect==1: 180 | gradientColor="00000000" 181 | 182 | gradientColor = ( 183 | gradientColor[6:] 184 | + gradientColor[4:6] 185 | + gradientColor[2:4] 186 | + gradientColor[:2] 187 | ) 188 | gradientColor = DWORD(int(gradientColor, base=16)) 189 | # matte animation 190 | animationId = DWORD(animationId) 191 | # window shadow 192 | accentFlags = DWORD(0x20 | 0x40 | 0x80 | 193 | 0x100) if isEnableShadow else DWORD(0) 194 | 195 | self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_ENABLE_ACRYLICBLURBEHIND.value[0] 196 | self.accentPolicy.GradientColor = gradientColor 197 | self.accentPolicy.AccentFlags = accentFlags 198 | self.accentPolicy.AnimationId = animationId 199 | 200 | # enable acrylic effect 201 | self.SetWindowCompositionAttribute(int(hWnd), pointer(self.winCompAttrData)) 202 | 203 | def setAeroEffect(self, hWnd): 204 | """ Add the aero effect to the window 205 | 206 | Parameters 207 | ---------- 208 | hWnd: int or `sip.voidptr` 209 | Window handle 210 | """ 211 | # self.accentPolicy = ACCENT_POLICY() 212 | self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_ENABLE_BLURBEHIND.value[0] 213 | 214 | accentFlags = DWORD(0x20 | 0x40 | 0x80 | 215 | 0x100) 216 | self.accentPolicy.AccentFlags = accentFlags 217 | 218 | # enable Aero effect 219 | self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) 220 | 221 | def removeBackgroundEffect(self, hWnd): 222 | """ Remove background effect 223 | 224 | Parameters 225 | ---------- 226 | hWnd: int or `sip.voidptr` 227 | Window handle 228 | """ 229 | self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_DISABLED.value[0] 230 | self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) 231 | 232 | # def reBackgroundEffect(self, hWnd): 233 | # """ Remove background effect 234 | # 235 | # Parameters 236 | # ---------- 237 | # hWnd: int or `sip.voidptr` 238 | # Window handle 239 | # """ 240 | # self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_INVALID_STATE.value[0] 241 | # self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) 242 | 243 | # @staticmethod 244 | # def moveWindow(hWnd): 245 | # """ Move the window 246 | # 247 | # Parameters 248 | # ---------- 249 | # hWnd: int or `sip.voidptr` 250 | # Window handle 251 | # """ 252 | # win32gui.ReleaseCapture() 253 | # win32api.SendMessage( 254 | # hWnd, win32con.WM_SYSCOMMAND, win32con.SC_MOVE + win32con.HTCAPTION, 0 255 | # ) 256 | 257 | def addShadowEffect(self, hWnd): 258 | """ Add DWM shadow to the window 259 | 260 | Parameters 261 | ---------- 262 | hWnd: int or `sip.voidptr` 263 | Window handle 264 | """ 265 | hWnd = int(hWnd) 266 | self.DwmSetWindowAttribute( 267 | hWnd, 268 | DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY.value, 269 | byref(c_int(DWMNCRENDERINGPOLICY.DWMNCRP_ENABLED.value)), 270 | 4, 271 | ) 272 | margins = MARGINS(-1, -1, -1, -1) 273 | self.DwmExtendFrameIntoClientArea(hWnd, byref(margins)) 274 | 275 | def removeShadowEffect(self, hWnd): 276 | """ Remove DWM shadow from the window 277 | 278 | Parameters 279 | ---------- 280 | hWnd: int or `sip.voidptr` 281 | Window handle 282 | """ 283 | hWnd = int(hWnd) 284 | self.DwmSetWindowAttribute( 285 | hWnd, 286 | DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY.value, 287 | byref(c_int(DWMNCRENDERINGPOLICY.DWMNCRP_DISABLED.value)), 288 | 4, 289 | ) 290 | 291 | @staticmethod 292 | def removeMenuShadowEffect(hWnd): 293 | """ Remove shadow from pop-up menu 294 | 295 | Parameters 296 | ---------- 297 | hWnd: int or `sip.voidptr` 298 | Window handle 299 | """ 300 | style = win32gui.GetClassLong(hWnd, win32con.GCL_STYLE) 301 | style &= ~0x00020000 # CS_DROPSHADOW 302 | win32api.SetClassLong(hWnd, win32con.GCL_STYLE, style) 303 | 304 | @staticmethod 305 | def addWindowAnimation(hWnd): 306 | """ Enables the maximize and minimize animation of the window 307 | 308 | Parameters 309 | ---------- 310 | hWnd : int or `sip.voidptr` 311 | Window handle 312 | """ 313 | style = win32gui.GetWindowLong(hWnd, win32con.GWL_STYLE) 314 | win32gui.SetWindowLong( 315 | hWnd, 316 | win32con.GWL_STYLE, 317 | style 318 | | win32con.WS_MAXIMIZEBOX 319 | | win32con.WS_CAPTION 320 | | win32con.CS_DBLCLKS 321 | | win32con.WS_THICKFRAME, 322 | ) 323 | -------------------------------------------------------------------------------- /ttkbootstrap/dialogs/calendar.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | from datetime import datetime 3 | import tkinter as tk 4 | import ttkbootstrap as ttk 5 | 6 | 7 | class DatePickerPopup: 8 | 9 | def __init__( 10 | self, 11 | parent=None, 12 | title='', 13 | firstweekday=6, 14 | startdate=None, 15 | bootstyle='primary', 16 | ): 17 | """A widget that displays a calendar popup and returns the 18 | selected date as a datetime object. 19 | 20 | The current date is displayed by default unless the `startdate` 21 | parameter is provided. 22 | 23 | The month can be changed by clicking the chevrons to the left 24 | and right of the month-year title. 25 | 26 | Left-click the arrow to move the calendar by one month. 27 | Right-click the arrow to move the calendar by one year. 28 | Right-click the title to reset the calendar to the start date. 29 | 30 | The starting weekday can be changed with the `firstweekday` 31 | parameter for geographies that do not start the calendar on 32 | Sunday, which is the default. 33 | 34 | The widget grabs focus and all screen events until released. 35 | If you want to cancel a date selection, click the 'X' button 36 | at the top-right corner of the widget. 37 | 38 | The bootstyle api may be used to change the style of the widget. 39 | The available colors include -> primary, secondary, success, 40 | info, warning, danger, light, dark. 41 | 42 | Parameters 43 | ---------- 44 | parent : Widget 45 | The parent widget; the popup will appear to the bottom-right 46 | of the parent widget. If no parent is provided, the widget 47 | is centered on the screen. 48 | 49 | title : str 50 | The text that appears on the titlebar. By default = ''. 51 | 52 | firstweekday : int 53 | Specifies the first day of the week. 0=Monday, 1=Tuesday, 54 | etc.... Default = 6 (Sunday). 55 | 56 | startdate : datetime 57 | The date to be in focus when the widget is displayed. 58 | Default = Current date. 59 | 60 | bootstyle : str 61 | The following colors can be used to change the color of the 62 | title and hover / pressed color -> primary, secondary, info, 63 | warning, success, danger, light, dark. 64 | """ 65 | self.parent = parent 66 | self.root = tk.Toplevel() 67 | self.root.title(title) 68 | self.firstweekday = firstweekday 69 | self.startdate = startdate or datetime.today().date() 70 | self.bootstyle = bootstyle or 'primary' 71 | 72 | self.date_selected = self.startdate 73 | self.date = startdate or self.date_selected 74 | self.calendar = calendar.Calendar(firstweekday=firstweekday) 75 | 76 | self.titlevar = tk.StringVar() 77 | self.datevar = tk.IntVar() 78 | 79 | self.setup_calendar() 80 | self.root.grab_set() 81 | self.root.wait_window() 82 | 83 | def setup_calendar(self): 84 | """Setup the calendar widget""" 85 | # create the widget containers 86 | self.frm_calendar = ttk.Frame( 87 | master=self.root, 88 | padding=0, 89 | borderwidth=1, 90 | relief=tk.RAISED 91 | ) 92 | self.frm_calendar.pack(fill=tk.BOTH, expand=tk.YES) 93 | self.frm_title = ttk.Frame(self.frm_calendar, padding=(3, 3)) 94 | self.frm_title.pack(fill=tk.X) 95 | self.frm_header = ttk.Frame(self.frm_calendar, bootstyle='secondary') 96 | self.frm_header.pack(fill=tk.X) 97 | 98 | # setup the toplevel widget 99 | self.root.withdraw() 100 | self.root.transient(self.parent) 101 | #self.root.overrideredirect(True) 102 | self.root.resizable(False, False) 103 | self.frm_calendar.update_idletasks() # actualize geometry 104 | 105 | # create visual components 106 | self.draw_titlebar() 107 | self.draw_calendar() 108 | 109 | # make toplevel visible 110 | self.set_window_position() 111 | self.root.deiconify() 112 | self.root.attributes('-topmost', True) 113 | 114 | def update_widget_bootstyle(self): 115 | self.frm_title.configure(bootstyle=self.bootstyle) 116 | self.title.configure(bootstyle=f'{self.bootstyle}-inverse') 117 | self.prev_period.configure(style=f'Chevron.{self.bootstyle}.TButton') 118 | self.next_period.configure(style=f'Chevron.{self.bootstyle}.TButton') 119 | 120 | def draw_calendar(self): 121 | self.update_widget_bootstyle() 122 | self.root.minsize(width=226, height=1) 123 | self.set_title() 124 | self.current_month_days() 125 | self.frm_dates = ttk.Frame(self.frm_calendar) 126 | self.frm_dates.pack(fill=tk.BOTH, expand=tk.YES) 127 | 128 | for row, weekday_list in enumerate(self.monthdays): 129 | for col, day in enumerate(weekday_list): 130 | self.frm_dates.columnconfigure(col, weight=1) 131 | if day == 0: 132 | ttk.Label( 133 | master=self.frm_dates, 134 | text=self.monthdates[row][col].day, 135 | anchor=tk.CENTER, 136 | padding=5, 137 | bootstyle='secondary' 138 | ).grid( 139 | row=row, column=col, sticky=tk.NSEW 140 | ) 141 | else: 142 | if all([ 143 | day == self.date_selected.day, 144 | self.date.month == self.date_selected.month, 145 | self.date.year == self.date_selected.year 146 | ]): 147 | day_style = 'secondary-toolbutton' 148 | else: 149 | day_style = f'{self.bootstyle}-calendar' 150 | 151 | def selected(x=row, y=col): self.on_date_selected(x, y) 152 | 153 | btn = ttk.Radiobutton( 154 | master=self.frm_dates, 155 | variable=self.datevar, 156 | value=day, 157 | text=day, 158 | bootstyle=day_style, 159 | padding=5, 160 | command=selected 161 | ) 162 | btn.grid(row=row, column=col, sticky=tk.NSEW) 163 | 164 | def draw_titlebar(self): 165 | """Draw the calendar title bar which includes the month title 166 | and the buttons that increment and decrement the selected 167 | month. 168 | 169 | In addition to the previous and next MONTH commands that are 170 | assigned to the button press, a "right-click" event is assigned 171 | to each button that causes the calendar to move to the previous 172 | and next YEAR. 173 | """ 174 | # create and pack the title and action buttons 175 | self.prev_period = ttk.Button( 176 | master=self.frm_title, 177 | text='«', 178 | command=self.on_prev_month 179 | ) 180 | self.prev_period.pack(side=tk.LEFT) 181 | 182 | self.title = ttk.Label( 183 | master=self.frm_title, 184 | textvariable=self.titlevar, 185 | anchor=tk.CENTER, 186 | font='-size 10 -weight bold' 187 | ) 188 | self.title.pack(side=tk.LEFT, fill=tk.X, expand=tk.YES) 189 | 190 | self.next_period = ttk.Button( 191 | master=self.frm_title, 192 | text='»', 193 | command=self.on_next_month, 194 | ) 195 | self.next_period.pack(side=tk.LEFT) 196 | 197 | # bind "year" callbacks to action buttons 198 | self.prev_period.bind('', self.on_prev_year, '+') 199 | self.next_period.bind('', self.on_next_year, '+') 200 | self.title.bind('', self.on_reset_date) 201 | 202 | # create and pack days of the week header 203 | for col in self.header_columns(): 204 | ttk.Label( 205 | master=self.frm_header, 206 | text=col, 207 | anchor=tk.CENTER, 208 | padding=5, 209 | bootstyle='secondary-inverse' 210 | ).pack( 211 | side=tk.LEFT, 212 | fill=tk.X, 213 | expand=tk.YES 214 | ) 215 | 216 | def set_title(self): 217 | _titledate = f'{self.date.strftime("%B %Y")}' 218 | self.titlevar.set(value=_titledate) 219 | 220 | def current_month_days(self): 221 | """Fetch the day numbers and dates for all days in the current 222 | month. `monthdays` is a list of days as integers, and 223 | `monthdates` is a list of `datetime` objects. 224 | """ 225 | self.monthdays = self.calendar.monthdayscalendar( 226 | year=self.date.year, 227 | month=self.date.month 228 | ) 229 | self.monthdates = self.calendar.monthdatescalendar( 230 | year=self.date.year, 231 | month=self.date.month 232 | ) 233 | 234 | def header_columns(self): 235 | """Create and return a list of weekdays to be used as a header 236 | in the calendar. The order of the weekdays is based on the 237 | `firstweekday` property. 238 | 239 | Returns 240 | ------- 241 | List[str] 242 | A list of weekday column names for the calendar header. 243 | """ 244 | weekdays = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] 245 | header = weekdays[self.firstweekday:] + weekdays[:self.firstweekday] 246 | return header 247 | 248 | def on_date_selected(self, row, col): 249 | """Callback for selecting a date. 250 | 251 | An index is assigned to each date button that corresponds to 252 | the dates in the `monthdates` matrix. When the user clicks a 253 | button to select a date, the index from this button is used 254 | to lookup the date value of the button based on the row and 255 | column index reference. This value is saved in the 256 | `date_selected` property and the `Toplevel` is destroyed. 257 | 258 | Parameters 259 | ---------- 260 | index : Tuple[int, int] 261 | A row and column index of the date selected; to be found 262 | in the `monthdates` matrix. 263 | 264 | Returns 265 | ------- 266 | datetime 267 | The date selected 268 | """ 269 | self.date_selected = self.monthdates[row][col] 270 | self.root.destroy() 271 | 272 | def selection_callback(func): 273 | """Calls the decorated `func` and redraws the calendar.""" 274 | def inner(self, *args): 275 | func(self, *args) 276 | self.frm_dates.destroy() 277 | self.draw_calendar() 278 | return inner 279 | 280 | @selection_callback 281 | def on_next_month(self): 282 | """Increment the calendar data to the next month""" 283 | year, month = calendar._nextmonth(self.date.year, self.date.month) 284 | self.date = datetime(year=year, month=month, day=1).date() 285 | 286 | @selection_callback 287 | def on_next_year(self, *_): 288 | """Increment the calendar data to the next year""" 289 | year = self.date.year + 1 290 | month = self.date.month 291 | self.date = datetime(year=year, month=month, day=1).date() 292 | 293 | @selection_callback 294 | def on_prev_month(self): 295 | """Decrement the calendar to the previous year""" 296 | year, month = calendar._prevmonth(self.date.year, self.date.month) 297 | self.date = datetime(year=year, month=month, day=1).date() 298 | 299 | @selection_callback 300 | def on_prev_year(self, *_): 301 | year = self.date.year - 1 302 | month = self.date.month 303 | self.date = datetime(year=year, month=month, day=1).date() 304 | 305 | @selection_callback 306 | def on_reset_date(self, *_): 307 | """Set the calendar to the start date""" 308 | self.date = self.startdate 309 | 310 | def set_window_position(self): 311 | """Move the window the to bottom-right of the parent widget, or 312 | to the middle of the screen if no parent is provided. 313 | """ 314 | width = self.root.winfo_reqwidth() 315 | height = self.root.winfo_reqheight() 316 | if self.parent: 317 | xpos = self.parent.winfo_rootx() + self.parent.winfo_width() 318 | ypos = self.parent.winfo_rooty() + self.parent.winfo_height() 319 | self.root.geometry(f'+{xpos}+{ypos}') 320 | else: 321 | xpos = self.root.winfo_screenwidth() // 2 - width 322 | ypos = self.root.winfo_screenheight() // 2 - height 323 | self.root.geometry(f'+{xpos}+{ypos}') 324 | 325 | 326 | def ask_date( 327 | parent=None, 328 | title='', 329 | firstweekday=6, 330 | startdate=None, 331 | bootstyle='primary' 332 | ): 333 | """Shows a calendar popup and returns the selection. 334 | 335 | Parameters 336 | ---------- 337 | parent : Widget 338 | The parent widget; the popup will appear to the bottom-right of 339 | the parent widget. If no parent is provided, the widget is 340 | centered on the screen. 341 | 342 | title: str 343 | The text that appears on the popup titlebar. By default = ''. 344 | 345 | firstweekday : int 346 | Specifies the first day of the week. ``0`` is Monday, ``6`` is 347 | Sunday (the default). 348 | 349 | startdate : datetime 350 | The date to be in focus when the widget is displayed; 351 | Default = `datetime.today().date()` 352 | 353 | bootstyle : str 354 | The following colors can be used to change the color of the 355 | title and hover / pressed color -> primary, secondary, info, 356 | warning, success, danger, light, dark. 357 | 358 | Returns 359 | ------- 360 | datetime 361 | The date selected; the current date if no date is selected. 362 | """ 363 | chooser = DatePickerPopup( 364 | parent=parent, 365 | title=title, 366 | firstweekday=firstweekday, 367 | startdate=startdate, 368 | bootstyle=bootstyle 369 | ) 370 | return chooser.date_selected 371 | -------------------------------------------------------------------------------- /ttkbootstrap/widgets/meter.py: -------------------------------------------------------------------------------- 1 | import math 2 | import tkinter as tk 3 | from tkinter import ttk 4 | from PIL import Image, ImageTk, ImageDraw 5 | from ttkbootstrap.style.colors import Colors 6 | import ttkbootstrap.style.utility as util 7 | 8 | FULL = 'full' 9 | SEMI = 'semi' 10 | PRIMARY = 'primary' 11 | DEFAULT = 'default' 12 | SECONDARY = 'secondary' 13 | 14 | class Meter(ttk.Frame): 15 | 16 | def __init__( 17 | self, 18 | master=None, 19 | bootstyle=DEFAULT, 20 | arcrange=None, 21 | arcoffset=None, 22 | amounttotal=100, 23 | amountused=0, 24 | wedgesize=0, 25 | metersize=200, 26 | metertype=FULL, 27 | meterthickness=10, 28 | showtext=True, 29 | interactive=False, 30 | stripethickness=0, 31 | textleft=None, 32 | textright=None, 33 | textfont='-size 25 -weight bold', 34 | subtext=None, 35 | subtextstyle=DEFAULT, 36 | subtextfont='-size 10', 37 | **kwargs 38 | ): 39 | """A radial meter that can be used to show progress of long 40 | running operations or the amount of work completed; can also be 41 | used as a dial when set to `interactive=True`. 42 | 43 | This widget is very flexible. There are two primary meter types 44 | which can be set with the `metertype` parameter: 'full' and 45 | 'semi', which shows the arc of the meter in a full or 46 | semi-circle. You can also customize the arc of the circle with 47 | the `arcrange` and `arcoffset` parameters. 48 | 49 | The meter indicator can be displayed as a solid color or with 50 | stripes using the `stripethickness` parameter. By default, the 51 | `stripethickness` is 0, which results in a solid meter 52 | indicator. A higher `stripethickness` results in larger wedges 53 | around the arc of the meter. 54 | 55 | Various text and label options exist. The center text and 56 | meter indicator is formatted with the `meterstyle` parameter. 57 | You can set text on the left and right of this center label 58 | using the `textleft` and `textright` parameters. This is most 59 | commonly used for '$', '%', or other such symbols. 60 | 61 | If you need access to the variables that update the meter, you 62 | you can access these via the `amountusedvar`, `amounttotalvar`, 63 | and the `labelvar`. The value of these properties can also be 64 | retrieved via the `configure` method. 65 | 66 | Parameters 67 | ---------- 68 | master : Widget 69 | The parent widget. 70 | 71 | arcrange : int 72 | The rnage of the arc if degrees from start to end. 73 | Default = None. 74 | 75 | arcoffset : int 76 | The amount to offset the arc's starting position in degrees. 77 | 0 is at 3 o'clock. Default = None. 78 | 79 | amounttotal : int 80 | The maximum value of the meter. Default = 100. 81 | 82 | amountused : int 83 | The current value of the meter; displayed in a center label 84 | if the `showtext` property is set to True. Default = 0. 85 | 86 | wedgesize : int 87 | Sets the length of the indicator wedge around the arc. If 88 | greater than 0, this wedge is set as an indicator centered 89 | on the current meter value. Default = 0. 90 | 91 | metersize : int 92 | The meter is square. This represents the size of one side 93 | if the square as measured in screen units. Default = 200. 94 | 95 | bootstyle : str 96 | Sets the indicator and center text color. One of primary, 97 | secondary, success, info, warning, danger, light, dark. 98 | Default = 'primary' 99 | 100 | metertype : { full, semi } 101 | Displays the meter as a full circle or semi-circle. 102 | Default = 'full'. 103 | 104 | meterthickness : int 105 | The thickness of the indicator. Default = 10. 106 | 107 | showtext : bool 108 | Indicates whether to show the left, center, and right text 109 | labels on the meter. Default = True. 110 | 111 | interactive : bool 112 | Indicates that the user may adjust the meter value with 113 | mouse interaction. Default = False. 114 | 115 | stripethickness : int 116 | The indicator can be displayed as a solid band or as 117 | striped wedges around the arc. If the value is greater than 118 | 0, the indicator changes from a solid to striped, where the 119 | value is the thickness of the stripes (or wedges). 120 | Default = 0. 121 | 122 | textleft : str 123 | A short string inserted to the left of the center text. 124 | 125 | textright: str 126 | A short string inserted to the right of the center text. 127 | 128 | textfont : Union[str, Font] 129 | The font used to render the center text. 130 | Default = '-size 25 -weight bold' 131 | 132 | subtext : str 133 | Supplemental text that appears below the center text. 134 | 135 | subtextstyle : str 136 | The bootstyle color of the subtext. One of primary, 137 | secondary, success, info, warning, danger, light, dark. 138 | The default color is Theme specific and is a lighter 139 | shade based on whether it is a 'light' or 'dark' theme. 140 | 141 | subtextfont : Union[str, Font] 142 | The font used to render the subtext. 143 | Default = '-size 10' 144 | 145 | **kwargs : Dict[str, Any] 146 | Other keyword arguments that are passed directly to the 147 | `ttk.Frame` widget that contains the meter components. 148 | """ 149 | super().__init__(master=master, **kwargs) 150 | 151 | # widget variables 152 | self.amountusedvar = tk.IntVar(value=amountused) 153 | self.amountusedvar.trace_add('write', self._draw_meter) 154 | self.amounttotalvar = tk.IntVar(value=amounttotal) 155 | self.labelvar = tk.StringVar(value=subtext) 156 | 157 | # misc settings 158 | self._set_arc_offset_range(metertype, arcoffset, arcrange) 159 | self._towardsmaximum = True 160 | self._metersize = util.scale_size(self, metersize) 161 | self._meterthickness = util.scale_size(self, meterthickness) 162 | self._stripethickness = stripethickness 163 | self._showtext = showtext 164 | self._wedgesize = wedgesize 165 | 166 | self._textleft = textleft 167 | self._textright = textright 168 | self._textfont = textfont 169 | self._subtext = subtext 170 | self._subtextfont = subtextfont 171 | self._subtextstyle = subtextstyle 172 | self._bootstyle = bootstyle 173 | self._interactive = interactive 174 | self._bindids = {} 175 | 176 | self._setup_widget() 177 | 178 | def _setup_widget(self): 179 | self.meterframe = ttk.Frame( 180 | master=self, 181 | width=self._metersize, 182 | height=self._metersize 183 | ) 184 | self.indicator = ttk.Label(self.meterframe) 185 | self.textframe = ttk.Frame(self.meterframe) 186 | self.textleft = ttk.Label( 187 | master=self.textframe, 188 | text=self._textleft, 189 | font=self._subtextfont, 190 | bootstyle=(self._subtextstyle, 'metersubtxt'), 191 | anchor=tk.S, 192 | padding=(0, 5) 193 | ) 194 | self.textcenter = ttk.Label( 195 | master=self.textframe, 196 | textvariable=self.amountusedvar, 197 | bootstyle=(self._bootstyle, 'meter'), 198 | font=self._textfont 199 | ) 200 | self.textright = ttk.Label( 201 | master=self.textframe, 202 | text=self._textright, 203 | font=self._subtextfont, 204 | bootstyle=(self._subtextstyle, 'metersubtxt'), 205 | anchor=tk.S, 206 | padding=(0, 5) 207 | ) 208 | self.subtext = ttk.Label( 209 | master=self.meterframe, 210 | text=self._subtext, 211 | bootstyle=(self._subtextstyle, 'metersubtxt'), 212 | font=self._subtextfont 213 | ) 214 | 215 | self.bind('<>', self._on_theme_change) 216 | self.bind('<>', self._on_theme_change) 217 | self._set_interactive_bind() 218 | self._draw_base_image() 219 | self._draw_meter() 220 | 221 | # set widget geometery 222 | self.indicator.place(x=0, y=0) 223 | self.meterframe.pack() 224 | self._set_show_text() 225 | 226 | def _set_widget_colors(self): 227 | bootstyle = (self._bootstyle, 'meter', 'label') 228 | ttkstyle = util.ttkstyle_name(string='-'.join(bootstyle)) 229 | textcolor = self._lookup_style_option(ttkstyle, 'foreground') 230 | background = self._lookup_style_option(ttkstyle, 'background') 231 | troughcolor = self._lookup_style_option(ttkstyle, 'space') 232 | self._meterforeground = textcolor 233 | self._meterbackground = Colors.update_hsv(background, vd=-0.1) 234 | self._metertrough = troughcolor 235 | 236 | def _set_meter_text(self): 237 | """Setup and pack the widget labels in the appropriate order""" 238 | self._set_show_text() 239 | self._set_subtext() 240 | 241 | def _set_subtext(self): 242 | if self._subtextfont: 243 | if self._showtext: 244 | self.subtext.place(relx=0.5, rely=0.6, anchor=tk.CENTER) 245 | else: 246 | self.subtext.place(relx=0.5, rely=0.5, anchor=tk.CENTER) 247 | 248 | def _set_show_text(self): 249 | self.textframe.pack_forget() 250 | self.textcenter.pack_forget() 251 | self.textleft.pack_forget() 252 | self.textright.pack_forget() 253 | self.subtext.pack_forget() 254 | #self.update_idletasks() 255 | 256 | if self._showtext: 257 | if self._subtext: 258 | self.textframe.place(relx=0.5, rely=0.45, anchor=tk.CENTER) 259 | else: 260 | self.textframe.place(relx=0.5, rely=0.5, anchor=tk.CENTER) 261 | 262 | self._set_text_left() 263 | self._set_text_center() 264 | self._set_text_right() 265 | self._set_subtext() 266 | 267 | def _set_text_left(self): 268 | if self._showtext and self._textleft: 269 | self.textleft.pack(side=tk.LEFT, fill=tk.Y) 270 | 271 | def _set_text_center(self): 272 | if self._showtext: 273 | self.textcenter.pack(side=tk.LEFT, fill=tk.Y) 274 | 275 | def _set_text_right(self): 276 | self.textright.configure(text=self._textright) 277 | if self._showtext and self._textright: 278 | self.textright.pack(side=tk.RIGHT, fill=tk.Y) 279 | 280 | def _set_interactive_bind(self): 281 | seq1 = '' 282 | seq2 = '' 283 | 284 | if self._interactive: 285 | self._bindids[seq1] = self.indicator.bind(seq1, self._on_dial_interact) 286 | self._bindids[seq2] = self.indicator.bind(seq2, self._on_dial_interact) 287 | return 288 | 289 | if seq1 in self._bindids: 290 | self.indicator.unbind(seq1, self._bindids.get(seq1)) 291 | self.indicator.unbind(seq2, self._bindids.get(seq2)) 292 | self._bindids.clear() 293 | 294 | def _set_arc_offset_range(self, metertype, arcoffset, arcrange): 295 | if metertype == SEMI: 296 | self._arcoffset = 135 if arcoffset is None else arcoffset 297 | self._arcrange = 270 if arcrange is None else arcrange 298 | else: 299 | self._arcoffset = -90 if arcoffset is None else arcoffset 300 | self._arcrange = 360 if arcrange is None else arcrange 301 | self._metertype = metertype 302 | 303 | def _draw_meter(self, *_): 304 | """Draw a meter""" 305 | img = self._base_image.copy() 306 | draw = ImageDraw.Draw(img) 307 | if self._stripethickness > 0: 308 | self._draw_striped_meter(draw) 309 | else: 310 | self._draw_solid_meter(draw) 311 | 312 | self._meterimage = ImageTk.PhotoImage( 313 | img.resize((self._metersize, self._metersize), Image.CUBIC) 314 | ) 315 | self.indicator.configure(image=self._meterimage) 316 | 317 | def _draw_base_image(self): 318 | """Draw base image to be used for subsequent updates""" 319 | self._set_widget_colors() 320 | self._base_image = Image.new( 321 | mode='RGBA', 322 | size=(self._metersize*5, self._metersize*5) 323 | ) 324 | draw = ImageDraw.Draw(self._base_image) 325 | 326 | x1 = y1 = self._metersize * 5 - 20 327 | width = self._meterthickness * 5 328 | # striped meter 329 | if self._stripethickness > 0: 330 | _from = self._arcoffset 331 | _to = self._arcrange + self._arcoffset 332 | _step = 2 if self._stripethickness == 1 else self._stripethickness 333 | for x in range(_from, _to, _step): 334 | draw.arc( 335 | xy=(0, 0, x1, y1), 336 | start=x, 337 | end=x + self._stripethickness - 1, 338 | fill=self._metertrough, 339 | width=width 340 | ) 341 | # solid meter 342 | else: 343 | draw.arc( 344 | xy=(0, 0, x1, y1), 345 | start=self._arcoffset, 346 | end=self._arcrange+self._arcoffset, 347 | fill=self._metertrough, 348 | width=width 349 | ) 350 | 351 | def _draw_solid_meter(self, draw): 352 | """Draw a solid meter. 353 | 354 | Parameters 355 | ---------- 356 | draw : ImageDraw.Draw 357 | An object used to draw an arc on the meter. 358 | """ 359 | x1 = y1 = self._metersize * 5 - 20 360 | width = self._meterthickness * 5 361 | 362 | if self._wedgesize > 0: 363 | meter_value = self._meter_value() 364 | draw.arc( 365 | xy=(0, 0, x1, y1), 366 | start=meter_value - self._wedgesize, 367 | end=meter_value + self._wedgesize, 368 | fill=self._meterforeground, 369 | width=width 370 | ) 371 | else: 372 | draw.arc( 373 | xy=(0, 0, x1, y1), 374 | start=self._arcoffset, 375 | end=self._meter_value(), 376 | fill=self._meterforeground, 377 | width=width 378 | ) 379 | 380 | def _draw_striped_meter(self, draw): 381 | """Draw a striped meter 382 | 383 | Parameters 384 | ---------- 385 | draw : ImageDraw.Draw 386 | An object usd to draw an arc on the meter. 387 | """ 388 | meter_value = self._meter_value() 389 | x1 = y1 = self._metersize * 5 - 20 390 | width = self._meterthickness * 5 391 | 392 | if self._wedgesize > 0: 393 | draw.arc( 394 | xy=(0, 0, x1, y1), 395 | start=meter_value - self._wedgesize, 396 | end=meter_value + self._wedgesize, 397 | fill=self._meterforeground, 398 | width=width 399 | ) 400 | else: 401 | _from = self._arcoffset 402 | _to = meter_value - 1 403 | _step = self._stripethickness 404 | for x in range(_from, _to, _step): 405 | draw.arc( 406 | xy=(0, 0, x1, y1), 407 | start=x, 408 | end=x + self._stripethickness - 1, 409 | fill=self._meterforeground, 410 | width=width 411 | ) 412 | 413 | def _meter_value(self) -> int: 414 | """Calculate the value to be used to draw the arc length of 415 | the progress meter.""" 416 | value = int( 417 | (self['amountused'] / self['amounttotal']) * 418 | self._arcrange + 419 | self._arcoffset 420 | ) 421 | return value 422 | 423 | def _on_theme_change(self, *_): 424 | self._draw_base_image() 425 | self._draw_meter() 426 | 427 | def _on_dial_interact(self, e): 428 | """Callback for mouse drag motion on meter indicator 429 | 430 | Parameters 431 | ---------- 432 | e : Event 433 | Event callback for drag motion. 434 | """ 435 | dx = e.x - self._metersize // 2 436 | dy = e.y - self._metersize // 2 437 | rads = math.atan2(dy, dx) 438 | degs = math.degrees(rads) 439 | 440 | if degs > self._arcoffset: 441 | factor = degs - self._arcoffset 442 | else: 443 | factor = 360 + degs - self._arcoffset 444 | 445 | # clamp the value between 0 and `amounttotal` 446 | amounttotal = self.amounttotalvar.get() 447 | amountused = int(amounttotal / self._arcrange * factor) 448 | if amountused < 0: 449 | self.amountusedvar.set(0) 450 | elif amountused > amounttotal: 451 | self.amountusedvar.set(amounttotal) 452 | else: 453 | self.amountusedvar.set(amountused) 454 | 455 | def _lookup_style_option(self, style, option): 456 | """Wrapper around the tcl style lookup command 457 | 458 | Parameters 459 | ---------- 460 | style : str 461 | The name of the style used for rendering the widget. 462 | 463 | option: str 464 | The option to lookup from the style option database. 465 | 466 | Returns 467 | ------- 468 | Any 469 | The value of the option looked up. 470 | """ 471 | value = self.tk.call( 472 | "ttk::style", "lookup", style, '-%s' % option, None, None 473 | ) 474 | return value 475 | 476 | def _configure_get(self, cnf): 477 | """Override the configuration get method""" 478 | if cnf == 'arcrange': 479 | return self._arcrange 480 | elif cnf == 'arcoffset': 481 | return self._arcoffset 482 | elif cnf == 'amounttotal': 483 | return self.amounttotalvar.get() 484 | elif cnf == 'amountused': 485 | return self.amountusedvar.get() 486 | elif cnf == 'interactive': 487 | return self._interactive 488 | elif cnf == 'subtextfont': 489 | return self._subtextfont 490 | elif cnf == 'subtextstyle': 491 | return self._subtextstyle 492 | elif cnf == 'subtext': 493 | return self._subtext 494 | elif cnf == 'metersize': 495 | return self._metersize 496 | elif cnf == 'bootstyle': 497 | return self._bootstyle 498 | elif cnf == 'metertype': 499 | return self._metertype 500 | elif cnf == 'meterthickness': 501 | return self._meterthickness 502 | elif cnf == 'showtext': 503 | return self._showtext 504 | elif cnf == 'stripethickness': 505 | return self._stripethickness 506 | elif cnf == 'textleft': 507 | return self._textleft 508 | elif cnf == 'textright': 509 | return self._textright 510 | elif cnf == 'textfont': 511 | return self._textfont 512 | elif cnf == 'wedgesize': 513 | return self._wedgesize 514 | else: 515 | return super(ttk.Frame, self).configure(cnf) 516 | 517 | def _configure_set(self, **kwargs): 518 | """Override the configuration set method""" 519 | meter_text_changed = False 520 | 521 | if 'arcrange' in kwargs: 522 | self._arcrange = kwargs.pop('arcrange') 523 | if 'arcoffset' in kwargs: 524 | self._arcoffset = kwargs.pop('arcoffset') 525 | if 'amounttotal' in kwargs: 526 | amounttotal = kwargs.pop('amounttotal') 527 | self.amounttotalvar.set(amounttotal) 528 | if 'amountused' in kwargs: 529 | amountused = kwargs.pop('amountused') 530 | self.amountusedvar.set(amountused) 531 | if 'interactive' in kwargs: 532 | self._interactive = kwargs.pop('interactive') 533 | self._set_interactive_bind() 534 | if 'subtextfont' in kwargs: 535 | self._subtextfont = kwargs.pop('subtextfont') 536 | self.subtext.configure(font=self._subtextfont) 537 | self.textleft.configure(font=self._subtextfont) 538 | self.textright.configure(font=self._subtextfont) 539 | if 'subtextstyle' in kwargs: 540 | self._subtextstyle = kwargs.pop('subtextstyle') 541 | self.subtext.configure(bootstyle=[self._subtextstyle, 'meter']) 542 | if 'metersize' in kwargs: 543 | self._metersize = util.scale_size(kwargs.pop('metersize')) 544 | self.meterframe.configure(height=self._metersize, width=self._metersize) 545 | if 'bootstyle' in kwargs: 546 | self._bootstyle = kwargs.pop('bootstyle') 547 | self.textcenter.configure(bootstyle=[self._bootstyle, 'meter']) 548 | if 'metertype' in kwargs: 549 | self._metertype = kwargs.pop('metertype') 550 | if 'meterthickness' in kwargs: 551 | self._meterthickness = self.scale_size(kwargs.pop('meterthickness')) 552 | if 'stripethickness' in kwargs: 553 | self._stripethickness = kwargs.pop('stripethickness') 554 | if 'subtext' in kwargs: 555 | self._subtext = kwargs.pop('subtext') 556 | self.subtext.configure(text=self._subtext) 557 | meter_text_changed = True 558 | if 'textleft' in kwargs: 559 | self._textleft = kwargs.pop('textleft') 560 | self.textleft.configure(text=self._textleft) 561 | meter_text_changed = True 562 | if 'textright' in kwargs: 563 | self._textright = kwargs.pop('textright') 564 | meter_text_changed = True 565 | if 'showtext' in kwargs: 566 | self._showtext = kwargs.pop('showtext') 567 | meter_text_changed = True 568 | if 'textfont' in kwargs: 569 | self._textfont = kwargs.pop('textfont') 570 | self.textcenter.configure(font=self._textfont) 571 | if 'wedgesize' in kwargs: 572 | self._wedgesize = kwargs.pop('wedgesize') 573 | 574 | if meter_text_changed: 575 | self._set_meter_text() 576 | 577 | try: 578 | if self._metertype: 579 | self._set_arc_offset_range( 580 | metertype=self._metertype, 581 | arcoffset=self._arcoffset, 582 | arcrange=self._arcrange 583 | ) 584 | except AttributeError: 585 | return 586 | 587 | self._draw_base_image() 588 | self._draw_meter() 589 | 590 | # pass remaining configurations to `ttk.Frame.configure` 591 | super(ttk.Frame, self).configure(**kwargs) 592 | 593 | def __getitem__(self, key: str): 594 | return self._configure_get(key) 595 | 596 | def __setitem__(self, key: str, value) -> None: 597 | self._configure_set(**{key: value}) 598 | 599 | def configure(self, cnf=None, **kwargs): 600 | if cnf is not None: 601 | return self._configure_get(cnf) 602 | else: 603 | self._configure_set(**kwargs) 604 | 605 | def step(self, delta=1): 606 | """Increase the indicator value by `delta` 607 | 608 | The default increment is 1. The indicator will reverse 609 | direction and count down once it reaches the maximum value. 610 | 611 | Parameters 612 | ---------- 613 | delta : int 614 | The amount to change the indicator 615 | """ 616 | amountused = self.amountusedvar.get() 617 | amounttotal = self.amounttotalvar.get() 618 | if amountused >= amounttotal: 619 | self._towardsmaximum = True 620 | self.amountusedvar.set(amountused - delta) 621 | elif amountused <= 0: 622 | self._towardsmaximum = False 623 | self.amountusedvar.set(amountused + delta) 624 | elif self._towardsmaximum: 625 | self.amountusedvar.set(amountused - delta) 626 | else: 627 | self.amountusedvar.set(amountused + delta) 628 | -------------------------------------------------------------------------------- /Tile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """ 3 | @Project :CountBoard 4 | @File :Tile.py 5 | @Author :Gao yongxian 6 | @Date :2021/11/8 11:00 7 | @contact: g1695698547@163.com 8 | """ 9 | import json 10 | import queue 11 | import random 12 | import tkinter as tk 13 | import traceback 14 | import webbrowser 15 | from datetime import datetime 16 | from pathlib import Path 17 | from queue import Queue 18 | from threading import Thread 19 | from tkinter import * 20 | import ttkbootstrap as ttk 21 | from tkinter.filedialog import askopenfilename 22 | import pywintypes 23 | import requests 24 | import win32gui 25 | from markdown2 import Markdown 26 | from CustomWindow import CustomWindow 27 | from TkHtmlView import TkHtmlView 28 | from ttkbootstrap.widgets.date_entry import DateEntry 29 | from WindowEffect import WindowEffect 30 | 31 | 32 | class Tile(CustomWindow): 33 | """磁贴窗口""" 34 | 35 | def __init__(self, bg, exe_dir_path, mydb_dict, mysetting_dict, tile_queue, logger, *args, **kwargs): 36 | self.root = tk.Toplevel() 37 | super().__init__(*args, **kwargs) 38 | 39 | # 布局初始化 40 | self.__init__2(bg, exe_dir_path, mydb_dict, mysetting_dict, tile_queue, logger, *args, **kwargs) 41 | 42 | # 开启更新UI队列 43 | self.root.after(1, self.relay) 44 | 45 | # 开启耗时操作线程 46 | self.initialization_thread = Thread(target=self.initialization) 47 | self.initialization_thread.setDaemon(True) 48 | self.initialization_thread.start() 49 | 50 | self.root.mainloop() 51 | 52 | def __init__2(self, bg, exe_dir_path, mydb_dict, mysetting_dict, tile_queue, logger, *args, **kwargs): 53 | """ 54 | 布局初始化(必须在主线程进行的操作,比如设置主题,窗口布局,变量初始化) 55 | """ 56 | # 传参 57 | self.logger = logger 58 | self.bg = bg 59 | self.exe_dir_path = exe_dir_path 60 | self.mydb_dict = mydb_dict 61 | self.mysetting_dict = mysetting_dict 62 | self.tile_queue = tile_queue # 子线程与主线程的队列作为中继 63 | # 布局 64 | self.frame_top = Frame(self.root, bg=self.bg) 65 | # self.frame_top.configure(highlightbackground="#202020") 66 | # self.frame_top.configure(highlightthickness=1) 67 | self.frame_top.pack(side=TOP, fill="both", expand=True) 68 | # 画布 69 | self.canvas = Canvas(self.frame_top) 70 | self.canvas.config(highlightthickness=1) 71 | self.canvas.configure(highlightbackground="#000000") 72 | self.canvas.pack(side=tk.LEFT, fill="both", expand=True) 73 | # 事件 74 | self.canvas.bind('', self._on_tap) 75 | self.canvas.bind('', self._on_release) 76 | self.canvas.bind('', self._on_move) 77 | self.canvas.bind("", self._on_right_menu) # 绑定右键鼠标事件 78 | self.menubar = Menu(self.canvas, tearoff=False) # 创建一个菜单 79 | 80 | # 窗口特效 81 | self.hwnd = pywintypes.HANDLE(int(self.root.frame(), 16)) 82 | self.window_effect = WindowEffect() 83 | 84 | # 保存单个日程的tag 85 | self.tag_name = tk.StringVar() 86 | # 父窗口句柄 87 | self.hhwnd = 0 88 | 89 | def _on_right_menu(self, event): 90 | self.tile_queue.put(("right_menu", (event.x_root, event.y_root))) 91 | 92 | def right_menu(self, content): 93 | event_x_root = content[0] 94 | event_y_root = content[1] 95 | self.menubar.delete(0, END) 96 | self.menubar.add_command(label='新建日程', command=self.open_new_winodw) 97 | if self.tag_name.get() != "": 98 | self.menubar.add_command(label='编辑日程', command=self.open_edit_winodw) 99 | self.menubar.add_command(label='删除日程', command=self.open_del_winodw) 100 | self.menubar.post(event_x_root, event_y_root) 101 | 102 | def open_new_winodw(self): 103 | """打开新窗口""" 104 | self.tile_queue.put("NewTaskWindow") 105 | 106 | def open_edit_winodw(self): 107 | """打开编辑窗口""" 108 | self.tile_queue.put("EidtTaskWindow") 109 | 110 | def open_del_winodw(self): 111 | self.tile_queue.put("DelTaskWindow") 112 | 113 | def modify_transparent(self, tile_transparent): 114 | """修改透明度""" 115 | if tile_transparent == 1: 116 | self.tile_transparent = tile_transparent 117 | else: 118 | self.tile_transparent = tile_transparent 119 | 120 | self.set_theme(self.tile_theme_name) 121 | 122 | '''-----------------------------------更新UI 线程-----------------------------------------------''' 123 | 124 | def relay(self): 125 | """更新UI队列""" 126 | try: 127 | # 队列不可阻塞 128 | content = self.tile_queue.get(False) 129 | self.logger.info(self.__class__.__name__ + " --Queue接收到消息:" + str(content)) 130 | # 回调函数要在之前回调,因为如果在队列中打开窗体,窗体的 mailoop 会让函数卡死,死循环. 131 | self.root.after(1, self.relay) 132 | # 具体的更新Ui操作 133 | self.UpdateUI(content) 134 | except queue.Empty: 135 | self.root.after(200, self.relay) 136 | 137 | def UpdateUI(self, content): 138 | if content == "update_theme_Acrylic": 139 | self.set_theme("Acrylic") 140 | elif content == "update_theme_Aero": 141 | self.set_theme("Aero") 142 | elif content == "set_task_right_angle": 143 | self.set_task_radius(0) 144 | elif content == "set_task_round_angle": 145 | self.set_task_radius(25) 146 | elif content == "set_window_top": 147 | self.set_top(1) 148 | elif content == "cancel_window_top": 149 | self.set_top(0) 150 | elif content == "refresh_tasks": 151 | self.tasks.refresh_tasks() 152 | elif content == "show_all_tasks": 153 | self.tasks.show_all() 154 | elif content == "del_all": 155 | self.tasks.del_all() 156 | 157 | elif content == "NewTaskWindow": 158 | # 打开新建日程 159 | NewTaskWindow(title="新建日程", height=180, tile_queue=self.tile_queue) 160 | 161 | elif content == "exit": 162 | self.exit() 163 | 164 | elif content == "update_sqlite": 165 | self.tasks.update_sqlite_time() 166 | 167 | elif content == "set_tag_name_": 168 | self.tag_name.set("") 169 | 170 | elif content == "set_data": 171 | # 获取边框颜色 172 | self.windows_border_color = "#" + str(hex(self.windows_border))[-1] * 6 173 | self.canvas.config(highlightbackground=self.windows_border_color) 174 | self.tasks_border_color = "#" + str(hex(self.tasks_border))[-1] * 6 175 | # 设置主题 176 | self.set_theme(tile_theme_name=self.tile_theme_name, remove_=False) 177 | self.set_background(bg=self.bg) 178 | self.set_task_radius(self.task_radius, refresh_=False) 179 | self.set_top(self.tile_top) 180 | self.update_win_mode(self.win_mode) 181 | # 任务列表初始化 182 | self.tasks = Tasks(tile_queue=self.tile_queue, pre_window=self) 183 | 184 | elif content == "set_background": 185 | self.set_background(bg=self.bg) 186 | 187 | elif content == "EidtTaskWindow": 188 | for value in self.mydb_dict.itervalues(): 189 | if value[4] == self.tag_name.get(): 190 | NewTaskWindow( 191 | title="修改日程", 192 | height=180, 193 | tile_queue=self.tile_queue, 194 | value=value) 195 | return 1 196 | print("no!" + self.tag_name.get()) 197 | self.tag_name.set("") 198 | 199 | elif content == "DelTaskWindow": 200 | for value in self.mydb_dict.itervalues(): 201 | if value[4] == self.tag_name.get(): 202 | self.tasks.del_one(value[0]) 203 | self.tile_queue.put("refresh_tasks") 204 | self.tag_name.set("") 205 | 206 | elif type(content) == tuple: 207 | if content[0] == "del_one": 208 | self.tasks.del_one(content[1]) 209 | elif content[0] == "add_one": 210 | self.tasks.add_one(content[1]) 211 | elif content[0] == "modify_offset": 212 | self.modify_offset(content[1]) 213 | elif content[0] == "modify_auto_margin": 214 | self.modify_auto_margin(content[1]) 215 | elif content[0] == "modify_transparent": 216 | self.modify_transparent(content[1]) 217 | elif content[0] == "update_win_mode": 218 | self.update_win_mode(content[1]) 219 | elif content[0] == "right_menu": 220 | self.right_menu(content[1]) 221 | elif content[0] == "set_tag_name": 222 | self.tag_name.set(content[1]) 223 | 224 | def update_win_mode(self, win_mode): 225 | # 实现代码很简单 226 | # 关于嵌入桌面的笔记: 227 | # 1.使用工具winspy(网上自行下载)查看窗口句柄 228 | # 2.SHELLDLL_DefView是你的桌面窗口类名,它的子窗口是图标窗口,但是默认情况它是在“Progman”窗口下。 229 | # 3.一旦你点击右下角的桌面按钮,或者使用Win+Tab, Win+D快捷键,你会发现桌面窗口跑到了“WorkerW”窗口下。 230 | # 4.经过测试发现SHELLDLL_DefView很灵活,如果你把自己的窗口设置为Progman的子窗口,那么SHELLDLL_DefView会自动设置顶层窗口, 231 | # 自己成为自己的主窗口。而你一旦进行桌面操作,它就自动跑到“WorkerW”窗口下。 232 | # 5.发现如果设置Progman的子窗口,并不能显示在桌面,只有在“WorkerW”窗口下才可以。 233 | # 6.另外“WorkerW”窗口默认是没有激活的,只有进行桌面相关操作才会激活(向桌面发送 0x052c) 234 | # 7.最后发现想要嵌入桌面,一种简单的做法就是直接把你的窗体的父窗口设置成“WorkerW”窗口 235 | 236 | # 网上的blog: 237 | # 默认的桌面窗口是“SHELLDLL_DefView”,在“Progman”窗口下,你写个程序检测,会发现,点击显示桌面后,这时,激活任意程序窗口,这个特殊状态就消失了,桌面又回到了“Progman”窗口下。 238 | # 其实系统的显示桌面功能,并不是将桌面上的所有应用程序窗口隐藏或最小化,而是一个特殊的状态,“WorkerW”默认是隐藏,当要显示桌面时,会被显示出来,并且窗口Z次序跑到顶层, 239 | # 然后将“SHELLDLL_DefView”桌面的父窗口由“Progman”改为“WorkerW”,这时的应用程序可能也是在某种特殊状态下。所以你用IsWindowVisble、IsIconic等函数是检测不出来的,除非点了显示桌面后,又激活了任意某个窗口。 240 | # 1. 使用win32api.EnumWindows()枚举窗口; 241 | # 2. 先找到"SHELLDLL_DefView"窗口的父窗口; 242 | # 3. 再找到该窗口的下一层窗口“WorkerW”; 243 | # 4. 将我们的窗口设为该“WorkerW”窗口的子窗口即可。 244 | 245 | if win_mode == "独立窗体": 246 | win32gui.SetParent(self.hwnd, 0) 247 | else: 248 | pWnd = win32gui.FindWindow("Progman", "Program Manager") 249 | win32gui.SendMessage(pWnd, 0x052c, 0, 0) 250 | win32gui.EnumWindows(self.get_workw_hwnd, 0) 251 | win32gui.SetParent(self.hwnd, self.hhwnd) 252 | 253 | def get_workw_hwnd(self, hwnd, lParam): 254 | """遍历找到workw""" 255 | if win32gui.IsWindowVisible(hwnd): 256 | hNextWin = win32gui.FindWindowEx(hwnd, None, "SHELLDLL_DefView", None) 257 | if hNextWin: 258 | self.hhwnd = hwnd 259 | 260 | def set_theme(self, tile_theme_name, remove_=True): 261 | """ 262 | 更新主题:remove_是否先去除效果 263 | """ 264 | self.tile_theme_name = tile_theme_name 265 | if remove_: 266 | self.window_effect.removeBackgroundEffect(self.hwnd) 267 | if tile_theme_name == "Acrylic": 268 | self.window_effect.setAcrylicEffect(self.hwnd, self.tile_transparent) 269 | else: 270 | self.window_effect.setAeroEffect(self.hwnd) 271 | 272 | def set_task_radius(self, task_radius, refresh_=True): 273 | """ 274 | 设置是否圆角,refresh_是否刷新列表 275 | """ 276 | self.task_radius = task_radius 277 | 278 | if refresh_: 279 | self.tasks.refresh_tasks() 280 | 281 | def set_background(self, bg): 282 | """设置背景""" 283 | self.root.configure(bg=bg) 284 | self.frame_top.configure(bg=bg) 285 | self.canvas.configure(bg=bg) 286 | self.canvas.config(highlightbackground=self.windows_border_color) 287 | 288 | def random_color(self): 289 | """随机颜色""" 290 | colors1 = '0123456789ABCDEF' 291 | num = "#" 292 | for i in range(6): 293 | num += random.choice(colors1) 294 | return num 295 | 296 | def set_top(self, flag): 297 | """ 298 | 设置是否置顶 299 | """ 300 | if flag == 1: 301 | self.root.wm_attributes('-topmost', 1) 302 | else: 303 | self.root.wm_attributes('-topmost', 0) 304 | 305 | '''-----------------------------------耗时操作线程-----------------------------------------------''' 306 | 307 | def initialization(self): 308 | """ 309 | 执行耗时操作(先在布局初始化中设置变量,然后此线程中动态修改) 310 | """ 311 | # 数据库读取数据 312 | self.tile_theme_name = self.mysetting_dict['tile_theme_name'][0] 313 | self.task_radius = self.mysetting_dict['task_radius'][0] 314 | self.task_geometry = self.mysetting_dict['task_geometry'][0] 315 | self.tile_top = self.mysetting_dict['tile_top'][0] 316 | self.tile_transparent = self.mysetting_dict['tile_transparent'][0] 317 | self.tasks_border = self.mysetting_dict['tasks_border'][0] 318 | self.windows_border = self.mysetting_dict['windows_border'][0] 319 | self.win_mode = self.mysetting_dict["win_mode"][0] 320 | # 数据初始化 321 | self.tile_queue.put("set_data") 322 | # 展示所以数据 323 | self.tile_queue.put("show_all_tasks") 324 | 325 | '''-----------------------------------重写父类方法-----------------------------------------------''' 326 | 327 | def _on_release(self, event, *kw): 328 | if self.tile_theme_name == "Acrylic": 329 | self.window_effect.setAcrylicEffect(self.hwnd, self.tile_transparent) 330 | self.set_background(bg=self.bg) 331 | super()._on_release(event, mysetting_dict=self.mysetting_dict) 332 | 333 | def _on_tap(self, event): 334 | if self.tile_theme_name == "Acrylic": 335 | self.set_background(bg="grey") 336 | self.window_effect.removeBackgroundEffect(self.hwnd) 337 | super()._on_tap(event) 338 | 339 | 340 | class Tasks: 341 | """任务列表""" 342 | 343 | def __init__(self, tile_queue, pre_window, **kwargs): 344 | 345 | # 传参 346 | self.pre_window = pre_window 347 | self.tile_queue = tile_queue 348 | # 数据初始化 349 | self.pre_window_root = pre_window.root 350 | self.exe_dir_path = pre_window.exe_dir_path 351 | self.canvas = pre_window.canvas 352 | self.mydb_dict = pre_window.mydb_dict 353 | self.mysetting_dict = pre_window.mysetting_dict 354 | self.tasks_border_color = pre_window.tasks_border_color 355 | 356 | # 更新时间 357 | self.update_sqlite_time() 358 | 359 | def update_sqlite_time(self): 360 | # 初始化更新时间 361 | from datetime import datetime 362 | for key, value in self.mydb_dict.iteritems(): 363 | startdate = datetime.today() 364 | enddate = datetime.strptime(value[1], '%Y-%m-%d') 365 | days = str((enddate - startdate).days) 366 | self.mydb_dict[key] = [value[0], value[1], days, value[3], value[4], value[5]] 367 | 368 | def __get_int_day(self, value): 369 | """按照时间排序(返回第三个值,时间值)""" 370 | return int(value[2]) 371 | 372 | def __round_rectangle(self, x1, y1, x2, y2, radius=25, **kwargs): 373 | """画长方形""" 374 | points = [x1 + radius, y1, 375 | x1 + radius, y1, 376 | x2 - radius, y1, 377 | x2 - radius, y1, 378 | x2, y1, 379 | x2, y1 + radius, 380 | x2, y1 + radius, 381 | x2, y2 - radius, 382 | x2, y2 - radius, 383 | x2, y2, 384 | x2 - radius, y2, 385 | x2 - radius, y2, 386 | x1 + radius, y2, 387 | x1 + radius, y2, 388 | x1, y2, 389 | x1, y2 - radius, 390 | x1, y2 - radius, 391 | x1, y1 + radius, 392 | x1, y1 + radius, 393 | x1, y1] 394 | # self.canvas.create_polygon(points, **kwargs, smooth=True, width=1, outline="#080808") 395 | self.canvas.create_polygon(points, **kwargs, smooth=True, width=1, outline=self.tasks_border_color) 396 | 397 | def __handler(self, fun, **kwds): 398 | # 实际上是可以直接 lambda: button.bind("", lambda e: handler(e, a=1, b=2, c=3)) 399 | return lambda event, fun=fun, kwds=kwds: fun(event, **kwds) 400 | 401 | def __add_task(self, value): 402 | """添加每一项任务""" 403 | self.task_main_text = value[0] 404 | self.task_time_text = value[1] 405 | mode = self.mysetting_dict["mode"][0] 406 | if mode == "普通模式": 407 | self.task_countdown_text = str(int(value[2]) + 1) 408 | else: 409 | self.task_countdown_text = value[2] 410 | self.task_color = value[3] 411 | self.task_tag_name = value[4] # tag是组件的标识符 412 | self.task_text_color = value[5] 413 | self.task_text_color = value[5] 414 | 415 | self.__round_rectangle( 416 | self.task_margin_x, 417 | self.task_y, 418 | self.task_margin_x + self.task_width, 419 | self.task_y + self.task_height, 420 | radius=self.task_radius, 421 | fill=self.task_color, 422 | tag=(self.task_tag_name)) 423 | 424 | self.canvas.create_text( 425 | self.task_margin_x + self.task_width / 25, 426 | self.task_y + self.task_height / 9, 427 | text=self.task_main_text, 428 | font=('microsoft yahei', self.title_scale, 'normal'), 429 | fill=self.task_text_color, 430 | anchor="nw", 431 | justify=LEFT, 432 | tag=(self.task_tag_name)) 433 | 434 | self.canvas.create_text( 435 | self.task_margin_x + self.task_width / 25, 436 | self.task_y + self.task_height * 7 / 8, 437 | text=self.task_time_text, 438 | font=('Times', self.time_scale, 'normal'), 439 | fill=self.task_text_color, 440 | anchor="sw", 441 | justify=LEFT, 442 | tag=(self.task_tag_name)) 443 | 444 | self.canvas.create_text( 445 | self.task_margin_x + self.task_width - self.task_width / 20, 446 | self.task_y + self.task_height / 2, 447 | text=self.task_countdown_text + "天", 448 | font=('microsoft yahei', self.count_scale, 'bold'), 449 | fill=self.task_text_color, 450 | anchor="e", # 以右侧为毛点 451 | justify=RIGHT, 452 | tag=(self.task_tag_name)) 453 | 454 | # 添加绑定函数 455 | self.canvas.tag_bind( 456 | self.task_tag_name, 457 | '', 458 | func=self.__handler(self.__double_click, task_tag_name=self.task_tag_name)) 459 | self.canvas.tag_bind( 460 | self.task_tag_name, 461 | '', 462 | func=self.__handler(self.__right_click, task_tag_name=self.task_tag_name)) 463 | # self.canvas.unbind('') 464 | 465 | def __right_click(self, event, task_tag_name): 466 | self.tile_queue.put(("set_tag_name", task_tag_name)) 467 | 468 | def __double_click(self, event, task_tag_name): 469 | for value in self.mydb_dict.itervalues(): 470 | if value[4] == task_tag_name: 471 | NewTaskWindow( 472 | title="修改日程", 473 | height=180, 474 | tile_queue=self.tile_queue, 475 | value=value) 476 | return 1 477 | print("no!" + task_tag_name) 478 | 479 | def add_one(self, value): 480 | self.mydb_dict[value[0]] = value 481 | 482 | def del_one(self, value): 483 | self.mydb_dict.__delitem__(value) 484 | 485 | def refresh_tasks(self): 486 | # 画布删除,重新画 487 | self.canvas.delete("all") 488 | self.show_all() 489 | 490 | def del_all(self): 491 | """删除所有数据""" 492 | self.canvas.delete("all") 493 | self.tile_queue.put("set_tag_name_") 494 | for key in self.mydb_dict.iterkeys(): 495 | self.mydb_dict.__delitem__(key) 496 | 497 | def show_all(self): 498 | """展示所有数据""" 499 | self.task_radius = self.mysetting_dict["task_radius"][0] 500 | self.task_geometry = self.mysetting_dict["task_geometry"][0] 501 | self.tile_geometry = self.mysetting_dict["tile_geometry"][0] 502 | self.time_scale = self.mysetting_dict["time_scale"][0] 503 | self.title_scale = self.mysetting_dict["title_scale"][0] 504 | self.count_scale = self.mysetting_dict["count_scale"][0] 505 | self.tasks_border = self.mysetting_dict['tasks_border'][0] 506 | self.windows_border = self.mysetting_dict['windows_border'][0] 507 | 508 | self.windows_border_color = "#" + str(hex(self.windows_border))[-1] * 6 509 | self.canvas.config(highlightbackground=self.windows_border_color) 510 | 511 | self.tasks_border_color = "#" + str(hex(self.tasks_border))[-1] * 6 512 | 513 | self.task_width = self.task_geometry[0] # 高度,宽度,是否圆角 514 | self.task_height = self.task_geometry[1] 515 | self.task_margin_x = self.task_geometry[2] # x左右边距,y上下边距 516 | self.task_margin_y = self.task_geometry[3] 517 | 518 | self.canvas.config(highlightbackground=self.windows_border_color) 519 | self.tasks_border_color = "#" + str(hex(self.tasks_border))[-1] * 6 520 | 521 | self.canvas.delete("all") 522 | 523 | self.task_y = self.task_margin_y 524 | 525 | # print("数据库中的tile_geometry:", self.tile_geometry) 526 | 527 | # 没有任务项目时的大小 528 | self.pre_window_root.geometry("%dx%d+%d+%d" % (self.task_width + self.task_margin_x * 2, 529 | self.task_y + self.task_height + self.task_margin_y, 530 | self.tile_geometry[2], 531 | self.tile_geometry[3])) 532 | 533 | # print("没有任务项目时的tile_geometry:", (self.task_width + self.task_margin_x * 2, 534 | # self.task_y + self.task_height + self.task_margin_y, 535 | # self.tile_geometry[2], 536 | # self.tile_geometry[3])) 537 | 538 | for value in sorted(self.mydb_dict.itervalues(), key=self.__get_int_day): 539 | self.__add_task(value) 540 | 541 | self.task_y = self.task_y + self.task_height + self.task_margin_y # 更新新添加的高度 542 | 543 | self.pre_window_root.geometry("%dx%d+%d+%d" % (self.task_width + self.task_margin_x * 2, 544 | self.task_y, 545 | self.tile_geometry[2], 546 | self.tile_geometry[3])) 547 | self.mysetting_dict["tile_geometry"] = [(self.task_width + self.task_margin_x * 2, 548 | self.task_y, 549 | self.tile_geometry[2], 550 | self.tile_geometry[3])] 551 | 552 | 553 | class NewTaskWindow(CustomWindow): 554 | """新建日程 or 修改日程""" 555 | 556 | def __init__(self, tile_queue, *args, **kwargs): 557 | self.root = tk.Toplevel() 558 | super().__init__(*args, **kwargs) 559 | 560 | # 传递参数 561 | self.tile_queue = tile_queue 562 | 563 | # 窗口布局 564 | self.main_frame = ttk.Frame(self.root, padding=20) 565 | self.main_frame.pack(fill=tk.X) 566 | 567 | # 第一行框架 568 | entry_spin_frame = ttk.Frame(self.main_frame) 569 | entry_spin_frame.pack(fill=tk.X, pady=5) 570 | ttk.Label( 571 | master=entry_spin_frame, 572 | text='日程名称 ' 573 | ).pack(side=tk.LEFT, fill=tk.X) 574 | self.task_name_entry = ttk.Entry(entry_spin_frame, validate="focus", validatecommand=self.clear) 575 | self.task_name_entry.pack(side=tk.LEFT, fill=tk.X, expand=tk.YES) 576 | 577 | # 第二行框架 578 | timer_frame = ttk.Frame(self.main_frame) 579 | timer_frame.pack(fill=tk.X, pady=5) 580 | ttk.Label( 581 | master=timer_frame, 582 | text='选择时间 ' 583 | ).pack(side=tk.LEFT, fill=tk.X) 584 | self.date_entry = DateEntry(timer_frame) 585 | self.date_entry.pack(side=tk.LEFT, fill=tk.X, expand=tk.YES, padx=3) 586 | 587 | # 第三行框架 588 | ok_frame = ttk.Frame(self.main_frame) 589 | ok_frame.pack(fill=tk.X, pady=5) 590 | ttk.Button( 591 | master=ok_frame, 592 | text='确认', 593 | bootstyle='outline', 594 | command=self.ok, 595 | ).pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 596 | 597 | # 其他初始化 598 | self.modify_flag = 0 599 | self.task_name_entry.insert(0, "创建你的日程吧!") 600 | 601 | # '''***********************************下面区别于new_task**************************************************''' 602 | for key in kwargs: 603 | if key == "value": 604 | # 其他初始化 605 | self.modify_flag = 1 606 | self.value = kwargs["value"] 607 | 608 | # 配置参数,初始化 609 | self.task_name_entry.delete(0, "end") 610 | self.task_name_entry.insert(0, self.value[0]) 611 | 612 | self.date_entry.entry.delete(0, "end") 613 | self.date_entry.entry.insert(0, self.value[1]) 614 | 615 | self.del_task_button = ttk.Button( 616 | master=ok_frame, 617 | text='删除', 618 | style='danger.Outline.TButton', 619 | command=self.del_task, 620 | ).pack(side=tk.LEFT, padx=3) 621 | 622 | # self.root.mainloop() 623 | 624 | def del_task(self): 625 | """删除一项""" 626 | self.tile_queue.put(("del_one", self.value[0])) 627 | self.tile_queue.put("refresh_tasks") 628 | self.root.destroy() 629 | 630 | def clear(self): 631 | """点击输入框的回调,删除提示内容""" 632 | if "创建你的日程" in self.task_name_entry.get(): 633 | self.task_name_entry.delete(0, "end") 634 | 635 | def ok(self): 636 | """点击确认""" 637 | if self.modify_flag == 1: 638 | # 先删除一项,然后再添加一项 639 | self.tile_queue.put(("del_one", self.value[0])) 640 | 641 | # 点击确认按钮,更新数据库 642 | startdate = datetime.today() 643 | enddate = datetime.strptime(self.date_entry.entry.get(), '%Y-%m-%d') 644 | days = str((enddate - startdate).days) 645 | value = [self.task_name_entry.get(), 646 | self.date_entry.entry.get(), 647 | days, 648 | "#080808", 649 | ''.join(random.sample('zyxwvutsrqponmlkjihgfedcba1234567890', 5)), 650 | "white"] 651 | self.tile_queue.put(("add_one", value)) 652 | self.tile_queue.put(("refresh_tasks")) 653 | 654 | self.root.destroy() 655 | 656 | 657 | class AskDelWindow(CustomWindow): 658 | def __init__(self, tile_queue, *args, **kwargs): 659 | self.root = tk.Toplevel() 660 | super().__init__(*args, **kwargs) 661 | 662 | # 传递参数 663 | self.tile_queue = tile_queue 664 | 665 | # 布局 666 | self.frame_top = Frame(self.root) 667 | self.frame_top.pack(side=TOP, padx=20, pady=5, expand=True, fill=X) 668 | self.frame_bottom = Frame(self.root) 669 | self.frame_bottom.pack(side=BOTTOM, padx=20, expand=True, fill=X) 670 | 671 | self.lable = ttk.Label(self.frame_top, text="是否要删除全部?") 672 | self.lable.pack(side=tk.LEFT, padx=5, pady=2, expand=True, fill=X) 673 | 674 | self.cancel_button = ttk.Button( 675 | master=self.frame_bottom, 676 | text='取消', 677 | bootstyle='outline', 678 | command=self.cancel, ) 679 | self.cancel_button.pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 680 | 681 | self.ok_button = ttk.Button( 682 | master=self.frame_bottom, 683 | text='确认', 684 | bootstyle='outline', 685 | command=self.ok, ) 686 | self.ok_button.pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 687 | 688 | # self.root.mainloop() 689 | 690 | def cancel(self): 691 | self.root.destroy() 692 | 693 | def ok(self): 694 | self.tile_queue.put("del_all") 695 | self.tile_queue.put("refresh_tasks") 696 | self.root.destroy() 697 | 698 | 699 | class AskResetWindow(CustomWindow): 700 | def __init__(self, main_window_queue, *args, **kwargs): 701 | self.root = tk.Toplevel() 702 | super().__init__(*args, **kwargs) 703 | 704 | # 传递参数 705 | self.main_window_queue = main_window_queue 706 | 707 | # 布局 708 | self.frame_top = Frame(self.root) 709 | self.frame_top.pack(side=TOP, padx=20, pady=5, expand=True, fill=X) 710 | self.frame_bottom = Frame(self.root) 711 | self.frame_bottom.pack(side=BOTTOM, padx=20, expand=True, fill=X) 712 | 713 | self.lable = ttk.Label(self.frame_top, text="是否要恢复默认(将会自动关闭软件)?") 714 | self.lable.pack(side=tk.TOP, padx=5, pady=2, expand=True, fill=X) 715 | 716 | self.cancel_button = ttk.Button( 717 | master=self.frame_bottom, 718 | text='取消', 719 | bootstyle='outline', 720 | command=self.cancel, ) 721 | self.cancel_button.pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 722 | 723 | self.ok_button = ttk.Button( 724 | master=self.frame_bottom, 725 | text='确认', 726 | bootstyle='outline', 727 | command=self.ok, ) 728 | self.ok_button.pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 729 | 730 | # self.root.mainloop() 731 | 732 | def cancel(self): 733 | self.root.destroy() 734 | 735 | def ok(self): 736 | self.main_window_queue.put("reset") 737 | self.root.destroy() 738 | self.main_window_queue.put("exit") 739 | 740 | 741 | class HelpWindow(CustomWindow): 742 | def __init__(self, path, *args, **kwargs): 743 | self.root = tk.Toplevel() 744 | super().__init__(*args, **kwargs) 745 | 746 | # 传递参数 747 | self.path = path 748 | 749 | # 窗口布局 750 | self.frame_bottom = ttk.Frame(self.root) 751 | self.frame_bottom.pack(side=tk.BOTTOM, fill=BOTH, expand=False) 752 | self.frame_top = ttk.Frame(self.root, padding=20) 753 | self.frame_top.pack(side=tk.TOP, fill=BOTH, expand=True) 754 | 755 | self.HtmlView = TkHtmlView(self.frame_top, background="white") 756 | self.HtmlView.pack(fill='both', expand=True) 757 | 758 | self.filename = tk.StringVar() 759 | ttk.Entry(self.frame_bottom, textvariable=self.filename).pack(side=tk.LEFT, padx=20, pady=10, fill='x', 760 | expand=True) 761 | ttk.Button(self.frame_bottom, text='选择文件', command=self.open_file).pack(side=tk.LEFT, padx=20, pady=10) 762 | 763 | # 加载默认md文件 764 | self.p = Thread(target=self.init_file) 765 | self.p.setDaemon(True) 766 | self.p.start() 767 | 768 | self.root.mainloop() 769 | 770 | def init_file(self): 771 | with open(self.path, encoding='utf-8') as f: 772 | self.filename.set(self.path) 773 | md2html = Markdown() 774 | html = md2html.convert(f.read()) 775 | self.HtmlView.set_html(html) 776 | 777 | def open_file(self): 778 | path = askopenfilename() 779 | if not path: 780 | return 781 | with open(path, encoding='utf-8') as f: 782 | self.filename.set(path) 783 | md2html = Markdown() 784 | html = md2html.convert(f.read()) 785 | self.HtmlView.set_html(html) 786 | 787 | 788 | class ScaleFrame(Frame): 789 | """自定义滑动条""" 790 | 791 | def __init__(self, widget_frame, name, init_value, from_, to, func, **kw): 792 | super().__init__(master=widget_frame, **kw) 793 | 794 | ttk.Label(master=self, text=name).pack(side=tk.LEFT, fill=tk.X, padx=(0, 2)) 795 | self.scale_var = tk.IntVar(value=init_value) 796 | ttk.Scale(master=self, variable=self.scale_var, from_=from_, to=to, command=func).pack(side=tk.LEFT, fill=tk.X, 797 | expand=tk.YES, 798 | padx=(0, 2)) 799 | ttk.Entry(self, textvariable=self.scale_var, width=4).pack(side=tk.RIGHT) 800 | 801 | def get_value(self): 802 | return self.scale_var.get() 803 | 804 | def set_value(self, value): 805 | self.scale_var.set(value) 806 | 807 | 808 | class WaitWindow(CustomWindow): 809 | """自定义等待窗体""" 810 | 811 | def __init__(self, queue, *args, **kwargs): 812 | self.root = tk.Toplevel() 813 | super(WaitWindow, self).__init__(*args, **kwargs) 814 | 815 | # 窗体布局1 816 | self.frame_bottom = Frame(self.root) 817 | self.frame_bottom.pack(side=BOTTOM, padx=15, pady=10, expand=True, fill=X) 818 | # 进度条 819 | self.bar = ttk.Progressbar(self.frame_bottom, mode="indeterminate", orient=tk.HORIZONTAL) 820 | self.bar.pack(expand=True, fill=X) 821 | self.bar.start(10) 822 | 823 | # 窗体布局2 824 | self.frame_top = Frame(self.root) 825 | self.frame_top.pack(side=TOP, padx=5, pady=10) 826 | # 提示内容 827 | self.content_lable = tk.Label(self.frame_top, text="正在初始化,请不要操作,请耐心等待......") 828 | self.content_lable.pack() 829 | 830 | self.queue = queue # 子线程与主线程的队列作为中继 831 | self.root.after(1000, self.relay) 832 | 833 | # root.mainloop() 834 | # 1.无法在threading中启动mainloop,main_loop方法必须在主线程当中进行。子线程直接操作UI会有很大的隐患。推荐使用队列与主线程交互。 835 | # 2.另外mainloop()是一个阻塞函数,在外部调用其函数,会阻塞,除非那种一次性的回调(例如按钮的点击事件) 836 | self.root.mainloop() 837 | 838 | def relay(self): 839 | """更新UI队列""" 840 | try: 841 | # 队列不可阻塞 842 | content = self.queue.get(False) 843 | # self.logger.info(self.__class__.__name__ + " Queue接收到消息:" + str(content)) 844 | # 回调函数要在之前回调,因为如果在队列中打开窗体,窗体的 mailoop 会让函数卡死,死循环. 845 | self.root.after(1, self.relay) 846 | # 具体的更新Ui操作 847 | self.UpdateUI(content) 848 | except queue.Empty: 849 | self.root.after(200, self.relay) 850 | 851 | def UpdateUI(self, content): 852 | if content == "exit": 853 | self.exit() 854 | 855 | 856 | class UpdateWindow(CustomWindow): 857 | """检查更新页面""" 858 | 859 | def __init__(self, version, logger, *args, **kwargs): 860 | self.root = tk.Toplevel() 861 | super().__init__(*args, **kwargs) 862 | 863 | self.version = version 864 | self.logger = logger 865 | 866 | self.update_version = version 867 | self.update_url = "" 868 | 869 | # 布局 870 | self.frame_top = Frame(self.root) 871 | self.frame_top.pack(side=TOP, padx=20, pady=5, expand=True, fill=X) 872 | self.frame_bottom = Frame(self.root) 873 | self.frame_bottom.pack(side=BOTTOM, padx=20, expand=True, fill=X) 874 | 875 | self.lable = ttk.Label(self.frame_top, text="正在检查更新中......") 876 | self.lable.pack(side=tk.TOP, padx=5, pady=2, expand=True, fill=X) 877 | 878 | # 开启更新UI队列 879 | self.queue = Queue() # 子线程与主线程的队列作为中继 880 | self.root.after(500, self.relay) 881 | 882 | # 开启请求线程 883 | self.update_thread = Thread(target=self.update) 884 | self.update_thread.setDaemon(True) 885 | self.update_thread.start() 886 | 887 | # self.root.mainloop() 888 | 889 | '''-----------------------------------更新UI 线程-----------------------------------------------''' 890 | 891 | def relay(self): 892 | """ 893 | 更新UI 894 | """ 895 | while not self.queue.empty(): 896 | content = self.queue.get() 897 | if content == "需要更新": 898 | self.lable.config(text="发现新版本:CountBoard V" + self.update_version) 899 | 900 | self.cancel_button = ttk.Button( 901 | master=self.frame_bottom, 902 | text='取消', 903 | bootstyle='outline', 904 | command=self.cancel, ) 905 | self.cancel_button.pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 906 | 907 | self.ok_button = ttk.Button( 908 | master=self.frame_bottom, 909 | text='更新', 910 | bootstyle='outline', 911 | command=self.ok, ) 912 | self.ok_button.pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 913 | 914 | elif content == "不需更新": 915 | self.lable.config(text="当前已经是最新版本") 916 | 917 | self.cancel_button = ttk.Button( 918 | master=self.frame_bottom, 919 | text='取消', 920 | bootstyle='outline', 921 | command=self.cancel, ) 922 | self.cancel_button.pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 923 | 924 | self.ok_button = ttk.Button( 925 | master=self.frame_bottom, 926 | text='确认', 927 | bootstyle='outline', 928 | command=self.ok, ) 929 | self.ok_button.pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 930 | 931 | elif content == "网络错误": 932 | self.lable.config(text="当前网络错误(请检查网络,关闭代理)") 933 | 934 | self.cancel_button = ttk.Button( 935 | master=self.frame_bottom, 936 | text='取消', 937 | bootstyle='outline', 938 | command=self.cancel, ) 939 | self.cancel_button.pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 940 | 941 | self.ok_button = ttk.Button( 942 | master=self.frame_bottom, 943 | text='确认', 944 | bootstyle='outline', 945 | command=self.cancel) 946 | self.ok_button.pack(side=tk.RIGHT, fill=tk.X, expand=tk.YES, padx=3) 947 | 948 | self.root.after(100, self.relay) 949 | 950 | def ok(self): 951 | if self.version != self.update_version: 952 | webbrowser.open_new_tab(self.update_url) 953 | self.root.destroy() 954 | 955 | def cancel(self): 956 | self.root.destroy() 957 | 958 | '''-----------------------------------请求线程-----------------------------------------------''' 959 | 960 | def update(self): 961 | update_path = str(Path(self.exe_dir_path).joinpath("update.txt")) 962 | try: 963 | with open(update_path, "wb") as f: 964 | f.write(requests.get("https://aidcs-1256440297.cos.ap-beijing.myqcloud.com/update.txt").content) 965 | with open(update_path, "r", encoding='utf8') as f: 966 | config = f.readline() 967 | config_dict = json.loads(config) 968 | self.update_version = config_dict["version"] 969 | self.update_url = config_dict["url"] 970 | if self.version != self.update_version: 971 | self.queue.put("需要更新") 972 | else: 973 | self.queue.put("不需更新") 974 | except: 975 | self.logger.info(traceback.format_exc()) 976 | self.queue.put("网络错误") 977 | 978 | 979 | class ResizingCanvas(Canvas): 980 | """ 981 | 自定义缩放画布 982 | """ 983 | 984 | def __init__(self, parent, **kwargs): 985 | Canvas.__init__(self, parent, **kwargs) 986 | self.bind("", self.on_resize) 987 | self.height = self.winfo_reqheight() 988 | self.width = self.winfo_reqwidth() 989 | 990 | def on_resize(self, event): 991 | wscale = float(event.width) / self.width 992 | hscale = float(event.height) / self.height 993 | self.width = event.width 994 | self.height = event.height 995 | self.config(width=self.width, height=self.height) 996 | self.scale("", 0, 0, wscale, hscale) 997 | # self.scale("all", 0, 0, wscale, hscale) 998 | --------------------------------------------------------------------------------