通过基础篇的学习,相信大家已经掌握了简单的tkinter编程,但如果想做出真正实用的程序,还需要学习一些高级用法,一些更复杂的控件。
控件类 | 名称 | 简要说明 |
LabelFrame | 标签框架 | 带有标签和边框的一个容器,通常用于控件分组 |
Message | 消息框 | 类似于标签,但可以自动换行,用于显示多行文本 |
OptionMenu | 选项菜单 | 即一个下拉菜单 |
Spinbox | 输入控件 | 与Entry类似,但是可以指定输入范围值 |
Menu | 菜单 | 点下菜单按钮后弹出的一个选项列表,用户可以从中选择 |
Listbox | 列表框 | 一个选项列表,用户可以从中选择 |
Scrollbar | 滚动条 | 对其支持的组件(文本域、画布、列表框、文本框)提供滚动功能 |
Text | 多行文本框 | 显示多行文本 |
Toplevel | 顶级窗口 | 它是独立存在的窗口 |
示例代码
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()
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
在菜单项名称的右侧显示一个快捷键提示。注意,它只是一个提示,快捷键功能需要监听按键来实现。
列表框通常用于数据展示或者作为选项菜单使用。滚动条则是一种辅助的小控件,它通常与列表框或者多行文本框结合使用。
列表框简单示例
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
控件是非常灵活复杂的控件,既可以插入文字,还能插入图片和其他小控件,我们没有必要一次掌握它的全部用法,这里介绍一下基本用法
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()
实际上我们的根窗口就是一个顶级窗口。它是独立存在的一个窗口,当我们需要编写多窗口程序或者自定义对话框时,就需要使用它。
简单示例
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"
w
、h
表示窗口的宽和高x
、y
表示窗口显示位置的横向和纵向坐标+
、-
则表示正方向还是负方向,基于屏幕左上角为坐标原点,向下或向右为正方向。扩展
窗口居中的方法
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的流畅。
tkinter中共提供了三种标准对话框
messagebox
filedialog
colorchooser
消息对话框
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()
文件对话框
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
文件类型筛选。它的值是一个列表,列表中的元素必须是一个二元元组。元组的第一个元素是类型名称,第二个是类型后缀名。
颜色选择对话框
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()
ttk
是Tk 8.5版本开始加入的模块。在之前,我们学习的小控件外观看起来都很陈旧过时,编写的界面会比较丑陋。而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个原核心控件
ttk
新增了6个控件,这里我们主要介绍4个重要的
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"
表示步进样式
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()
树形结构简单示例
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控件时,会发现它的控件不支持
bg
、fg
、border
这样涉及样式的属性,这是因为它对外观样式进行了重新定义。
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()
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()
ttk
中虽然添加了Notebook,但其功能过于简单,无法支持双击创建选项卡,删除选项卡等功能,这里我们自定义一个tabview,从而学习一下如何在tkinter中自定义控件
源码地址
示例
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()
学习了tkinter的知识体系,还需要整体上对这些知识进行整合,能真正运用到实际GUI编程中,通过本章两个项目的学习,相信大家一定会熟练掌握它,开发出属于自己的小软件。
在之前章节中已经学习了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())
使用自定义的选项卡控件重构编辑器
我们之前已经自定义了一个选项卡控件,实现了双创建选项卡,删除选项卡等功能,使用该控件重构编辑器,使编辑器更加实用
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()
实现了基本的界面框架之后,只需将相应的功能一个一个添加上去即可。
支持本地音频以及流媒体在线播放。
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
文档中并未描述,我们需要查看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的安装与详细使用,请阅览本人博客 传送门
关于在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()
VCL
库的所有调用均在主线程中进行,当VCL
库的API
耗时或阻塞时,会造成界面无响应。要改善该问题,可以考虑将音频播放移入后台线程中运行,而不阻塞前台的GUI
我们编写好了GUI应用后,还需要将其打包发布出去,给大家使用,这就涉及到桌面应用的打包知识了。对于桌面程序而言,打包发布是非常重要的环节,特单列一章阐述。
通常来说,源码发布不是一个好主意!笔者工作中接触过一些测试部门的人员,其中一些测试人员由于自身水平的问题,只有半桶水,非常喜欢将自己编写的一些质量测试工具直接以Python源码的形式给到一线同事,一线同事并不太懂技术,往往折腾一上午环境安装,最后还是跑不起来。究其根本来说,就是不懂桌面程序的打包发布,而且往往还不能引起其重视。Python虽然是跨平台语言,但对于普通用户而言,直接获得源码并不能运行程序,第一步需要安装正确版本的Python解释器。Python不同版本的兼容性往往是个头疼的问题,除了众所周知的2.x和3.x兼容问题外,还有高版本新特性与低版本之间的兼容问题。没有一定的Python功力水平,写出的代码往往存在容易被忽视的兼容问题,源码在自己机器上可能运行良好,但是到了对方机器上,哪怕Python解释器版本相同,也有可能无法运行,这很可能是引用的第三方库版本兼容问题。
另外,这些半桶水的人有的还喜欢给编译后的pyc
文件,这简直就是灾难性的巨坑,你连源码也无法直接查看,完全不知道他的源码引用了哪些第三方库,就算是懂Python的人也没法迅速搭建好对应的环境。
基于以上这些坑,笔者不愿意介绍源码发布方法,包括编译pyc
,准备requirements.txt
文件等,相信会的人无需我来说,不会的我也不想介绍,以免坑人坑己,请大家理解。
通常python中的脚本文件都是.py
后缀格式,但如果编写的是tkinter
的图形程序,则应当以.pyw
格式为脚本后缀名。.pyw
格式与.py
最显著的区别是,运行前者不会生成一个黑框控制台,而是直接以图形界面程序运行。具体来说,pyw
格式有如下区别
stdout
和stderr
的输出都无效stdin
的读取都只会得到EOF
对于桌面程序而言,打包成exe
可执行程序是最好的方式。
将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程序启动时,背后会有一个控制台黑窗口(类似py
与pyw
的区别),建议添加该参数
-i
为可执行程序指定一个.ico
格式的小图标
不论工程中引用了多少库,最终都只会压缩生成一个单独的
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
,将整个文件夹拷贝至打包的项目根路径下,最后再执行上述的打包命令打包即可。
将整个工程打包成一个单独exe
文件后,程序的启动速度会明显变慢。如果希望我们自己的程序启动快速,有良好的用户体验,则应该将工程打成多文件包,只需要在上述打包命令中去除-F
参数即可。
以上示例打包后,会在dist
文件夹下生成hello
文件夹,文件夹中会产生非常多的文件,包含dll
、pyd
文件等等。对于普通用户而言,这样的包是非常不友好的,进入到文件夹后会有发懵的感觉,不知道到底从哪里启动程序。实际上,在hello
文件夹中,仍然有一个hello.exe
文件,它就是启动程序。
打成多文件包可以让程序启动更加快速,但是生成太多文件,又会让普通用户感到困惑,降低用户体验。实际上,想要解决这个问题,大多数商业软件都是将多文件包制作成安装包的形式,用户启动安装包后,会将我们的多文件包解压到指定文件夹,然后还会生成桌面快捷方式,自动卸载程序等。
制作安装包其实不难,我们首先需要下载NSIS
工具
NSIS 下载地址
下载并安装好NSIS
后,需要编写NIS
脚本来制作安装包,仅仅制作安装包,没有必要去学习这种脚本,因此我们还需要下载一个工具,来自动生成NIS
脚本,它就是NIS EDIT
NIS EDIT 下载地址
下载并安装好NIS EDIT
后,按照上述的命令,将Python程序打成多文件包,然后按照以下步骤生成NIS
脚本文件
打开NIS EDIT
,选择【新建脚本:向导】
2.填写应用信息。主要是应用名和版本号
3.设置应用的Icon
图标。选择准备好的ico格式图标,并为安装器起一个有意义的名字,语言则选择简体中文,要去除English的勾选。
4.创建授权文件并指定。我这里事先创建了一个叫SoftwareLicence.txt
的文件,内容请根据实际编写
许可协议
这是许可协议,默认你同意一切条款,关于该协议的所有条款的解释权归本公司所有。
5.添加我们之前已经打包好的多文件包目录
6.依次下一步
7.选择我们多文件包中的启动程序
8.配置卸载程序
9.选中如下三项,完成后,弹出另存为对话框,为我们生成的.nsi
脚本起一个名字,保存后,该脚本将开始自动编译,编译完成后,在脚本同级目录下,会生成安装器程序hello-Setup.exe
,将安装器程序发布出去即可。