Python / [Из песочницы] Реализация паттерна MVC для PyQt

в 12:42, , рубрики: mvc, patterns, pyqt, python, qt, метки: , , , ,

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

0 Введение

MVC – паттерн проектирования позволяющий эффективно разделить модель данных, представление и обработку действий. В состав MVC входят три паттерна проектирования: наблюдатель, стратегия и <a rel="nofollow" href="http://en.wikipedia.org/wiki/Composite_pattern">компоновщик. В статье рассматривается классическая схема MVC с активной моделью, описанная в книге Эрика Фримена «Паттерны проектирования».

Для дальнейшей работы потребуется:

  • Python 3.2
  • PyQt 4.9.1
  • IDE (я использовал PyCharm 2.0.2)

1 Структура проекта

Назовём проект SimpleMVC. Чтобы задача не казалась такой тривиальной, будем складывать не A и B, а C и D. Проект состоит из четырёх модулей. Модули Model, Controller и View представляют реализацию модели, контроллера и представления, соответственно. В модуле Utility собраны вспомогательные классы. Файл main.pyw предназначен для запуска приложения.

SimpleMVC     Controller         __init__.py         CplusDController.py     Model         __init__.py         CplusDModel.py     Utility         __init__.py         CplusDMeta.py         CplusDObserver.py     View         __init__.py         CplusDView.py         MainWindow.py         MainWindow.ui     main.pyw

2 Создание модели

Модель (в первую очередь!) отвечает за логику приложения. Задача CplusDModel – сложение двух чисел. Файл CplusDModel.py:

class CplusDModel:     """     Класс CplusDModel представляет собой реализацию модели данных.     В модели хранятся значения переменных c, d и их сумма.     Модель предоставляет интерфейс, через который можно работать     с хранимыми значениями.      Модель содержит методы регистрации, удаления и оповещения     наблюдателей.     """     def __init__( self ):         self._mC = 0         self._mD = 0         self._mSum = 0          self._mObservers = [] # список наблюдателей      @property     def c( self ):         return self._mC          @property     def d( self ):         return self._mD          @property      def sum( self ):         return self._mSum      @c.setter     def c( self, value ):         self._mC = value         self._mSum = self._mC + self._mD         self.notifyObservers()      @d.setter     def d( self, value ):         self._mD = value         self._mSum = self._mC + self._mD         self.notifyObservers()      def addObserver( self, inObserver ):         self._mObservers.append( inObserver )      def removeObserver( self, inObserver ):         self._mObservers.remove( inObserver )      def notifyObservers( self ):         for x in self._mObservers:             x.modelIsChanged()

Модель реализует паттерн наблюдатель. Это означает, что класс должен поддерживать функции добавления, удаления и оповещения наблюдателей. При этом модель полностью не зависит от контроллеров и представлений.
Важно чтобы все зарегистрированные наблюдатели реализовывали определённый метод, который будет вызываться моделью при их оповещении (в данном случае, это метод modelIsChanged()). Для этого наблюдатели должны быть потомками абстрактного класса, наследуя который, метод modelIsChanged() обязательно необходимо переопределить. Файл CplusDObserver.py:

from abc import ABCMeta, abstractmethod  class CplusDObserver( metaclass = ABCMeta ):     """     Абстрактный суперкласс для всех наблюдателей.     """     @abstractmethod     def modelIsChanged( self ):         """         Метод который будет вызван у наблюдателя при изменении модели.         """         pass

Безусловно, «очень гибкий питон» позволяет обходиться совсем без абстрактного суперкласса или использовать хитрое исключение NotImplementedError(). На мой взгляд, это может негативно отразиться на архитектуре приложения.
Хотелось бы отметить, что используя PyQt, можно было бы задействовать слот-сигнальную модель. В таком случае, при изменении состояния, моделью будет высылаться сигнал, который смогут получать все присоединённые наблюдатели. Такой подход представляется менее универсальным – возможно в будущем вы захотите использовать другую библиотеку.
В более сложных системах, функционал, реализующий паттерн наблюдатель (регистрацию, удаление и оповещение наблюдателей) можно выделять в отдельный абстрактный класс. В зависимости от архитектуры класс может и не быть абстрактным.

3 Создание представления

Представление является комбинацией графических компонентов и реализует паттерн компоновщик. Для создания представления воспользуемся визуальным редактором Designer, входящим в комплект PyQt. Создадим форму на основе MainWindow. Структура формы:

MainWindow – QMainWindow     CentralWidget – QWidget         lbl_equal – QLabel         lbl_plus – QLabel         le_c – QLineEdit         le_d – QLineEdit         le_result – QLineEdit

Внешний вид формы:

Python / [Из песочницы] Реализация паттерна MVC для PyQt

Для предварительного просмотра формы используйте Ctrl+R. Сохраните файл в каталоге View под именем MainWindow.ui. Теперь сгенерируем на его основе файл MainWindow.py. Для этого необходимо воспользоваться pyuic4.bat (…Python32Libsite-packagesPyQt4). Выполните:

pyuic4.bat …SimpleMVCViewMainWindow.ui -o …SimpleMVCViewMainWindow.py

Вместо «…» необходимо добавить путь к каталогу с проектом. Например, полный путь может выглядеть так:

D:ProjectsPythonProjectsSimpleMVCViewMainWindow.ui

Если в проекте присутствует файл ресурсов, то для его преобразования, нужно воспользоваться pyrcc4.exe (…Python32Libsite-packagesPyQt4). Для Python 3.x необходимо передавать флаг -py3:

pyrcc4.exe -o …SimpleMVCViewMainWindow_rc.py -py3 …SimpleMVCViewMainWindow_rc.qrc

Обратите внимание на «_rc». Дело в том, что если бы к нашей форме будет подключён файл ресурсов, то pyuic4.bat автоматически добавит в конце файла:

import MainWindow_rc

Теперь необходимо создать класса представления. Согласно архитектуре MVC представление должно быть наблюдателем. Файл CplusDView.py:

from PyQt4.QtGui import QMainWindow, QDoubleValidator from PyQt4.QtCore import SIGNAL from Utility.CplusDObserver import CplusDObserver from Utility.CplusDMeta import CplusDMeta from View.MainWindow import Ui_MainWindow  class CplusDView( QMainWindow, CplusDObserver, metaclass = CplusDMeta  ):     """     Класс отвечающий за визуальное представление CplusDModel.     """     def __init__( self, inController, inModel, parent = None ):         """         Конструктор принимает ссылки на модель и контроллер.         """         super( QMainWindow, self ).__init__( parent )         self.mController = inController         self.mModel = inModel          # подключаем визуальное представление         self.ui = Ui_MainWindow()         self.ui.setupUi( self )          # регистрируем представление в качестве наблюдателя         self.mModel.addObserver( self )          # устанавливаем валидаторы полей ввода данных         self.ui.le_c.setValidator( QDoubleValidator() )         self.ui.le_d.setValidator( QDoubleValidator() )          # связываем событие завершения редактирования с методом контроллера         self.connect( self.ui.le_c, SIGNAL( "editingFinished()" ),              self.mController.setC )         self.connect( self.ui.le_d, SIGNAL( "editingFinished()" ),             self.mController.setD )      def modelIsChanged( self ):         """         Метод вызывается при изменении модели.         Запрашивает и отображает значение суммы.         """         sum = str( self.mModel.sum )         self.ui.le_result.setText( sum )

Обратите внимание что представление является потомком двух классов: QMainWindow и CplusDObserver. Оба класса используют разные метаклассы. Следовательно, для класса CplusDView необходимо установить метакласс, который будет наследовать метаклассы, используемые QMainWindow и CplusDObserver. В данном случае это CplusDMeta. Файл CplusDMeta.py:

""" Модуль реализации метакласса, необходимого для работы представления.  pyqtWrapperType - метакласс общий для оконных компонентов Qt. ABCMeta - метакласс для реализации абстрактных суперклассов.  CplusDMeta - метакласс для представления. """  from PyQt4.QtCore import pyqtWrapperType from abc import ABCMeta  class CplusDMeta( pyqtWrapperType, ABCMeta ): pass

4 Создание контроллера

Контроллер реализует паттерн стратегия. Контроллер подключается к представлению для управления его действиями. В данном случае, реализация контроллера весьма тривиальна. Файл CplusDController.py:

from View.CplusDView import CplusDview  class CplusDController():     """     Класс CplusDController представляет реализацию контроллера.     Согласовывает работу представления с моделью.     """     def __init__( self, inModel ):         """         Конструктор принимает ссылку на модель.         Конструктор создаёт и отображает представление.         """         self.mModel = inModel         self.mView = CplusDview( self, self.mModel )          self.mView.show()      def setC( self ):         """         При завершении редактирования поля ввода данных для C,         контроллер изменяет свойство c модели.         """         c = self.mView.ui.le_c.text()         self.mModel.c = float( c )      def setD( self ):         """         При завершении редактирования поля ввода данных для D,         контроллер изменяет свойство d модели.         """         d = self.mView.ui.le_d.text()         self.mModel.d = float( d )

5 Соединение компонентов

Осталось объединить все компоненты и проверить работу программы. Файл main.pyw:

import sys from PyQt4.QtGui import QApplication  from Model.CplusDModel import CplusDModel from Controller.CplusDController import CplusDController  def main():     app = QApplication( sys.argv )      # создаём модель     model = CplusDModel()      # создаём контроллер и передаём ему ссылку на модель     controller = CplusDController( model )      app.exec()  if __name__ == '__main__':     sys.exit(  main() )

Спасибо за внимание!


Исходный код

Автор: vt4a2h

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js