使用wxpython开发跨平台桌面应用,基类列表窗体的抽象封装处理
在开发一套系统框架的时候,除了关注实现系统的功能实现外,我们对于系统的各个方面都是应该精益求精,以最少的编码做最好的事情,在开发的各个层次上,包括前端后端,界面处理、后端处理、常用辅助类、控件封装等等方面,我们都可以通过抽象、重用等方式,实现代码的优化、简化,以期达到快速开发的目的。本篇随笔我们就来聊聊界面的抽象迭代处理,以及最终的实现过程。
1、列表窗体界面的抽丝剥茧式的迭代抽象
例如对于系统的窗体来说,一般我们可以按主要的功能视图来区分,一个是列表展示界面,一个是编辑/查看详细内容界面,前面有一篇文章《使用wxpython开发跨平台桌面应用,基类对话框窗体的封装处理》我专门介绍了编辑对话框的抽象设计,所以本篇随笔主要针对列表界面进行介绍。
如我们大致需要一个展示列表的界面,列表界面一般分为查询区、列表界面展示区和分页信息区,我们把它分为两个主要的部分,如下界面所示。
对于查询条件标号为1区的内容,又可以继续细分,根据不同的业务模块,内容有变化区和固定区,如下标识。
不同的业务模块,查询的条件肯定是不同的,这部分为内容变化区。
而对于常见的功能按钮,基本上是固定的,我们后期可以根据一些条件进行动态的按钮显示/隐藏,但是这里业务按钮就这些,虽然触发的界面肯定有所不同,但是这些按钮的处理逻辑是不会变化的,所以称为固定逻辑区。
对于内容变化的,我们可以把它们下发到子类里面,每一个业务模块的列表界面为一个子类,继承基类即可。
其中业务列表窗体界面分为两个部分
而对于通过wx.Grid展示的列表界面部分,虽然分为列表内容和分页栏内容,但是它们的数据变化,控件却是不会变化的,如下界面截图所示。
也就是说,这些控件的相关排版信息,我们可以抽象到父类中进行创建,而数据则由子类进行更新变化即可。
其中包括表格的列名、中文名称对应、列的宽度,数据集合等信息,而分页栏这是根据每页的大小、当前页码、总数等信息进行按钮的状态控制即可。
2、引入泛型定义,实现更丰富的界面控制
我在文章《基于SqlAlchemy+Pydantic+FastApi的Python开发框架》 中介绍过使用泛型来构建更加弹性化的基类处理。
如对于路由器,我们通过泛型参数的处理,让基类的接口更加个性化一些,如下代码所示。
在 Python 中,泛型(Generic) 是一种允许类型参数化的特性,通常用于类型注解和类型检查。Python 的泛型支持主要通过 typing
模块中的类型提示来实现,例如 Generic
、TypeVar
、List[T]
等。使用泛型定义基类有以下几个好处:
1. 提高类型安全性
泛型允许在编写类和函数时,明确指定类型参数,这样在使用时可以进行更严格的类型检查,减少类型错误。
如果基类使用了泛型 T
,这样在子类或实例化时可以指定具体的类型(如 User
),从而在编译期(使用 IDE 或类型检查工具时)获得类型安全性,避免类型错误。
2. 增强代码重用性
泛型允许编写更具通用性的基类,不必针对每种数据类型重复实现类似功能,增加了代码的重用性。
3. 提升代码的可读性和维护性
泛型使得类型参数在类定义中显式化,这样可以帮助开发者更清楚地了解类或函数的预期类型,减少类型推断的复杂度,提升可读性。
4. 与类型提示和类型检查工具集成
现代 Python 开发中,使用类型提示(type hinting)已经成为一种最佳实践。泛型定义能够更好地与类型检查工具(如 mypy
、pyright
等)集成,帮助在编写代码时发现潜在的类型错误。
5. 增强代码的灵活性
使用泛型定义基类可以让类的功能更通用,从而在不修改类代码的情况下,通过类型参数化来实现不同的数据处理逻辑。
以上这些优势使得在大型项目或库开发中使用泛型变得尤为重要,尤其是在设计通用数据结构、工具类或框架时。
泛型定义,我们声明一个类型,如下所示。
ModelType = TypeVar("ModelType") # 定义泛型基类
然后就可以根据需要采用泛型类型了,如下是窗体基类的定义,采用一个泛型的类型来定义业务模块的子类DTO对象,从而使得该父类很多接口都具有很好的类型化定义。
# 创建泛型基类 BaseFrameList,并继承 wx.Panel
class BaseFrameList(wx.Panel, Generic[ModelType]):
而对于业务模块的子类,如其中业务列表界面的子类定义如下所示。
# 继承BaseFrameList类,并传入实体类SystemTypeInfo,作为泛型类型
class FrmSystemType(BaseFrameList[SystemTypeDto]):
这样我们构建的列表界面父子类的关系如下 所示,其中包括两个业务模块的列表界面:系统类型定义,客户信息。
例如我们以客户列表界面的子类代码进行分析,如下所示。我们只需要传入所需的一些字段显示及中文解析,并传入相关的DTO对象,如下代码所示。
# 继承BaseFrameList类,并传入实体类CustomerInfo,作为泛型类型
class FrmCustomer(BaseFrameList[CustomerDto]):
"""客户信息"""
# 显示的字段名称,逗号分隔
display_columns = "id,name,age,creator,createtime"
# 列名映射(字段名到显示名的映射)
column_mapping = {
"id": "编号", "name": "姓名", "age": "年龄", "creator": "创建人", "createtime": "创建时间",
}
def __init__(self, parent):
# 初始化基类信息
super().__init__(
parent,
model=CustomerDto,
display_columns=self.display_columns,
column_mapping=self.column_mapping
)
这些内容肯定是必须的,另外,如果我们需要自定义列表界面中单元格列的宽度,也可以指定宽度的字典参照,不指定则使用默认宽度即可(在基类定义默认宽度,如为150像素)。
例如,我在系统类型定义中就包含了列表宽度的字典参考,代码如下所示。
# 继承BaseFrameList类,并传入实体类SystemTypeInfo,作为泛型类型
class FrmSystemType(BaseFrameList[SystemTypeDto]):
"""系统类型定义"""
# 显示的字段名称,逗号分隔
display_columns = "id,name,customid,authorize,note"
# 列名映射(字段名到显示名的映射)
column_mapping = {
"id": "系统标识",
"name": "系统名称",
"customid": "客户编码",
"authorize": "授权编码",
"note": "备注",
}
# 表格显示的列宽
column_widths = {"id": 150, "name": 250, "customid": 100, "authorize": 100}
def __init__(self, parent):
# 初始化基类信息
super().__init__(
parent,
model=SystemTypeDto,
display_columns=self.display_columns,
column_mapping=self.column_mapping,
column_widths=self.column_widths,
use_left_panel=False,
)
3、变化中提取不变的逻辑,界面代码的简化
而对于子类查询条件的内容,我们前面说它是动态不同的,因此需要子类来具体实现。
下面这个是客户信息的查询内容,我们只需要添加对应的标签和输入控件即可,不需要理会布局的处理,默认的FlexGridSizer为4*2=8列,每列间隔5px。
下面是客户信息的列表界面,重写父类函数的实现代码
def CreateConditions(self, pane: wx.Window) -> List[wx.Window]:
"""创建折叠面板中的查询条件输入框控件"""
# 创建控件,不用管布局,交给CreateConditionsWithSizer控制逻辑
# 默认的FlexGridSizer为4*2=8列,每列间隔5px
lblName = wx.StaticText(pane, -1, "姓名:")
self.txtName = wx.TextCtrl(pane, -1, size=(150, -1))
lblAge = wx.StaticText(pane, -1, "年龄:")
self.txtAge = ctrl.MyNumericRange(pane, -1, size=(150, -1))
return [lblName, self.txtName, lblAge, self.txtAge]
如果有时候需要重新定义布局,那么可以重写它上一级包含布局的函数即可实现更高维度的定制处理。
如对于系统类型的列表界面,如下所示
为了更好介绍对于面板布局的控制,我重写其上一级的函数,包含FlexGridSizer的定义信息,这里我们的代码如下所示。
def CreateConditionsWithSizer(self, pane: wx.Window):
"""子类可重写该方法,创建折叠面板中的查询条件,包括布局FlexGridSizer"""
# 先创建控件
lblSystemType = wx.StaticText(pane, -1, "系统标识:")
self.txtSystemType = wx.TextCtrl(pane, -1, "", size=(150, -1))
lblName = wx.StaticText(pane, -1, "系统名称:")
self.txtName = wx.TextCtrl(pane, -1, "", size=(150, -1))
lblCustomCode = wx.StaticText(pane, -1, "客户编码:")
self.txtCustomCode = wx.TextCtrl(pane, -1, "", size=(150, -1))
lblAuthCode = wx.StaticText(pane, -1, "授权编码:")
self.txtAuthCode = wx.TextCtrl(pane, -1, "", size=(150, -1))
# 增加条件面板
input_sizer = wx.FlexGridSizer(cols=4 * 2, hgap=5, vgap=5)
# 统一处理查询条件控件的添加,使用默认的布局方式
list = [lblSystemType, self.txtSystemType, lblName, self.txtName, lblCustomCode, self.txtCustomCode, lblAuthCode, self.txtAuthCode]
for i in range(len(list)):
input_sizer.Add(list[i], 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
# input_sizer.AddGrowableCol(1)
# input_sizer.Add((5, 5))
return input_sizer
而对于子类的编辑查看对话框,我们通过触发按钮进行弹出,或者右键的菜单中弹出,都是同样的逻辑,不过就是界面内容不同,我们可以让子类进行实现即可。
子类对于新增编辑的界面实现代码如下所示,其中Add和OnEditById都是父类的空函数,由子类来具体实现即可(覆盖重写)。
def OnAdd(self, event: wx.Event) -> None:
"""子类重写-打开新增对话框"""
dlg = FrmSystemTypeEdit(self)
if dlg.ShowModal() == wx.ID_OK:
# 新增成功,刷新表格
self.update_grid()
dlg.Destroy()
def OnEditById(self, entity_id: Any | str):
"""子类重写-根据主键值打开编辑对话框"""
dlg = FrmSystemTypeEdit(self, entity_id)
if dlg.ShowModal() == wx.ID_OK:
# 更新grid列表数据
self.update_grid()
dlg.Destroy()
其他处理,如删除记录、导入、导出的处理,大同小异,从变化中找到不变的逻辑交给父类处理,子类负责最原始变化的内容即可。
当然对于一些复杂的列表界面,可能还需要考虑左侧放置一些树列表以便快速的选择不同分类的数据,如下Winform上的界面。
这个界面的内容,左侧就是折叠的两个树列表:机构列表、角色列表,以便方便选择用户信息。
那么对于这样的效果,我们在基类窗体中是否可以抽象出来,答案当然是可以的,还记得我们前面《使用wxpython开发跨平台桌面应用,动态工具的创建处理 》介绍过的工具栏的时候,使用了Manager类的实现效果。
不过这个是主窗体级别的,我们需要为具体的业务列表界面定义一个类似的效果。
我们在父类窗体中定义一个开关变量,用来开启或者关闭左侧树列表面板的,如下代码所示。
这样构建树列表就交给函数 create_tree_panels 实现即可,它会构建一到多个的树列表,父界面窗体负责整合它们显示即可。
如子类定义重写创建树列表的函数,如下代码所示。
def create_tree_panels(self) -> dict[str, wx.Panel]:
"""子类重写该方法,创建左侧树列表面板-可以多个树列表"""
dict = {}
# 示例代码
for key in ["机构列表", "角色列表"]:
tree_panel = wx.Panel(self)
tree_sizer = wx.BoxSizer(wx.VERTICAL)
tree = CT.CustomTreeCtrl(tree_panel, style=wx.TR_DEFAULT_STYLE)
self._populate_tree(tree)
tree_sizer.Add(tree, 1, wx.EXPAND)
tree_panel.SetSizer(tree_sizer)
dict[key] = tree_panel
return dict
那么界面效果会获得如下所示。
我们根据需要实现具体的树数据显示即可。
以上就是我对于界面的剖析和逐步的抽象处理,把主要的逻辑提取到父类中去,变化的小部分内容,交给子类差别实现即可,减少代码,提高效率。