Python GUI库 Tkinter入门资料 -- 高级应用

3. 高级用法

通过基础篇的学习,相信大家已经掌握了简单的tkinter编程,但如果想做出真正实用的程序,还需要学习一些高级用法,一些更复杂的控件。

 

3.1 高级控件学习

控件类 名称 简要说明
LabelFrame 标签框架 带有标签和边框的一个容器,通常用于控件分组
Message 消息框 类似于标签,但可以自动换行,用于显示多行文本
OptionMenu 选项菜单 即一个下拉菜单
Spinbox 输入控件 与Entry类似,但是可以指定输入范围值
Menu 菜单 点下菜单按钮后弹出的一个选项列表,用户可以从中选择
Listbox 列表框 一个选项列表,用户可以从中选择
Scrollbar 滚动条 对其支持的组件(文本域、画布、列表框、文本框)提供滚动功能
Text 多行文本框 显示多行文本
Toplevel 顶级窗口 它是独立存在的窗口
  • LabelFrame
  • Message
  • OptionMenu
  • Spinbox

示例代码

from tkinter import *

content = "汉皇重色思倾国,御宇多年求不得。杨家有女初长成,养在深闺人未识。\
            天生丽质难自弃,一朝选在君王侧。回眸一笑百媚生,六宫粉黛无颜色。"

root = Tk()
root.geometry("300x400")
top = LabelFrame(root, text="这是 Label")
top.pack(padx=8, pady=8)

# 创建一个Label
Label(top, text=content, bg="yellow").pack()

bottom = LabelFrame(root, text="这是 Message")
bottom.pack(padx=8, pady=8)

# 创建一个Message
Message(bottom, text=content, bg="blue").pack()

# 下拉菜单
op_list = ["选项1", "选项2", "选项3"]
val = StringVar()
val.set(op_list[0])
# 注意,传入的列表前需要加一个*号,这是表示不定参的传递,
# 两个*则是表示字典类型的不定参传递
OptionMenu(root, val, *op_list).pack()

# 指定数字范围
var_range = StringVar()
var_range.set(0)
Spinbox(root, textvariable=var_range, from_=-10, to=10).pack()

# 指定列表范围
Spinbox(root, value=op_list).pack()

root.mainloop()
  • Menu
from tkinter import Tk, Menu

root = Tk()

# 创建窗口顶部的菜单栏对象
menu_bar = Menu(root)
# 将菜单栏对象设置给根窗口
root["menu"] = menu_bar  # 等价于 root.config(menu=menu_bar)

# 创建“文件”联级菜单
file_menu = Menu(menu_bar, tearoff=0)
# 在菜单栏上添加菜单标签,并将该标签与相应的联级菜单关联起来
menu_bar.add_cascade(label='文件', menu=file_menu)

# 在文件联级菜单中添加菜单项
file_menu.add_command(label='新建', accelerator='Ctrl+N')
file_menu.add_command(label='打开', accelerator='Ctrl+O')
file_menu.add_command(label='保存', accelerator='Ctrl+S')
# 添加分割线
file_menu.add_separator()
file_menu.add_command(label='退出', accelerator='Alt+F4')

about_menu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label='关于', menu=about_menu)
about_menu.add_command(label='关于')
about_menu.add_command(label='帮助')

root.mainloop()

参数说明:

  • tearoff

    菜单项列表中的第一个位置(位置0)会被一个“脱离”元素占用,tearoff=0时,菜单将没有脱离功能

  • accelerator

    在菜单项名称的右侧显示一个快捷键提示。注意,它只是一个提示,快捷键功能需要监听按键来实现。


  • Listbox
  • Scrollbar

列表框通常用于数据展示或者作为选项菜单使用。滚动条则是一种辅助的小控件,它通常与列表框或者多行文本框结合使用。

列表框简单示例

from tkinter import *

root = Tk()

list_var = StringVar()
list_var.set(["Go", "Python", "Java", "Dart", "JavaScript"])

Listbox(root, listvariable=list_var, selectmode=BROWSE).pack()

root.mainloop()

参数说明:

  • selectmode

    设置列表框的选择模式。共有四个值,SINGLE表示单选,BROWSE也是单选,但该模式可以通过拖动鼠标来单选,而不仅仅只是点击。MULTIPLE表示多选,EXTENDED则表示可以通过拖动鼠标来多选,当然,也可以配合Shift键通过点击来多选。selectmode属性默认值是BROWSE


结合滚动条示例

from tkinter import *

root = Tk()

list_var = StringVar()
list_var.set(["Go", "Python", "Java", "Dart",
              "JavaScript", "C", "C++", "PHPHPHPHPHPHPHPHPHPHPHP"])

# 分别创建x方向、y方向的两个滚动条。orient属性设置其滚动方向
y_bar = Scrollbar(root, orient=VERTICAL)
x_bar = Scrollbar(root, orient=HORIZONTAL)

# 创建列表框
list_box = Listbox(root, yscrollcommand=y_bar.set,
                   xscrollcommand=x_bar.set,
                   listvariable=list_var, height=5)

y_bar['command'] = list_box.yview
x_bar['command'] = list_box.xview

# 设置布局方位
y_bar.pack(side=RIGHT, fill=Y)
x_bar.pack(side=BOTTOM, fill=X)
list_box.pack(anchor=NW, fill=BOTH, expand=YES)

root.mainloop()

Listbox 参数说明:

  • yscrollcommand

    列表框纵向滚动时的回调监听,该属性的值是一个回调函数

  • xscrollcommand

    列表框横向滚动时的回调监听。

Scrollbar 参数说明:

  • command

    滚动条拖动时的回调监听,其属性值是一个回调函数


  • Text

Text控件是非常灵活复杂的控件,既可以插入文字,还能插入图片和其他小控件,我们没有必要一次掌握它的全部用法,这里介绍一下基本用法

from tkinter import *

content = "汉皇重色思倾国,御宇多年求不得。杨家有女初长成,养在深闺人未识。" \
          "天生丽质难自弃,一朝选在君王侧。回眸一笑百媚生,六宫粉黛无颜色。"


def touch(event):
    print(text_area.index(CURRENT))
    print(text_area.get(CURRENT, END))


# 清空Text
def clear():
    text_area.delete("1.0", END)

root = Tk()

# 创建垂直滚动条
y_bar = Scrollbar(root, orient=VERTICAL)
y_bar.pack(side=RIGHT, fill=Y)

text_area = Text(root, yscrollcommand=y_bar.set, wrap=WORD)
y_bar["command"] = text_area.yview

text_area.bind("", touch)
text_area.pack()

# 插入文本内容
text_area.insert(INSERT, content)
text_area.insert('1.0', "这是一句XXX话")

# 插入图片
photo = PhotoImage(file="img.gif")
text_area.image_create(END, image=photo)

# 插入控件
btn = Button(text_area, text="点我", command=clear)
text_area.window_create(END, window=btn)

root.mainloop()

Text的索引

其索引表示比较复杂,有常量,也有字符串,常用的有如下

  • INSERT 等价于字符串"insert",表示当前光标的位置
  • CURRENT 等价于字符串"current",当前鼠标所在的位置
  • END 等价于字符串"end",表示文本最末的位置
  • line.column 直接指定行列位置,如"1.0",表示第一行第一列,注意,行号从1开始,列号从0开始。
  • line.end 指定行末位置,如"1.end",表示第一行结尾的位置

不常用的

  • + n chars 如"1.0+5c",表示在第一行第一列的位置向右移动五个字符的位置
  • linestart 如"current linestart",表示当前光标所在行的行首位置

Mark与Tags的简单用法

Mark主要用来控制位置,Tags主要用来改变内容的样式

from tkinter import *

content = "汉皇重色思倾国,御宇多年求不得。杨家有女初长成,养在深闺人未识。" \
          "天生丽质难自弃,一朝选在君王侧。回眸一笑百媚生,六宫粉黛无颜色。"


root = Tk()

text_area = Text(root, wrap=WORD)
text_area.pack()

# 插入文本内容
text_area.insert(INSERT, content)

# 创建一个mark
text_area.mark_set('xxx', "1.0")

# 在名为"xxx"的mark处插入文本,第三参数为插入的文本设置一个名为"here_red"的tag
text_area.insert('xxx', "这是一句XXX话", "here_red")

# 设置Tag的样式
text_area.tag_config('here_red', foreground='red')

# 解除mark
text_area.mark_unset("xxx")

# 在指定范围创建一个tag
text_area.tag_add("high_light", "1.50", "1.end")
text_area.tag_config('high_light', background='yellow')

# 在全局删除指定的tag
# text_area.tag_delete("here_red")
# 在指定的范围内删除tag
# text_area.tag_remove("here_red", "1.0", "1.end")

root.mainloop()
  • Toplevel

实际上我们的根窗口就是一个顶级窗口。它是独立存在的一个窗口,当我们需要编写多窗口程序或者自定义对话框时,就需要使用它。

简单示例

from tkinter import *

def onclick():
    window = Toplevel()
    Label(window, text="我是新窗口").pack()

    # 设置它所依托的父窗口
    window.transient(root)
    # 必须调用mainloop,打开一个新窗口后,需要进入新窗口的事件循环
    window.mainloop()

root = Tk()
Button(root, text="打开窗口", command=onclick).pack()

root.mainloop()

既然根窗口也是一个顶级窗口,那么顶级窗口的属性和方法,根窗口同样具有。

from tkinter import *

def test(window):
    # 让窗口最小化
    window.iconify()
    # 将最小化的窗口显示出来
    # window.deiconify()

    # 销毁窗口
    # window.destroy()
    # 退出mainloop循环
    # window.quit()

def onclick():
    window = Toplevel()
    # 设置窗口出现的位置
    window.geometry("+300+300")
    window.title("子窗口")

    # 该方法传入True时,去除窗口边框
    # window.overrideredirect(True)
    Button(window, text="点你妹", command=lambda: test(window)).pack()

    # 设置所依托的父窗口
    # window.transient(root)
    # 必须要加上此行,打开一个新窗口后,必须进入新窗口的事件循环
    window.mainloop()

root = Tk()

# 设置窗口标题
root.title("主窗口")

# 设置窗口的宽高与显示的位置
root.geometry("300x200+200+200")

# 固定窗口的长宽,不可改变窗口大小(width=None, height=None)
root.resizable(False, False)

# 设置窗口小图标(必须位于geometry与resizable方法之后)
root.iconbitmap("tools.ico")

Button(root, text="打开窗口", command=onclick).pack()

root.mainloop()

关于geometry的参数设置

该方法需要传入一个固定格式的字符串,格式:"wxh±x±y"

  • 其中wh表示窗口的宽和高
  • xy表示窗口显示位置的横向和纵向坐标
  • +-则表示正方向还是负方向,基于屏幕左上角为坐标原点,向下或向右为正方向。

扩展

窗口居中的方法

from tkinter import *

root = Tk()

# 获取当前根窗口的宽、高
cur_width = root.winfo_width()
cur_height = root.winfo_height()

# 获取电脑屏幕的宽、高
scn_width, scn_height = root.maxsize()

# 窗口显示的坐标拼接成固定格式字符串
tmpcnf = '+%d+%d' % ((scn_width-cur_width)/2, (scn_height-cur_height)/2)

root.geometry(tmpcnf)

# 固定窗口的长宽,不可改变窗口大小(width=None, height=None)
root.resizable(False, False)

Label(root, text="我是标签", bg="yellow").pack(side=LEFT)
Button(root, text="这是按钮").pack(side=LEFT)

root.mainloop()

注意,当窗口大小不固定时,是通过动态获取根窗口的宽高来计算位置,如果窗口的宽高一开始就是确定的,请使用确定值,尽量避免动态获取窗口的大小,以免影响GUI的流畅。

 

3.2 对话框

tkinter中共提供了三种标准对话框

  • messagebox
  • filedialog
  • colorchooser

messagebox

消息对话框

from tkinter import *
from tkinter import messagebox

root = Tk()

# 设置窗口标题
root.title("主窗口")

# parent指定依托的父窗口,若不指定,默认为根窗口
result = messagebox.askokcancel("标题", "这是内容", parent=root)
print(result)

messagebox.askquestion("标题", "这是question窗口")
messagebox.askretrycancel("标题", "这是retry cancel窗口")
messagebox.showerror("标题", "这是error窗口")
messagebox.showinfo("标题", "这是info窗口")
messagebox.showwarning("标题", "这是warning窗口")

root.mainloop()

filedialog

文件对话框

from tkinter import *
from tkinter import filedialog


def onclick():
    file_name = filedialog.askopenfilename(
        title="打开我的文件", initialdir="D:\\",
        filetypes=[("PNG", ".png"), ("文本文档", ".txt")])
    print(file_name)

def save_file():
    file_name = filedialog.asksaveasfilename(
        title="保存文件", initialdir="D:\\")
    print(file_name)


root = Tk()

Button(root, text="浏览", command=onclick).pack()
Button(root, text="保存", command=save_file).pack()

root.mainloop()

除上述对话框外,还有一个选择文件夹对话框,这个在tkinter文档中没有写,但是通过查看源码可以找到

from tkinter import *
from tkinter import filedialog

def onclick():
    # 选择文件夹对话框
    file_name = filedialog.askdirectory(initialdir="D:\\")
    print(file_name)

root = Tk()

Button(root, text="浏览", command=onclick).pack()

root.mainloop()

参数说明:

  • title

    设置文件对话框的标题

  • initialdir

    设置默认打开的路径

  • filetypes

    文件类型筛选。它的值是一个列表,列表中的元素必须是一个二元元组。元组的第一个元素是类型名称,第二个是类型后缀名。


colorchooser

颜色选择对话框

from tkinter import *
from tkinter import colorchooser


def onclick():
    file_name = colorchooser.askcolor()

    # 返回值是一个元组,第一个元素是RGB色值元组,第二个是对应的16进制色值
    print(file_name)

root = Tk()

Button(root, text="选色", command=onclick).pack()

root.mainloop()

 

3.3 现代风格:TTK

ttk 是Tk 8.5版本开始加入的模块。在之前,我们学习的小控件外观看起来都很陈旧过时,编写的界面会比较丑陋。而ttk模块的出现正是为了解决这个问题,它使小控件的外观更接近于系统平台所特定的外观,不仅如此,它还能支持主题的定制,使我们能更简便的改进界面的美观程度。

TTK的使用

如果是使用from tkinter import *方式导包,则只需在其下增加from tkinter.ttk import *即可应用ttk风格。使用ttk模块后,小控件外观会产生差别。

from tkinter import *
# 增加如下导包语句即可
from tkinter.ttk import *

root = Tk()
root.geometry("300x300")

top = LabelFrame(root, text="Label")
top.pack(padx=8, pady=8)
Label(top, text="我是标签,哈哈").pack()

body = LabelFrame(root, text="Button")
body.pack(padx=8, pady=8)
Button(body, text="你点啊").pack()

bottom = LabelFrame(root, text="其他")
bottom.pack(padx=8, pady=8)
Checkbutton(bottom, text="唱歌").pack()
Checkbutton(bottom, text="跳舞").pack()
Checkbutton(bottom, text="健身").pack()

Scale(bottom, orient='horizonta', from_=0, to=100).pack()

root.mainloop()

如使用其他方式导包,则需指定ttk模块控件

import tkinter as tk
import tkinter.ttk as ttk

root = tk.Tk()

ttk.Label(root, text="标签").pack()
ttk.Button(root, text="点啊").pack()

root.mainloop()

ttk仅支持11个原核心控件

  • Button
  • Checkbutton
  • Entry
  • Frame
  • Label
  • LabelFrame
  • Menubutton
  • PanedWindow
  • Radiobutton
  • Scale
  • Scrollbar

TTK 新增控件

ttk 新增了6个控件,这里我们主要介绍4个重要的

  • Combobox 输入框下拉选项菜单
  • Progressbar 进度条控件
import tkinter as tk
import tkinter.ttk as ttk

root = tk.Tk()
ttk.Label(root, text="编程语言").pack(side=tk.LEFT, padx=5, pady=5)

combo = ttk.Combobox(root, values=["Go", "Python", "Java", "C++"])
# 设置当前选中的项
combo.current(1)
combo.pack(side=tk.LEFT, padx=5, pady=5)

# 创建进度条控件
progress = ttk.Progressbar(root, mode='indeterminate', length=100)
progress.pack(pady=10, padx=10)

# 启动进度条控件
progress.start()

progress2 = ttk.Progressbar(root, mode='determinate', length=100)
progress2.pack(pady=10, padx=10)
progress2.start()

root.mainloop()

Progressbar 参数说明:

  • mode

    有两个值可选。"indeterminate"表示来回反弹样式,"determinate"表示步进样式


  • Notebook 选项卡控件
import tkinter as tk
from tkinter.ttk import *

root = tk.Tk()
root.geometry("300x300")

notebook = Notebook(root)

page1 = tk.Frame(notebook, background="yellow")
Label(page1, text="这是 tab1 的界面").pack()


page2 = tk.Frame(notebook,  background="pink")
Label(page2, text="这是 tab2 的界面").pack()


notebook.add(page1, text="Tab1")
notebook.add(page2, text="Tab2")
notebook.pack(fill=tk.BOTH, expand="yes")

root.mainloop()
  • Treeview 树形控件

树形结构简单示例

import tkinter as tk
from tkinter import ttk


def item_select(event):
    for select in tree.selection():
        print(tree.item(select, "text"))

root = tk.Tk()
tree = ttk.Treeview(root, show='tree')

# 监听tree中item被选中的事件
tree.bind("<>", item_select)

# 第一个参数为父节点, 第二个为此项在父节点中的位置(父节点为空时,默认为根节点)
item1 = tree.insert("", 0, text="广东省")

# 在第一个节点中插入如下子节点
tree.insert(item1, 0, text="广州市")
tree.insert(item1, 1, text="深圳市")

item2 = tree.insert("", 1, text="湖北省")
tree.insert(item2, 0, text="武汉市")

tree.pack()
root.mainloop()

用 Treeview 制作表格

import tkinter as tk
from tkinter import ttk


def item_select(event):
    for select in tree.selection():
        print(tree.item(select, "values"))


def head_onclick(type):
    print(type)

root = tk.Tk()

# show用于禁止列顶部标签。columns用于设置每一列的列标识字符串
tree = ttk.Treeview(root, show='headings', columns=['0', '1', '2'])

# 监听tree中item被选中的事件
tree.bind("<>", item_select)

# 设置表头名称
tree.heading(0, text='序号', command=lambda: head_onclick('序号'))
tree.heading(1, text='姓名', command=lambda: head_onclick('姓名'))
tree.heading(2, text='年龄', command=lambda: head_onclick('年龄'))

# 设置每列中元素的样式
tree.column(0, anchor='center')
tree.column(1, anchor='center')
tree.column(2, anchor='center')

# "end" 表示往父节点的最后一个位置插入
item1 = tree.insert("", "end", values=("1", "赵二", "19"))
item1 = tree.insert("", "end", values=("2", "张三", "20"))
item1 = tree.insert("", "end", values=("3", "李四", "22"))
item1 = tree.insert("", "end", values=("4", "王五", "18"))

tree.pack()
root.mainloop()

Treeview 参数说明:

  • show

    用于禁止列顶部标签。有两个值,'tree'表示禁止每一列的顶部标签栏,'headings'表示禁止首列显示。


设置主题与样式

在使用ttk控件时,会发现它的控件不支持 bgfgborder 这样涉及样式的属性,这是因为它对外观样式进行了重新定义。

ttk 对外观样式的抽象共有三个级别

  • 主题
  • 样式
  • 状态样式

主题的查询与切换

from tkinter import ttk

style = ttk.Style()
# 获取所有支持的主题
print(style.theme_names())

# 获取当前使用的主题
print(style.theme_use())

# 切换主题
style.theme_use("classic")

样式与控件状态样式的定制

ttk中,控件实际上是一个字符串,要改变控件样式,需要指定这个字符串名称,而不是类名,它们的对应关系如下

类名 控件样式名
Button TButton
Checkbutton TCheckbutton
Combobox TCombobox
Entry TEntry
Frame TFrame
Label TLabel
LabelFrame TLabelFrame
Menubutton TMenubutton
Notebook TNotebook
PanedWindow TPanedwindow
Progressbar Horizontal.TProgressbar或Vertical.TProgressbar
Radiobutton TRadiobutton
Scale Horizontal.TScale 或 Vertical.TScale
Scrollbar Horizontal.TScrollbar 或Vertical.TScrollbar
Separator TSeparator
Sizegrip TSizegrip
Treeview Treeview

需要注意,在创建新样式时,应当定义newName.oldName形式的名称

from tkinter import Tk
from tkinter import ttk

root = Tk()

style = ttk.Style()

# 定义一个全局样式作为默认样式("."表示此样式将应用于顶级窗口及其所有子元素)
style.configure('.', font='Arial 14', foreground='brown', background='yellow')

# 未指定样式时,使用全局默认样式
ttk.Label(root, text='我没有指定样式').pack()

# 定义一个名为danger的新样式(newName.oldName格式)
style.configure('danger.TButton', font='Times 12', foreground='red', padding=1)
ttk.Button(root, text='我使用danger样式',  style='danger.TButton').pack()

# 为小控件的不同状态指定样式
style.map("new_state_style.TButton", foreground=[('pressed', 'red'), ('active', 'blue')])
ttk.Button(text="不同状态不同样式", style="new_state_style.TButton").pack()

# 覆盖Entry的当前主题(即使没有指定样式,也会受到主题更改的影响)
current_theme = style.theme_use()
style.theme_settings(current_theme,
                     {"TEntry": {
                         "configure": {"padding": 10},
                         "map": {"foreground": [("focus", "red")]}}})


ttk.Entry().pack()

root.mainloop()

 

3.4 tkinter扩展包

Pmw

Pmw 在线文档

一个使用Tkinter模块在Python中构建高级复合小控件的扩展包。它是一个第三方包,因此使用前需要安装。

推荐的安装方式

python -m pip install pmw

查看离线文档和示例

安装完成后,进入你的Python环境的根目录,即 python.exe 所在的目录

在根目录下,进入如下路径 Lib\site-packages\Pmw ,即打开Pmw包的所在文件夹。在该文件夹下,再次进入Pmw_2_0_1目录,如你的环境下与之不同,则进入Pmw_后接版本号的目录,这个下面有两个需要关注的目录

  • doc目录 离线文档所在目录,找到starting.html打开查看文档

  • demos目录 所有示例所在目录,运行其下的All.py,即可看到其支持的控件外观和使用方法。

注意,Pmw包长期没有维护与更新,其下的小部分控件可能在Python环境下无法使用,因此挑选合适的有必要的使用,例如:Balloon等几个,其他的实用性不强。

Balloon 简单示例

from tkinter import *
import Pmw

root = Tk()
root.geometry("300x300")

lab = Label(root, text="别点我")
lab.pack()

balloon = Pmw.Balloon(root)
balloon.bind(lab, '指你妹啊')

root.mainloop()

3.5 自定义控件

ttk中虽然添加了Notebook,但其功能过于简单,无法支持双击创建选项卡,删除选项卡等功能,这里我们自定义一个tabview,从而学习一下如何在tkinter中自定义控件

Python GUI库 Tkinter入门资料 -- 高级应用_第1张图片

 

源码地址

示例

import tkinter as tk
from tkinter import messagebox
from tabview import TabView

# 在body中生成widget的函数,返回的widget将被添加到tabview的body中
def create_body():
    global body
    return tk.Label(body, text="this is body")

# 点击选项卡时的回调
def select(index):
    print("current selected -->", index)

# 删除选项卡时的回调,如果返回False将不会删除
def remove(index):
    print("remove tab -->", index)
    if messagebox.askokcancel("标题", "确定要关闭该选项卡吗?"):
        return True
    else:
        return False

# ----------------------- 使用示例 ----------------------------
root = tk.Tk()
root.geometry("640x300")

tab_view = TabView(root, generate_body=create_body,
                   select_listen=select, remove_listen=remove)

body = tab_view.body

label_1 = tk.Label(tab_view.body, text="this is tab1")
label_2 = tk.Label(tab_view.body, text="this is tab2")

# 第一个参数是向body中添加的widget, 第二个参数是tab标题
tab_view.add_tab(label_1, "tabs1")
tab_view.add_tab(label_2, "tabs2")

# TabView需要向x、y方向填充,且expand应设置为yes
tab_view.pack(fill="both", expand='yes', pady=2)

root.mainloop()

4. 实战编程

学习了tkinter的知识体系,还需要整体上对这些知识进行整合,能真正运用到实际GUI编程中,通过本章两个项目的学习,相信大家一定会熟练掌握它,开发出属于自己的小软件。

4.1 自制编辑器

Python GUI库 Tkinter入门资料 -- 高级应用_第2张图片

 

项目实战要点

在之前章节中已经学习了tkinter的大量知识,但仍然有部分知识是没有覆盖到的,换句话说,本教程并不是一本事无巨细的帮助文档,未提到的知识,我将在项目实战中列出,根据具体使用场景来学习。

  • 覆写窗口关闭事件

    self.protocol('WM_DELETE_WINDOW', self.exit_editor)
    
  • 实现鼠标右键菜单

    def _create_right_popup_menu(self):
        popup_menu = Menu(self.content_text, tearoff=0)
        for it1, it2 in zip(['剪切', '复制', '粘贴', '撤销', '恢复'],
                            ['cut', 'copy', 'paste', 'undo', 'redo']):
            popup_menu.add_command(label=it1, compound='left',
                                   command=self._shortcut_action(it2))
            popup_menu.add_separator()
            popup_menu.add_command(label='全选', command=self.select_all)
            self.content_text.bind('',
                                   lambda event: popup_menu.tk_popup(event.x_root, event.y_root))
    
  • 使用闭包处理回调事件

        def _shortcut_action(self, type):
            def handle():
                if type == "new_file":
                    self.new_file()
                elif type == "open_file":
                    self.open_file()
                elif type == "save":
                    self.save()
                elif type == "cut":
                    # ………… 省略部分条件判断
                if type != "copy" and type != "save":
                    self._update_line_num()
    
            return handle
    
  • 使用tkinter中的定时回调

    self.content_text.after(200, self._toggle_highlight)
    
  • 实现文本搜索功能

    start_pos = self.content_text.search(key, start_pos, nocase=ignore_case, stopindex="end")
    

遗留待完善问题

  • 处理文本文件编码

    自制的编辑器目前无法打开不同编码格式的文本文件,需要能自动识别文本文件的编码

    建议考虑使用chardet 模块来识别编码

    import chardet
    
    with open('xxx.py', 'rb') as file:
        print(chardet.detect(file.read(1024)))
    
  • 搜索框定位

    搜索框应当根据当前编辑器的相对位置来显示

    # 获取根窗口的绝对位置,依据根窗口的位置计算搜索框的显示位置
    print(self.winfo_rootx(), self.winfo_rooty())
    
  • 使用自定义的选项卡控件重构编辑器

    我们之前已经自定义了一个选项卡控件,实现了双创建选项卡,删除选项卡等功能,使用该控件重构编辑器,使编辑器更加实用

搭建 UI 框架

from tkinter import *
from tkinter.ttk import Scrollbar

theme_color = {
    'Default': '#000000.#FFFFFF',
    'Greygarious': '#83406A.#D1D4D1',
    'Aquamarine': '#5B8340.#D1E7E0',
    'Bold Beige': '#4B4620.#FFF0E1',
    'Cobalt Blue': '#ffffBB.#3333aa',
    'Olive Green': '#D1E7E0.#5B8340',
    'Night Mode': '#FFFFFF.#000000',
}


class EditorPlus(Tk):
    def __init__(self):
        super().__init__()
        self._set_window_()
        self._create_menu_bar_()
        self._create_shortcut_bar_()
        self._create_body_()

    # 设置初始窗口的属性
    def _set_window_(self):
        self.title("EditorPlus")
        self.geometry('650x450')

    # 创建整个菜单栏
    def _create_menu_bar_(self):
        menu_bar = Menu(self)
        # 创建文件的联级菜单
        file_menu = Menu(menu_bar, tearoff=0)
        file_menu.add_command(label='新建', accelerator='Ctrl+N')
        file_menu.add_command(label='打开', accelerator='Ctrl+O')
        file_menu.add_command(label='保存', accelerator='Ctrl+S')
        file_menu.add_command(label='另存为', accelerator='Shift+Ctrl+S')
        file_menu.add_separator()
        file_menu.add_command(label='退出', accelerator='Alt+F4')

        # 在菜单栏上添加菜单标签,并将该标签与相应的联级菜单关联起来
        menu_bar.add_cascade(label='文件', menu=file_menu)

        # 创建编辑的联级菜单
        edit_menu = Menu(menu_bar, tearoff=0)
        edit_menu.add_command(label='撤销', accelerator='Ctrl+Z')
        edit_menu.add_command(label='恢复', accelerator='Ctrl+Y')
        edit_menu.add_separator()
        edit_menu.add_command(label='剪切', accelerator='Ctrl+X')
        edit_menu.add_command(label='复制', accelerator='Ctrl+C')
        edit_menu.add_command(label='粘贴', accelerator='Ctrl+V')
        edit_menu.add_separator()
        edit_menu.add_command(label='查找', accelerator='Ctrl+F')
        edit_menu.add_separator()
        edit_menu.add_command(label='全选', accelerator='Ctrl+A')
        menu_bar.add_cascade(label='编辑', menu=edit_menu)

        # 视图菜单
        view_menu = Menu(menu_bar, tearoff=0)
        show_line_number = IntVar()
        show_line_number.set(1)
        view_menu.add_checkbutton(label='显示行号', variable=show_line_number)

        highlight_line = IntVar()
        view_menu.add_checkbutton(label='高亮当前行', onvalue=1, offvalue=0, variable=highlight_line)

        # 在主题菜单中再添加一个子菜单列表
        themes_menu = Menu(menu_bar, tearoff=0)
        view_menu.add_cascade(label='主题', menu=themes_menu)

        theme_choice = StringVar()
        theme_choice.set('Default')
        for k in sorted(theme_color):
            themes_menu.add_radiobutton(label=k, variable=theme_choice)

        menu_bar.add_cascade(label='视图', menu=view_menu)

        about_menu = Menu(menu_bar, tearoff=0)
        about_menu.add_command(label='关于')
        about_menu.add_command(label='帮助')
        menu_bar.add_cascade(label='关于', menu=about_menu)
        self["menu"] = menu_bar

    # 创建快捷菜单栏
    def _create_shortcut_bar_(self):
        shortcut_bar = Frame(self, height=25, background='#20b2aa')
        shortcut_bar.pack(fill='x')

    # 创建程序主体
    def _create_body_(self):
        # 创建行号栏 (takefocus=0 屏蔽焦点)
        line_number_bar = Text(self, width=4, padx=3, takefocus=0, border=0,
                               background='#F0E68C', state='disabled')
        line_number_bar.pack(side='left', fill='y')

        # 创建文本输入框
        content_text = Text(self, wrap='word')
        content_text.pack(expand='yes', fill='both')

        # 创建滚动条
        scroll_bar = Scrollbar(content_text)
        scroll_bar["command"] = content_text.yview
        content_text["yscrollcommand"] = scroll_bar.set
        scroll_bar.pack(side='right', fill='y')


if "__main__" == __name__:
    app = EditorPlus()
    app.mainloop()

完善编辑器的功能

实现了基本的界面框架之后,只需将相应的功能一个一个添加上去即可。

 

4.2 自制音频播放器

支持本地音频以及流媒体在线播放。

UI 架子

Python GUI库 Tkinter入门资料 -- 高级应用_第3张图片

 

 

from tkinter import *
import tkinter.ttk as ttk
from tkinter.font import Font
from PIL import Image, ImageTk
from resource import control_icon, bottom_icon
from seekbar import Seekbar
import Pmw


class AudioView(Tk):

    def __init__(self):
        super().__init__()
        self._init_data_()
        self._set_window_()
        self._create_menu_bar()
        self._create_top_view()
        self._create_control_panel()
        self._create_list_box()
        self._create_bottom_view()


if "__main__" == __name__:
    app = AudioView()
    app.mainloop()

Tkinter 的细节问题

Tkinter 的各小控件在实际使用中,都会存在一些细节问题,这些细节问题不是在初识小控件的时候能弄明白的,需要有一定的实战经验,并结合具体的项目才能体会出来。

  • 设置窗体透明度

    这部分描述,在tkinter文档中并未描述,我们需要查看tcl/Tk的 相关文档 ,关于窗口feature的设置,推荐阅览本人另一篇博客 传送门,利用这点能实现桌面悬浮控件,可用于歌词展现

    self.wm_attributes("-alpha", 0.9)
    
  • LabelFrame的扩展用法

    menu_frame = Frame(bg="black")
    
    frame = LabelFrame(self, labelwidget=menu_frame, bg="black", borderwidth=2,
                       padx=10, pady=8, relief="sunken")
    
  • Listbox的深入学习

    Listbox添加右键选中菜单

    self.list_box = Listbox(frame, bg="black", yscrollcommand=y_bar.set, fg="white",
                            xscrollcommand=x_bar.set, border=0, highlightthickness=0,
                            selectforeground="#F0F126", selectbackground="black",
                            activestyle="none", font=("微软雅黑", -18), height=8)
    
    self.list_box.bind('', self.list_selected)
    self.list_box.bind("", self.show_context_menu)
    
    # 设置选中
    if self.list_box.size() == 1:
        self.list_box.selection_set(0)
    
    def show_context_menu(self, event):
        # 清除鼠标右键选中色
        for i in range(self.list_box.size()):
            self.list_box.itemconfig(i, background="black")
    
        # 获取当前鼠标右键选中的索引
        index = self.list_box.nearest(event.y)
        # 选中后改变背景色
        self.list_box.itemconfig(index, background="gray")
    
        self.context_menu.entryconfigure(0, command=lambda: self.remove_at(index))
        self.context_menu.tk_popup(event.x_root, event.y_root)
    
  • Canvas与自定义控件

    该项目中,我们主要自定义的是一个音频进度条控件Seekbar。主要使用Canvas的coords函数来移动对象,关于Canvas支持的一些功能,列举如下

    • create_arc():绘制弧。

    • create_bitmap():绘制位图。

    • create_image():绘制图像。

    • create_line():绘制线段。

    • create_oval():绘制椭圆。

    • create_polygon():绘制多边形。

    • create_rectangle():绘制矩形。

    • create_text():绘制文本。

    • create_window():绘制矩形窗口。

    Seekbar中,我们监听了鼠标按下和移动事件,关于事件的修饰符,可以查看tcl/Tk的官方文档,内容比tkinter要全面很多,相关部分文档

VLC 多媒体框架

它是一款自由、开源的跨平台多媒体播放器及框架,全面支持绝大部分的多媒体格式,以及各类流媒体协议。也就是说,使用它既能播放本地音视频文件,也能在线播放各类流媒体资源。

关于VLC的安装与详细使用,请阅览本人博客 传送门

Tkinter 与异步编程

关于在Tkinter中使用消息队列 + 多线程 实现异步任务方法,我早前已写过详细博客 传送门 看一下博客即可。

当前这个项目,我们将使用另一种更加简洁高效的方式实现异步任务——线程池

from concurrent.futures import ThreadPoolExecutor


class RequestTask():
    task = None

    def __init__(self):
        self.executor = ThreadPoolExecutor(max_workers=1)

    # 用submit添加耗时任务,该函数会立即返回,不会阻塞
    def request(self, url, count):
        self.task = self.executor.submit(get_music_list, url, count)

    # 检查异步任务是否执行完成
    def check_task(self):
        return self.task.done()

    # 获取异步任务执行结果
    def get_result(self):
        return self.task.result()

遗留问题

  1. 该视频播放器为简单实现,后续大家可以添加收藏、本地歌单、网易云音乐本地缓存、在线歌单筛选、桌面歌词等等功能
  2. 对于VCL库的所有调用均在主线程中进行,当VCL库的API耗时或阻塞时,会造成界面无响应。要改善该问题,可以考虑将音频播放移入后台线程中运行,而不阻塞前台的GUI
  3. 本地音频文件格式筛选,不要将非音频文件导入
  4. 响应快捷键,包括控制栏上的播放、暂停、快进等
  5. 界面美化

 

5. 打包发布

我们编写好了GUI应用后,还需要将其打包发布出去,给大家使用,这就涉及到桌面应用的打包知识了。对于桌面程序而言,打包发布是非常重要的环节,特单列一章阐述。

5.1 源码发布

通常来说,源码发布不是一个好主意!笔者工作中接触过一些测试部门的人员,其中一些测试人员由于自身水平的问题,只有半桶水,非常喜欢将自己编写的一些质量测试工具直接以Python源码的形式给到一线同事,一线同事并不太懂技术,往往折腾一上午环境安装,最后还是跑不起来。究其根本来说,就是不懂桌面程序的打包发布,而且往往还不能引起其重视。Python虽然是跨平台语言,但对于普通用户而言,直接获得源码并不能运行程序,第一步需要安装正确版本的Python解释器。Python不同版本的兼容性往往是个头疼的问题,除了众所周知的2.x和3.x兼容问题外,还有高版本新特性与低版本之间的兼容问题。没有一定的Python功力水平,写出的代码往往存在容易被忽视的兼容问题,源码在自己机器上可能运行良好,但是到了对方机器上,哪怕Python解释器版本相同,也有可能无法运行,这很可能是引用的第三方库版本兼容问题。

另外,这些半桶水的人有的还喜欢给编译后的pyc 文件,这简直就是灾难性的巨坑,你连源码也无法直接查看,完全不知道他的源码引用了哪些第三方库,就算是懂Python的人也没法迅速搭建好对应的环境。

基于以上这些坑,笔者不愿意介绍源码发布方法,包括编译pyc ,准备requirements.txt文件等,相信会的人无需我来说,不会的我也不想介绍,以免坑人坑己,请大家理解。

5.2 py文件与pyw文件

通常python中的脚本文件都是.py后缀格式,但如果编写的是tkinter的图形程序,则应当以.pyw格式为脚本后缀名。.pyw格式与.py最显著的区别是,运行前者不会生成一个黑框控制台,而是直接以图形界面程序运行。具体来说,pyw格式有如下区别

  1. 运行时不会弹出控制台窗口(DOS 窗口)
  2. 所有向stdoutstderr的输出都无效
  3. 所有从stdin的读取都只会得到EOF

5.3 打包成exe可执行程序

对于桌面程序而言,打包成exe可执行程序是最好的方式。

5.3.1 安装 pyinstaller

将Python程序打包为exe时,推荐使用pyinstaller库,相对来说使用更简单,我们首先使用pip来安装该库,如下

python -m pip install pyinstaller

如果大家网速不好,访问外网速度不给力,可以使用国内阿里的镜像,速度会很快,使用-i指定镜像即可

python -m pip install pyinstaller -i http://mirrors.aliyun.com/pypi/simple/

特别注意

想在命令行运行pyinstaller命令,需要配置环境变量,将pyinstaller.exe加入到系统PATH中,如同pip命令一样,我非常不建议在Windows上配置大量命令,既然pyinstaller是Python的一个第三方库,我们可以直接使用python命令加-m参数去调用,但要注意,作为包名时,其首字母是大写的,因此调用方式如下

python -m PyInstaller

基本参数说明

  • -F

    生成单个可执行文件

  • -w

    禁用控制台窗口,不加该参数生成的GUI程序启动时,背后会有一个控制台黑窗口(类似pypyw的区别),建议添加该参数

  • -i

    为可执行程序指定一个.ico格式的小图标

5.3.2 打包为单 exe 程序

不论工程中引用了多少库,最终都只会压缩生成一个单独的exe文件

简单示例

我在D:\workspace\pythoncode路径下创建了一个测试脚本 hello.py

则在命令行中cd到该目录下,然后执行以下命令

python -m PyInstaller -F hello.py

执行完成后,会在该目录下生成一个dist文件夹,里面放的就是hello.exe

示例只有一个脚本文件,如果你的工程中有多个python脚本文件,则应该在命令中指定入口脚本文件名。

指定小图标

如果我们想为窗口程序指定一个图标,则将相应的.ico文件拷贝到当前目录下,添加-i参数指定图标

python -m PyInstaller -F hello.py -i hello.ico

包含第三方库

如果我们的Python代码引用了第三方库,则需要拷贝相应的第三方库源码。

hello.py 代码如下,我们引用了爬虫中常用的requests

import requests

resp = requests.get("https://www.baidu.com/")
print(resp.content)

则需要拷贝一份requests库源码到当前打包的目录下,进入到Python安装的根目录,然后进到\Lib\site-packages下,我们知道,site-packages文件夹就是用来存放所有已安装的第三方库的,在其下找到我们引用的第三方库文件夹,如上例中的requests,将整个文件夹拷贝至打包的项目根路径下,最后再执行上述的打包命令打包即可。

5.3.3 打成多文件包

将整个工程打包成一个单独exe文件后,程序的启动速度会明显变慢。如果希望我们自己的程序启动快速,有良好的用户体验,则应该将工程打成多文件包,只需要在上述打包命令中去除-F参数即可。

以上示例打包后,会在dist文件夹下生成hello文件夹,文件夹中会产生非常多的文件,包含dllpyd文件等等。对于普通用户而言,这样的包是非常不友好的,进入到文件夹后会有发懵的感觉,不知道到底从哪里启动程序。实际上,在hello文件夹中,仍然有一个hello.exe文件,它就是启动程序。

5.3.4 制作安装包

打成多文件包可以让程序启动更加快速,但是生成太多文件,又会让普通用户感到困惑,降低用户体验。实际上,想要解决这个问题,大多数商业软件都是将多文件包制作成安装包的形式,用户启动安装包后,会将我们的多文件包解压到指定文件夹,然后还会生成桌面快捷方式,自动卸载程序等。

制作安装包其实不难,我们首先需要下载NSIS工具

NSIS 下载地址

下载并安装好NSIS后,需要编写NIS脚本来制作安装包,仅仅制作安装包,没有必要去学习这种脚本,因此我们还需要下载一个工具,来自动生成NIS脚本,它就是NIS EDIT

NIS EDIT 下载地址

下载并安装好NIS EDIT后,按照上述的命令,将Python程序打成多文件包,然后按照以下步骤生成NIS脚本文件

  1. 打开NIS EDIT,选择【新建脚本:向导】

Python GUI库 Tkinter入门资料 -- 高级应用_第4张图片

 

2.填写应用信息。主要是应用名和版本号

Python GUI库 Tkinter入门资料 -- 高级应用_第5张图片

 

 

3.设置应用的Icon图标。选择准备好的ico格式图标,并为安装器起一个有意义的名字,语言则选择简体中文,要去除English的勾选。

Python GUI库 Tkinter入门资料 -- 高级应用_第6张图片

 

4.创建授权文件并指定。我这里事先创建了一个叫SoftwareLicence.txt的文件,内容请根据实际编写

许可协议

这是许可协议,默认你同意一切条款,关于该协议的所有条款的解释权归本公司所有。

 

Python GUI库 Tkinter入门资料 -- 高级应用_第7张图片

 

5.添加我们之前已经打包好的多文件包目录

Python GUI库 Tkinter入门资料 -- 高级应用_第8张图片

 

Python GUI库 Tkinter入门资料 -- 高级应用_第9张图片

 

 

6.依次下一步

Python GUI库 Tkinter入门资料 -- 高级应用_第10张图片

 

7.选择我们多文件包中的启动程序

Python GUI库 Tkinter入门资料 -- 高级应用_第11张图片

8.配置卸载程序

Python GUI库 Tkinter入门资料 -- 高级应用_第12张图片

 

9.选中如下三项,完成后,弹出另存为对话框,为我们生成的.nsi脚本起一个名字,保存后,该脚本将开始自动编译,编译完成后,在脚本同级目录下,会生成安装器程序hello-Setup.exe,将安装器程序发布出去即可。

Python GUI库 Tkinter入门资料 -- 高级应用_第13张图片

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Python)