Diseño MVC con Qt Designer y PyQt / PySide

El novato de Python viene de Java (+ SWT / Windowbuilder) y tengo dificultades para averiguar cómo codificar correctamente una aplicación de escritorio grande en Python / Qt4 (QtDesigner) / PySide.

Me gustaría mantener cualquier lógica de vista en una clase de controlador fuera del archivo .ui (y su conversión de .py). En primer lugar, como entonces, la lógica es independiente del marco de la GUI y, en segundo lugar, el archivo .py y el archivo .py resultante se sobrescriben con cualquier cambio.

Solo los ejemplos que he encontrado agregan código de acción a un MainWindow.py monolítico (generado desde la interfaz de usuario) o MyForm.py (también generado desde .ui). No puedo ver ninguna forma de vincular una clase de controlador POPO a acciones en QtDesigner.

¿Puede alguien dirigirme a flujos de trabajo para crear una aplicación a gran escala utilizando QtDesigner en una metodología MVC / P escalable?

En primer lugar, tenga en cuenta que Qt ya utiliza el concepto de vistas y modelos, pero eso no es realmente lo que está buscando. En resumen, es una forma de vincular automáticamente un widget (por ejemplo, un QListView) a una fuente de datos (por ejemplo, un QStringListModel) para que los cambios a los datos en el modelo aparezcan automáticamente en el widget y viceversa. Esta es una característica útil, pero es algo diferente al diseño de MVC a escala de aplicación, aunque los dos se pueden usar juntos y ofrece algunos atajos obvios. Sin embargo, el diseño de MVC a escala de aplicación debe progtwigrse manualmente.

Aquí hay un ejemplo de aplicación MVC que tiene una sola vista, controlador y modelo. La vista tiene 3 widgets que cada uno escucha y reactjs de forma independiente a los cambios en los datos del modelo. El cuadro de giro y el botón pueden manipular los datos en el modelo a través del controlador.

mvc_app

La estructura del archivo se organiza así:

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 

Solicitud

mvc_app.py sería responsable de crear una instancia de cada vista, controladores y modelo (s) y de pasar las referencias entre ellos. Esto puede ser bastante mínimo:

 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_ctrl) self.main_view.show() if __name__ == '__main__': app = App(sys.argv) sys.exit(app.exec_()) 

Puntos de vista

Utilice el diseñador Qt para crear los archivos de diseño .ui en la medida en que asigne nombres de variables a los widgets y ajuste sus propiedades básicas. No se moleste en agregar señales o ranuras, ya que generalmente es más fácil simplemente conectarlas a funciones desde la clase de vista.

Los archivos de diseño .ui se convierten en archivos de diseño .py cuando se procesan con pyuic o pyside-uic. Los archivos de vista .py pueden luego importar las clases relevantes generadas automáticamente desde los archivos de diseño .py.

Las clases de vista deben contener el código mínimo requerido para conectarse a las señales provenientes de los widgets en su diseño. Los eventos de vista pueden llamar y pasar información básica a un método en la clase de vista y a un método en una clase de controlador, donde debería haber cualquier lógica. Se vería algo así como:

 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) 

La vista no hace mucho más que vincular eventos de widgets con la función del controlador relevante, y escuchar los cambios en el modelo, que se emiten como señales Qt.

Controladores

La (s) clase (s) de controlador realiza cualquier lógica y luego establece los datos en el modelo. Un ejemplo:

 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 

La función change_amount toma el nuevo valor del widget, realiza la lógica y establece atributos en el modelo.

Modelo

La clase modelo almacena los datos y el estado del progtwig y cierta lógica mínima para anunciar cambios en estos datos. Este modelo no debe confundirse con el modelo Qt ( consulte http://qt-project.org/doc/qt-4.8/model-view-programming.html ) ya que no es realmente lo mismo.

El modelo podría verse como:

 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 

Las escrituras en el modelo emiten automáticamente señales a cualquier vista de audición a través del código en las funciones decoradas del setter . Alternativamente, el controlador podría disparar manualmente la señal cuando así lo decida.

En el caso de que los tipos de modelos Qt (por ejemplo, QStringListModel) se hayan conectado con un widget, la vista que contiene ese widget no necesita actualizarse en absoluto; esto sucede automáticamente a través del marco Qt.

Archivo fuente UI

Para completar, el archivo main_view.ui ejemplo se incluye aquí:

   MainWindow    0 0 93 86              false          

Se convierte en main_view_ui.py llamando:

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

El archivo de recursos mvc_app.qrc se convierte a mvc_app_rc.py llamando a:

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

Enlaces interesantes

¿Por qué Qt está haciendo mal uso de la terminología del modelo / vista?