在MVC(Model-View-Controller)模式中,Model负责处理数据和业务逻辑,View负责显示用户界面,Controller负责处理用户输入并更新Model和View。
而在之前所有的文章和示例代码中,Model、View、Controller 三者基本都是混为一体的,都是基于 PySide6 的基本组件自身的能力来实现的。
对于上述的功能界面,用户可以输入姓名、年龄、身份证号和选择性别,用户输入的信息会被用来生成一个唯一ID,我们希望实现如下的效果:
from __future__ import annotations
import hashlib
import sys
from datetime import datetime
from typing import Dict
from PySide6.QtWidgets import QApplication, QComboBox, QFormLayout, QLabel, QLineEdit, QMainWindow, QPushButton, QSpinBox, \
QVBoxLayout, QWidget
def get_time_str() -> str:
return datetime.now().isoformat()
def gen_unique_id(data: Dict) -> str:
"""
生成唯一 ID
"""
data_str = data['name'] + data['age'] + data['id_number'] + data['gender']
return hashlib.sha3_256(data_str.encode('utf-8')).hexdigest()
class MyMainWindowUI(QMainWindow):
"""
UI 界面布局
"""
def __init__(self):
super().__init__()
self.setWindowTitle('Hello, MVC Pattern')
self.setToolTip('A PySide6 GUI Application Demo')
self.data = {
'name': '张三', 'age': '18', 'id_number': '123456789012345678', 'gender': '男'
}
self.backups = []
self.name = QLineEdit(parent = self)
self.name.setPlaceholderText('输入姓名')
self.name.returnPressed.connect(self.on_name_input)
self.name.textEdited.connect(self.on_name_input)
self.name.textChanged.connect(self.on_name_input)
self.age = QSpinBox(parent = self)
self.age.setRange(7, 80)
self.age.setValue(18)
self.age.setSingleStep(1)
self.age.valueChanged.connect(self.on_age_input)
self.id_number = QLineEdit(parent = self)
self.id_number.setPlaceholderText('输入身份证号')
self.id_number.returnPressed.connect(self.on_id_number_input)
self.id_number.textEdited.connect(self.on_id_number_input)
self.id_number.textChanged.connect(self.on_id_number_input)
self.gender = QComboBox(parent = self)
self.gender.addItems(['男', '女'])
self.gender.currentIndexChanged.connect(self.on_gender_input)
self.input_view_layout = QFormLayout()
self.input_view_layout.addRow('姓名', self.name)
self.input_view_layout.addRow('年龄', self.age)
self.input_view_layout.addRow('身份证号', self.id_number)
self.input_view_layout.addRow('性别', self.gender)
self.unique_id_label = QLabel(parent = self)
self.reset_button = QPushButton('重置数据', parent = self)
self.reset_button.clicked.connect(self.on_reset_button_clicked)
self.restore_button = QPushButton('恢复到上一次', parent = self)
self.restore_button.clicked.connect(self.on_restore_button_clicked)
self.v_layout = QVBoxLayout()
self.v_layout.addLayout(self.input_view_layout)
self.v_layout.addWidget(self.unique_id_label)
self.v_layout.addWidget(self.reset_button)
self.v_layout.addWidget(self.restore_button)
container = QWidget(self)
container.setLayout(self.v_layout)
self.setCentralWidget(container)
# 初始化刷新
self.update_ui(get_time_str())
def on_name_input(self):
# self.name.text() 获取输入的文本
if len(self.name.text()) > 5 or (not self.name.text().isalpha() and not self.name.text().isascii()):
self.name.setStyleSheet('background-color: red')
elif 0 < len(self.name.text()) <= 5:
self.name.setStyleSheet('background-color: green')
self.data['name'] = self.name.text()
self.backups.append(self.data.copy())
self.update_ui(get_time_str())
def on_id_number_input(self):
# self.id_number.text() 获取输入的文本
if len(self.id_number.text()) > 18 or (not self.id_number.text().isdigit()):
self.id_number.setStyleSheet('background-color: red')
elif 0 < len(self.id_number.text()) <= 18:
self.id_number.setStyleSheet('background-color: green')
self.data['id_number'] = self.id_number.text()
self.backups.append(self.data.copy())
self.update_ui(get_time_str())
def on_age_input(self):
# self.age.value() 获取输入的文本
self.backups.append(self.data.copy())
self.data['age'] = str(self.age.value())
self.update_ui(get_time_str())
def on_gender_input(self):
self.backups.append(self.data.copy())
# self.gender.currentText() 获取输入的文本
self.data['gender'] = self.gender.currentText()
self.update_ui(get_time_str())
def on_reset_button_clicked(self):
self.backups = []
self.data = {
'name': '张三', 'age': '18', 'id_number': '123456789012345678', 'gender': '男'
}
self.update_ui(get_time_str())
def on_restore_button_clicked(self):
if len(self.backups) > 0:
self.data = self.backups.pop(-1)
self.update_ui(get_time_str())
def update_ui(self, time_str: str):
# 刷新 UI 数据
print('update_ui @ {}'.format(time_str), self.data)
self.name.setText(self.data['name'])
self.age.setValue(int(self.data['age']))
self.id_number.setText(self.data['id_number'])
self.gender.setCurrentText(self.data['gender'])
self.unique_id_label.setText(gen_unique_id(self.data))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyMainWindowUI()
window.show()
app.exec()
在给出的代码片段中,不使用MVC模式的实现存在以下代码风格上的问题:
MyMainWindowUI
类中。这使得MyMainWindowUI
类的职责不清晰,既要处理界面显示,又要处理数据和用户输入,这使得代码难以理解和维护MyMainWindowUI
类的实例变量self.data
和self.backups
中,这使得数据与界面显示紧密耦合。当需要修改数据结构或处理逻辑时,可能需要同时修改界面显示的代码,增加出错的风险on_name_input
、on_id_number_input
等方法)直接修改self.data
,这使得数据处理逻辑分散在各个方法中,这降低了代码的可读性和可维护性。update_ui
方法负责刷新界面显示,但它也直接访问和操作self.data
。这使得界面显示与数据处理逻辑紧密耦合,降低了代码的可读性和可维护性self.data
、self.backups
以及update_ui
方法同时由于逻辑耦合,在编码时也很容易引入一些逻辑问题:
from __future__ import annotations
import json
from collections import UserDict
class MyDict(UserDict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __setitem__(self, key, value):
# print(f'{key} = {value}')
super().__setitem__(key, value)
if __name__ == '__main__':
# 创建字典实例并初始化
my_dict = MyDict({'a': 1, 'b': 2, 'c': 3})
print(my_dict['a'], my_dict['b'], my_dict['c'])
my_dict['d'] = 4
print(my_dict['d'])
print(my_dict.items())
print(json.dumps(my_dict.items(), indent = 4, sort_keys = True, ensure_ascii = False))
# 拷贝完整的 dict
tmp_dict = my_dict.copy()
print('tmp_dict:', tmp_dict)
# 将备份的 dict 清空,不影响原来的 dict
tmp_dict.clear()
print(tmp_dict, my_dict)
# 更新数据
my_dict.update({'a': -1, 'b': -2, 'c': -3, 'd': -4, 'e': 10, 'f': 20, 'g': 30})
print(my_dict)
# 清空数据
my_dict.clear()
print(my_dict)
UserDict 是一个类,而不是直接使用 C 语言实现的内置 dict,因此在某些情况下,UserDict 的性能可能不如内置 dict。
UserDict 对象通常比标准 dict 占用更多的内存,因为它包含额外的开销,用于存储类的元数据和可能的额外方法。
UserDict
允许用户重载以下方法:
__init__(self, *args, **kwargs)
: 构造函数,用于初始化字典。
__getitem__(self, key)
: 获取指定键的值。
__setitem__(self, key, value)
: 设置指定键的值。
__delitem__(self, key)
: 删除指定键及其对应的值。
__iter__(self)
: 返回一个迭代器,用于遍历字典的键。
__len__(self)
: 返回字典中键值对的数量。
clear(self)
: 清空字典中的所有键值对。
copy(self)
: 创建并返回字典的一个浅拷贝。
fromkeys(self, iterable, value=None)
: 创建一个新字典,其中包含来自可迭代对象的键,以及可选的默认值。
get(self, key, default=None)
: 获取指定键的值,如果键不存在,则返回默认值。
items(self)
: 返回一个视图对象,表示字典中的键值对。
keys(self)
: 返回一个视图对象,表示字典中的键。
pop(self, key, default=None)
: 删除并返回指定键的值,如果键不存在,则返回默认值。
popitem(self)
: 删除并返回字典中的最后一个键值对。
setdefault(self, key, value=None)
: 设置指定键的值,如果键不存在,则插入键并设置默认值。
update(self, *args, **kwargs)
: 更新字典,将另一个字典或键值对的序列合并到当前字典中。
请注意,这些方法中的许多方法在 UserDict
中都有默认实现,但你可以根据需要重载它们以实现自定义行为。
from __future__ import annotations
import hashlib
import sys
from collections import UserDict
from datetime import datetime
from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QApplication, QComboBox, QFormLayout, QLabel, QLineEdit, QMainWindow, QPushButton, QSpinBox, \
QVBoxLayout, QWidget
class DataModelSignal(QObject):
"""
在类级别定义 data_changed 信号(而不是在 __init__ 方法中)是因为所有的 DataModelSignal 实例都应该能够发出这个信号
而且这个信号的类型(在这个例子中是 str)在所有实例之间都是相同的
如果我们在 __init__ 方法中定义 data_changed
那么每个实例都会有自己的 data_changed 信号,这不仅浪费内存,也可能导致错误,因为信号的连接可能会丢失
"""
data_changed = Signal(str)
class DataModel(UserDict):
def __init__(self):
# 将 self.signals 和 self.backups 放在 super().__init__() 之前有一个优势:
# 确保在调用父类的 __init__() 方法之前,这两个变量已经被初始化。
self.signals = DataModelSignal()
self.backups = []
super().__init__({'name': '张三', 'age': '18', 'id_number': '123456789012345678', 'gender': '男'})
def update_data(self, key, value):
"""
更新指定的 key-value 数据对
"""
if (key not in self.keys()) or (self.get(key) != value):
self.append_backup()
super().__setitem__(key, str(value))
self.signals.data_changed.emit(get_time_str())
def append_backup(self):
if len(self.backups) > 10:
self.backups.pop(0)
self.backups.append(self.copy())
def restore_backup(self):
"""
恢复到上一次备份的数据
"""
if len(self.backups) <= 0:
return
tmp_data = self.backups.pop(-1)
for key, value in tmp_data.items():
super().__setitem__(key, value)
self.signals.data_changed.emit(get_time_str())
def clear(self):
"""
清空所有数据
"""
super().clear()
self.backups = []
super().__setitem__('name', '张三')
super().__setitem__('age', '18')
super().__setitem__('id_number', '123456789012345678')
super().__setitem__('gender', '男')
self.signals.data_changed.emit(get_time_str())
def get_time_str() -> str:
return datetime.now().isoformat()
def gen_unique_id(data: DataModel) -> str:
"""
生成唯一 ID
"""
data_str = data['name'] + data['age'] + data['id_number'] + data['gender']
return hashlib.sha3_256(data_str.encode('utf-8')).hexdigest()
class MyMainWindowUI(QMainWindow):
"""
UI 界面布局
"""
def __init__(self):
super().__init__()
self.setWindowTitle('Hello, MVC Pattern')
self.setToolTip('A PySide6 GUI Application Demo')
self.data = DataModel()
self.data.signals.data_changed.connect(self.update_ui)
self.name = QLineEdit(parent = self)
self.name.setPlaceholderText('输入姓名')
self.name.returnPressed.connect(self.on_name_input)
self.name.textEdited.connect(self.on_name_input)
self.name.textChanged.connect(self.on_name_input)
self.age = QSpinBox(parent = self)
self.age.setRange(7, 80)
self.age.setValue(18)
self.age.setSingleStep(1)
self.age.valueChanged.connect(self.on_age_input)
self.id_number = QLineEdit(parent = self)
self.id_number.setPlaceholderText('输入身份证号')
self.id_number.returnPressed.connect(self.on_id_number_input)
self.id_number.textEdited.connect(self.on_id_number_input)
self.id_number.textChanged.connect(self.on_id_number_input)
self.gender = QComboBox(parent = self)
self.gender.addItems(['男', '女'])
self.gender.currentIndexChanged.connect(self.on_gender_input)
self.input_view_layout = QFormLayout()
self.input_view_layout.addRow('姓名', self.name)
self.input_view_layout.addRow('年龄', self.age)
self.input_view_layout.addRow('身份证号', self.id_number)
self.input_view_layout.addRow('性别', self.gender)
self.unique_id_label = QLabel(parent = self)
self.reset_button = QPushButton('重置数据', parent = self)
self.reset_button.clicked.connect(self.on_reset_button_clicked)
self.restore_button = QPushButton('恢复到上一次', parent = self)
self.restore_button.clicked.connect(self.on_restore_button_clicked)
self.v_layout = QVBoxLayout()
self.v_layout.addLayout(self.input_view_layout)
self.v_layout.addWidget(self.unique_id_label)
self.v_layout.addWidget(self.reset_button)
self.v_layout.addWidget(self.restore_button)
container = QWidget(self)
container.setLayout(self.v_layout)
self.setCentralWidget(container)
# 初始化刷新
self.update_ui(get_time_str())
def on_name_input(self):
# self.name.text() 获取输入的文本
if len(self.name.text()) > 5 or (not self.name.text().isalpha() and not self.name.text().isascii()):
self.name.setStyleSheet('background-color: red')
elif 0 < len(self.name.text()) <= 5:
self.name.setStyleSheet('background-color: green')
self.data.update_data('name', self.name.text())
def on_id_number_input(self):
# self.id_number.text() 获取输入的文本
if len(self.id_number.text()) > 18 or (not self.id_number.text().isdigit()):
self.id_number.setStyleSheet('background-color: red')
elif 0 < len(self.id_number.text()) <= 18:
self.id_number.setStyleSheet('background-color: green')
self.data.update_data('id_number', self.id_number.text())
def on_age_input(self):
# self.age.value() 获取输入的文本
self.data.update_data('age', str(self.age.value()))
def on_gender_input(self):
# self.gender.currentText() 获取输入的文本
self.data.update_data('gender', self.gender.currentText())
def on_reset_button_clicked(self):
self.data.clear()
def on_restore_button_clicked(self):
self.data.restore_backup()
def update_ui(self, time_str: str):
# 刷新 UI 数据
print('update_ui @ {}'.format(time_str), self.data)
self.name.setText(self.data['name'])
self.age.setValue(int(self.data['age']))
self.id_number.setText(self.data['id_number'])
self.gender.setCurrentText(self.data['gender'])
self.unique_id_label.setText(gen_unique_id(self.data))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyMainWindowUI()
window.show()
app.exec()
这段代码在这些方面有了提升:
但是仍然有以下的缺陷:
from __future__ import annotations
import hashlib
import sys
from collections import UserDict
from datetime import datetime
from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QApplication, QComboBox, QFormLayout, QLabel, QLineEdit, QMainWindow, QPushButton, QSpinBox, \
QVBoxLayout, QWidget
class DataModelSignal(QObject):
"""
在类级别定义 data_changed 信号(而不是在 __init__ 方法中)是因为所有的 DataModelSignal 实例都应该能够发出这个信号
而且这个信号的类型(在这个例子中是 str)在所有实例之间都是相同的
如果我们在 __init__ 方法中定义 data_changed
那么每个实例都会有自己的 data_changed 信号,这不仅浪费内存,也可能导致错误,因为信号的连接可能会丢失
"""
data_changed = Signal(str)
class DataModel(UserDict):
def __init__(self):
# 将 self.signals 和 self.backups 放在 super().__init__() 之前有一个优势:
# 确保在调用父类的 __init__() 方法之前,这两个变量已经被初始化。
self.signals = DataModelSignal()
self.backups = []
super().__init__({'name': '张三', 'age': '18', 'id_number': '123456789012345678', 'gender': '男'})
def update_data(self, key, value):
"""
更新指定的 key-value 数据对
"""
if (key not in self.keys()) or (self.get(key) != value):
self.append_backup()
super().__setitem__(key, str(value))
self.signals.data_changed.emit(get_time_str())
def append_backup(self):
if len(self.backups) > 10:
self.backups.pop(0)
self.backups.append(self.copy())
def restore_backup(self):
"""
恢复到上一次备份的数据
"""
if len(self.backups) <= 0:
return
tmp_data = self.backups.pop(-1)
for key, value in tmp_data.items():
super().__setitem__(key, value)
self.signals.data_changed.emit(get_time_str())
def clear(self):
"""
清空所有数据
"""
super().clear()
self.backups = []
super().__setitem__('name', '张三')
super().__setitem__('age', '18')
super().__setitem__('id_number', '123456789012345678')
super().__setitem__('gender', '男')
self.signals.data_changed.emit(get_time_str())
def get_time_str() -> str:
return datetime.now().isoformat()
def gen_unique_id(data: DataModel) -> str:
"""
生成唯一 ID
"""
data_str = data['name'] + data['age'] + data['id_number'] + data['gender']
return hashlib.sha3_256(data_str.encode('utf-8')).hexdigest()
class WindowDataController(QObject):
def __init__(self, view_obj: MyMainWindowUI, model: DataModel):
super().__init__()
# 视图层
self.view = view_obj
# 数据模型
self.model = model
# 数据模型逻辑控制设置
self._setup_model_controller_()
# 视图层逻辑控制设置
self._setup_view_controller_()
# 初始化刷新
self._update_ui_(get_time_str())
def _setup_model_controller_(self):
# 数据变更控制逻辑
self.model.signals.data_changed.connect(self._update_ui_)
def _setup_view_controller_(self):
# 姓名输入逻辑控制
self.view.name.returnPressed.connect(self._on_name_input_)
self.view.name.textEdited.connect(self._on_name_input_)
self.view.name.textChanged.connect(self._on_name_input_)
# 年龄输入逻辑控制
self.view.age.valueChanged.connect(self._on_age_input_)
# 身份证号输入逻辑控制
self.view.id_number.returnPressed.connect(self._on_id_number_input_)
self.view.id_number.textEdited.connect(self._on_id_number_input_)
self.view.id_number.textChanged.connect(self._on_id_number_input_)
# 性别输入逻辑控制
self.view.gender.currentIndexChanged.connect(self._on_gender_input_)
# 重置和恢复按钮逻辑控制
self.view.reset_button.clicked.connect(self._on_reset_button_clicked_)
self.view.restore_button.clicked.connect(self._on_restore_button_clicked_)
def _on_name_input_(self):
# self.name.text() 获取输入的文本
if len(self.view.name.text()) > 5 or (not self.view.name.text().isalpha() and not self.view.name.text().isascii()):
self.view.name.setStyleSheet('background-color: red')
elif 0 < len(self.view.name.text()) <= 5:
self.view.name.setStyleSheet('background-color: green')
self.model.update_data('name', self.view.name.text())
def _on_id_number_input_(self):
# self.id_number.text() 获取输入的文本
if len(self.view.id_number.text()) > 18 or (not self.view.id_number.text().isdigit()):
self.view.id_number.setStyleSheet('background-color: red')
elif 0 < len(self.view.id_number.text()) <= 18:
self.view.id_number.setStyleSheet('background-color: green')
self.model.update_data('id_number', self.view.id_number.text())
def _on_age_input_(self):
# self.age.value() 获取输入的文本
self.model.update_data('age', str(self.view.age.value()))
def _on_gender_input_(self):
# self.gender.currentText() 获取输入的文本
self.model.update_data('gender', self.view.gender.currentText())
def _on_reset_button_clicked_(self):
self.model.clear()
def _on_restore_button_clicked_(self):
self.model.restore_backup()
def _update_ui_(self, time_str: str):
# 刷新 UI 数据
print('update_ui @ {}'.format(time_str), self.model)
self.view.name.setText(self.model['name'])
self.view.age.setValue(int(self.model['age']))
self.view.id_number.setText(self.model['id_number'])
self.view.gender.setCurrentText(self.model['gender'])
self.view.unique_id_label.setText(gen_unique_id(self.model))
def app_view_run(self):
self.view.show()
class MyMainWindowUI(QMainWindow):
"""
UI 界面布局
"""
def __init__(self):
super().__init__()
self.setWindowTitle('Hello, MVC Pattern')
self.setToolTip('A PySide6 GUI Application Demo')
self._setup_ui_()
def _setup_ui_(self):
self.name = QLineEdit(parent = self)
self.name.setPlaceholderText('输入姓名')
self.age = QSpinBox(parent = self)
self.age.setRange(7, 80)
self.age.setValue(18)
self.age.setSingleStep(1)
self.id_number = QLineEdit(parent = self)
self.id_number.setPlaceholderText('输入身份证号')
self.gender = QComboBox(parent = self)
self.gender.addItems(['男', '女'])
self.input_view_layout = QFormLayout()
self.input_view_layout.addRow('姓名', self.name)
self.input_view_layout.addRow('年龄', self.age)
self.input_view_layout.addRow('身份证号', self.id_number)
self.input_view_layout.addRow('性别', self.gender)
self.unique_id_label = QLabel(parent = self)
self.reset_button = QPushButton('重置数据', parent = self)
self.restore_button = QPushButton('恢复到上一次', parent = self)
self.v_layout = QVBoxLayout()
self.v_layout.addLayout(self.input_view_layout)
self.v_layout.addWidget(self.unique_id_label)
self.v_layout.addWidget(self.reset_button)
self.v_layout.addWidget(self.restore_button)
container = QWidget(self)
container.setLayout(self.v_layout)
self.setCentralWidget(container)
if __name__ == "__main__":
app = QApplication(sys.argv)
controller = WindowDataController(MyMainWindowUI(), DataModel())
controller.app_view_run()
app.exec()
使用MVC设计模式后,代码的结构变得更加清晰和模块化:
其主要的代码层次为:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。