tkintertools.py 34.9 KB
Newer Older
小康2022's avatar
小康2022 已提交
1 2 3 4 5 6 7
"""
## tkintertools
tkinter 模块的扩展模块

这个模块将给用户提供可透明的、可自定义的、现代化的控件,以及一些特殊的功能函数

模块作者: 小康2022
小康2022's avatar
小康2022 已提交
8
模块版本: 2.2
小康2022's avatar
小康2022 已提交
9
上次更新: 2022/10/19
小康2022's avatar
小康2022 已提交
10
---
小康2022's avatar
小康2022 已提交
11
### 模块精华
小康2022's avatar
小康2022 已提交
12
- 容器类控件: `Tk`、`Canvas`
小康2022's avatar
小康2022 已提交
13
- 工具类: `PhotoImage`
小康2022's avatar
小康2022 已提交
14 15 16
- 虚拟画布类控件: `CanvasLabel`、`CanvasButton`、`CanvasEntry`、`CanvasText`
- 处理函数: `move_widget`、`correct_text`、`process_color`
---
小康2022's avatar
小康2022 已提交
17
##### 更多内容见: http://t.csdn.cn/gFg9A
小康2022's avatar
小康2022 已提交
18 19 20 21
"""


import random
小康2022's avatar
小康2022 已提交
22
import tkinter
小康2022's avatar
小康2022 已提交
23
import types
小康2022's avatar
小康2022 已提交
24 25
import typing

小康2022's avatar
小康2022 已提交
26
__all__ = ['Tk', 'Canvas',
小康2022's avatar
小康2022 已提交
27 28 29
           'CanvasLabel', 'CanvasButton',
           'CanvasEntry', 'CanvasText',
           'move_widget', 'correct_text', 'process_color']
小康2022's avatar
小康2022 已提交
30 31


小康2022's avatar
小康2022 已提交
32
# 常量
小康2022's avatar
小康2022 已提交
33 34


小康2022's avatar
小康2022 已提交
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
# 默认的文本前景颜色
COLOR_BLACK = ('#000000', '#000000', '#000000')
# 默认文本背景颜色
COLOR_WHITE = ('#FFFFFF', '#FFFFFF', '#FFFFFF')
# 默认的内部颜色
COLOR_FILL = ('#E1E1E1', '#E5F1FB', '#CCE4F7')
# 默认按钮外框颜色
COLOR_BUTTON = ('#C0C0C0', '#4A9EE0', '#4884B4')
# 默认文本外框颜色
COLOR_TEXT = ('#C0C0C0', '#5C5C5C', '#4A9EE0')
# 默认字体
FONT = ('楷体', 15)
# 默认控件外框宽度
BORDERWIDTH = 1
# 默认控件显示文本
TEXT = ''
小康2022's avatar
小康2022 已提交
51 52


小康2022's avatar
小康2022 已提交
53 54 55 56
# 容器控件


class Canvas(tkinter.Canvas):
小康2022's avatar
小康2022 已提交
57 58
    """
    画布类
小康2022's avatar
小康2022 已提交
59

小康2022's avatar
小康2022 已提交
60 61
    用于承载虚拟的画布控件
    """
小康2022's avatar
小康2022 已提交
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

    def __init__(self,
                 master,  # type: Tk
                 width: int,
                 height: int,
                 lock: bool = True,
                 expand: bool = True,
                 **kwargs) -> None:
        """
        ### 参数说明
        `master`: 父控件
        `width`: 画布宽度
        `height`: 画布高度
        `lock`: 画布内控件的功能锁(False 时没有功能)
        `expand`: 画布内控件是否能缩放
        `**kwargs`: 与原 tkinter 模块内 Canvas 类的参数相同
        """
        tkinter.Canvas.__init__(self, master,
小康2022's avatar
小康2022 已提交
80 81
                                width=width, height=height,
                                highlightthickness=0, **kwargs)
小康2022's avatar
小康2022 已提交
82 83 84 85 86 87 88 89 90 91
        self.master: Tk
        self.master.canvas_list.append(self)  # 将实例添加到 Tk 的画布列表中
        self.widget_list: list[CanvasButton | CanvasEntry |
                               CanvasLabel | CanvasText] = []  # 子控件列表(与绑定有关)

        self.lock = lock
        self.width = width
        self.height = height
        self.expand = expand

小康2022's avatar
小康2022 已提交
92 93 94 95 96
        # 放缩比率
        self.rate_x = 1
        self.rate_y = 1


小康2022's avatar
小康2022 已提交
97
class Tk(tkinter.Tk):
小康2022's avatar
小康2022 已提交
98 99 100 101 102
    """
    Tk类

    用于集中处理 `Canvas` 绑定的关联事件以及缩放操作
    """
小康2022's avatar
小康2022 已提交
103

小康2022's avatar
小康2022 已提交
104 105 106 107 108 109 110 111 112 113 114 115 116 117
    def __init__(self,
                 title: str | None = None,
                 geometry: str | None = None,
                 minisize: tuple[int, int] | None = None,
                 proportion_lock: bool = False,
                 *args, **kwargs) -> None:
        """
        ### 参数说明
        `title`: 窗口标题
        `geometry`: 窗口大小及位置(格式:'宽度x高度+左上角横坐标+左上角纵坐标' 或者 '宽度x高度')
        `minisize`: 窗口的最小缩放大小(默认为参数 geometry 的宽度与高度)
        `proportion_lock`: 窗口缩放是否保持原比例
        `*args`, `**kwargs`: 与原 tkinter 模块中的 Tk 类的参数相同
        """
小康2022's avatar
小康2022 已提交
118
        tkinter.Tk.__init__(self, *args, **kwargs)
小康2022's avatar
小康2022 已提交
119 120 121 122 123

        if title:
            self.title(title)
        if geometry:
            self.geometry(geometry)
小康2022's avatar
小康2022 已提交
124 125
            if not minisize:
                self.minsize(*map(int, geometry.split('+')[0].split('x')))
小康2022's avatar
小康2022 已提交
126 127 128 129
        if minisize:
            self.minsize(*minisize)

        self.canvas_list: list[Canvas] = []  # 子画布列表(与缩放绑定有关)
小康2022's avatar
小康2022 已提交
130 131 132 133
        # 开启窗口缩放检测
        self.bind('<Configure>', lambda event: self.__zoom(
            event, proportion_lock))

小康2022's avatar
小康2022 已提交
134
    def __zoom(self, event: tkinter.Event, lock: bool, geometry: list = []) -> None:  # NOTE: 字体缩放方法可改进
小康2022's avatar
小康2022 已提交
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
        """ 画布缩放检测 """
        if not geometry:
            # 记住初始化窗口宽高比例
            geometry.append(event.width / event.height)
            # 记住初始化的窗口大小
            geometry.append(event.width)
            geometry.append(event.height)

        elif event.width != geometry[1] or event.height != geometry[2]:
            # 窗口大小改变
            if lock:
                # 使高度和宽度成比例同步变化
                delta = event.width / geometry[0] - event.height
                if event.width not in (geometry[1], self.winfo_screenwidth()):
                    # 宽度改变
                    event.height += round(delta)
                elif event.height not in (geometry[2], self.winfo_screenheight()):
                    # 高度改变
                    event.width -= round(delta * geometry[0])
                self.geometry('%sx%s' % (event.width, event.height))

            # 计算横向缩放倍率
            rate_x = event.width / geometry[1]
            # 计算纵向缩放倍率
            rate_y = event.height / geometry[2]

            # 更新子画布控件的大小
            for canvas in self.canvas_list:
小康2022's avatar
小康2022 已提交
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
                if canvas.expand:
                    # 更新画布的横纵缩放比率
                    canvas.rate_x = event.width / canvas.width
                    canvas.rate_y = event.height / canvas.height
                    # 更新子画布控件的子虚拟画布控件的位置
                    for item in canvas.find_all():
                        canvas.coords(item, [coord * rate_y if ind % 2 else coord * rate_x for ind, coord in
                                             enumerate(canvas.coords(item))])
                        size: str = canvas.itemcget(item, 'tags')
                        if size.isdigit():
                            # 字体大小修改
                            font: str = canvas.itemcget(item, 'font')
                            font = font.split()
                            font[1] = round(int(size) * canvas.rate_x)
                            canvas.itemconfigure(item, font=font)

                    # 更新子画布控件的子虚拟画布控件位置数据
                    for widget in canvas.widget_list:
                        widget.x1 *= rate_x
                        widget.x2 *= rate_x
                        widget.y1 *= rate_y
                        widget.y2 *= rate_y
小康2022's avatar
小康2022 已提交
185 186 187

            # 更新默认参数
            geometry[1], geometry[2] = event.width, event.height
小康2022's avatar
小康2022 已提交
188 189

    @staticmethod
小康2022's avatar
小康2022 已提交
190 191 192 193 194
    def __touch(event: tkinter.Event, canvas: Canvas) -> None:
        """ 绑定鼠标触碰控件事件 """
        if canvas.lock:
            for widget in canvas.widget_list:
                widget.touch(event)
小康2022's avatar
小康2022 已提交
195 196

    @staticmethod
小康2022's avatar
小康2022 已提交
197 198 199 200 201 202
    def __press(event: tkinter.Event, canvas: Canvas) -> None:
        """ 绑定鼠标左键按下事件 """
        if canvas.lock:
            for widget in canvas.widget_list:
                if isinstance(widget, CanvasButton | CanvasEntry | CanvasText):
                    widget.press(event)
小康2022's avatar
小康2022 已提交
203 204

    @staticmethod
小康2022's avatar
小康2022 已提交
205 206 207 208 209 210 211
    def __release(event: tkinter.Event, canvas: Canvas) -> None:
        """ 绑定鼠标左键松开事件 """
        if canvas.lock:
            for widget in canvas.widget_list:
                if isinstance(widget, CanvasButton):
                    widget.execute(event)
                    widget.touch(event)
小康2022's avatar
小康2022 已提交
212 213

    @staticmethod
小康2022's avatar
小康2022 已提交
214 215 216
    def __mousewheel(event: tkinter.Event, canvas: Canvas) -> None:
        """ 绑定鼠标滚轮滚动事件 """
        if canvas.lock:
小康2022's avatar
小康2022 已提交
217
            for widget in canvas.widget_list:
小康2022's avatar
小康2022 已提交
218 219
                if isinstance(widget, CanvasText):
                    widget.scroll(event)
小康2022's avatar
小康2022 已提交
220

小康2022's avatar
小康2022 已提交
221 222 223 224 225 226 227 228 229 230
    def __input(self, event: tkinter.Event) -> None:
        """ 绑定键盘输入字符事件 """
        for canvas in self.canvas_list:
            if canvas.lock:
                for widget in canvas.widget_list:
                    if isinstance(widget, _TextWidget):
                        widget.input(event)
                break

    def __bind(self) -> None:
小康2022's avatar
小康2022 已提交
231
        """ 关联事件的绑定 """
小康2022's avatar
小康2022 已提交
232 233 234
        # 绑定键盘输入字符
        self.bind('<Any-Key>', self.__input)

小康2022's avatar
小康2022 已提交
235
        for canvas in self.canvas_list:
小康2022's avatar
小康2022 已提交
236
            # 绑定鼠标触碰控件
小康2022's avatar
小康2022 已提交
237
            canvas.bind('<Motion>', lambda event,
小康2022's avatar
小康2022 已提交
238
                        _canvas=canvas: self.__touch(event, _canvas))
小康2022's avatar
小康2022 已提交
239
            # 绑定鼠标左键按下
小康2022's avatar
小康2022 已提交
240
            canvas.bind('<Button-1>', lambda event,
小康2022's avatar
小康2022 已提交
241
                        _canvas=canvas: self.__press(event, _canvas))
小康2022's avatar
小康2022 已提交
242
            # 绑定鼠标左键松开
小康2022's avatar
小康2022 已提交
243
            canvas.bind('<ButtonRelease-1>', lambda event,
小康2022's avatar
小康2022 已提交
244
                        _canvas=canvas: self.__release(event, _canvas))
小康2022's avatar
小康2022 已提交
245
            # 绑定鼠标左键按下移动
小康2022's avatar
小康2022 已提交
246
            canvas.bind('<B1-Motion>', lambda event,
小康2022's avatar
小康2022 已提交
247
                        _canvas=canvas: self.__press(event, _canvas))
小康2022's avatar
小康2022 已提交
248
            # 绑定鼠标滚轮滚动
小康2022's avatar
小康2022 已提交
249
            canvas.bind('<MouseWheel>', lambda event,
小康2022's avatar
小康2022 已提交
250
                        _canvas=canvas: self.__mousewheel(event, _canvas))
小康2022's avatar
小康2022 已提交
251

小康2022's avatar
小康2022 已提交
252 253 254 255
    def mainloop(self) -> None:
        """ 调用 Tk 的主循环 """
        self.__bind()
        tkinter.Tk.mainloop(self)
小康2022's avatar
小康2022 已提交
256 257


小康2022's avatar
小康2022 已提交
258
# 控件基类
小康2022's avatar
小康2022 已提交
259 260


小康2022's avatar
小康2022 已提交
261 262
class _BaseWidget:
    """ 内部类 """
小康2022's avatar
小康2022 已提交
263 264

    def __init__(self,
小康2022's avatar
小康2022 已提交
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
                 canvas: Canvas,
                 x: float,
                 y: float,
                 width: float,
                 height: float,
                 text: str,
                 justify: str,
                 borderwidth: float,
                 font: tuple[str, int, str],
                 color_text: tuple[str, str, str],
                 color_fill: tuple[str, str, str],
                 color_outline: tuple[str, str, str]) -> None:
        """
        ### 标准参数
        `canvas`: 父画布容器控件
        `x`, `y`: 控件左上角的横纵坐标
        `width`, `height`: 控件的宽度与高度
        `text`: 控件显示的文本
        `justify`: 文本的对齐方式
        `borderwidth`: 外框的宽度
        `font`: 控件的字体设定 (字体, 大小, 样式)
        `color_text`: 控件文本的颜色
        `color_fill`: 控件内部的颜色
        `color_outline`: 控件外框的颜色
        ---
        ### 特定参数
        `command`: 按钮控件的关联函数
        `show`: 文本控件的显示文本
        `limit`: 文本控件的输入字数限制
        `space`: 文本控件能否输入空格的标识
        `read`: 文本控件的只读模式
        ---
        ### 详细说明
        字体的值为一个三元组 
        例如: `('微软雅黑', 15, 'bold') `

        颜色为一个包含三个 RGB 颜色字符串的元组 
        详细: `(正常颜色, 触碰颜色, 交互颜色)`
        """
        # 将实例添加到父画布控件
小康2022's avatar
小康2022 已提交
305
        canvas.widget_list.append(self)
小康2022's avatar
小康2022 已提交
306 307
        # 控件活跃标志
        self.live = True
小康2022's avatar
小康2022 已提交
308

小康2022's avatar
小康2022 已提交
309
        self.master = canvas
小康2022's avatar
小康2022 已提交
310
        self.value = text
小康2022's avatar
小康2022 已提交
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
        self.font = font
        self.color_text = color_text
        self.color_fill = color_fill
        self.color_outline = color_outline

        self.x1, self.y1 = x, y  # 控件左上角坐标
        self.x2, self.y2 = x + width, y + height  # 控件左下角坐标

        # 虚拟控件的外框
        self.rect = canvas.create_rectangle(x, y, x + width, y + height,
                                            width=borderwidth,
                                            outline=color_outline[0],
                                            fill=color_fill[0])
        # 虚拟控件显示的文字
        self.text = canvas.create_text(x + width / 2, y + height / 2,
                                       text=text,
                                       font=font,
                                       justify=justify,
                                       fill=color_text[0],
                                       tags=str(font[1]))

    def state(self, mode: typing.Literal['normal', 'touch', 'press']) -> None:
        """
        改变虚拟控件的状态
        `normal`: 正常状态
        `touch`: 鼠标经过时的状态
        `press`: 鼠标按下时的状态
        """
        mode = 0 if mode == 'normal' else 1 if mode == 'touch' else 2
小康2022's avatar
小康2022 已提交
340 341 342
        self.master.itemconfigure(self.text, fill=self.color_text[mode])
        self.master.itemconfigure(self.rect, fill=self.color_fill[mode])
        self.master.itemconfigure(self.rect, outline=self.color_outline[mode])
小康2022's avatar
小康2022 已提交
343

小康2022's avatar
小康2022 已提交
344
    def move(self, dx: float, dy: float) -> None:
小康2022's avatar
小康2022 已提交
345 346 347 348 349 350
        """ 改变控件的位置 """
        self.x1 += dx
        self.x2 += dx
        self.y1 += dy
        self.y2 += dy

小康2022's avatar
小康2022 已提交
351 352
        self.master.move(self.rect, dx, dy)
        self.master.move(self.text, dx, dy)
小康2022's avatar
小康2022 已提交
353

小康2022's avatar
小康2022 已提交
354 355
        if hasattr(self, 'cursor'):
            self.cursor: tkinter._CanvasItemId
小康2022's avatar
小康2022 已提交
356
            self.master.move(self.cursor, dx, dy)
小康2022's avatar
小康2022 已提交
357

小康2022's avatar
小康2022 已提交
358 359 360 361
    def coords(self, x: int, y: int, width: int, height: int) -> None:
        """ 重设控件位置 """
        self.x1, self.y1 = x, y
        self.x2, self.y2 = x + width, y + height
小康2022's avatar
小康2022 已提交
362

小康2022's avatar
小康2022 已提交
363 364
        self.master.coords(self.rect, x, y, x + width, y + height)
        self.master.coords(self.text, x + width / 2, y + height / 2)
小康2022's avatar
小康2022 已提交
365

小康2022's avatar
小康2022 已提交
366 367 368
    def configure(self, **kwargs) -> None:
        """
        改变原有参数的值
小康2022's avatar
小康2022 已提交
369

小康2022's avatar
小康2022 已提交
370 371 372 373 374 375 376 377 378 379
        可供修改的参数有 `text`、`color_text`、`color_fill` 及 `color_outline`
        """
        if value := kwargs.get('text', self.value):
            self.value = value
        if text := kwargs.get('color_text', self.color_text):
            self.color_text = text
        if fill := kwargs.get('color_fill', self.color_fill):
            self.color_fill = fill
        if outline := kwargs.get('color_outline', self.color_outline):
            self.color_outline = outline
小康2022's avatar
小康2022 已提交
380

小康2022's avatar
小康2022 已提交
381 382
        self.master.itemconfigure(self.text, text=value, fill=text[0])
        self.master.itemconfigure(self.rect, fill=fill[0], outline=outline[0])
小康2022's avatar
小康2022 已提交
383

小康2022's avatar
小康2022 已提交
384 385 386 387
    def destroy(self) -> None:
        """ 摧毁控件释放内存 """
        if hasattr(self, 'live'):
            self.live = False
小康2022's avatar
小康2022 已提交
388 389
        self.master.delete(self.rect)
        self.master.delete(self.text)
小康2022's avatar
小康2022 已提交
390
        # self.master.widget_list.remove(self)  NOTE: 暂不知道的 BUG
小康2022's avatar
小康2022 已提交
391 392


小康2022's avatar
小康2022 已提交
393 394
class _TextWidget(_BaseWidget):
    """ 内部类 """
小康2022's avatar
小康2022 已提交
395

小康2022's avatar
小康2022 已提交
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
    def __init__(self,
                 canvas: Canvas,
                 x: int,
                 y: int,
                 width: int,
                 height: int,
                 text: tuple[str] | str,
                 limit: int,
                 space: bool,
                 justify: str,
                 borderwidth: int,
                 font: tuple[str, int, str],
                 color_text: tuple[str, str, str],
                 color_fill: tuple[str, str, str],
                 color_outline: tuple[str, str, str]) -> None:
        _BaseWidget.__init__(self, canvas, x, y, width, height, '', justify,
                             borderwidth, font, color_text, color_fill, color_outline)
        if type(text) == tuple:
            self.value_normal, self.value_touch = text
        else:
            self.value_normal = text
        # 表面显示值
        self.value_surface = ''
        # 鼠标左键按下标志
        self._press = False
        # 鼠标光标
        self.cursor = canvas.create_text(x+width/2, y+height/2,
                                         font=FONT, tags=str(FONT[1]),
                                         fill=color_text[2])
        # 光标闪烁间隔
        self.cursor_time = 300
        # 光标闪烁标志
        self._cursor = False

        self.limit = limit
        self.space = space

    def press_on(self) -> None:
        """ 控件获得焦点 """
        if not getattr(self, 'read', None):
            self._press = True
            self.state('press')
小康2022's avatar
小康2022 已提交
438
            self.master.itemconfigure(self.text, text=self.value_surface)
小康2022's avatar
小康2022 已提交
439
            self.cursor_flash()
小康2022's avatar
小康2022 已提交
440

小康2022's avatar
小康2022 已提交
441 442 443 444 445
    def press_off(self) -> None:
        """ 控件失去焦点 """
        if not getattr(self, 'read', None):
            self._press = False
            self.state('normal')
小康2022's avatar
小康2022 已提交
446
            self.master.itemconfigure(self.text, text=self.value_surface)
小康2022's avatar
小康2022 已提交
447

小康2022's avatar
小康2022 已提交
448 449
    def press(self, event: tkinter.Event) -> None:
        """ 交互状态检测 """
小康2022's avatar
小康2022 已提交
450
        if self.master.lock:
小康2022's avatar
小康2022 已提交
451 452 453 454 455 456 457
            if self.x1 <= event.x <= self.x2 and self.y1 <= event.y <= self.y2:
                if not self._press:
                    self.press_on()
            else:
                self.press_off()

    def touch(self,  # type: CanvasEntry | CanvasText
小康2022's avatar
小康2022 已提交
458
              event: tkinter.Event) -> None:
小康2022's avatar
小康2022 已提交
459
        """ 触碰状态检测 """
小康2022's avatar
小康2022 已提交
460
        if self.master.lock:  # NOTE: 可将 lock 的检测放到Canvas类或者Tk类去做
小康2022's avatar
小康2022 已提交
461 462 463 464 465 466 467 468 469 470
            if self.x1 <= event.x <= self.x2 and self.y1 <= event.y <= self.y2:
                self.touch_on()
            else:
                self.touch_off()

    def cursor_flash(self) -> None:
        """ 鼠标光标闪烁 """
        if self.cursor_time >= 300:
            self.cursor_time, self._cursor = 0, not self._cursor
            if self._cursor:
小康2022's avatar
小康2022 已提交
471
                if self.master.itemcget(self.text, 'justify') == tkinter.CENTER:
小康2022's avatar
小康2022 已提交
472
                    # 居中的文本
小康2022's avatar
小康2022 已提交
473
                    self.master.itemconfigure(
小康2022's avatar
小康2022 已提交
474
                        self.cursor, text=self.__text(self.value) + '│')
小康2022's avatar
小康2022 已提交
475
                elif self.master.itemcget(self.text, 'justify') == tkinter.LEFT:
小康2022's avatar
小康2022 已提交
476
                    # 靠左的文本
小康2022's avatar
小康2022 已提交
477
                    self.master.itemconfigure(
小康2022's avatar
小康2022 已提交
478 479
                        self.cursor, text=self.__text(self.value_surface) + '│')
            else:
小康2022's avatar
小康2022 已提交
480
                self.master.itemconfigure(self.cursor, text='')
小康2022's avatar
小康2022 已提交
481

小康2022's avatar
小康2022 已提交
482 483
        if self._press:
            self.cursor_time += 10
小康2022's avatar
小康2022 已提交
484
            self.master.after(10, self.cursor_flash)
小康2022's avatar
小康2022 已提交
485 486
        else:
            self.cursor_time, self._cursor = 300, False  # 恢复默认值
小康2022's avatar
小康2022 已提交
487
            self.master.itemconfigure(self.cursor, text='')
小康2022's avatar
小康2022 已提交
488 489 490 491

    def cursor_update(self) -> None:
        """ 鼠标光标更新 """
        self.cursor_time, self._cursor = 300, False  # 恢复默认值
小康2022's avatar
小康2022 已提交
492
        if self.master.itemcget(self.text, 'justify') == tkinter.CENTER:
小康2022's avatar
小康2022 已提交
493
            # 居中的文本
小康2022's avatar
小康2022 已提交
494
            self.master.itemconfigure(
小康2022's avatar
小康2022 已提交
495
                self.cursor, text=self.__text(self.value) + '│')
小康2022's avatar
小康2022 已提交
496
        elif self.master.itemcget(self.text, 'justify') == tkinter.LEFT:
小康2022's avatar
小康2022 已提交
497
            # 靠左的文本
小康2022's avatar
小康2022 已提交
498
            self.master.itemconfigure(
小康2022's avatar
小康2022 已提交
499
                self.cursor, text=self.__text(self.value_surface) + '│')
小康2022's avatar
小康2022 已提交
500

小康2022's avatar
小康2022 已提交
501 502 503 504 505 506 507 508 509 510 511 512
    @staticmethod
    def __text(string: str) -> str:
        """ 内部函数 """
        out = ''
        for i in string:
            if i == '\n':
                out += i
            elif 0 <= ord(i) <= 256:
                out += ' '
            else:
                out += '  '
        return out
小康2022's avatar
小康2022 已提交
513 514


小康2022's avatar
小康2022 已提交
515
# 虚拟画布控件
小康2022's avatar
小康2022 已提交
516 517


小康2022's avatar
小康2022 已提交
518 519 520
class CanvasLabel(_BaseWidget):
    """
    虚拟画布标签控件
小康2022's avatar
小康2022 已提交
521

小康2022's avatar
小康2022 已提交
522 523
    创建一个虚拟的标签控件,用于显示少量文本
    """
小康2022's avatar
小康2022 已提交
524

小康2022's avatar
小康2022 已提交
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
    def __init__(self,
                 canvas: Canvas,
                 x: int,
                 y: int,
                 width: int,
                 height: int,
                 text: str = TEXT,
                 borderwidth: int = BORDERWIDTH,
                 justify: str = tkinter.CENTER,
                 font: tuple[str, int, str] = FONT,
                 color_text: tuple[str, str, str] = COLOR_BLACK,
                 color_fill: tuple[str, str, str] = COLOR_FILL,
                 color_outline: tuple[str, str, str] = COLOR_BUTTON) -> None:
        _BaseWidget.__init__(self, canvas, x, y, width, height, text, justify,
                             borderwidth, font, color_text, color_fill, color_outline)

    def touch(self, event: tkinter.Event) -> None:
        """ 触碰状态检测 """
        if self.x1 <= event.x <= self.x2 and self.y1 <= event.y <= self.y2:
            self.state('touch')
        else:
            self.state('normal')
小康2022's avatar
小康2022 已提交
547 548


小康2022's avatar
小康2022 已提交
549 550 551 552 553
class CanvasButton(_BaseWidget):
    """
    虚拟画布按钮控件

    创建一个虚拟的按钮,并执行关联函数
小康2022's avatar
小康2022 已提交
554
    """
小康2022's avatar
小康2022 已提交
555

小康2022's avatar
小康2022 已提交
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572
    def __init__(self,
                 canvas: Canvas,
                 x: int,
                 y: int,
                 width: int,
                 height: int,
                 text: str = TEXT,
                 borderwidth: int = BORDERWIDTH,
                 justify: str = tkinter.CENTER,
                 font: tuple[str, int, str] = FONT,
                 command: types.FunctionType | None = None,
                 color_text: tuple[str, str, str] = COLOR_BLACK,
                 color_fill: tuple[str, str, str] = COLOR_FILL,
                 color_outline: tuple[str, str, str] = COLOR_BUTTON) -> None:
        _BaseWidget.__init__(self, canvas, x, y, width, height, text, justify,
                             borderwidth, font, color_text, color_fill, color_outline)
        self.command = command
小康2022's avatar
小康2022 已提交
573 574 575

    def execute(self, event: tkinter.Event) -> None:
        """ 执行关联函数 """
小康2022's avatar
小康2022 已提交
576 577
        if self.x1 <= event.x <= self.x2 and self.y1 <= event.y <= self.y2:
            if self.live and self.command:
小康2022's avatar
小康2022 已提交
578 579 580
                self.command()

    def press(self, event: tkinter.Event) -> None:
小康2022's avatar
小康2022 已提交
581 582 583 584 585
        """ 交互状态检测 """
        if self.x1 <= event.x <= self.x2 and self.y1 <= event.y <= self.y2:
            self.state('press')
        else:
            self.state('normal')
小康2022's avatar
小康2022 已提交
586

小康2022's avatar
小康2022 已提交
587 588 589 590 591 592
    def touch(self, event: tkinter.Event) -> None:
        """ 触碰状态检测 """
        if self.x1 <= event.x <= self.x2 and self.y1 <= event.y <= self.y2:
            self.state('touch')
        else:
            self.state('normal')
小康2022's avatar
小康2022 已提交
593 594


小康2022's avatar
小康2022 已提交
595
class CanvasEntry(_TextWidget):
小康2022's avatar
小康2022 已提交
596
    """
小康2022's avatar
小康2022 已提交
597
    虚拟画布输入框控件
小康2022's avatar
小康2022 已提交
598

小康2022's avatar
小康2022 已提交
599 600
    创建一个虚拟的输入框控件,可输入单行少量字符,并获取这些字符
    """
小康2022's avatar
小康2022 已提交
601 602

    def __init__(self,
小康2022's avatar
小康2022 已提交
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617
                 canvas: Canvas,
                 x: int,
                 y: int,
                 width: int,
                 height: int,
                 text: tuple[str] | str = TEXT,
                 show: str | None = None,
                 limit: int = 15,
                 space: bool = False,
                 borderwidth: int = BORDERWIDTH,
                 justify: str = tkinter.CENTER,
                 font: tuple[str, int, str] = FONT,
                 color_text: tuple[str, str, str] = COLOR_BLACK,
                 color_fill: tuple[str, str, str] = COLOR_WHITE,
                 color_outline: tuple[str, str, str] = COLOR_TEXT) -> None:
小康2022's avatar
小康2022 已提交
618 619 620 621 622
        if type(text) == str:
            text = (text, '')
        elif type(text) == None:
            text = ('', '')

小康2022's avatar
小康2022 已提交
623 624
        _TextWidget.__init__(self, canvas, x, y, width, height, text, limit, space, justify,
                             borderwidth, font, color_text, color_fill, color_outline)
小康2022's avatar
小康2022 已提交
625
        self.master.itemconfigure(self.text, text=self.value_normal)
小康2022's avatar
小康2022 已提交
626 627 628 629 630 631
        self.show = show

    def press_off(self) -> None:
        # 重写父类方法
        _TextWidget.press_off(self)
        if self.value == '':
小康2022's avatar
小康2022 已提交
632
            self.master.itemconfigure(self.text, text=self.value_normal)
小康2022's avatar
小康2022 已提交
633 634

    def touch_on(self) -> None:
小康2022's avatar
小康2022 已提交
635
        """ 鼠标悬停状态 """
小康2022's avatar
小康2022 已提交
636 637
        if not self._press:
            self.state('touch')
小康2022's avatar
小康2022 已提交
638 639

            # 判断显示的值是否为第一默认值
小康2022's avatar
小康2022 已提交
640
            if self.master.itemcget(self.text, 'text') == self.value_normal:
小康2022's avatar
小康2022 已提交
641
                # 更新为第二默认值
小康2022's avatar
小康2022 已提交
642
                self.master.itemconfigure(self.text, text=self.value_touch)
小康2022's avatar
小康2022 已提交
643

小康2022's avatar
小康2022 已提交
644
    def touch_off(self) -> None:
小康2022's avatar
小康2022 已提交
645
        """ 鼠标离开状态 """
小康2022's avatar
小康2022 已提交
646 647
        if not self._press:
            self.state('normal')
小康2022's avatar
小康2022 已提交
648 649

            # 判断显示的值是否为第二默认值
小康2022's avatar
小康2022 已提交
650
            if self.master.itemcget(self.text, 'text') == self.value_touch:
小康2022's avatar
小康2022 已提交
651
                # 更新为第一默认值
小康2022's avatar
小康2022 已提交
652
                self.master.itemconfigure(self.text, text=self.value_normal)
小康2022's avatar
小康2022 已提交
653 654 655 656

    def change_text(self, value: str) -> None:
        """ 重新设定显示文字 """
        # 改变真实值
小康2022's avatar
小康2022 已提交
657
        self.value = value
小康2022's avatar
小康2022 已提交
658 659 660
        # 改变显示值
        self.value_surface = len(value) * self.show if self.show else value
        # 更新显示值
小康2022's avatar
小康2022 已提交
661
        self.master.itemconfigure(self.text, text=self.value_surface)
小康2022's avatar
小康2022 已提交
662 663 664

    def input(self, event: tkinter.Event) -> None:
        """ 文本输入 """
小康2022's avatar
小康2022 已提交
665
        if self.master.lock:
小康2022's avatar
小康2022 已提交
666
            if self._press and self.live:
小康2022's avatar
小康2022 已提交
667 668
                if event.keysym == 'BackSpace':
                    # 按下退格键
小康2022's avatar
小康2022 已提交
669 670 671 672
                    self.value = self.value[:-1]
                if len(event.char) and len(self.value) < self.limit:
                    key = ord(event.char)
                    if 32 < key < 127 or key > 255 or (key == 32 and self.space):
小康2022's avatar
小康2022 已提交
673
                        # 按下普通按键
小康2022's avatar
小康2022 已提交
674
                        self.value += event.char
小康2022's avatar
小康2022 已提交
675 676

                # 更新表面显示值
小康2022's avatar
小康2022 已提交
677
                self.value_surface = len(
小康2022's avatar
小康2022 已提交
678
                    self.value) * self.show if self.show else self.value
小康2022's avatar
小康2022 已提交
679
                # 更新显示
小康2022's avatar
小康2022 已提交
680
                self.master.itemconfigure(
小康2022's avatar
小康2022 已提交
681 682
                    self.text, text=self.value_surface)
                self.cursor_update()
小康2022's avatar
小康2022 已提交
683 684


小康2022's avatar
小康2022 已提交
685
class CanvasText(_TextWidget):
小康2022's avatar
小康2022 已提交
686 687 688 689
    """虚拟画布文本框类

    创建一个透明的虚拟文本框,
    用于输入多行文本和显示多行文本(只读模式)
小康2022's avatar
小康2022 已提交
690
    """
小康2022's avatar
小康2022 已提交
691

小康2022's avatar
小康2022 已提交
692 693 694 695 696 697 698 699 700 701 702 703 704 705 706
    def __init__(self,
                 canvas: Canvas,
                 x: int,
                 y: int,
                 width: int,
                 height: int,
                 limit: int = 100,
                 space: bool = True,
                 read: bool = False,
                 borderwidth: int = BORDERWIDTH,
                 justify: str = tkinter.LEFT,
                 font: tuple[str, int, str] = FONT,
                 color_text: tuple[str, str, str] = COLOR_BLACK,
                 color_fill: tuple[str, str, str] = COLOR_WHITE,
                 color_outline: tuple[str, str, str] = COLOR_TEXT) -> None:
小康2022's avatar
小康2022 已提交
707
        _TextWidget.__init__(self, canvas, x, y, width, height, TEXT, limit, space, justify,
小康2022's avatar
小康2022 已提交
708 709 710
                             borderwidth, font, color_text, color_fill, color_outline)
        # 只读模式
        self.read = read
小康2022's avatar
小康2022 已提交
711
        # 修改多行文本靠左显示
小康2022's avatar
小康2022 已提交
712 713 714 715
        self.master.coords(self.text, self.x1 + 2, self.y1 + 2)
        self.master.coords(self.cursor, self.x1 - 7, self.y1 + 2)
        self.master.itemconfigure(self.text, anchor='nw', justify=justify)
        self.master.itemconfigure(self.cursor, anchor='nw', justify=justify)
小康2022's avatar
小康2022 已提交
716
        # 计算单行文本容纳量
小康2022's avatar
小康2022 已提交
717
        self.row = round((self.x2 - self.x1) / int(font[1] * 3/4)) + 1
小康2022's avatar
小康2022 已提交
718
        # 计算文本容纳行数
小康2022's avatar
小康2022 已提交
719
        self.line = round((self.y2 - self.y1) / int(font[1] * 3/2)) + 1
小康2022's avatar
小康2022 已提交
720
        # 文本位置
小康2022's avatar
小康2022 已提交
721
        self._pos = self.line
小康2022's avatar
小康2022 已提交
722

小康2022's avatar
小康2022 已提交
723
    def touch_on(self) -> None:
小康2022's avatar
小康2022 已提交
724
        """ 鼠标悬停状态 """
小康2022's avatar
小康2022 已提交
725 726
        if not self._press:
            self.state('touch')
小康2022's avatar
小康2022 已提交
727

小康2022's avatar
小康2022 已提交
728
    def touch_off(self) -> None:
小康2022's avatar
小康2022 已提交
729
        """ 鼠标离开状态 """
小康2022's avatar
小康2022 已提交
730 731
        if not self._press:
            self.state('normal')
小康2022's avatar
小康2022 已提交
732 733 734 735

    def change_text(self, value: str) -> None:
        """ 重新设定显示文字 """
        # 改变文本
小康2022's avatar
小康2022 已提交
736
        self.value_surface = self.value = value
小康2022's avatar
小康2022 已提交
737 738 739 740 741 742 743
        # 更新显示值
        self.append('')

    def append(self, value: str) -> None:
        """ 添加文本 """
        # 改变文本
        self.value += value
小康2022's avatar
小康2022 已提交
744
        key = self.value.count('\n')
小康2022's avatar
小康2022 已提交
745
        if key <= self.line:
小康2022's avatar
小康2022 已提交
746
            # 更新显示值
小康2022's avatar
小康2022 已提交
747
            self.master.itemconfigure(self.text, text=self.value)
小康2022's avatar
小康2022 已提交
748
        else:
小康2022's avatar
小康2022 已提交
749
            # 同步更新文本上下位置数据
小康2022's avatar
小康2022 已提交
750
            self._pos += value.count('\n')
小康2022's avatar
小康2022 已提交
751
            # 计算显示文本的部分
小康2022's avatar
小康2022 已提交
752
            ind = key - self.line
小康2022's avatar
小康2022 已提交
753
            self.value_surface = '\n'.join(
小康2022's avatar
小康2022 已提交
754
                self.value.split('\n')[ind:ind + self.line])
小康2022's avatar
小康2022 已提交
755
            self.master.itemconfigure(self.text, text=self.value_surface)
小康2022's avatar
小康2022 已提交
756 757 758

    def scroll(self, event: tkinter.Event) -> None:
        """ 文本滚动 """
小康2022's avatar
小康2022 已提交
759
        if self.master.lock:
小康2022's avatar
小康2022 已提交
760
            if self.x1 <= event.x <= self.x2 and self.y1 <= event.y <= self.y2:
小康2022's avatar
小康2022 已提交
761
                if event.delta > 0 and self._pos > self.line:
小康2022's avatar
小康2022 已提交
762
                    # 鼠标向上滚动,显示文本部分向下滚动
小康2022's avatar
小康2022 已提交
763 764
                    self._pos -= 1
                elif event.delta < 0 and self._pos < self.value.count('\n'):
小康2022's avatar
小康2022 已提交
765
                    # 鼠标向下滚动,显示文本部分向上滚动
小康2022's avatar
小康2022 已提交
766
                    self._pos += 1
小康2022's avatar
小康2022 已提交
767
                # 计算显示文本的部分
小康2022's avatar
小康2022 已提交
768
                ind = self._pos - self.line
小康2022's avatar
小康2022 已提交
769
                self.value_surface = '\n'.join(
小康2022's avatar
小康2022 已提交
770
                    self.value.split('\n')[ind:ind + self.line])
小康2022's avatar
小康2022 已提交
771
                self.master.itemconfigure(self.text, text=self.value_surface)
小康2022's avatar
小康2022 已提交
772 773 774

    def input(self, event: tkinter.Event) -> None:
        """ 文本输入 """
小康2022's avatar
小康2022 已提交
775
        if self.master.lock:
小康2022's avatar
小康2022 已提交
776
            if self._press and self.live and not self.read:
小康2022's avatar
小康2022 已提交
777 778 779 780 781 782 783 784
                if event.keysym == 'BackSpace':
                    # 按下退格键
                    if len(self.value) > 1 and self.value[-2] == '\n':
                        self.value = self.value[:-2]
                    elif len(self.value):
                        self.value = self.value[:-1]
                elif len(event.char) and len(self.value) < self.limit:
                    # 按下普通按键
小康2022's avatar
小康2022 已提交
785 786
                    key = ord(event.char)
                    if 32 < key < 127 or key > 255 or (key == 32 and self.space):
小康2022's avatar
小康2022 已提交
787
                        line = sum(
小康2022's avatar
小康2022 已提交
788 789 790
                            [1 if 32 <= ord(i) < 127 else 2 for i in self.value.split('\n')[-1]])
                        line += 1 if 32 <= key < 127 else 2
                        if line > self.row:
小康2022's avatar
小康2022 已提交
791 792 793 794 795
                            self.value += '\n' + event.char
                        else:
                            self.value += event.char

                # 更新显示
小康2022's avatar
小康2022 已提交
796
                self.value_surface = self.value
小康2022's avatar
小康2022 已提交
797
                self.master.itemconfigure(self.text, text=self.value)
小康2022's avatar
小康2022 已提交
798
                self.cursor_update()
小康2022's avatar
小康2022 已提交
799 800


小康2022's avatar
小康2022 已提交
801 802 803
# 工具类


小康2022's avatar
小康2022 已提交
804
class PhotoImage(tkinter.PhotoImage):
小康2022's avatar
小康2022 已提交
805 806 807
    """
    图片类

小康2022's avatar
小康2022 已提交
808
    生成图片并进行相应的一些处理(支持png和gif格式)
小康2022's avatar
小康2022 已提交
809
    """
小康2022's avatar
小康2022 已提交
810 811 812 813

    def __init__(self,
                 file: str | bytes,
                 *args, **kwargs):
小康2022's avatar
小康2022 已提交
814
        """
小康2022's avatar
小康2022 已提交
815
        ### 参数说明
小康2022's avatar
小康2022 已提交
816

小康2022's avatar
小康2022 已提交
817 818
        `file`: 图片文件的路径
        `*args`、`**kwargs`: 其他参数
小康2022's avatar
小康2022 已提交
819
        """
小康2022's avatar
小康2022 已提交
820
        self.file = file
小康2022's avatar
小康2022 已提交
821 822 823
        if file.split('.')[-1] == 'gif':
            self.frames = []
        else:
小康2022's avatar
小康2022 已提交
824 825
            return tkinter.PhotoImage.__init__(self, file=file, *args, **kwargs)

小康2022's avatar
小康2022 已提交
826 827 828
    def parse(self, _ind: int = 0):
        """
        解析动图
小康2022's avatar
小康2022 已提交
829

小康2022's avatar
小康2022 已提交
830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859
        返回一个生成器
        """
        try:
            while True:
                self.frames.append(tkinter.PhotoImage(
                    file=self.file, format='gif -index %d' % _ind))
                _ind += 1
                yield _ind
        except:
            pass

    def play(self,
             canvas: Canvas,
             id,  # type: tkinter._CanvasItemId
             interval: int,
             _ind: int = 0):
        """
        播放动图

        #### 参数说明

        `canvas`: 播放动画的画布
        `id`: 播放动画的 _CanvasItemId(就是 create_text 的返回值)
        `interval`: 每帧动画的间隔时间
        """
        if _ind == len(self.frames):
            _ind = 0
        canvas.itemconfigure(id, image=self.frames[_ind])
        canvas.after(interval, self.play, canvas, id,
                     interval, _ind + canvas.lock)
小康2022's avatar
小康2022 已提交
860 861


小康2022's avatar
小康2022 已提交
862
# 功能函数
小康2022's avatar
小康2022 已提交
863 864


小康2022's avatar
小康2022 已提交
865
def move_widget(widget: Canvas | _BaseWidget,
小康2022's avatar
小康2022 已提交
866 867
                dx: int,
                dy: int,
小康2022's avatar
小康2022 已提交
868
                times: float,
小康2022's avatar
小康2022 已提交
869
                mode: typing.Literal['smooth', 'shake', 'flat'],
小康2022's avatar
小康2022 已提交
870 871 872
                _x: int = 0,
                _y: int = 0,
                _ind: int = 0) -> None:
小康2022's avatar
小康2022 已提交
873
    """
小康2022's avatar
小康2022 已提交
874
    ### 控件移动函数
小康2022's avatar
小康2022 已提交
875

小康2022's avatar
小康2022 已提交
876 877 878 879 880 881 882 883 884 885 886 887
    以特定方式移动由 Place 布局的某个控件或某些控件的集合或图像

    #### 参数说明

    `widget`: 要移动位置的控件
    `dx`: 横向移动的距离(单位:像素)
    `dy`: 纵向移动的距离
    `times`: 移动总时长(单位:秒)
    `mode`: 模式,可选三种(如下)
    1. `smooth`: 速度先慢后快再慢
    2. `shake`: 和 smooth 一样,但是最后会回弹一下
    3. `flat`: 匀速平移
小康2022's avatar
小康2022 已提交
888
    """
小康2022's avatar
小康2022 已提交
889 890 891

    # 三种模式的速度变化列表
    if mode == 'smooth':
小康2022's avatar
小康2022 已提交
892
        _ = [1, 2, 2, 3, 3, 5, 6, 7, 9, 12, 12, 9, 7, 6, 5, 3, 3, 2, 2, 1]
小康2022's avatar
小康2022 已提交
893
    elif mode == 'shake':
小康2022's avatar
小康2022 已提交
894
        _ = [10, 10, 9, 9, 8, 8, 7, 7, 6, 6, 5, 5, 4, 4, 3, 3, 1, -1, - 2, -3]
小康2022's avatar
小康2022 已提交
895
    elif mode == 'flat':
小康2022's avatar
小康2022 已提交
896
        _ = [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
小康2022's avatar
小康2022 已提交
897

小康2022's avatar
小康2022 已提交
898 899 900 901 902 903 904 905 906 907 908 909 910 911
    # 总计实际应该偏移值
    total = sum(_[:_ind + 1]) / 100

    # 计算偏移量
    x = int(_[_ind] * dx / 100)
    y = int(_[_ind] * dy / 100)

    # 累计偏移量(用于修正偏移)
    _x += x
    _y += y

    # 修正值
    _dx = round(total * dx) - _x
    _dy = round(total * dy) - _y
小康2022's avatar
小康2022 已提交
912

小康2022's avatar
小康2022 已提交
913 914 915 916 917 918 919 920
    # 修正值矫正
    x += _dx
    y += _dy
    _x += _dx
    _y += _dy

    if isinstance(widget.master, tkinter.Tk | tkinter.Frame):
        # 原坐标
小康2022's avatar
小康2022 已提交
921 922 923
        origin_x = int(widget.place_info()['x'])
        origin_y = int(widget.place_info()['y'])
        widget.place(x=origin_x + x, y=origin_y + y)
小康2022's avatar
小康2022 已提交
924
    elif isinstance(widget.master, Canvas):
小康2022's avatar
小康2022 已提交
925
        widget.move(x, y)
小康2022's avatar
小康2022 已提交
926

小康2022's avatar
小康2022 已提交
927
    if _ind < 19:
小康2022's avatar
小康2022 已提交
928
        # 更新函数
小康2022's avatar
小康2022 已提交
929
        widget.master.after(round(times * 50), move_widget, widget,
小康2022's avatar
小康2022 已提交
930
                            dx, dy, times, mode, _x, _y, _ind + 1)
小康2022's avatar
小康2022 已提交
931 932 933


def correct_text(length: int, string: str) -> str:
小康2022's avatar
小康2022 已提交
934
    """
小康2022's avatar
小康2022 已提交
935
    ### 修正字符串长度
小康2022's avatar
小康2022 已提交
936

小康2022's avatar
小康2022 已提交
937
    可将目标字符串改为目标长度并居中对齐
小康2022's avatar
小康2022 已提交
938 939 940 941 942

    #### 参数说明

    `length`: 目标长度
    `string`: 要修改的字符串
小康2022's avatar
小康2022 已提交
943
    """
小康2022's avatar
小康2022 已提交
944 945 946 947 948 949 950 951 952

    # 修正长度
    n = length - sum([1 + (ord(i) > 256) for i in string])
    # 修正空格数
    space = (n // 2) * ' '
    # 居中对齐
    value = space + string + space
    # 奇偶处理
    return value if n % 2 == 0 else value + ' '
小康2022's avatar
小康2022 已提交
953 954


小康2022's avatar
小康2022 已提交
955
def process_color(color: tuple[str, str] | None = None, proportion: float = 0) -> str:
小康2022's avatar
小康2022 已提交
956
    """
小康2022's avatar
小康2022 已提交
957
    ### 颜色字符串处理函数
小康2022's avatar
小康2022 已提交
958 959

    随机产生一个RGB颜色字符串,以及给出已有RGB颜色字符串的渐变RGB颜色字符串
小康2022's avatar
小康2022 已提交
960

小康2022's avatar
小康2022 已提交
961
    #### 参数说明
小康2022's avatar
小康2022 已提交
962

小康2022's avatar
小康2022 已提交
963 964
    `color`: 颜色元组 (要修改的颜色, 目标颜色)(为空时随机生成一个颜色)
    `proportion`: 改变比例(范围为0~1)
小康2022's avatar
小康2022 已提交
965 966
    """

小康2022's avatar
小康2022 已提交
967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985
    if color:
        # 渐变RGB颜色字符串的生成
        key = 256 if len(color[0]) == 7 else 16

        _ = int(color[0][1:], 16)
        _, B = divmod(_, key)
        R, G = divmod(_, key)

        _ = int(color[1][1:], 16)
        _, _B = divmod(_, key)
        _R, _G = divmod(_, key)

        RGB = R + round((_R - R) * proportion)
        RGB *= key
        RGB += G + round((_G - G) * proportion)
        RGB *= key
        RGB += B + round((_B - B) * proportion)
        return '#%0*X' % (6 if key == 256 else 3, RGB)
    else:
小康2022's avatar
小康2022 已提交
986
        # 随机RGB颜色字符串
小康2022's avatar
小康2022 已提交
987
        return '#' + ''.join(['0123456789ABCDEF'[random.randint(0, 15)] for _ in range(6)])
小康2022's avatar
小康2022 已提交
988 989 990 991 992


if __name__ == '__main__':
    """ 测试 """
    print(__doc__.replace('# ', '').replace('#', '').replace('---', ''))
小康2022's avatar
小康2022 已提交
993 994 995 996 997 998 999 1000
    root = Tk('Test', '960x540', None, True)
    canvas = Canvas(root, 960, 540)
    canvas.pack(expand=True, fill='both')
    CanvasButton(canvas, 100, 100, 100, 25, '按钮', command=lambda: print('Yes'))
    CanvasLabel(canvas, 100, 200, 150, 100)
    CanvasEntry(canvas, 300, 100, 150, 25, ('输入框', '点击输入'), limit=9)
    CanvasText(canvas, 300, 200, 300, 200, limit=400)
    root.mainloop()