跳转至

§1.2 认识 tkintertools#

约 2279 个字 223 行代码 预计阅读时间 30 分钟

一、基本介绍#

tkintertools 是一款基于 tkinter 模块、二次开发的轻量级界面编程模块。它的设计初心是为了辅助 tkinter 进行界面的开发,以达到 美化界面快捷开发简单易用 的目的。

提示

想要充分地利用 tkintertools 模块,开发人员首先要对 tkinter 模块有一定的了解(不需要了解太多)!

1.1 基本原理#

tkintertools 的控件并非真实的控件,而是在 tkinter 模块中 Canvas 对象中绘制而成的,也就是说,所有的控件都是没有句柄的,都只是 Canvas 对象中的线条和颜色块罢了。但就是这些线条和颜色块,就能重新构建我们的控件,尽管它们并非真的控件。由于是绘制而成的,自然我们就可以自定义我们控件的外观,而不再被原始控件的外观所约束。这就是所谓的 “美化界面” 啦!

有了我们自己绘制的控件,当然还不够,还需要辅以一些工具类或者函数来帮助我们进行更加快捷的开发!比如能够快速形成动画的 Animation 类,不用我们自己管理控件缩放的重写的 Canvas 控件等等!这就是 “快捷开发”!

有了这些东西,要是小白程序员不会用,看不懂文档,那这个模块也是没有用的!因此我了解了很多 Python 关于 type hints(类型提示)的知识,直接把开发文档作为 docstring(文档字符串)放在了每个类和函数源代码对应的位置,程序员可以在 IDE 中将鼠标移到类或者函数的名字上,简明文档就会自动弹出来。简明文档里面包含了这个类或者函数的用法、参数名称及作用,还有重载说明。当然,为了兼容不同的 Python 版本,我使用的 type hints 是最朴素的方式,直接以注释的方式呈现。这就是设计初心中的 “简单易用” 了。关于 type hints,请看 PEP 484 - Type Hints

1.2 模块优点#

tkintertools 完全没有使用任何第三方模块和库,同时它也没有任何依赖包(除了 tkinter 本身),它的功能完全由内置模块和函数实现,而且它还是跨平台的!
它的控件是在 tkinter 模块中 Canvas 对象中绘制而成的,这就赋予了 tkintertools 控件一些在 tkinter 中没有的特性,列举如下:

  • 控件背景可以是透明的(实际上就是没有填充颜色);
  • 控件的样式是高度自定义的(比如按钮有圆角);
  • 模块十分轻量,模块本身不包含任何资源文件,只有代码,且没有任何第三方模块的依赖,且只有仅仅一百多 KB,不像那些大型的库,动不动就几十 MB;

1.3 模块缺点#

模块使用了原生的 Canvas 对象作为底层基础,必然就牺牲了一些特性,同时还产生了一些缺点:

  • 虚拟的控件无法获取焦点(Tab 键切换的那个焦点);
  • 虚拟的控件在文本输入和显示的功能上存在一些缺陷,文本可能会显示在边框外面;

二、特色功能#

下面列举一些 tkintertools 模块还具有的一些特色功能(不一定完整)。

2.1 高度可配置的控件#

tkintertools 模块的控件拥有许多参数供我们设置,比如圆角的半径、文本和边框以及控件内部的颜色,关联事件等等。
这里要说明的是,每个控件可以设置的关联事件不止一种,在鼠标经过控件时可以绑定事件,鼠标点击控件也可以,鼠标点击后松开也行等等。
文本和边框以及控件的填充色也是类似的,在鼠标经过控件、点击控件、点击后松开都可以设定颜色。
文本类控件还能够从右边逐步输入文本,文本输入提示符也可以不是单调无趣的竖线,可以是其他的,比如下划线等。

下面给大家简单展示几张图,让大家感受一下效果!

png

倒数第五排的控件有个透明的效果,上图展示不出来,这里加上背景图片会更清晰一点:

png

下面是源代码,上图中左下按钮点击后可以打开模块源代码的仓库,倒数第二排的开关可以切换背景。

不接受反驳,我觉得这比 tkinter 原生的控件好看了 100 倍!

源代码
import webbrowser

import tkintertools as tkt


def colorful(canvas: tkt.Canvas, x: int, y: int, width: int, height: int, colortup: tuple[str, str], flag: bool = True) -> None:
    """渐变色"""
    if flag:
        for i in range(width):
            fill = tkt.color(colortup, i / width)
            canvas.create_line(x + i, y, x + i, y + height, width=1, fill=fill)
    else:
        for i in range(height):
            fill = tkt.color(colortup, i / height)
            canvas.create_line(x, y + i, x + width, y + i, width=1, fill=fill)


root = tkt.Tk('tkintertools', 1280, 720)
canvas = tkt.Canvas(root, 1280, 720, 0, 0)
try:
    background = tkt.PhotoImage('background.png')
except:
    background = None
bg_item = canvas.create_image(640, 360, image=background)

colorful(canvas, 10, 360, 150, 40, ('#FF0000', '#00FF00'))
colorful(canvas, 170, 360, 250, 40, ('#00FF00', '#0000FF'))
colorful(canvas, 430, 360 + 5, 60, 30, ('#0000FF', '#FF0000'))
colorful(canvas, 500, 360, 200, 40, ('#00FFFF', '#FF00FF'))
colorful(canvas, 720, 360 + 5, 30, 30, ('#FF00FF', '#FFFF00'))
colorful(canvas, 935, 360 + 5, 335, 30, ('#FFFF00', '#00FFFF'))

colorful(canvas, 10, 410, 150, 40, ('#FF0000', '#00FF00'), False)
colorful(canvas, 170, 410, 250, 40, ('#00FF00', '#0000FF'), False)
colorful(canvas, 430, 410 + 5, 60, 30, ('#0000FF', '#FF0000'), False)
colorful(canvas, 500, 410, 200, 40, ('#00FFFF', '#FF00FF'), False)
colorful(canvas, 720, 410 + 5, 30, 30, ('#FF00FF', '#FFFF00'), False)
colorful(canvas, 935, 410 + 5, 335, 30, ('#FFFF00', '#00FFFF'), False)

tkt.Button(canvas, 10, 10, 150, 40, text='Button', radius=0)
tkt.Button(canvas, 10, 60, 150, 40, text='Button')
tkt.Button(canvas, 10, 110, 150, 40, text='Button', radius=20)
tkt.Button(canvas, 10, 160, 150, 40, text='Button').set_live(False)
tkt.Button(canvas, 10, 210, 150, 40, text='Button', radius=0, borderwidth=3)
tkt.Button(canvas, 10, 260, 150, 40, text='Button', color_fill=(
    'lightgreen', 'springgreen', 'green'), color_outline=('grey', 'black', 'black'))
tkt.Button(canvas, 10, 310, 150, 40, text='Button',
           color_text=('orange', 'red', 'red'))
tkt.Button(canvas, 10, 360, 150, 40, text='Button',
           radius=0, color_fill=tkt.COLOR_NONE)
tkt.Button(canvas, 10, 410, 150, 40, text='Button',
           radius=0, color_fill=tkt.COLOR_NONE)
tkt.Button(canvas, 10, 460, 150, 40, text='Button',
           radius=0, color_fill=tkt.COLOR_NONE)
tkt.Button(canvas, 10, 510, 150, 40, text='Button', justify='left')
tkt.Button(canvas, 10, 560, 150, 40, text='Button',
           color_outline=('red', 'red', 'red'))
tkt.Button(canvas, 10, 610, 150, 40, text='Button', font=('华文行楷', tkt.SIZE))

tooltip = tkt.ToolTip("Click this to give me a star!\nThanks! (●'◡'●)")
tkt.Button(canvas, 10, 660, 150, 40, text='👉Like👈', tooltip=tooltip,
           command=lambda: webbrowser.open('https://github.com/Xiaokang2022/tkintertools'))

tkt.Entry(canvas, 170, 10, 250, 40, text=('Entry', 'Input'), radius=0)
tkt.Entry(canvas, 170, 60, 250, 40, text=('Entry', 'Input'))
tkt.Entry(canvas, 170, 110, 250, 40, text=('Entry', 'Input'), radius=20)
tkt.Entry(canvas, 170, 160, 250, 40, text=('Entry', 'Input')).set_live(False)
tkt.Entry(canvas, 170, 210, 250, 40, text=(
    'Entry', 'Input'), radius=0, borderwidth=3)
tkt.Entry(canvas, 170, 260, 250, 40, text=('Entry', 'Input'), color_fill=(
    'skyblue', 'cyan', 'cyan'), color_outline=('grey', 'black', 'black'))
tkt.Entry(canvas, 170, 310, 250, 40, text=('Entry', 'Input'),
          color_text=('lightgreen', 'springgreen', 'springgreen'))
tkt.Entry(canvas, 170, 360, 250, 40, text=('Entry', 'Input'),
          radius=0, color_fill=tkt.COLOR_NONE)
tkt.Entry(canvas, 170, 410, 250, 40, text=('Entry', 'Input'),
          radius=0, color_fill=tkt.COLOR_NONE)
tkt.Entry(canvas, 170, 460, 250, 40, text=('Entry', 'Input'),
          radius=0, color_fill=tkt.COLOR_NONE)
tkt.Entry(canvas, 170, 510, 250, 40, text=('Entry', 'Input'), justify='center')
tkt.Entry(canvas, 170, 560, 250, 40, text=('Entry', 'Input'),
          color_outline=('red', 'red', 'red'))
tkt.Entry(canvas, 170, 610, 250, 40, text=(
    'Entry', 'Input'), font=('华文行楷', tkt.SIZE))
tkt.Entry(canvas, 170, 660, 250, 40, text=(
    'Entry', 'Input'), show='●').set('Entry')

tkt.Switch(canvas, 430, 10 + 5, 30, radius=0)
tkt.Switch(canvas, 430, 60 + 5, 30, radius=4)
tkt.Switch(canvas, 430, 110 + 5, 30)
tkt.Switch(canvas, 430, 160 + 5, 30, radius=4).set_live(False)
tkt.Switch(canvas, 430, 210 + 5, 30, radius=0, borderwidth=3)
tkt.Switch(canvas, 430, 260 + 5, 30, default=True, color_fill_on=('orange',
           'yellow', 'yellow'), color_outline_on=('grey', 'black', 'black'))
tkt.Switch(canvas, 430, 310 + 5, 30,
           color_fill_slider=('red', 'yellow', 'yellow'))
tkt.Switch(canvas, 430, 360 + 5, 30, radius=0,
           color_fill_on=tkt.COLOR_NONE, color_fill_off=tkt.COLOR_NONE)
tkt.Switch(canvas, 430, 410 + 5, 30, radius=0,
           color_fill_on=tkt.COLOR_NONE, color_fill_off=tkt.COLOR_NONE)
tkt.Switch(canvas, 430, 460 + 5, 30, radius=0,
           color_fill_on=tkt.COLOR_NONE, color_fill_off=tkt.COLOR_NONE)
tkt.Switch(canvas, 430, 510 + 10, 20, width=60, default=True)
tkt.Switch(canvas, 430, 560 + 5, 30, color_outline_on=('red',
           'red', 'red'), color_outline_off=('red', 'red', 'red'))
tkt.Switch(canvas, 430, 610 + 5, 30, on=lambda: canvas.itemconfigure(bg_item,
           image=background), off=lambda: canvas.itemconfigure(bg_item, image=''), default=True)

tkt.Label(canvas, 500, 10, 200, 40, text='Label', radius=0)
tkt.Label(canvas, 500, 60, 200, 40, text='Label')
tkt.Label(canvas, 500, 110, 200, 40, text='Label', radius=20)
tkt.Label(canvas, 500, 160, 200, 40, text='Label').set_live(False)
tkt.Label(canvas, 500, 210, 200, 40, text='Label', radius=0, borderwidth=3)
tkt.Label(canvas, 500, 260, 200, 40, text='Label', color_fill=(
    'lightpink', 'deeppink'), color_outline=('grey', 'black'))
tkt.Label(canvas, 500, 310, 200, 40, text='Label',
          color_text=('skyblue', 'cyan'))
tkt.Label(canvas, 500, 360, 200, 40, text='Label',
          radius=0, color_fill=tkt.COLOR_NONE)
tkt.Label(canvas, 500, 410, 200, 40, text='Label',
          radius=0, color_fill=tkt.COLOR_NONE)
tkt.Label(canvas, 500, 460, 200, 40, text='Label', radius=0,
          color_fill=tkt.COLOR_NONE, color_text=('grey', 'white', 'white'))
tkt.Label(canvas, 500, 510, 200, 40, text='Label', justify='right')
tkt.Label(canvas, 500, 560, 200, 40, text='Label',
          color_outline=('red', 'red', 'red'))
tkt.Label(canvas, 500, 610, 200, 40, text='Label', font=('华文行楷', tkt.SIZE))

tkt.CheckButton(canvas, 720, 10 + 5, 30, text='CheckButton', radius=0)
tkt.CheckButton(canvas, 720, 60 + 5, 30, text='CheckButton')
tkt.CheckButton(canvas, 720, 110 + 5, 30, text='CheckButton', radius=15)
tkt.CheckButton(canvas, 720, 160 + 5, 30, text='CheckButton').set_live(False)
tkt.CheckButton(canvas, 720, 210 + 5, 30,
                text='CheckButton', radius=0, borderwidth=3)
tkt.CheckButton(canvas, 720, 260 + 5, 30, text='CheckButton', color_fill=(
    'skyblue', 'cyan', 'cyan'), color_outline=('grey', 'black', 'black'))
tkt.CheckButton(canvas, 720, 310 + 5, 30, text='CheckButton',
                color_text=('pink', 'orange', 'orange'))
tkt.CheckButton(canvas, 720, 360 + 5, 30, text='CheckButton', radius=0,
                color_fill=tkt.COLOR_NONE, color_text=('grey', 'white', 'white'))
tkt.CheckButton(canvas, 720, 410 + 5, 30, text='CheckButton', radius=0,
                color_fill=tkt.COLOR_NONE, color_text=('grey', 'white', 'white'))
tkt.CheckButton(canvas, 720, 460 + 5, 30, text='CheckButton', radius=0,
                color_fill=tkt.COLOR_NONE, color_text=('grey', 'white', 'white'))
tkt.CheckButton(canvas, 895, 510 + 5, 30, text='CheckButton',
                justify='left', color_text=('grey', 'white', 'white'))
tkt.CheckButton(canvas, 720, 560 + 5, 30, text='CheckButton',
                color_outline=('red', 'red', 'red'), color_text=('grey', 'white', 'white'))
tkt.CheckButton(canvas, 720, 610 + 5, 30, text='CheckButton',
                font=('华文行楷', tkt.SIZE), color_text=('grey', 'white', 'white'))
tkt.CheckButton(canvas, 720, 660 + 5, 30, text='CheckButton',
                tick='✕', value=True, color_text=('grey', 'white', 'white'))

tkt.ProgressBar(canvas, 935, 10 + 5, 335, 30)
tkt.ProgressBar(canvas, 935, 60 + 5, 335, 30).load(0.5)
tkt.ProgressBar(canvas, 935, 110 + 5, 335, 30).load(1)
tkt.ProgressBar(canvas, 935, 160 + 5, 335, 30).set_live(False)
tkt.ProgressBar(canvas, 935, 210 + 5, 335, 30, borderwidth=3)
tkt.ProgressBar(canvas, 935, 260 + 5, 335, 30, color_fill=('lightpink',
                'deeppink'), color_outline=('grey', 'black')).load(2 / 3)
tkt.ProgressBar(canvas, 935, 310 + 5, 335, 30,
                color_text=('brown', 'red', 'red')).load(1 / 3)
tkt.ProgressBar(canvas, 935, 360 + 5, 335, 30,
                color_fill=('', 'pink')).load(1 / 6)
tkt.ProgressBar(canvas, 935, 410 + 5, 335, 30,
                color_fill=('', 'deeppink')).load(1 / 7)
tkt.ProgressBar(canvas, 935, 460 + 5, 335, 30, color_fill=('',
                'skyblue'), color_text=('grey', 'white', 'white')).load(1 / 9)
tkt.ProgressBar(canvas, 935, 510 + 5, 335, 30, justify='left').load(5 / 7)
tkt.ProgressBar(canvas, 935, 560 + 5, 335, 30,
                color_outline=('red', 'red', 'red'))
tkt.ProgressBar(canvas, 935, 610 + 5, 335, 30, font=('华文行楷', tkt.SIZE))
tkt.ProgressBar(canvas, 935, 660 + 5, 335, 30, mode='indeterminate').load(0.3)

canvas.create_text(565 + 1, 680 + 1, text='Created by 小康2022', fill='grey')
canvas.create_text(565, 680, text='Created by 小康2022', fill='gold')

root.mainloop()

2.2 自动控制大小#

tkintertools 中的控件,其大小和形状可以随着窗口的变化而成比例地变化,不仅仅是控件中的文本,Canvas 绘制的图形也会随之变动,更让人兴奋的是,png 类型的图片也会随之成比例地缩放!当然,你也可以设定参数让其不随之变动,也可以设定参数使其在缩放的时候保持横纵方向的比例。
总之,很方便,很舒适!

下面一张图展示了什么叫“解放双手,全自动控制”!顺便一提,下面的是旧版 tkintertools 的测试图像,暂时把图拿来充数,从操作系统看得出来,是以前的图。

gif

2.3 高清显示#

其实这算是 tkinter 的一个 bug 没有修复而已……这是在 Windows 系统上独有的问题,对于电脑缩放比率不是 100% 的伙计们而言,这个问题就会出现,也就是窗口模糊。但好在 tkintertools 解决了这个问题!顺便一提,这个解决办法是在 Python 内置库 idlelib 中找到的。

说明一下,这方法是我在 idlelib 源代码里面泡了好几个小时才找到的(我裂开来)……

我们以 IDLE 为例,若是执行处理了 DPI 的相关功能,效果是这样的:

png

若是不执行相关的函数,效果是这样的:

png

这两张图片的清晰度完全不一样,这里可不是我故意改变了图片的清晰度!这在我的笔记本电脑上是真实存在的效果!下面是我笔记本的缩放比率(显然不是 100%):

png

2.4 简单的动画效果#

通过 tkintertools ,要实现一个简单的动画效果十分容易,下面就是一个示例(旧版图片,拿来暂时充数):

gif

当然,不仅仅是这样的动画效果,还可以这样骚操作:

gif

这桌面壁纸有点抽风???

2.5 渐变色#

渐变色实际上并不是主要的功能,只是 tkintertools 附带的功能。
下面是一个简单的渐变色示例:

png

下面这张是不是更加逼真呢?

png

嗯,很逼真,但并没有什么用,而且还非常吃内存和性能(哭……)

上面展示控件时的渐变色就是这么来的。

2.6 详细的类型提示#

参考 PEP 526PEP 586PEP 604PEP 612tkintertools 采用了最兼容的方式去实现详细的类型提示(兼容不同的 Python 版本),可适用 IDE 有 Visual Studio Code、PyCharm、Visual Studio 等。
那什么是类型提示呢?话不多说,直接看图就行(下面的图是以前的,现在有点不一样了):

png

在 Visual Studio Code 编辑器中,当鼠标移至类或者函数的名字上面时,会自动显示该类或者函数的注释文档。通过这种方式,不需要看太多的帮助文档和资料就能熟练地使用 tkintertools 模块!

这些类型提示,对应到源代码上去就是这些注释文档:

png

2.7 跨平台#

目前已经测试通过的操作系统有:

Windows10 Windows11 macOS macOS Ubuntu22.04 Deepin20.9

但我估摸着通过测试的要求应该是下面这样的:

  • Windows: 版本大于等于 7 应该都行,简单说就是 Windows 7 及以上
  • macOS: 版本大于 11 的应该可以,更低版本的没有测试过
  • Linux: 没啥大问题,应该都可以

不同操作系统上的具体效果(并非完全一样,有一点点区别)可以在下一节末尾看到,这里就不展示啦。

2.8 简单 3D 绘图#

tkintertools 的子模块 tools_3d 可以方便快捷地绘制一些直边的几何体以及单独的点、线和面,还有文本,以及进行一些简单的操作,如平移、旋转、缩放等。

png

第二张图片可以更好地显示空间感,而且呢,运行下面的源代码的程序,按下键盘上面的 “+” 和 “-” 可以缩放每个面的自身的大小,这样就可以看到下面这个三维结构内部的样子了!

png

你就说,这酷不酷吧?哇噻!这真的是泰裤啦!

源代码
import itertools
import math
import statistics

import tkintertools as tkt
from tkintertools import tools_3d as t3d

root = tkt.Tk('3D', 1280, 720)
space = t3d.Space(root, 1280, 720, 0, 0, bg='black', keep=False)

m = 200 * math.sqrt(50 - 10 * math.sqrt(5)) / 10
n = 200 * math.sqrt(50 + 10 * math.sqrt(5)) / 10
points = []  # 顶点列表
dis_side = 200 * (3 * math.sqrt(3) + math.sqrt(15)) / 12 / \
    ((math.sqrt(10 + 2 * math.sqrt(5))) / 4)  # 面到中心的距离
count, color_lst = 0, ['00', '77', 'FF']  # 颜色可能值
color = ['#%s%s%s' % (r, g, b)
         for r in color_lst for g in color_lst for b in color_lst]  # 颜色列表

for i in m, -m:
    for j in n, -n:
        points.append([0, j, i])
        points.append([i, 0, j])
        points.append([j, i, 0])

for p in itertools.combinations(points, 3):  # 所有的顶点组合
    dis = math.hypot(*[statistics.mean(c[i] for c in p) for i in range(3)])
    if math.isclose(dis, dis_side):
        t3d.Side(space, *p, fill=color[count], outline='white')
        count += 1


def scale(event):
    """ 缩放事件 """
    k = 1.05 if event.keysym == 'equal' else 0.95 if event.keysym == 'minus' else 1
    for geo in space.items_3d():
        geo.scale(k, k, k)
        geo.update()
    space.space_sort()


space.space_sort()
root.bind('<Key-equal>', scale)
root.bind('<Key-minus>', scale)
root.mainloop()

此处只是一些基础操作,还有更多高级操作等你去挖掘!

欢迎在评论区留下你的想法


下面没有更多评论啦😆