PySide6 GUI 编程(39):MVC 设计原则的简单探索

2024-09-01 11:52:19 浏览数 (3)

MVC设计原则

在MVC(Model-View-Controller)模式中,Model负责处理数据和业务逻辑,View负责显示用户界面,Controller负责处理用户输入并更新Model和View。

而在之前所有的文章和示例代码中,Model、View、Controller 三者基本都是混为一体的,都是基于 PySide6 的基本组件自身的能力来实现的。

一个具体的例子

界面效果

功能界面功能界面

对于上述的功能界面,用户可以输入姓名、年龄、身份证号和选择性别,用户输入的信息会被用来生成一个唯一ID,我们希望实现如下的效果:

  • 用户可以在输入框中输入姓名。如果输入的姓名长度超过5个字符或包含非字母字符,则输入框背景变为红色;否则,背景变为绿色
  • 用户可以使用数字选择器输入年龄
  • 用户可以在输入框中输入身份证号,如果输入的身份证号长度超过18个字符或包含非数字字符,则输入框背景变为红色;否则,背景变为绿色
  • 用户可以使用下拉框选择性别
  • 根据用户输入的信息,程序会生成一个唯一ID,并在界面上显示
  • 点击“重置数据”按钮,程序会将用户输入的所有信息恢复到默认值
  • 点击“恢复到上一次”按钮,程序会将用户输入的信息恢复到上一次备份的数据

耦合式代码实现

代码语言:python代码运行次数:0复制
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模式的实现存在以下代码风格上的问题:

  1. 数据处理、界面显示和用户输入处理的代码混合在MyMainWindowUI类中。这使得MyMainWindowUI类的职责不清晰,既要处理界面显示,又要处理数据和用户输入,这使得代码难以理解和维护
  2. 数据存储在MyMainWindowUI类的实例变量self.dataself.backups中,这使得数据与界面显示紧密耦合。当需要修改数据结构或处理逻辑时,可能需要同时修改界面显示的代码,增加出错的风险
  3. 用户输入处理的代码(如on_name_inputon_id_number_input等方法)直接修改self.data,这使得数据处理逻辑分散在各个方法中,这降低了代码的可读性和可维护性。
  4. update_ui方法负责刷新界面显示,但它也直接访问和操作self.data。这使得界面显示与数据处理逻辑紧密耦合,降低了代码的可读性和可维护性
  5. 当需要扩展功能或修改数据处理逻辑时,可能需要同时修改多个方法和实例变量,这增加了出错的风险。例如,如果需要添加一个新的数据字段,可能需要修改self.dataself.backups以及update_ui方法

同时由于逻辑耦合,在编码时也很容易引入一些逻辑问题:

  • 数据恢复逻辑:在on_restore_button_clicked方法中,从self.backups中取出最近一次的备份数据进行恢复,但是在各个输入事件(如on_name_input、on_age_input等)中,又将恢复后的数据添加到了self.backups中,这会导致self.backups中出现重复的数据,正确的逻辑应该是在恢复数据后,不再将这份数据添加到备份中
  • 数据备份逻辑:在各个输入事件中,在任何数据变更前都将当前数据添加到了self.backups中,这意味着即使数据没有发生实际的改变,也会创建一个新的备份。正确的逻辑应该是只有在数据实际发生改变时,才创建一个新的备份
  • 数据验证逻辑:在on_name_input和on_id_number_input方法中,在任何情况下都会调用update_ui方法。这意味着即使输入的数据无效(如姓名长度超过5个字符),界面也会被刷新,正确的逻辑应该是只有在数据有效时,才刷新界面 ......

代码解耦改造

定义数据模型

先认识下UserDict

代码语言:python代码运行次数:0复制
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 中都有默认实现,但你可以根据需要重载它们以实现自定义行为。

基于 UserDict 抽象出数据模型 Model

代码语言:python代码运行次数:0复制
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()

这段代码在这些方面有了提升:

  • 数据处理逻辑封装在DataModel类中:这个类负责管理数据和备份,提供了更新数据、恢复备份和清空数据的方法。将数据处理逻辑封装在一个类中,使得数据处理的代码更易于管理和维护。
  • 使用了信号和槽来处理数据变化:当数据发生变化时,DataModel会发出data_changed信号,MyMainWindowUI会接收到这个信号并更新界面。这使得数据变化和界面更新之间的关系更加清晰,降低了出错的风险。
  • 在DataModel类中使用了UserDict:这使得DataModel可以像字典一样使用,同时还可以添加自定义的方法和属性。

但是仍然有以下的缺陷:

  • 数据验证逻辑仍然分散在MyMainWindowUI类中:例如,在on_name_input和on_id_number_input方法中,你对输入的数据进行了验证。这使得数据验证的逻辑分散在多个地方,不易于管理和维护。可以考虑将数据验证的逻辑封装在DataModel类中,使得数据处理的代码更加集中。
  • DataModel类中的append_backup方法没有检查备份的数据是否与当前数据相同:这可能导致self.backups中出现重复的数据。可以在添加备份之前,检查备份的数据是否与当前数据相同。
  • DataModel类中的clear方法直接使用了super().setitem来设置数据,而没有调用update_data方法:这使得在清空数据时,不会创建备份。可以考虑在清空数据时,也调用update_data方法,以创建备份。

使代码更贴近 MVC 原则

代码语言:python代码运行次数:0复制
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设计模式后,代码的结构变得更加清晰和模块化:

  • 数据处理和业务逻辑被封装在DataModel类(Model层)中。这个类负责管理数据,以及处理与数据相关的业务逻辑。将数据处理逻辑与界面显示和用户输入处理分离,使得代码更容易理解和维护。
  • 界面显示被封装在MyMainWindowUI类(View层)中。这个类负责创建和显示用户界面,并不直接处理数据。将界面显示与数据处理和用户输入处理分离,使得界面的修改和扩展变得更加容易,同时降低了出错的风险。
  • 用户输入处理被封装在WindowDataController类(Controller层)中。这个类负责处理用户输入,并根据用户输入更新Model和View。将用户输入处理与数据处理和界面显示分离,使得代码更加模块化,便于修改和扩展。

其主要的代码层次为:

  • Model层(DataModel类): DataModel类继承自UserDict,使得它具有字典的基本功能,同时可以添加自定义的方法和属性。 DataModel类负责管理数据和备份,提供了更新数据、恢复备份和清空数据的方法。这使得数据处理逻辑集中在一个地方,有利于代码的管理和维护。 使用DataModelSignal类定义了一个data_changed信号,当数据发生变化时,DataModel会发出这个信号。这使得数据变化和界面更新之间的关系更加清晰,降低了出错的风险。
  • View层(MyMainWindowUI类): MyMainWindowUI类负责创建和显示用户界面,包括输入框、选择器、按钮等。 MyMainWindowUI类通过update_ui方法刷新界面,当接收到DataModel发出的data_changed信号时,会调用这个方法。这使得界面显示与数据处理逻辑分离,降低了出错的风险。 通过信号和槽的机制,将用户输入事件与对应的处理方法进行关联,如self.name.returnPressed.connect(self.on_name_input)。
  • Controller层(WindowDataController类中的事件处理方法): MyMainWindowUI类中的事件处理方法(如on_name_input、on_id_number_input等)负责处理用户输入,并根据用户输入更新DataModel。 事件处理方法中对用户输入的数据进行了验证,如检查姓名长度是否超过5个字符,身份证号长度是否超过18个字符等。这有助于确保数据的有效性。 事件处理方法通过调用DataModel类的update_data方法更新数据。这使得用户输入处理与数据处理逻辑分离,降低了出错的风险。

运行效果

UI 交互运行效果UI 交互运行效果

0 人点赞