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("<Motion>", 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("<<TreeviewSelect>>", 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("<<TreeviewSelect>>", 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中,控件实际上是一个字符串,要改变控件样式,需要指定这个字符串名称,而不是类名,它们的对应关系如下

类名控件样式名
ButtonTButton
CheckbuttonTCheckbutton
ComboboxTCombobox
EntryTEntry
FrameTFrame
LabelTLabel
LabelFrameTLabelFrame
MenubuttonTMenubutton
NotebookTNotebook
PanedWindowTPanedwindow
ProgressbarHorizontal.TProgressbar或Vertical.TProgressbar
RadiobuttonTRadiobutton
ScaleHorizontal.TScale 或 Vertical.TScale
ScrollbarHorizontal.TScrollbar 或Vertical.TScrollbar
SeparatorTSeparator
SizegripTSizegrip
TreeviewTreeview

需要注意,在创建新样式时,应当定义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中自定义控件

 

源码地址

示例

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 自制编辑器

 

项目实战要点

在之前章节中已经学习了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('<Button-3>',
                                   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 架子

 

 

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('<Double-Button-1>', self.list_selected)
    self.list_box.bind("<Button-3>", 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,选择【新建脚本:向导】

 

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

 

 

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

 

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

许可协议

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

 

 

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

 

 

 

6.依次下一步

 

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

8.配置卸载程序

 

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

 

 

 

 

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值