Дизайн MVC с Qt Designer и PyQt/PySide

Новичок Python из Java (+ SWT/Windowbuilder), и мне трудно понять, как правильно закодировать большое настольное приложение в Python/Qt4 (QtDesigner)/PySide.

Я хотел бы сохранить любую логику представления в классе контроллера вне файла .ui(и это преобразование .py). Во-первых, как и тогда, логика не зависит от структуры GUI, а во-вторых, поскольку .ui и полученный .py файл могут быть перезаписаны при любых изменениях!

Только примеры, которые я нашел, добавляют код действия в монолитный MainWindow.py(сгенерированный из ui) или MyForm.py(также созданный с .ui). Я не вижу никакого способа связать класс контроллера POPO с действиями в QtDesigner.

Может ли кто-нибудь указать мне на рабочие процессы для создания крупномасштабного приложения с использованием QtDesigner в масштабируемой методологии MVC/P?

Ответ 1

Во-первых, просто знайте, что Qt уже использует концепцию представлений и моделей, но на самом деле это не то, что вам нужно. Короче говоря, это способ автоматически связать виджет (например, QListView) с источником данных (например, QStringListModel), чтобы изменения в данных в модели автоматически появлялись в виджете и наоборот. Это полезная функция, но она отличается от дизайна MVC масштаба приложения, хотя эти два можно использовать вместе, и они предлагают некоторые очевидные сокращения. Однако применение шкалы MVC должно быть запрограммировано вручную.

Вот пример приложения MVC, которое имеет один вид, контроллер и модель. В представлении есть 3 виджета, каждый из которых независимо прослушивает и реагирует на изменения данных в модели. Вращающийся блок и кнопка могут манипулировать данными в модели через контроллер.

mvc_app

Структура файла устроена так:

project/
    mvc_app.py              # main application with App class
    mvc_app_rc.py           # auto-generated resources file (using pyrcc.exe or equivalent)
    controllers/
        main_ctrl.py        # main controller with MainController class
        other_ctrl.py
    model/
        model.py            # model with Model class
    resources/
        mvc_app.qrc         # Qt resources file
        main_view.ui        # Qt designer files
        other_view.ui
        img/
            icon.png
    views/
        main_view.py        # main view with MainView class
        main_view_ui.py     # auto-generated ui file (using pyuic.exe or equivalent)
        other_view.py
        other_view_ui.py

заявка

mvc_app.py будет отвечать за создание экземпляров каждого из представлений, контроллеров и моделей и передачу ссылок между ними. Это может быть довольно минимальным:

import sys
from PyQt5.QtWidgets import QApplication
from model.model import Model
from controllers.main_ctrl import MainController
from views.main_view import MainView


class App(QApplication):
    def __init__(self, sys_argv):
        super(App, self).__init__(sys_argv)
        self.model = Model()
        self.main_controller = MainController(self.model)
        self.main_view = MainView(self.model, self.main_controller)
        self.main_view.show()


if __name__ == '__main__':
    app = App(sys.argv)
    sys.exit(app.exec_())

Просмотры

Используйте Qt designer для создания файлов макета .ui в той степени, в которой вы назначаете имена переменных для виджетов и настраиваете их основные свойства. Не беспокойтесь о добавлении сигналов или слотов, так как обычно проще просто подключить их к функциям из класса представления.

Файлы макетов .ui преобразуются в файлы макетов .py при обработке с помощью Pyuic или Pyside-UIC. Файлы представления .py затем могут импортировать соответствующие автоматически сгенерированные классы из файлов макета .py.

Класс представления должен содержать минимальный код, необходимый для подключения к сигналам, поступающим от виджетов в вашем макете. События представления могут вызывать и передавать основную информацию методу в классе представления и методу в классе контроллера, где должна быть любая логика. Это будет выглядеть примерно так:

from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtCore import pyqtSlot
from views.main_view_ui import Ui_MainWindow


class MainView(QMainWindow):
    def __init__(self, model, main_controller):
        super().__init__()

        self._model = model
        self._main_controller = main_controller
        self._ui = Ui_MainWindow()
        self._ui.setupUi(self)

        # connect widgets to controller
        self._ui.spinBox_amount.valueChanged.connect(self._main_controller.change_amount)
        self._ui.pushButton_reset.clicked.connect(lambda: self._main_controller.change_amount(0))

        # listen for model event signals
        self._model.amount_changed.connect(self.on_amount_changed)
        self._model.even_odd_changed.connect(self.on_even_odd_changed)
        self._model.enable_reset_changed.connect(self.on_enable_reset_changed)

        # set a default value
        self._main_controller.change_amount(42)

    @pyqtSlot(int)
    def on_amount_changed(self, value):
        self._ui.spinBox_amount.setValue(value)

    @pyqtSlot(str)
    def on_even_odd_changed(self, value):
        self._ui.label_even_odd.setText(value)

    @pyqtSlot(bool)
    def on_enable_reset_changed(self, value):
        self._ui.pushButton_reset.setEnabled(value)

Представление не делает ничего, кроме событий связывания виджета с соответствующей функцией контроллера, и отслеживает изменения в модели, которые передаются в виде сигналов Qt.

Контроллеры

Класс контроллера выполняет любую логику и затем устанавливает данные в модели. Пример:

from PyQt5.QtCore import QObject, pyqtSlot


class MainController(QObject):
    def __init__(self, model):
        super().__init__()

        self._model = model

    @pyqtSlot(int)
    def change_amount(self, value):
        self._model.amount = value

        # calculate even or odd
        self._model.even_odd = 'odd' if value % 2 else 'even'

        # calculate button enabled state
        self._model.enable_reset = True if value else False

Функция change_amount берет новое значение из виджета, выполняет логику и устанавливает атрибуты в модели.

модель

Класс модели хранит программные данные и состояние и некоторую минимальную логику для объявления изменений в этих данных. Эту модель не следует путать с моделью Qt (см. Http://qt-project.org/doc/qt-4.8/model-view-programming.html), поскольку в действительности это не одно и то же.

Модель может выглядеть так:

from PyQt5.QtCore import QObject, pyqtSignal


class Model(QObject):
    amount_changed = pyqtSignal(int)
    even_odd_changed = pyqtSignal(str)
    enable_reset_changed = pyqtSignal(bool)

    @property
    def amount(self):
        return self._amount

    @amount.setter
    def amount(self, value):
        self._amount = value
        self.amount_changed.emit(value)

    @property
    def even_odd(self):
        return self._even_odd

    @even_odd.setter
    def even_odd(self, value):
        self._even_odd = value
        self.even_odd_changed.emit(value)

    @property
    def enable_reset(self):
        return self._enable_reset

    @enable_reset.setter
    def enable_reset(self, value):
        self._enable_reset = value
        self.enable_reset_changed.emit(value)

    def __init__(self):
        super().__init__()

        self._amount = 0
        self._even_odd = ''
        self._enable_reset = False

Пишет модель, автоматически испускает сигналы на любые прослушиваемые виды с помощью кода в setter функции setter. В качестве альтернативы контроллер может вручную инициировать сигнал всякий раз, когда он решает.

В случае, когда типы моделей Qt (например, QStringListModel) были связаны с виджетом, тогда представление, содержащее этот виджет, вообще не нуждается в обновлении; это происходит автоматически через инфраструктуру Qt.

Исходный файл пользовательского интерфейса

Для завершения, пример файла main_view.ui включен здесь:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>93</width>
    <height>86</height>
   </rect>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout">
    <item>
     <widget class="QSpinBox" name="spinBox_amount"/>
    </item>
    <item>
     <widget class="QLabel" name="label_even_odd"/>
    </item>
    <item>
     <widget class="QPushButton" name="pushButton_reset">
      <property name="enabled">
       <bool>false</bool>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

Он конвертируется в main_view_ui.py путем вызова:

pyuic5 main_view.ui -o ..\views\main_view_ui.py

Файл ресурсов mvc_app.qrc преобразуется в mvc_app_rc.py путем вызова:

pyrcc5 mvc_app.qrc -o ..\mvc_app_rc.py

Интересные ссылки

Почему Qt неправильно использует терминологию модель/представление?