3行代码,为“任意”Python程序生成GUI界面!

7 阅读10分钟

PyGUIAdapter:一个将“任意”Python程序转换为GUI应用的库

一、背景

在Python开发中,GUI程序的开发一直是一个比较难办的问题,为了照顾到那些不习惯使用命令行的用户,我们常常需要为我们的python程序套上一层GUI的壳。Python的GUI框架还算是比较丰富的,有内置的tk,也有像wxWidgets、GTK、QT这样成熟的GUI框架的绑定,借助这些框架,完全可以为任何Python程序构建出合适的甚至是花里胡哨的图形用户界面,但问题在于:如果你是一个GUI方面的新手,学习一种GUI框架无疑将引入巨大的学习成本。很多时候,我们只是想给我们写的小工具套一个图形化的外壳而已,不等于我们真的想去写大量的GUI代码,事实上,手动去处理界面的状态、交互、事件、数据等确实是一件比较折磨人的事情......

那么,有没有一种工具,可以让我们尽可能多地关注功能的实现,而它在背后偷偷地帮帮我们处理好所有(或者是)大部分界面相关的细节,让我们可以“无痛”地为我们的程序套上一层图形界面的“外衣”,(当然,没有那么花里胡哨也可以)。经过一番研究,我发现,嘿还真有。现成的,比较成熟的一个解决方案是Gooey,一个号称可以 “Turn (almost) any Python 3 Console Program into a GUI application with one line” 的Python库。

二、Gooey以及为什么没有选择它

Gooey 是一个python库,目前在 github上20.3K star,是一个比较受欢迎的项目。

它使用wxWidgets作为底层GUI框架,可以将几乎所有python命令行程序转换为GUI程序,它生成的界面如下图所示:

bV3tWrjI5TW4CnmB0PP9Sp-0Eh-xgGpwssUBlev1FbI.png

它的原理简单来说,就是把命令行解析器(argparse)解析到的命令行参数转换成对应的输入控件,然后通过这些控件,接受用户的输入。它的使用也确实足够简单,有时候一个装饰器@Gooey就解决了问题。

但是,我最后还是没有选择使用Gooey,这里有几个原因:一是在我的机器上Gooey生成的界面总感觉有些卡卡的(不知道是不是因为wxWidgets在Windows平台上有些“水土不服”);二是在高分屏上,Gooey界面上的文字总是有些模糊(找了一圈没找到开启high dpi的接口);三是Gooey这个库好久没有更新了,它的最后一次提交已经是在两年前。

除了上面这几个原因,还有一个很重要的因素,那就是,Gooey帮我们摆脱了GUI代码不假,但我们还是要写argparse代码。

不想写GUI代码≠想写命令行代码(嘿嘿,公式做题就是快)

那么,有没有那么一种可能,我是说可能,存在那么一种东西,可以歘的一下,把任意一个函数转换成图形界面,它的参数变成了输入控件,点一下按钮,就可以运这个函数。如果存在这样的东西,那么不就解决了我们既不想写GUI代码,也不想写命令行代码的矛盾了吗?

很遗憾的是,我并没有发现这样的存在。既然如此,本着没有轮子,那就自己造一个的思路,我开启了一个新的项目——PyGUIAdapter

三、自己手搓一个吧:PyGUIAdapter的诞生

从功能上看,PyGUIAdapter`和`Gooey有些类似,但在原理上,二者存在很大区别。

如上文所讲,Gooey是面向命令行的,它主要是做了把命令行参数转化为输入控件的工作。

PyGUIAdapter从一开始就是面向函数的。我想,既然都打算使用图形界面了,那么干嘛还需要argparse这个中间商赚差价呢。直接把要实现的功能封装成函数,把用户输入对应为函数的参数不就行了吗。这样,我们只需要解析函数,提取它的参数,然后生成对应的界面控件就可以了,是不是非常简单呢?

为了实现从函数到控件的映射,我另写了一个库function2widgets,它是PyGUIAdapter的基础,主要的功能就是从函数签名和函数的文档字符串中提取信息,通过一系列规则,为函数每个参数生成对应的控件。它默认从函数参数的类型推断其对应的输入控件,如:

参数类型控件类型
intIntLineEdit
boolCheckBox
floatFloatLineEdit
strLineEdit
listListEditor
dictDictEditor
tupleTupleEditor
datetimeDateTimeEdit
dateDateTime
timeTimeEdit
LiteralComboBox
AnyJsonEditor

除了以上控件,function2widgets还实现了许多其他控件,如Dial、Slider、FilePathEdit、DirPathEdit、CheckBoxGroup、RadioButtonGroup、PlainTextEdit等等。

PyGUIAdapter在设计之初就考虑到了扩展性和灵活性的问题,我们既可以依赖内置的规则,由function2widgets库自动推导函数参数所对应的控件类型;我们也可以通过一些方法,手动指定参数的控件类型,同时配置控件的属性,如我们可以手动设置LineEdit的占位符文本(placeholder)、FilePathEdit的文件名过滤器(filters)等等,function2widgets实现的每种控件都有大量可配置的属性。

对了,PyGUIAdapterGooey的另一个区别是,PyGUIAdapter基于PyQT6,通过它生成的界面对high dpi更加友好,而且从流畅度上看,PyQT6似乎也要更好一些(至少在我的机器上是这样的)。

四、PyGUIAdapter的基本使用

PyGUIAdapter的使用非常简单,最少只需要三行代码就可以把一个python函数转换为GUI应用。

下面,是一个简单的使用指南。

1.安装

从pypi安装PyGUIAdapter最新版本:

使用pip:

pip install pyguiadapter

使用poetry:

poetry add pyguiadapter

2. 将需要提供给用户的功能封装成一个函数

假设我们有这么一个函数,我们忽略它的具体功能,我们只需注意到,它需要输入4个参数,每个参数都用类型标注语法标注了参数类型。

def create_file(path: str, filename: str, content: str, overwrite: bool = False):
    path = os.path.join(path, filename)
    if not os.path.isfile(path) or overwrite:
        with open(path, "w", encoding="utf-8") as f:
            f.write(content)
            return True
    return False

3.三行代码(忽略import),为这个函数创建GUI界面

from pyguiadapter.adapter.adapter import GUIAdapter
gui_adapter = GUIAdapter()
gui_adapter.add(create_file)
gui_adapter.run()

完整代码如下:

import os.path


def create_file(path: str, filename: str, content: str, overwrite: bool = False):
    path = os.path.join(path, filename)
    if not os.path.isfile(path) or overwrite:
        with open(path, "w", encoding="utf-8") as f:
            f.write(content)
            return True
    return False


if __name__ == "__main__":
    from pyguiadapter.adapter.adapter import GUIAdapter

    gui_adapter = GUIAdapter()
    gui_adapter.add(create_file)
    gui_adapter.run()

运行这个程序,我们就得到了以下界面:

otrxu3LpOcbzsQQseJoDBB34FyVQYR5taVxFEpHUsEA.png

是不是非常简单,我们没有写一行gui代码和argparse就得到了一个不错的gui界面。

4.一些常用的自定义方法

当然,你可能会提出,根据语义,path使用一个专门编辑路径的控件更合适,content用一个多行文本控件会更好,每个参数名称如果可以自定义就好了,如果有详细的说明就更好......

这一切,都是可以实现的,这里提供一种常用的自定义方法(更多自定义的选项和方法会有单独的文章进行介绍,也可以直接阅读PyGUIAdapter仓库中examples/下的示例源代码,这些示例几乎涵盖了PyGUIAdapter使用的每一个方面)。

为了进行自定义配置,我们需要借助函数的文档字符串,就是函数体开头使用三个引号包裹起来的多行字符串:

def foo():
    """                 
    这里就是函数foo的文档字符串
    """
    pass

PyGUIAdapter通过文档字符串中@widgets和@end标记包裹起来的一段toml 格式的文本来对参数的控件进行配置

比如要指定path参数的控件类型和控件属性等,可以像下面这样做:

"""
...
@widgets
# 要应用自定义配置的参数的名称
[path]
# 指定控件类型
widget_class="DirPathEdit"
# 指定界面上显示的参数的名称
label="文件保存目录"
# 指定参数的描述
description="请选择生成的文件的保存目录"
# 配置控件的其他属性
# 具体可配置的属性取决于控件的类型,可参考function2widgets下的XXXArgs类,其中XXX为控件的类名
# 例如DirPathEdit对应的就是DirPathEidtArgs类
placeholder="选择文件保存的目录"
start_dir="./"
....
@end
"""

除了可以在@widgets和@end块中指定参数的描述文本,PyGUIAdapter还会从 ReST、Google、Numpydoc-style以及Epydoc风格的文档注释中提取参数的描述信息,如:

def foo(a: int):
    """
    :param a: 这是参数a                  
    """

下面,我们来改造一下上面的例子,使得生成的界面用户体验更好:

import os.path


def create_file(
    path: str,
    filename: str,
    content: str,
    overwrite: bool = True,
):
    """
    这是一个演示程序,用于演示<b>PyGUIAdapter</b>的功能,这段文字会被提取为函数的Document并显示在界面上。

    :param path: <b>生成文件的保存的目录,若为空则保存到当前路径下</b>
    :param filename: <b>生成文件的文件名称。注意:<font color=red>不可为空!</font></b>
    :param content: <b>生成文件的内容</b>
    :param overwrite:
    :return:

    @widgets
    [path]
    widget_class="DirPathEdit"
    label="保存路径"
    button_text="选择目录"
    placeholder="选择文件保存的目录"

    [filename]
    label="文件名"
    placeholder="生成文件的名称"
    clear_button=true

    [content]
    widget_class="PlainTextEdit"
    label="文件内容"
    placeholder="生成文件的内容"

    [overwrite]
    label=""
    text="是否覆盖现有文件?"

    @end
    """
    if not path:
        path = "./"
    if not filename:
        raise ValueError("文件名不可为空,请指定文件名称")
    path = os.path.join(path, filename)
    if not os.path.isfile(path) or overwrite:
        with open(path, "w", encoding="utf-8") as f:
            f.write(content)
            return True
    return False


if __name__ == "__main__":
    from pyguiadapter.adapter.adapter import GUIAdapter

    gui_adapter = GUIAdapter()
    gui_adapter.add(create_file)
    gui_adapter.run()

经过配置,界面变成了下面这个样子,相比之前,用户体验提升了不少:

aEDnVfxxewgppTkFpirw7pXOTiRCqLFY6Eq5lpXZtgk.png

create_file() 函数文档字符串中的描述也被正确提取出来,显示在Document区域中:

ykBF-LI-ROzBYeSlv_RiRGiizhJ0eldetKJd3BzJt9c.png

现在,可以在控件内填入参数,然后点击Execute按钮运行这个函数了:

image.png

image.png

可以看到,函数内的异常也被正确地捕获,并通过对话框的方式提示给用户了,没错,这就是PyGUIAdapter进行参数校验的方式,对于不符合要求的参数,直接在函数内部抛出异常就可以了,非常符合我们写程序的直觉。

五、小结

PyGUIAdapter的使用非常简单,但是提供的功能和可配置的选项非常丰富,上面本文提到的那些仅仅是非常小的一部分,除了这些,你可以:

  1. 自定义窗口和控件的外观、样式、图标、文字等
  2. 自定义对话框的显示与否及其内容
  3. 将一个函数配置为可取消的函数并显示一个取消按钮
  4. 配合qt-material等第三方库进行界面美化
  5. 添加菜单和工具栏
  6. 添加多个函数并显示函数选择界面
  7. 在函数中弹出对话框、输入框
  8. 在函数中向Output区域打印文字
  9. ......

这些更高级的内容,如果后续有时间的话,将写一些文章单独讲讲,当然,也可以查看examples/ 示例代码,这些示例几乎涵盖了上面列举的全部主题。

最后,如果你喜欢这个库的话,麻烦给个star,谢谢啦。后续,我会持续维护这个库,如果有什么改进的建议,也欢迎通过issue的方式提出。