在开发一套系统框架的时候,除了关注实现系统的功能实现外,我们对于系统的各个方面都是应该精益求精,以最少的编码做最好的事情,在开发的各个层次上,包括前端后端,界面处理、后端处理、常用辅助类、控件封装等等方面,我们都可以通过抽象、重用等方式,实现代码的优化、简化,以期达到快速开发的目的。本篇随笔我们就来聊聊界面的抽象迭代处理,以及最终的实现过程。

1、列表窗体界面的抽丝剥茧式的迭代抽象

例如对于系统的窗体来说,一般我们可以按主要的功能视图来区分,一个是列表展示界面,一个是编辑/查看详细内容界面,前面有一篇文章《使用wxpython开发跨平台桌面应用,基类对话框窗体的封装处理》我专门介绍了编辑对话框的抽象设计,所以本篇随笔主要针对列表界面进行介绍。

如我们大致需要一个展示列表的界面,列表界面一般分为查询区、列表界面展示区和分页信息区,我们把它分为两个主要的部分,如下界面所示。

对于查询条件标号为1区的内容,又可以继续细分,根据不同的业务模块,内容有变化区和固定区,如下标识。

不同的业务模块,查询的条件肯定是不同的,这部分为内容变化区。

而对于常见的功能按钮,基本上是固定的,我们后期可以根据一些条件进行动态的按钮显示/隐藏,但是这里业务按钮就这些,虽然触发的界面肯定有所不同,但是这些按钮的处理逻辑是不会变化的,所以称为固定逻辑区。

对于内容变化的,我们可以把它们下发到子类里面,每一个业务模块的列表界面为一个子类,继承基类即可。

其中业务列表窗体界面分为两个部分

而对于通过wx.Grid展示的列表界面部分,虽然分为列表内容和分页栏内容,但是它们的数据变化,控件却是不会变化的,如下界面截图所示。

也就是说,这些控件的相关排版信息,我们可以抽象到父类中进行创建,而数据则由子类进行更新变化即可。

其中包括表格的列名、中文名称对应、列的宽度,数据集合等信息,而分页栏这是根据每页的大小、当前页码、总数等信息进行按钮的状态控制即可。

2、引入泛型定义,实现更丰富的界面控制

我在文章《基于SqlAlchemy+Pydantic+FastApi的Python开发框架》 中介绍过使用泛型来构建更加弹性化的基类处理。

如对于路由器,我们通过泛型参数的处理,让基类的接口更加个性化一些,如下代码所示。

在 Python 中,泛型(Generic) 是一种允许类型参数化的特性,通常用于类型注解和类型检查。Python 的泛型支持主要通过 typing 模块中的类型提示来实现,例如 GenericTypeVarList[T] 等。使用泛型定义基类有以下几个好处:

1. 提高类型安全性

泛型允许在编写类和函数时,明确指定类型参数,这样在使用时可以进行更严格的类型检查,减少类型错误。

如果基类使用了泛型 T,这样在子类或实例化时可以指定具体的类型(如 User),从而在编译期(使用 IDE 或类型检查工具时)获得类型安全性,避免类型错误。

2. 增强代码重用性

泛型允许编写更具通用性的基类,不必针对每种数据类型重复实现类似功能,增加了代码的重用性。

3. 提升代码的可读性和维护性

泛型使得类型参数在类定义中显式化,这样可以帮助开发者更清楚地了解类或函数的预期类型,减少类型推断的复杂度,提升可读性。

4. 与类型提示和类型检查工具集成

现代 Python 开发中,使用类型提示(type hinting)已经成为一种最佳实践。泛型定义能够更好地与类型检查工具(如 mypypyright 等)集成,帮助在编写代码时发现潜在的类型错误。

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

那么界面效果会获得如下所示。

我们根据需要实现具体的树数据显示即可。

以上就是我对于界面的剖析和逐步的抽象处理,把主要的逻辑提取到父类中去,变化的小部分内容,交给子类差别实现即可,减少代码,提高效率。

使用wxpython开发跨平台桌面应用,基类列表窗体的抽象封装处理的更多相关文章

  1. moviepy音视频开发:音频剪辑基类AudioClip

    ☞ ░ 前往老猿Python博文目录 ░ 一.背景知识介绍 1.1.声音三要素: 音调:人耳对声音高低的感觉称为音调(也叫音频).音调主要与声波的频率有关.声波的频率高,则音调也高. 音量:也就是响度 ...

  2. Electron+Vue开发跨平台桌面应用

    Electron+Vue开发跨平台桌面应用 xiangzhihong发布于 2019-12-23 虽然B/S是目前开发的主流,但是C/S仍然有很大的市场需求.受限于浏览器的沙盒限制,网页应用无法满足某 ...

  3. Delphi 继承基类的窗体,并显示基类的控件操作。

    1.  先建一个普通的窗体,until1 2.  先把类实现基类, 并需要实现基类需要继承的方法, 可以先不用再方法中写实现代码. TForm4 = class(TfrmmtAReportPeriod ...

  4. WPF组件开发之组件的基类

    之前在网上看到很多关于组件开发的资料,但真正可以用到框架内的却很少.今天贴出自己做的组件,并适合大部分框架的代码. 组件开发需要先做出组件的基类,然后由其他的各类组件去继承这个基类,下面是组件基类的代 ...

  5. Filter组件开发中的SDK基类分析

    DirectShow SDK提供了一套开发Filter的基类源代码.基于这些基类开发Filter将大大简化开发过程. 1.CBaseObject 大部分SDK类都从CBaseObject类(参见com ...

  6. iOS开发——项目实战OC篇&类QQ黏性按钮(封装)

    类QQ粘性按钮(封装) 那个,先来说说原理吧: 这里原理就是,在界面设置两个控件一个按钮在上面,一个View在下面(同样大小),当我们拖动按钮的时候显示下面的View,view不移动,但是会根据按钮中 ...

  7. 使用XUL开发跨平台桌面应用

    先上图: 现在使用html,css,js开发桌面的优势越来越明显了,硬件性能的不断提升,人力成本越发昂贵,用户对界面要求越来越高,全球化下企业间的竞争越发激烈. 桌面软件50%+的工作量都在界面开发这 ...

  8. Electron开发跨平台桌面程序入门教程

    最近一直在学习 Electron 开发桌面应用程序,在尝试了 java swing 和 FXjava 后,感叹还是 Electron 开发桌面应用上手最快.我会在这一篇文章中实现一个HelloWord ...

  9. moviepy音视频开发:音频剪辑基类AudioClip详解

    ☞ ░ 前往老猿Python博文目录 ░ 一.背景知识介绍 1.1.声音三要素: 音调:人耳对声音高低的感觉称为音调(也叫音频).音调主要与声波的频率有关.声波的频率高,则音调也高. 音量:也就是响度 ...

  10. c#基类 常用数据验证的封装,数字,字符,邮箱的验证

    #region 常用数据验证的封装,数字字符的验证       /// <summary>       /// 常用数据验证的封装,数字字符的验证       /// </summa ...

随机推荐

  1. USB入门系列(一)认识USB

    认识USB usb的类型 接头外形上 USB类型 描述 USB-A 最广泛的接口标准 USB-B 一般用于打印机.扫描仪.USBHUB等外部USB设备(j-tag就用到了) USB-C USB-C将成 ...

  2. Docker 导出容器 / 镜像

    有时由于镜像大小.网络限制等原因,我们不能将本地制作的容器 / 镜像上传到公共容器注册表.此时我们可以选择将镜像以本地文件的形式导出. 导入 / 导出容器 docker export "CO ...

  3. Mac 使用远程 Ubuntu 机器进行时间备份

    设置 SMB 服务 首先在 Ubuntu 中配置 SMB 服务.可以参考 Ubuntu 设置 SMB 服务. 创建 APFS 磁盘映像 我们在 Ubuntu 上创建出的 SMB 共享文件夹可以用来存放 ...

  4. python pyqt6 QMenu 设定圆角边框

    本来这个没有必要写,但是因为写的过程中,按照网上的写法运行,不知道为什么QMenu的右下角有圆角边框与直角背景颜色会覆盖显示 所以还是有必要写一下 menu = QMenu(self.tool_but ...

  5. [Udemy] AWS Certified Data Analytics Specialty - 3.Processing

    Lambda Lambda 经常起胶水的作用,就是粘合不同的service. 如下图例子 另外Requirement #1 也是一个例子,还有Requirement #3 除了Kinesis Data ...

  6. Qml 实现星级评分组件 已发布

    [写在前面] 在现代应用程序中,星级评分是一个常见的用户界面元素,它允许用户对产品.服务或内容进行评价. 想必大家在用各种带有评分的软件中看到过这个组件: 本文将指导你如何使用 Qml 创建一个简单而 ...

  7. tailwindcss 3.3.3(完成入门,需要补充每个单独样式)

    归纳 修饰符堆叠 <button class="dark:md:hover:bg-fuchsia-600"> 以下为修饰符 伪类伪元素 使用方法: 在实用程序类前添加h ...

  8. AJAX——简介

    AJAX 同步与异步 AJAX 快速入门   

  9. shell脚本参数读取列表文件,循环执行时,要注意这个列表文件转换成 unix 格式

    当报一些很奇怪的错,比如报错多了一个空格,但 vim 看打开没有,比如: 或者肉眼看上去路径正常,但却报路径不存在: 这种时候要使用 dos2unix 工具,把这个列表文件转换一下:

  10. BFS 马的遍历————洛谷p1443

    马的遍历 题目描述 有一个 \(n \times m\) 的棋盘,在某个点 \((x, y)\) 上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步. 输入格式 输入只有一行四个整数,分别为 ...