三个UI框架帮你用Python编写用户友好的应用程序

译文
开发 架构
了解如何选择合适的图形用户界面库来编写用户友好的应用程序。

译者 | 布加迪

审校 | 孙淑娟 梁策

Python有许多图形用户界面(GUI)框架可供使用。其中大多数非常成熟,得到了开源和商业支持;另一些主要绑定到可用的C/C++ UI库。无论如何,在使用库的选择上,可考虑三个因素:

  • 成熟度:它是否稳定且受到社区的大力支持,是否文档完备?
  • 与Python集成:可能听上去无关紧要,但它可能对工具包构成了很高的准入门槛(你不想觉得好像是在用汇编程序编写GUI;毕竟,它是Python)。
  • 它是否支持你的用例?如果你主要想编写表单,那么Pyforms或Tkinter之类的库可能更适合。(Tkinker家喻户晓。)如果你的GUI较复杂,那么wxPython可能更好,因为它支持的功能很广泛。

优秀的系统管理员应该知道如何创建用户友好的应用程序。它们在大幅提高你和用户的工作效率上会让你大吃一惊。

有很多框架可供选择。本文将概述其中的三个框架:Rich、Tkinter和DearPyGui。

准备好环境

如果想学习以下简短教程,请运行以下命令,准备好环境:

$ git clone https://github.com/josevnz/rpm_query
$ cd rpm_query
$ python3 -m venv --system-site-packages ~/virtualenv/rpm_query
$ . ~/virtualenv/rpm_query/bin/activate
$ python3 setup.py build
$ cp reporter build/scripts-3.?

准备完毕,现在开始。

显示按大小排序的RPM列表

该示例应用程序不是很复杂。它应该清晰地显示以下输出:

$ ./rpmq_simple.py --limit 10
linux-firmware-20210818: 395,099,476
code-1.61.2: 303,882,220
brave-browser-1.31.87: 293,857,731
libreoffice-core-7.0.6.2: 287,370,064
thunderbird-91.1.0: 271,239,962
firefox-92.0: 266,349,777
glibc-all-langpacks-2.32: 227,552,812
mysql-workbench-community-8.0.23: 190,641,403
java-11-openjdk-headless-11.0.13.0.8: 179,469,639
iwl7260-firmware-25.30.13.0: 148,167,043

它应该还可让用户重新运行查询,同时覆盖匹配数量和包名称,以及按大小(字节)来排序。

现在一切准备就绪,你可以开始创建应用程序了。以下三个框架可供参考。

1. Rich

Rich 是威尔·麦克古根 (Will McGugan) 编写的一款极易使用的框架。它不提供大量widget小组件(一个仍在测试阶段,名为Textual的姐妹项目更注重组件。)

安装Rich

安装Rich框架:

$ pip install rich

这是我的Python脚本代码。它在清晰的表上生成进度条和结果:

#!/usr/bin/env python
"""
# rpmq_rich.py - A simple CLI to query the sizes of RPM on your system
Author: Jose Vicente Nunez
"""
import argparse
import textwrap
from reporter import __is_valid_limit__
from reporter.rpm_query import QueryHelper
from rich.table import Table
from rich.progress import Progress
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=textwrap.dedent(__doc__))
parser.add_argument(
"--limit",
type=__is_valid_limit__, # Custom limit validator
action="store",
default=QueryHelper.MAX_NUMBER_OF_RESULTS,
help="By default results are unlimited but you can cap the results"
)
parser.add_argument(
"--name",
type=str,
action="store",
help="You can filter by a package name."
)
parser.add_argument(
"--sort",
action="store_false",
help="Sorted results are enabled bu default, but you fan turn it off"
)
args = parser.parse_args()
with QueryHelper(
name=args.name,
limit=args.limit,
sorted_val=args.sort
) as rpm_query:
rpm_table = Table(title="RPM package name and sizes")
rpm_table.add_column("Name", justify="right", style="cyan", no_wrap=True)
rpm_table.add_column("Size (bytes)", justify="right", style="green")
with Progress(transient=True) as progress:
querying_task = progress.add_task("[red]RPM query...", start=False)
current = 0
for package in rpm_query:
if current >= args.limit:
break
rpm_table.add_row(f"{package['name']}-{package['version']}", f"{package['size']:,.0f}")
progress.console.print(f"[yellow]Processed package: [green]{package['name']}-{package['version']}")
current += 1
progress.update(querying_task, advance=100.0)
progress.console.print(rpm_table)

为原始脚本添加表和进度条非常容易。

下面是全新改进后的文本UI的样子。

图1

2.Tkinter

Tkinter结合了多个框架:TCL、TK和widget小组件(Ttk)。

该框架相当成熟,文档和示例完备。建议先主要遵循​​官方教程​​,在掌握了基础知识后,可以继续阅读感兴趣的其他教程。

有几点需要注意:

  • 检查你的系统是否正确安装了Tkinter,如下所示:python -m tkinter。
  • 使用回调函数(command=),使你的GUI响应事件。
  • Tkinter使用特殊变量进行通信,这些变量帮你跟踪更改(Var,比如StringVar)。

Tkinter 中的代码是什么样的?

#!/usr/bin/env python
"""
# rpmq_tkinter.py - A simple CLI to query the sizes of RPM on your system
This example is more complex because:
* Uses callbacks (commands) to update the GUI and also deals
* Deals with the placement of components using a frame with Grid and a flow layout
Author: Jose Vicente Nunez
"""
import argparse
import textwrap
from tkinter import *
from tkinter.ttk import *
from reporter import __is_valid_limit__
from reporter.rpm_query import QueryHelper
def __initial__search__(*, window: Tk, name: str, limit: int, sort: bool, table: Treeview) -> NONE:
"""
Populate the table with an initial search using CLI args
:param window:
:param name:
:param limit:
:param sort:
:param table:
:return:
"""
with QueryHelper(name=name, limit=limit, sorted_val=sort) as rpm_query:
row_id = 0
for package in rpm_query:
if row_id >= limit:
break
package_name = f"{package['name']}-{package['version']}"
package_size = f"{package['size']:,.0f}"
table.insert(
parent='',
index='end',
iid=row_id,
text='',
values=(package_name, package_size)
)
window.update() # Update the UI as soon we get results
row_id += 1
def __create_table__(main_w: Tk) -> Treeview:
"""
* Create a table using a tree component, with scrolls on both sides (vertical, horizontal)
* Let the UI 'pack' or arrange the components, not using a grid here
* The table reacts to the actions and values of the components defined on the filtering components.
:param main_w
"""
scroll_y = Scrollbar(main_w)
scroll_y.pack(side=RIGHT, fill=Y)
scroll_x = Scrollbar(main_w, orient='horizontal')
scroll_x.pack(side=BOTTOM, fill=X)
tree = Treeview(main_w, yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
tree.pack()
scroll_y.config(command=tree.yview)
scroll_x.config(command=tree.xview)
tree['columns'] = ('package_name', 'package_size')
tree.column("#0", width=0, stretch=NO)
tree.column("package_name", anchor=CENTER, width=500)
tree.column("package_size", anchor=CENTER, width=100)
tree.heading("#0", text="", anchor=CENTER)
tree.heading("package_name", text="Name", anchor=CENTER)
tree.heading("package_size", text="Size (bytes)", anchor=CENTER)
return tree
def __cli_args__() -> argparse.Namespace:
"""
Command line argument parsing
:return:
"""
parser = argparse.ArgumentParser(description=textwrap.dedent(__doc__))
parser.add_argument(
"--limit",
type=__is_valid_limit__, # Custom limit validator
action="store",
default=QueryHelper.MAX_NUMBER_OF_RESULTS,
help="By default results are unlimited but you can cap the results"
)
parser.add_argument(
"--name",
type=str,
action="store",
default="",
help="You can filter by a package name."
)
parser.add_argument(
"--sort",
action="store_false",
help="Sorted results are enabled bu default, but you fan turn it off"
)
return parser.parse_args()
def __reset_command__() -> None:
"""
Callback to reset the UI form filters
Doesn't trigger a new search. This is on purpose!
:return:
"""
query_v.set(args.name)
limit_v.set(args.limit)
sort_v.set(args.sort)
def __ui_search__() -> None:
"""
Re-do a search using UI filter settings
:return:
"""
for i in results_tbl.get_children():
results_tbl.delete(i)
win.update()
__initial__search__(
window=win, name=query_v.get(), limit=limit_v.get(), sort=sort_v.get(), table=results_tbl)
def test(arg):
print(arg)
if __name__ == "__main__":
args = __cli_args__()
win = Tk()
win.title("RPM Search results")
# Search frame with filtering options. Force placement using a grid
search_f = LabelFrame(text='Search options:', labelanchor=N, relief=FLAT, padding=1)
query_v = StringVar(value=args.name)
query_e = Entry(search_f, textvariable=query_v, width=25)
limit_v = IntVar(value=args.limit)
limit_l = Label(search_f, text="Limit results: ")
query_l = Spinbox(
search_f,
from_=1, # from_ is not a typo and is annoying!
to=QueryHelper.MAX_NUMBER_OF_RESULTS,
textvariable=limit_v
)
sort_v = BooleanVar(value=args.sort)
sort_c = Checkbutton(search_f, text="Sort by size", variable=sort_v)
search_btn = Button(search_f, text="Search RPM", command=__ui_search__)
clear_btn = Button(search_f, text="Reset filters", command=__reset_command__)
package_l = Label(search_f, text="Package name: ").grid(row=0, column=0, sticky=W)
search_f.grid(column=0, row=0, columnspan=3, rowspan=4)
limit_l.grid(row=1, column=0, sticky=W)
query_e.grid(row=0, column=1, columnspan=2, sticky=W)
query_l.grid(row=1, column=1, columnspan=1, sticky=W)
sort_c.grid(row=2, column=0, columnspan=1, sticky=W)
search_btn.grid(row=3, column=0, columnspan=2, sticky=W)
clear_btn.grid(row=3, column=1, columnspan=1, sticky=W)
search_f.pack(side=TOP, fill=BOTH, expand=1)
results_tbl = __create_table__(win)
results_tbl.pack(side=BOTTOM, fill=BOTH, expand=1)
__initial__search__(
window=win, name=query_v.get(), limit=limit_v.get(), sort=sort_v.get(), table=results_tbl)
win.mainloop()

代码比较冗长,主要是由于事件处理。

图2

但是,这也意味着一旦脚本启动,就可以重新运行查询,只需在搜索选项框上修改参数。

3. DearPyGui

乔纳森·霍夫施塔特 (Jonathan Hoffstadt) 开发的DearPyGui可跨平台(Linux、Windows和macOS),具备一些出色的功能。

安装DearPyGui

如果你有当前系统(比如Fedora 33或Windows 10 Pro),安装起来应该很容易:

$ pip install dearpygui

以下是用DearPyGui重写的应用程序:

#!/usr/bin/env python
"""
# rpmq_dearpygui.py - A simple CLI to query the sizes of RPM on your system
Author: Jose Vicente Nunez
"""
import argparse
import textwrap
from reporter import __is_valid_limit__
from reporter.rpm_query import QueryHelper
import dearpygui.dearpygui as dpg
TABLE_TAG = "query_table"
MAIN_WINDOW_TAG = "main_window"
def __cli_args__() -> argparse.Namespace:
"""
Command line argument parsing
:return:
"""
parser = argparse.ArgumentParser(description=textwrap.dedent(__doc__))
parser.add_argument(
"--limit",
type=__is_valid_limit__, # Custom limit validator
action="store",
default=QueryHelper.MAX_NUMBER_OF_RESULTS,
help="By default results are unlimited but you can cap the results"
)
parser.add_argument(
"--name",
type=str,
action="store",
default="",
help="You can filter by a package name."
)
parser.add_argument(
"--sort",
action="store_false",
help="Sorted results are enabled bu default, but you fan turn it off"
)
return parser.parse_args()
def __reset_form__():
dpg.set_value("package_name", args.name)
dpg.set_value("limit_text", args.limit)
dpg.set_value("sort_by_size", args.sort)
def __run_initial_query__(
*,
package: str,
limit: int,
sorted_elem: bool
) -> None:
"""
Need to ensure the table gets removed.
See issue: https://github.com/hoffstadt/DearPyGui/issues/1350
:return:
"""
if dpg.does_alias_exist(TABLE_TAG):
dpg.delete_item(TABLE_TAG, children_only=False)
if dpg.does_alias_exist(TABLE_TAG):
dpg.remove_alias(TABLE_TAG)
with dpg.table(header_row=True, resizable=True, tag=TABLE_TAG, parent=MAIN_WINDOW_TAG):
dpg.add_table_column(label="Name", parent=TABLE_TAG)
dpg.add_table_column(label="Size (bytes)", default_sort=True, parent=TABLE_TAG)
with QueryHelper(
name=package,
limit=limit,
sorted_val=sorted_elem
) as rpm_query:
current = 0
for package in rpm_query:
if current >= args.limit:
break
with dpg.table_row(parent=TABLE_TAG):
dpg.add_text(f"{package['name']}-{package['version']}")
dpg.add_text(f"{package['size']:,.0f}")
current += 1
def __run__query__() -> None:
__run_initial_query__(
package=dpg.get_value("package_name"),
limit=dpg.get_value("limit_text"),
sorted_elem=dpg.get_value("sort_by_size")
)
if __name__ == "__main__":
args = __cli_args__()
dpg.create_context()
with dpg.window(label="RPM Search results", tag=MAIN_WINDOW_TAG):
dpg.add_text("Run a new search")
dpg.add_input_text(label="Package name", tag="package_name", default_value=args.name)
with dpg.tooltip("package_name"):
dpg.add_text("Leave empty to search all packages")
dpg.add_checkbox(label="Sort by size", tag="sort_by_size", default_value=args.sort)
dpg.add_slider_int(
label="Limit",
default_value=args.limit,
tag="limit_text",
max_value=QueryHelper.MAX_NUMBER_OF_RESULTS
)
with dpg.tooltip("limit_text"):
dpg.add_text(f"Limit to {QueryHelper.MAX_NUMBER_OF_RESULTS} number of results")
with dpg.group(horizontal=True):
dpg.add_button(label="Search", tag="search", callback=__run__query__)
with dpg.tooltip("search"):
dpg.add_text("Click here to search RPM")
dpg.add_button(label="Reset", tag="reset", callback=__reset_form__)
with dpg.tooltip("reset"):
dpg.add_text("Reset search filters")
__run_initial_query__(
package=args.name,
limit=args.limit,
sorted_elem=args.sort
)
dpg.create_viewport(title='RPM Quick query tool')
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()

请注意,DearPyGui在嵌套组件时使用上下文,因而创建GUI时容易得多。代码也没有Tkinter代码那么冗长,对类型的支持也要更好(比如说,PyCharm提供了自动完成方法参数的功能)。

DearPyGui还很年轻(目前是版本1.0.3),也有一些bug,尤其是在旧的Linux发行版上。但它很有前景,正在积极开发中。

那么,DearPyGui中UI是什么样子呢?

图3

原文标题:3 UI frameworks for writing user-friendly applications in Python,作者:Jose Vicente Nunez

责任编辑:华轩 来源: 51CTO
相关推荐

2018-06-22 09:00:00

Java框架Pronghorn

2021-09-14 09:39:06

设计系统框架设计原则

2015-07-07 09:06:32

云计算应用部署云计算成本

2010-11-03 13:19:28

2022-02-28 16:05:53

开发RTOS数据

2020-10-10 10:30:31

JavaScript开发技术

2018-12-03 08:25:24

2019-02-11 09:35:04

Python应用程序Tornado

2023-02-13 08:45:26

2009-07-14 18:10:38

Swing应用程序框架

2012-03-15 15:35:51

iUI框架EclipseiOS Web

2011-04-01 11:01:02

应用程序BlackBerryJava

2020-01-15 14:20:07

Node.js应用程序javascript

2018-06-06 09:00:16

2023-06-13 13:38:00

FlaskPython

2023-12-21 16:25:23

WeChatSnapchatShopee

2012-05-29 10:04:08

2021-04-03 12:31:48

Python开发数据科学

2024-01-02 00:18:56

Buffalo项目Go Web框架

2015-03-04 14:30:22

DIY平台移动应用
点赞
收藏

51CTO技术栈公众号