Реализация паттерна MVC для PyQt

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

0 Введение


MVC – паттерн проектирования позволяющий эффективно разделить модель данных, представление и обработку действий. В состав MVC входят три паттерна проектирования: наблюдатель, стратегия и компоновщик. В статье рассматривается классическая схема 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


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



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

pyuic4.bat …\SimpleMVC\View\MainWindow.ui -o …\SimpleMVC\View\MainWindow.py


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

D:\Projects\PythonProjects\SimpleMVC\View\MainWindow.ui


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

pyrcc4.exe -o …\SimpleMVC\View\MainWindow_rc.py -py3 …\SimpleMVC\View\MainWindow_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() )


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

Исходный код
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 16
  • 0
    так а что нового в статье? Реализация паттерна типовая, как работать с PyQt наверняка можно найти в гугле. Я не критики, а интереса ради. Просто с питоном не знаком, может что-то важное упустил?
    • 0
      так а что нового в статье?

      Возможно, что и ничего.
      Дело в том, что пример реализации классической версии MVC для PyQt я писал по просьбе знакомых, которые не нашли ничего подобного в интернете. Если честно, то не нашёл и я. Именно поэтому решил, что статья, в которой присутствует типовая реализация паттерна и основные этапы процесса создания приложения на PyQt, будет полезна. Следствием такого решения является эта публикация.
    • 0
      Согласно архитектуре MVC представление должно быть наблюдателем


      Это откуда, если не секрет?

      Контроллер реализует паттерн стратегия. Контроллер подключается к представлению для управления его действиями.


      Это откуда, если не секрет? По факту, в приведенном коде контроллер не делает ничего. Вопрос — в каких ситуациях в контроллере будет осмысленный код?
      • +1
        Позволю себе предположить, что взято из приведенной книги. Реализация сразу показалась знакомой, а когда увидел ссылку на книгу все встало на свои места (ссылку на книгу упустил из виду при первом прочтении).
        • 0
          Забавно. И что, авторы книги всерьез полагают, что для GUI прилоежния на фреймворке типа Qt, который сам в состоянии генерировать нотификации о произошедших с ним событиях, нужен контроллер? O_O
          • +1
            в книге примеры не на Qt, а на java.
            • 0
              Если мне не изменяет склероз, то на джаве и AWT, и SWING и даже SWT также генерируют нотификации о действиях пользователя. Роль контроллера в книге все еще непонятна.
              • 0
                тогда Вам проще прочитать книгу. Она очень просто написана, а глава про MVC там и вовсе не большая, вечера хватит. Во всяком случае это будет самый достоверный ответ. К слову, у меня тоже остались некоторые недопонимания его роли, если вдруг действительно решите разобраться, поделитесь соображениями в личку
                • 0
                  Я и не читая могу поделиться — я и так про MVC знаю все или почти все. Мне позиция автора поста и книги интересна, ради более глубокого понимания, так сказать.

                  Так что пишите в личку если какие вопросы, постараюсь рассказать какая по этому поводу актуальная позиция партии.
        • 0
          Согласно архитектуре MVC представление должно быть наблюдателем

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

          По факту, в приведенном коде контроллер не делает ничего.

          …кроме того что согласовывает работу представления и модели.

          Вопрос — в каких ситуациях в контроллере будет осмысленный код?

          Например, когда в зависимости от действий пользователя, будет необходимо как-то изменить представление – заблокировать несколько кнопок допустим.
          • 0
            Например, когда в зависимости от действий пользователя, будет необходимо как-то изменить представление – заблокировать несколько кнопок допустим.


            Если не секрет, а что мешает код изменения представления и написать в этом представлении? Что такого будет делать контроллер, чего не может / не хочет делать представление?
            • +1
              Ничего не мешает. В классе CplusDView (в нашем примере) можно смело обрабатывать эвенты и выполнять все те действия, которые делает контроллер. Мне кажется это несколько нелогичным – представление выполняет не слишком свойственные ему функции. События оно может обрабатывать, но не решать, как вся система должна на них реагировать.
              Так можно дойти до классического кода начинающих программистов Delphi. Думаю, вам знакома ситуация, когда весь код написан в обработчике события клика по кнопке.
              • 0
                Так можно дойти до классического кода начинающих программистов Delphi. Думаю, вам знакома ситуация, когда весь код написан в обработчике события клика по кнопке.


                Знакома конечно. Но также знакома и обратная ситуация — многокилометровые простыни controller которые отправляют сообщения от модели в представление и обратно, сами при этом ничего не делая. Вот и интересно — какой код вы считаете достойным помещения в Controller? Код, блокирующий кнопки на это, увы, не тянет — это не «решение, как вся система должна реагировать».
                • 0
                  Согласен, во всём надо знать меру. Понимание «как лучше» приходит с опытом.

                  Вот и интересно — какой код вы считаете достойным помещения в Controller? Код, блокирующий кнопки на это, увы, не тянет — это не «решение, как вся система должна реагировать».


                  К сожаленью, я не в силах ответить на ваш вопрос. Нужен контекст. Какая-то система, в которой с равным успехом можно применить оба решения. Это будет разбор частного случая.
                  Для данного примера, контроллер действительно не имеет смысла. Я считаю, что он не имеет смысла и для примера, приведённого в книге Эрика Фримена «Паттерны проектирования». Выбрав такой простой пример, я рассчитывал максимально сосредоточиться на деталях реализации MVC под PyQt, а не на разработки идеальной архитектуры программы сложения двух чисел.
        • +1
          Позволю себе предположить, что взято из приведенной книги. Реализация сразу показалась знакомой, а когда увидел ссылку на книгу все встало на свои места (ссылку на книгу упустил из виду при первом прочтении).
          • +1
            пардон за дубль. Браузер подвис :(

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.