Port设置功能开发实践: Pyside6 MVC架构与Model/View/Delegate模式的应用
Port设置功能开发实践: Pyside6 MVC架构与Model/View/Delegate模式的应用
前言
我们平时在进行GUI应用开发中,如何优雅地处理复杂的数据展示和交互逻辑是一个挑战。本文将通过一个实际的Port设置功能开发案例,深入探讨MVC架构模式在PySide6中的应用实践。
案例
示例效果1

示例效果2

我们需要开发一个Port设置功能,支持两种类型的Port生成:
- Net-Based Port:基于网络的集总端口
- Pin-Based Port:基于引脚的端口
功能需求包括:
- 组件选择和管理
- Port参数配置(参考类型、阻抗值、目标层等)
- Port列表的增删改查
- 数据验证和错误处理
架构设计思路
1. 为什么选择MVC架构?
传统的GUI开发往往将数据处理、界面展示和业务逻辑混杂在一起,导致代码难以维护和测试。MVC架构通过分离关注点,带来以下优势:
- 可维护性:各层职责清晰,修改某一层不会影响其他层**(代码解耦,便于排查Bug)**
- 可测试性:业务逻辑与UI分离,便于单元测试
- 可扩展性:新增功能时只需修改对应层级
- 代码复用:Model层可以被多个View复用
2. Model/View/Delegate模式的优势
Qt的Model/View/Delegate模式是MVC的变种,特别适合处理表格数据:
- Model:管理数据和业务逻辑
- View:负责数据展示
- Delegate:处理数据编辑和自定义渲染
核心实现
Model层:数据管理的核心
from dataclasses import dataclass
import random@dataclass
class Port:id: inttype: strcomponent: strref_type: strref_z: floattarget_layer: str = ''@dataclass
class Component:name: strlayer: strnet: strpins: intclass PortModel:def __init__(self):self.ports = []self.components = self.load_components()self.layers = self.extract_layers()def load_components(self):# 从接口或数据库加载组件数据return [Component(f'Comp{random.randint(1,100)}', f'Layer{random.randint(1,5)}', f'Net{random.randint(1,10)}', random.randint(1,10))for _ in range(20)]def extract_layers(self):return sorted(set(comp.layer for comp in self.components))def generate_lumped_port(self, selected_components, ref_type, ref_z, target_layer):for comp in selected_components:port = Port(len(self.ports) + 1, 'Lumped', comp.name, ref_type, ref_z, target_layer)self.ports.append(port)def generate_pin_based_port(self, selected_components, ref_type, ref_z):for comp in selected_components:port = Port(len(self.ports) + 1, 'Pin-Based', comp.name, ref_type, ref_z)self.ports.append(port)
设计目的:
- 使用
@dataclass简化数据类定义 - 将数据加载逻辑封装在Model中
- 提供清晰的业务方法接口
View层:Model/View/Delegate的精妙应用
自定义TableModel
class PortTableModel(QAbstractTableModel):def __init__(self, ports=None):super().__init__()self.ports = ports or []self.headers = ['ID', 'Type', 'Component', 'Ref Type', 'Ref Z(ohm)', 'Target Layer']def rowCount(self, parent=QModelIndex()):return len(self.ports)def columnCount(self, parent=QModelIndex()):return len(self.headers)def data(self, index, role=Qt.DisplayRole):if role == Qt.DisplayRole:port = self.ports[index.row()]if index.column() == 0: return port.idelif index.column() == 1: return port.typeelif index.column() == 2: return port.componentelif index.column() == 3: return port.ref_typeelif index.column() == 4: return f'{port.ref_z:.2f}'elif index.column() == 5: return port.target_layerreturn Nonedef setData(self, index, value, role=Qt.EditRole):if role == Qt.EditRole and index.column() == 4:self.ports[index.row()].ref_z = float(value)self.dataChanged.emit(index, index)return Truereturn Falsedef flags(self, index):flags = super().flags(index)if index.column() == 4: # Ref Z列可编辑flags |= Qt.ItemIsEditablereturn flags
自定义Delegate
class RefZDelegate(QStyledItemDelegate):def createEditor(self, parent, option, index):editor = QDoubleSpinBox(parent)editor.setMinimum(0.0)editor.setMaximum(1000.0)editor.setDecimals(2)return editordef setEditorData(self, editor, index):value = index.model().data(index, Qt.DisplayRole)editor.setValue(float(value))def setModelData(self, editor, model, index):value = editor.value()model.setData(index, value, Qt.EditRole)
技术目的:
- 继承
QAbstractTableModel实现自定义数据模型 - 通过
flags()方法控制单元格的可编辑性 - 使用
QStyledItemDelegate提供专业的数值编辑体验 - 信号机制确保数据变更的实时响应
ViewModel层:优雅的事件处理
class PortViewModel:def __init__(self, model, view):self.model = modelself.view = viewself.connect_signals()def connect_signals(self):self.view.generate_lumped_btn.clicked.connect(self.generate_lumped)self.view.generate_pin_based_btn.clicked.connect(self.generate_pin_based)self.view.delete_btn.clicked.connect(self.delete_selected_port)self.view.check_ports_btn.clicked.connect(self.check_ports)def generate_lumped(self):# 从视图获取选中的组件selected = self.view.net_comp_table.selectionModel().selectedRows()if not selected:QMessageBox.warning(self.view, 'Warning', 'Please select at least one component.')returnselected_components = [self.view.net_comp_table.model().components[idx.row()] for idx in selected]# 获取参考类型ref_type = 'Ground' if self.view.ref_ground.isChecked() else 'Plane' if self.view.ref_plane.isChecked() else Noneif not ref_type:QMessageBox.warning(self.view, 'Warning', 'Please select a reference type.')return# 获取目标层target_layer = self.view.target_layer_combo.currentText()if not target_layer:QMessageBox.warning(self.view, 'Warning', 'Please select a target layer.')return# 调用模型方法生成Portref_z = 50.0 # 默认阻抗值self.model.generate_lumped_port(selected_components, ref_type, ref_z, target_layer)self.view.update_port_table()
设计目的:
- ViewModel不直接操作数据,而是协调Model和View
- 完整的输入验证和用户友好的错误提示
- 通过信号槽机制实现松耦合
架构优势的体现
1. 数据与UI的彻底分离
# 数据更新时,UI自动响应
def update_port_table(self):self.port_table.model().layoutChanged.emit()
通过Qt的信号机制,数据变更会自动触发UI更新,无需手动同步。
2. 高度的可扩展性
想要添加新的Port类型?只需:
- 在Model中添加新的生成方法
- 在View中添加对应的UI元素
- 在ViewModel中连接新的信号
