Вы находитесь на странице: 1из 12

Программирование по системе

“Модель-Представление”
Руководство по расширяемой архитектуре “модель-представление” в PyQt.

Введение в архитектуру “Модель-


Представление”
PyQt содержит несколько классов представлений, использующих архитектуру
“модель-представление” для соединения данных и способа их показа
пользователю. Эта архитектура показывает, как можно разделять
функциональность частей программы, чтобы обеспечить разработчикам
возможность гибко настраивать представление различных данных, и
предоставляет стандартный интерфейс моделей, позволяющий использовать
различные источниики данных вместе с уже существующими классами
представлений. Данная статья — краткое введение в парадигму моделей и
представлений, очерчивающее основные используемые концепции и
описывающее архитектуру системы представлений для элементов данных.

Архитектура “Модель-представление”
Модель-представление-контроллер (Model-View-Controller, MVC) — паттерн
проектирования, ведущий свою историю от языка Smalltalk, который часто
используется для построения пользовательских интерфейсов. В книге “Паттерны
проектирования” (Design Patterns, Gamma et. al.) написано следующее:

MVC состоит из трёх видов объектов:

Модель — объект приложения.


Представление — отображение приложения на экране.
Контроллер — реакция на действия пользователя.

До появления MVC архитектура пользовательского интерфейса часто


«склеивала» эти объекты вместе, MVC же разделила их для гибкости и
повторного использования кода.

Представление и контроллер могут быть объединены в рамках одного объекта — в


результате и получится архитектура “модель-представление”. Она, аналогично
MVC, разделяет хранение данных и способ их представления пользователю,
однако, её основа проще, хоть и основана на всё тех же основных принципах.
Такое разделение позволяет достаточно просто отображать одни и те же данные
по-разному, и добавлять новые способы отображения без изменения самих
структур отображаемых данных. Для гибкой обработки пользовательского ввода
мы введём концепцию делегатов, которые позволяют легко настраивать способ
отображения и редактирования (в этом важное отличие MV от MVC).

Архитектура “Модель-представление”

Модель обменивается информацией с источником данных и предоставляет


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

В общем, классы архитектуры “Модель-представление” могут быть разделены на


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

Модели, представления и делегаты общаются между собой с помощью сигналов и


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

Модели
Все модели основываются на абстрактном классе  QAbstractItemModel ,
определяющем интерфейс, используемый представлениями и делегатами для
доступа к данным. Модель сама по себе не хранит данные; они могут находиться
как в некоторой структуре данных или хранилище, предоставляемом другим
классом, так и в базе данных, файле или в другом компоненте приложения.

Базовые концепции, связанные с моделями, представлены в секции Классы


моделей.

 QAbstractItemModel  предоставляет для доступа к данным интерфейс, достаточно


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

Процесс расширения моделей обсуждается в секции Создание новых моделей.

PyQt предоставляет некоторые готовые модели, подходящие для работы с


элементами данных:

 StringListModel  хранит простой список строк ( str ).


 QStandardItemModel  управляет более сложными древоподобными
структурами элементов, каждый из которых может содержать произвольные
данные.
 QFileSystemModel  предоставляет информацию о файлах и каталогах
локальной файловой системы.
 QSqlQueryModel ,  QSqlTableModel  и  QSqlRelationalTableModel  используются
для доступа к базам данных с соблюдением соглашения “модель-
представление”.
Если данные стандартные классы не соответствуют Вашим требованиям, Вы
можете расширить  QAbstractItemModel ,  QAbstractListModel  или
 QAbstractTableModel  для создания собственных произвольных моделей.

Представления
Полноценные реализации предоставлены для различных видов представлений:

 QListView  отображает список элементов.


 QTableView  отображает данные модели в виде таблицы.
 QTreeView  показывает элементы данных, полученных от модели, в виде
иерархического списка.

Каждый из этих классов основан на абстрактном классе  QAbstractItemView .


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

Доступные представления рассматриваются в секции Классы представлений

Сортировка
Существуют два подхода к сортировке в архитектуре “модель-представление”,
выбор между которыми основан на модели, лежащей в основе.

В случае, если модель сортируемая (т.е. переопределяет функцию  sort() ), и


 QTableView , и  QTreeView  предоставляют API, позволяющий программно
отсортировать данные модели. Кроме того, можно включить интерактивную
сортировку, т.е. позволить пользователям сортировать данные, нажимая на
заголовки представления), соединив сигнал  sortIndicatorChanged()  класса
 QHeaderView  со слотом  sortByColumn()  класса  QTableView  или  QTreeView .

Альтернативный подход может применяться в случае, если модель не


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

Вспомогательные классы
На основе стандартных классов представлений создано несколько
вспомогательных классов для использования в приложениях, опирающихся на
элементо-ориентированные классы представлений PyQt. Данные классы не
предназначены для расширения.

Вспомогательными классами являются, в частности,  QListWidget ,  QTreeWidget  и


 QTableWidget .

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


вместе с произвольными моделями. Мы рекомендуем использовать подход
“модель-представление” для обработки данных, если только у Вас нет строгой
необходимости использовать классы, ориентированные на элементы данных.

Для того, чтобы использовать преимущества архитектуры “модель-


представление”, используйте классы представлений, такие как  QListView ,
 QTableView  и  QTreeView , с  QStandardItemModel .

Использование моделей и представлений


В данном разделе объясняется, как использовать архитектуру “модель-
представление” в PyQt. Каждая секция раздела включает пример и пояснение, как
создавать новые компоненты.

Две модели, включённые в PyQt


Две стандартные модели, поставляемые с PyQt —  QStandardItemModel  и
 QFileSystemModel .

 QStandardItemModel  — многофункциональная модель, которую можно


использовать для представления различных структур данных вместе с
представлениями для списков, таблиц и деревьев. Эта модель также хранит
элементы данных.

 QFileSystemModel  предоставляет готовую к использованию модель и может быть


легко настроена для использования с существующими данными. С помощью этой
модели можно показать, как настроить модель для использования вместе с
готовыми представлениями, и изучить управление данными с помощью индексов
модели.

Использование представлений с существующей


моделью
Классы представлений  QListView  и  QTreeView  лучше всего подходят для
использования вместе с  QFileSystemModel . На данной картинке показано, как
содержимое одного и того же каталога отображается с помощью древоподобного
представления и с помощью представления для списка.
Используем  QFileSystemModel , уже готовый к использованию, и создадим
представления для отображения содержимого каталога. Такой способ
использования модели является наиболее простым. Создание и использование
модели выполняется в единственной функции —  main() .

def main():
app = QApplication(sys.argv)
splitter = QSplitter()
model = QFileSystemModel()
model.setRootPath(os.getcwd())

Модель настроена использовать данные из определённой файловой системы.


Вызов метода  setRootPath()  сообщает модели, какой путь файловой системы
использовать для передачи представлениям.

Создадим два представления, чтобы осмотреть элементы, содержащиеся в


модели, двумя различными способами.

tree = QTreeView(splitter)
tree.setModel(model)
tree.setRootIndex(model.index(os.getcwd()))

list = QListView(splitter)
list.setModel(model)
list.setRootIndex(model.index(os.getcwd()))

Представления создаются так же, как и обычные виджеты. Задание представления


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

Метод  index()  уникален для  QFileSystemModel ; он принимает директорию и


возвращает индекс модели. Индексы моделей обсуждаются в секции Классы
моделей.
Оставшаяся часть функции настраивает  splitter  и запускает цикл событий
приложения.

splitter.setWindowTitle("Two views onto the same directory")


splitter.show()
return app.exec_()

В примере мы пренебрегли упоминанием обработки выбора элементов. Данная


тема детально описывается в секции Обработка выбора в представлениях
элементов.

Полный код примера:

import os
import sys

from PyQt5.QtWidgets import QApplication, QSplitter, QFileSystemModel,


QTreeView, QListView

def main():
app = QApplication(sys.argv)
splitter = QSplitter()
splitter.setWindowTitle("Two views onto the same directory")

model = QFileSystemModel()
model.setRootPath(os.getcwd())

tree_view = QTreeView(splitter)
tree_view.setModel(model)
tree_view.setRootIndex(model.index(os.getcwd()))

list_view = QListView(splitter)
list_view.setModel(model)
list_view.setRootIndex(model.index(os.getcwd()))

splitter.show()

return app.exec_()

if __name__ == '__main__':
main()

Классы моделей
Прежде чем рассматривать вопрос обработки выделения элементов, Вам может
оказаться полезным обратиться к концепциям, используемым в фреймворке
«модель-представление».

Базовые концепции
В архитектуре «модель-представление» модель предоставлеяет стандартный
интерфейс, используемый представлениями и делегатами для доступа к данным.
В PyQt стандарт для такого интерфейса определяется классом
 QAbstractItemModel . Вне зависимости от способа хранения данных в структуре
данных, на которой основана модель, все классы-наследники  QAbstractItemModel 
представляют данные как иерархическую структуру, содержащую таблицы
элементов. Представления используют эту конвенцию для доступа к данным
модели, но они не ограничены в способах представления полученных данных
пользователю.

Модели также уведомляют все прикреплённые к ним представления об


изменениях данных с помощью механизма сигналов и слотов.

Данная секция описывает некоторые основные концепции, лежащие в основе того,


как компоненты программы получают доступ к данным с помощью классов
моделей. Расширения этой схемы обсуждаются в следующих секциях.

Индексы моделей
Чтобы проиллюстрировать, что представление данных отделено от способа
доступа к ним, введём концепцию индексов модели. Каждый фрагмент
информации, который может быть получен с помощью модели, отображается с
помощью индекса модели. Представления и делегаты используют эти индексы для
запросов на отображение элементов данных.

В результате, только модели требуется знать о способе получения данных, и тип


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

model = index.model()

Индексы модели предоставляют временные ссылки на фрагменты информации и


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

Временные индексы моделей предоставляются классом  QModelIndex , хранимые


— классом  QPersistentModelIndex .

Для получения индекса, соответствующего элементу данных, необходимо указать


три свойства этого элемента:

номер строки;
номер столбца;
индекс модели для родительского элемента.

Следующая секция описывает и детально объясняет эти свойства.

Строки и столбцы
В простейшем случае модель может быть представлена как простая таблица, в
которой элементы расположены в соответствии с номерами их строк и столбцов.
Из этого не следует, что соответствующие фрагменты данных хранятся в
массиве (в смысле структуры данных); использование строк и столбцов является
просто договорённостью для обеспечения взаимодействия компонентов
программы друг с другом. Мы можем получить информацию о любом данном
элементе, просто указав модели на его номера строки и столбца, также мы можем
получить индекс, представляющий данный элемент:
index = model.index(row, column, ...)

Модели, предоставляющие к простым одноуровневым структурам данных,


например, спискам и таблицам, не требуют дополнительной информации, однако,
как показывает приведённый выше фрагмент кода, нам необходимо передать
больше информации при получении индекса модели.

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

indexA = model.index(0, 0, QModelIndex())


indexB = model.index(1, 1, QModelIndex())
indexC = model.index(2, 1, QModelIndex())

Элементы верхнего уровня в модели всегда получаются с указанием


 QModelIndex()  как их родительского элемента. Это подробнее обсуждается в
следующем разделе.

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

При запросе индекса элемента модели, нам необходимо предоставить некоторую


информацию о родителе элемента. Единственный способ обратиться к элементу,
находясь при этом за пределами модели — с помощью индекса модели,
следовательно, требуется передать индекс родительского элемента:

index = model.index(row, column, parent)

Родительское элементы, строки и столбцы


Диаграмма показывает модели дерева, в которой к каждому элементу можно
обратиться по его родительскому элементу, номеру строки и номеру столбца.
Элементы “A” и “C” представляются потомками элемента верхнего уровня модели:

indexA = model.index(0, 0, QModelIndex())


indexC = model.index(2, 1, QModelIndex())

Элемент “A” имеет нескольких элементов-потомков. Индекс для элемента модели


“B” (потомка элемента “A”) можно получить с использованием следующего кода:

indexB = model.index(1, 0, indexA)

Роли элементов
Элементы модели могут играть различные роли для других компонентов
программы, позволяя в различных ситуациях предоставлять данные различных
типов. Например,  DisplayRole  используется при необходимости получить доступ
к строке, которая может отображаться представлением как текст. Обычно
элементы содержат данные для нескольких ролей, стандартные роли же
определяется  ItemDataRole .

Можно запросить у модели данные её элемента, передав в неё индекс,


соответствующий требуемому элементу, и роль, чтобы указать интересующий нас
тип данных.

value = model.data(index, role)

Роли элементов
Роль показывает модели, к какому типу данных обращаются. Представления могут
отображать роли различными способами, поэтому важно предоставлять
достаточную информацию о каждой роли. В секции Создание новых моделей
особенности использования ролей описаны более детально.
Наиболее частые способы использования элементов данных покрываются
стандартными ролями, определёнными в  ItemDataRole . Передавая подходящие
элементы данных для каждой роли, модели могут предоставлять
представлениями и делегатам подсказки о том, как элементы требуется
отображать пользователю. Различные типы представлений могут как использовать
эту информацию (так, как это требуется представлению), так и игнорировать её.
Также, если это требуется в конкретном приложении, можно определить
собственные дополнительные роли.

Итоги раздела
Индексы моделей дают представлениям и делегатам информацию о
местонахождении элементов модели, причём эта информация независима от
конкретной организации данных внутри модели.
К элементам модели можно обратиться по сочетанию их номера столбца,
номера строки и индексу их родительского элемента.
Индексы создаются моделями по запросу других компонентов, таких как
представления и делегаты.
Если при запросе индекса в  index()  передан валидный индекс элемента-
родителя, то полученный индекс указывает на элемент, находящийся в модели
под данным родительским элементом, т.е. полученный индекс указывает на
потомка элемента, индекс которого передаётся.
Если же переданный индекс не валиден, то полученный индекс указывает на
элемент верхнего уровня модели.
Роль позволяет различать различные типы данных, связанные с конкретным
элементом модели.