最近更新于 2023-07-05 09:59

前言

这些主要是 21 年的时候学习 Python 的官方图形库 Tkinter 时写的,在写比较简单的工具的时候用这个库会比较方便,也不依赖第三方,这些 demo 算是 tk 的简单应用,对于有需要的可以用作参考。

环境

编写时的测试环境:Python 3.8.11
本文再次验证的环境:
Ubuntu 22.04(Windows 11 专业工作站版 22H2 WSL2)
Python 3.11.3

源码注明的除外

代码

框架(Pack 布局)

"""
@brief Tkinter 面向对象设计框架

Copyright (C) 2021 IYATT-yx iyatt@iyatt.com
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
from tkinter import *
import tkinter.messagebox as messagebox
import tkinter.font as font

class Application(Frame):
    def __init__(self, master=None):
        super().__init__(master)  # 父类
        self.__master = master
        self.pack()

        self.createWidget()

    def createWidget(self):
        # 标签组件
        self.label = Label(
            self, text='这是一个标签', width=10, height=1, bg='red', fg='black'
        )
        self.label.pack()

        # 按钮组件
        self.btn1 = Button(self)
        self.btn1['text'] = '按钮'
        self.btn1['command'] = self.doWork
        self.btn1['anchor'] = NE  # 文字在按钮中的位置 north east
        self.btn1.config(width=5, height=5)
        self.btn1.pack()

        # 单行输入框
        self.entryVar = StringVar(value='hello')
        self.entry = Entry(self, textvariable=self.entryVar)
        self.entry.pack()

        self.btn2 = Button(self, text='查看单行框', command=self.viewEntry)
        self.btn2.pack()

        # 多行输入框
        self.text = Text(self, width=40, height=5, bg='blue')
        self.text.pack()

        self.btn3 = Button(self, text='查看多行框', command=self.viewText)
        self.btn3.pack()

        # 单选框
        self.radioVar = StringVar()
        self.radioVar.set('单选一')  # 默认初始选中
        self.radio1 = Radiobutton(self, text='单选一', value='单选一', variable=self.radioVar)
        self.radio2 = Radiobutton(self, text='单选二', value='单选二', variable=self.radioVar)
        self.radio1.pack(side='left')
        self.radio2.pack(side='left')

        # 多选框
        self.checkVar1 = IntVar()
        self.checkVar2 = IntVar()
        self.check1 = Checkbutton(
            self, text='选项一', variable=self.checkVar1, onvalue=1, offvalue=0
        )
        self.check2 = Checkbutton(
            self, text='选项二', variable=self.checkVar2, onvalue=1, offvalue=0
        )
        self.check1.pack(side='right')
        self.check2.pack(side='right')

        # 画布
        self.canvas = Canvas(self, width=100, height=100, bg='green')
        self.canvas.pack()
        self.canvas.create_line(10, 10, 30, 30, 50, 50, 70, 10, 90, 90)
        self.canvas.create_rectangle(20, 20, 80, 90)

        # 退出按钮
        self.btnQuit = Button(self, text='退出', command=self.__master.destroy)
        self.btnQuit.pack()

    def doWork(self):
        messagebox.showinfo('提示', '按钮被点击')

    # 查看你单行输入框的值
    def viewEntry(self):
        self.text.insert(INSERT, self.entryVar.get())
        # messagebox.showinfo('查看单行框', self.entryVar.get())
        messagebox.showinfo('查看单行框', self.entry.get())

    def viewText(self):
        messagebox.showinfo('查看多行框', self.text.get(1.0, END))

if __name__ == '__main__':
    root = Tk()
    root.geometry('400x500+300+200')  # 400x200 左距300 上距200
    defaultFont = font.Font(family='黑体', size=12)  # 字体
    root.option_add("*Font", defaultFont)
    root.title('Tkinter 面向对象设计框架示例')
    app = Application(master=root)

    root.mainloop()

选牌(place 布局)

图像资源下载:resources
文件下载解压后得到的文件夹放到源码所在的目录下

#!/usr/bin/env python3
"""
@brief 选牌

Copyright (C) 2021 IYATT-yx iyatt@iyatt.com
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
from tkinter import *
import os

class Application(Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.__master = master
        self.pack()

    def createWidgets(self):
        abspath = os.path.dirname(os.path.abspath(__file__))

        self.photo = [
            PhotoImage(file=abspath + '/resources/' + str(i) + '.png') for i in range(9)
        ]
        self.label = [Label(self.__master, image=self.photo[i]) for i in range(9)]

        for i in range(0, 9):
            self.label[i].place(x=30 + i * 30, y=120)

        self.label[0].bind_class('Label', '<Button-1>', self.play)

    # 选牌
    def play(self, event):
        if event.widget.winfo_y() == 120:
            event.widget.place(y=90)
        else:
            event.widget.place(y=120)

if __name__ == '__main__':
    root = Tk()
    root.geometry('400x400+400+400')
    root.title('选牌')
    app = Application(root)
    app.createWidgets()
    root.mainloop()

点击选牌

登录界面+计算器(Grid 布局)

"""
@brief 登录界面+计算器

Copyright (C) 2021 IYATT-yx iyatt@iyatt.com
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
from tkinter import *

class GridApplication(Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.__master = master
        self.pack()

    def createWidget(self):
        Label(self, text='用户名').grid(column=0, row=0)
        self.entry1 = Entry(self)
        self.entry1.grid(column=1, row=0)

        Label(self, text='密码').grid(column=0, row=1)
        self.entry2 = Entry(self, show='*')
        self.entry2.grid(column=1, row=1)

        Button(self, text='登录').grid(column=1, row=2, sticky=EW)
        Button(self, text='退出', command=self.__master.destroy).grid(
            column=2, row=2, sticky=E
        )

# 计算器布局
class Calculator(Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.__master = master
        self.pack()

    def createWidget(self):
        # 计算式显示框
        Text(self, height=3).grid(column=0, row=0, columnspan=4, sticky=EW)
        Button(self, text='7').grid(column=0, row=1, sticky=EW)
        Button(self, text='8').grid(column=1, row=1, sticky=EW)
        Button(self, text='9').grid(column=2, row=1, sticky=EW)
        Button(self, text='÷').grid(column=3, row=1, sticky=EW)
        Button(self, text='4').grid(column=0, row=2, sticky=EW)
        Button(self, text='5').grid(column=1, row=2, sticky=EW)
        Button(self, text='6').grid(column=2, row=2, sticky=EW)
        Button(self, text='×').grid(column=3, row=2, sticky=EW)
        Button(self, text='1').grid(column=0, row=3, sticky=EW)
        Button(self, text='2').grid(column=1, row=3, sticky=EW)
        Button(self, text='3').grid(column=2, row=3, sticky=EW)
        Button(self, text='-').grid(column=3, row=3, sticky=EW)
        Button(self, text='0').grid(column=0, row=4, sticky=EW)
        Button(self, text='.').grid(column=1, row=4, sticky=EW)
        Button(self, text='=').grid(column=2, row=4, sticky=EW)
        Button(self, text='+').grid(column=3, row=4, sticky=EW)

if __name__ == '__main__':
    grid = Tk()
    grid.geometry('300x100+300+400')
    grid.title('grid布局演示')
    gridApp = GridApplication(grid)
    gridApp.createWidget()

    calc = Tk()
    calc.geometry('700x200+700+400')
    calc.title('计算器布局')
    calcApp = Calculator(calc)
    calcApp.createWidget()

    mainloop()

选项菜单

#!/usr/bin/env python3
"""
@brief 选项菜单

Copyright (C) 2021 IYATT-yx iyatt@iyatt.com
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
from tkinter import *

class Application(Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.__master = master
        self.pack()

    def createWidgets(self):
        var = StringVar()
        var.set('第一个选项')
        self.__om = OptionMenu(self.__master, var, '第一个选项', '第二个选项', '第三个选项')
        self.__om['width'] = 50
        self.__om.pack()

if __name__ == '__main__':
    root = Tk()
    root.geometry('300x100+400+400')
    root.title('选项菜单')
    app = Application(root)
    app.createWidgets()
    root.mainloop()

file

滑块

#!/usr/bin/env python3
"""
@brief 滑块

Copyright (C) 2021 IYATT-yx iyatt@iyatt.com
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
from tkinter import *

class Application(Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.__master = master
        self.pack()

    def showVar(self, value):
        print('滑块值:', value)

    def createWidgets(self):
        Scale(
            self.__master,
            from_=0,
            to=50,
            length=500,
            tickinterval=5,
            orient=HORIZONTAL,
            command=self.showVar,
        ).pack()

if __name__ == '__main__':
    root = Tk()
    root.geometry('600x100+400+400')
    root.title('滑块')
    app = Application(root)
    app.createWidgets()
    root.mainloop()

file

记事本

"""
@brief 记事本

Copyright (C) 2021 IYATT-yx iyatt@iyatt.com
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
from tkinter import *
from tkinter.filedialog import *
from tkinter.messagebox import *
from tkinter.colorchooser import *
import os

class Notepad(Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.__master = master
        self.pack()
        self.__saveType = 'newFile'
        self.__master.protocol("WM_DELETE_WINDOW", self.__onClosing)

    def createWidgets(self):
        """ 下拉菜单
        """

        # 主菜单栏
        self.__menubar = Menu(self.__master)
        self.__master.config(menu=self.__menubar)

        # 文件
        self.__fileMenu = Menu(self.__menubar)
        self.__menubar.add_cascade(label='文件', menu=self.__fileMenu)
        # 设置
        self.__setMenu = Menu(self.__master)
        self.__menubar.add_cascade(label='设置', menu=self.__setMenu)
        # 帮助
        self.__aboutMenu = Menu(self.__master)
        self.__menubar.add_cascade(label='帮助', menu=self.__aboutMenu)

        # 文件 - 子选项
        self.__fileMenu.add_command(label='新建', command=self.__newFile)
        self.__fileMenu.add_command(label='打开', command=self.__openFile)
        self.__fileMenu.add_command(label='保存', command=self.__saveFile)
        self.__fileMenu.add_command(label='另存为', command=self.__saveAs)
        # 设置 - 子选项
        self.__setMenu.add_command(label='背景颜色', command=self.__setBg)
        # 帮助 - 子选项
        self.__aboutMenu.add_command(label='关于', command=self.__about)

        # 文本框
        self.__text = Text()
        self.__text.place(relx=0.001, rely=0.001, relwidth=0.998, relheight=0.998)

        # 文本框内容修改事件响应
        self.__isChanged = False
        self.__text.bind('<KeyPress>', self.__changeFlag)

    def __newFile(self):
        """ 新建文件
        """
        self.__saveType = 'newFile'

        if self.__isChanged == True:
            if askyesnocancel(title='提示', message='是否保存?'):
                self.__saveFile()
            else:
                return

        self.__text.delete(1.0, END)

    def __openFile(self):
        """ 打开文件
        """
        self.__saveType = 'openFile'

        if self.__isChanged == True:
            if self.__isChanged == True:
                confirm = askyesnocancel(title='提示', message='是否保存?')
                if confirm == True:
                    self.__saveFile()
                elif confirm == None:
                    return
                else:
                    pass

        self.__fileName = askopenfilename(title='打开文件')

        if not self.__fileName:
            return

        with open(self.__fileName, 'r') as f:
            self.__text.delete(1.0, END)
            self.__text.insert(INSERT, f.read())

        self.__isChanged = False

    def __saveFile(self):
        """ 保存文件
        """
        if self.__saveType == 'newFile' or self.__saveType == 'saveAs':
            self.__fileName = asksaveasfilename(
                title='新建文件',
                initialfile='未命名.txt',
                filetypes=[('文本文档', '*.txt')],
                defaultextension='*.txt',
            )
        elif self.__saveType == 'openFile':
            pass

        if not self.__fileName:
            return

        with open(self.__fileName, 'w') as f:
            f.write(self.__text.get(1.0, END))

        self.__isChanged = False

    def __saveAs(self):
        """ 另存为
        """
        self.__saveType = 'saveAs'
        self.__saveFile()

    def __changeFlag(self, key):
        """ 文本框修改标志
        """
        self.__isChanged = True

    def __onClosing(self):
        """ 关闭事件回调
        """
        if self.__isChanged == True:
            confirm = askyesnocancel(title='提醒', message='是否保存?')
            if confirm == True:
                self.__saveFile()
            elif confirm == None:
                return
            else:
                pass

        self.__master.destroy()

    def __setBg(self):
        """ 设置背景颜色
        """
        self.__bg = askcolor(color='white', title='选择背景颜色')
        self.__text.config(bg=self.__bg[1])

    def __about(self):
        """ 关于
        """
        showinfo('关于', 'Copyright (C) 2021 IYATT-yx\niyatt@iyatt.com')

def main():
    root = Tk()
    root.geometry('600x400+300+300')
    root.title('记事本')

    # 设置图标
    # absPath = os.path.dirname(os.path.abspath(__file__))
    # icon = PhotoImage(file=absPath + '/notepad.png')
    # root.iconphoto(False, icon)

    # 记事本界面实例化
    app = Notepad(root)
    app.createWidgets()

    root.mainloop()

if __name__ == '__main__':
    main()

可以新建、修改以及保存文件,在文本框内容发生修改后,试图新建、打开、关闭等操作会询问是否保存,可以设置背景显示颜色
file
file
file
file

简易图片查看器

"""
@brief 简易图片查看器

Copyright (C) 2021 IYATT-yx iyatt@iyatt.com
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
# pip install opencv-python==4.7.0.72
import cv2
import tkinter as tk
# pip install Pillow==9.5.0
# 注意:Tkinter 和 PIL 中都有名字为 Image 的类,所以这里导入 Tkinter 没有使用通配符,以避免出现错误
from PIL import ImageTk, Image, UnidentifiedImageError
from tkinter.filedialog import *
from tkinter.messagebox import *

class Viewer(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self._master = master
        self.pack()

    def createWidgets(self):
        global img
        self.imgLabel = tk.Label(self)
        self.imgLabel.pack()

        self._openWay = tk.IntVar(value=1)
        self.pillow = tk.Radiobutton(self, text='Pillow', value=1, variable=self._openWay)
        self.opencv = tk.Radiobutton(self, text='OpenCV', value=2, variable=self._openWay)
        self.pillow.pack()
        self.opencv.pack()

        tk.Button(self, text='打开', command=self.openImg).pack()

    def openImg(self):
        global img
        fileName = askopenfilename(title='打开文件')
        if not fileName:
            return
        try:
            if self._openWay.get() == 1:  # PIL 打开图片
                img = ImageTk.PhotoImage(image=Image.open(fileName))
            elif self._openWay.get() == 2:  # OpenCV 打开图片
                src = cv2.imread(fileName, cv2.IMREAD_COLOR)
                if src is None:
                    raise UnidentifiedImageError('不能识别图像文件 \'{}\''.format(fileName))
                src = cv2.cvtColor(src, cv2.COLOR_BGR2RGB) # OpenCV 图像数据为 BGR 的 NumPy 数组,需要先转为 RGB
                img = ImageTk.PhotoImage(image=Image.fromarray(src))
        except Exception as e:
            showerror("错误", e)
            return
        self.imgLabel.config(image=img)  # 更新图片显示

def main():
    global img
    img = ''
    root = tk.Tk()
    root.geometry('600x500+300+200')
    root.title('简单图片查看器')
    viewer = Viewer(root)
    viewer.createWidgets()
    root.mainloop()

if __name__ == '__main__':
    main()

这里提供了两种思路打开图片文件,一种是直接使用 Pillow,另外一种是使用 OpenCV,然后再转换数据类型(搞懂这个,就可以将 OpenCV 与 Tkinter 融合开发),其它一些图像数据类型可以参考:https://blog.iyatt.com/?p=2592
file
file

file

摄像头预览

"""
@brief 摄像头预览

Copyright (C) 2023 IYATT-yx iyatt@iyatt.com
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
# Windows 11 专业工作站版 22H2
# Python 3.11.4
# pip install opencv-python==4.7.0.72
import tkinter as tk
import cv2
# pip install Pillow==9.5.0
from PIL import ImageTk, Image

class CameraPreview(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.__master = master
        self.__master.title('摄像头预览')
        self.__master.geometry('800x600')
        self.__master.protocol('WM_DELETE_WINDOW', self.__on_closing) # 关闭事件
        self.pack()
        self.__cap = None # 视频捕获对象
        self.__state = False # 图像更新标志
        self.__create_widgets()

    def __create_widgets(self):
        """ 创建窗口组件 """
        # 图像预览
        self.__image_preview_label = tk.Label(self, text='暂无图像')
        self.__image_preview_label.pack()

        # 视频源输入
        self.__video_source_entry = tk.Entry(self)
        self.__video_source_entry.pack()

        # 视频源打开
        tk.Button(self, text='打开', command=self.__on_open_video_source_button).pack()

        # 图像预览更新
        self.__master.after(100, self.__update_image_preview_label)

    def __on_open_video_source_button(self):
        """ 打开按钮回调 """
        self.video_source = self.__video_source_entry.get()
        self.__state = False
        if self.__cap != None:
            self.__cap.release()
            print('释放旧视频资源')
        if len(self.video_source) == 0:
            self.__cap = None
            print('请填写视频源')
            return
        if self.video_source.isdigit():
            self.video_source = int(self.video_source)
        try:
            self.__cap = cv2.VideoCapture(self.video_source)
        except Exception as e:
            print(e)
        if not self.__cap.isOpened():
            self.__cap = None
            print('请检查视频源 {} 是否可用'.format(self.video_source))
            return
        print('打开视频源:{}'.format(self.video_source))
        self.__state = True

    def __on_closing(self):
        """ 关闭事件处理 """
        if self.__cap != None:
            self.__cap.release()
            print('释放视频资源')
        self.__master.destroy()
        print('销毁窗口')

    def __image_convert(self, src):
        """ OpenCV 图像转 Tkinter """
        src = cv2.cvtColor(src, cv2.COLOR_BGR2RGB)
        return ImageTk.PhotoImage(image=Image.fromarray(src))

    def __update_image_preview_label(self):
        if self.__state:
            ret, src = self.__cap.read()
            if ret:
                self.__img = self.__image_convert(src)
                self.__image_preview_label.config(image=self.__img)
        self.__master.after(100, self.__update_image_preview_label)

def main():
    root = tk.Tk()
    cp = CameraPreview(root)
    cp.mainloop()

if __name__ == '__main__':
    main()

file

file