В статье подробно разобран пример программы, написанной на языке C++ на основе кроссплатформенного фреймворка Qt. Программа InterView написана программистами компании Qt, и входит в состав примеров, поставляемых вместе с пакетом Qt Creator. На её примере показаны: приёмы разработки на основе технологии модель/представление, создание файловой директории, возможность подключать различные формы отображения к одному массиву данных и другие средства библиотеки Qt. Кроме формального пояснения кода, в статье разъяснены основы архитектуры Qt, касающиеся тех программных средств, которые присутствуют в коде. Для удобства восприятия материала статья включает интуитивно понятные диаграммы UML, которые позволяют разобраться в нюансах программы, а также помогают воспринять структуру программы в целом.
Ключевые слова: класс, директория, данные, представление, Qt, диаграмма, InterView
The article details the example of a program written in C ++ based on the Qt cross-platform framework. The InterView program is written by the Qt programmers, and is included in the examples provided with the Qt Creator package. Its example shows: development techniques based on the model / view technology, creating a file directory, the ability to connect different display formats to one data set and other Qt library tools. In addition to the formal explanation of the code, the article explains the basics of the Qt architecture regarding those software tools that are present in the code. For the convenience of the material, the article includes intuitive UML diagrams that allow you to understand the nuances of the program, and also help to perceive the structure of the program as a whole.
Keywords: class, directory, data, view, Qt, diagram, InterView
Приложение InterView выводит на экран изображение файловой директории в трех различных видах: в виде таблицы, в виде дерева, в виде списка (рис. 1). В табличном представлении и представлении в виде дерева активировано выделение по строке и столбцу.
Рис. 1. Виды файловой директории в приложении InterView.
Описание программы InterView
Входящая в список стандартных примеров, поставляемых вместе с пакетом Qt Creator, программа InterView предназначена для демонстрации методов отображения структуры файловой директории при помощи архитектуры модель/представление. Приложение InterView генерирует массив данных, который имитирует файловую директорию и отображает сгенерированные данные одновременно в трех различных вариантах (рис. 1). Концептуальная модель (рис. 2) отражает общую структуру программы. Предоставляемый объектом модели интерфейс, при помощи механизма сигналов и слотов, обеспечивает доступ к данным трем различным представлениям, каждое из которых отображает данные в определенной форме. Все формы скомпонованы вместе в одном окне, и между ними действует один и тот же объект выделения, что позволяет, выделяя элемент данных в одной из форм, находить элементы тех же данных в других формах.
Рис. 2. Концептуальная модель программы.
Описание диаграммы классов
На диаграмме классов (рис. 3) изображена модель программы с точки зрения реализации.
Рис. 3. Диаграмма классов.
На ней изображены используемые в программе классы и взаимодействия между ними, для некоторых классов указаны их ответственности. Классы, изображенные в прямоугольниках с жирными линиями, — основные элементы программы, которые встречаются в тексте файла main.cpp. Классы, обозначенные тонкими линиями, в частности, те из них, на которые указывают стрелки в виде замкнутых треугольников, используются в качестве предков при наследовании и участвуют в программе неявно. Другие классы, обозначенные тонкими линиями, являются основой для создания объектов, которые являются составными элементами классов файла main.cpp, что показывает наличие ромбиков на стрелках, указывающих на класс, составной частью которого они являются.
Классы, унаследованные от QWidget, отвечают за создание визуальных элементов приложения. На основе классов QTableView, QThreeView, QListView создаются объекты, ответственные за отображение данных в виде таблицы, дерева и списка. Класс QHeaderView отвечает за создание заголовков и модификацию их свойств, объекты этого класса являются атрибутами классов QTableView и QThreeView.
Линии, связывающие QHeaderView и классы QTableView и QThreeView, начинаются закрашенными ромбиками, поскольку заголовки являются составной частью представлений, создаваемых объектами этих классов. Табличное представление имеет два набора заголовков: горизонтальный и вертикальный, а представление в виде дерева — только вертикальный. Заголовки являются неотъемлемой составной частью представлений, создаваемых на экране объектами классов QTableView и QThreeView, а объекты заголовков являются атрибутами этих классов.
Класс QSplitter выполняет функции компоновки представлений, созданных классами QTableView, QThreeView, QListView, в окне приложения. Этот класс унаследован от QWidget, также как классы, реализующие визуальные элементы приложения. Здесь могут появиться сомнения в том, верно ли относить компоновщик к классам, объекты которых создают визуальные элементы приложения, поскольку он только изменяет свойства (расположение) уже имеющихся элементов, а не создает новый. Эта особенность объясняется тем, что в Qt присутствуют, и широко распространены в разработке, классы компоновки, не связанные с QWidget — это ряд классов, в качестве базового для которых, выступает QLayout. В программе InterView возможно заменить класс компоновщика потомком класса QLayout, переписав несколько строк кода. Преимущество класса QSplitter состоит в возможности изменять ширину отдельных представлений при помощи перетаскивания разделителя, находящегося между представлениями.
Класс QItemSelectionModel отвечает за создание модели выделения, которая хранит индексы выбранных элементов. В представлениях (QTableView, QThreeView и QListView) уже есть встроенные модели выделения, однако для того, чтобы во всех представлениях всегда были выделены одни и те же элементы, необходима общая модель выделения. Оповещение всех представлений об изменении выбранного элемента происходит с помощью механизма сигналов и слотов. Механизм сигналов и слотов реализован в QObject, который выступает для QItemSelectionModel в качестве предка.
Архитектура модель/представление
На основе классов QTableView, QThreeView, QListView создаются объекты, ответственные за отображение на экране данных в виде таблицы, дерева и списка. Это стандартные классы, которые являются частью архитектуры модель/представление (рис. 4). Другой частью архитектуры модель/представление является класс Model, являющийся наследником стандартного класса QAbstractItemModel. Объект, созданный на его основе, реализует интерфейс, посредством которого объекты классов QTableView, QThreeView, QListView получают доступ к данным.
Рис. 4. Архитектура модель / представление
Связь представлений с моделью происходит посредством механизма сигналов и слотов, содержащегося в классе QObject. В файле main.cpp методом setModel() происходит соединение объектов представлений с моделью, при этом с помощью метода connection(), принадлежащего классу QObject, происходит соединение сигналов, обозначающих, что представлению требуется доступ к данным объектов представлений со слотами модели. В терминологии описания интерфейсов такие сигналы называют запросами. Отправка запросов инициируется возникновением определенных событий.
Характерной особенностью архитектуры, обеспечивающей гибкость применения, является то, что форма демонстрации данных может быть полностью отличной от структуры основного хранилища данных. Это обеспечивается интерфейсом моделей. Отделение представления от способа доступа к данным осуществляется за счет возможности реализации функции Index(), предназначенной для создания индексов, через которые происходит доступ к данным. Отделение от структуры данных за счет возможности реализации функции data(), которая используется для получения данных из модели.
При создании модели на основе стандартных абстрактных классов необходимо переопределить три виртуальные функции: rowCount(), columnCount() и data(), поскольку во всех стандартных абстрактных классах моделей, предлагаемых библиотекой Qt, они являются чистыми (пустыми). Среди форм отображения данных, программы InterView, есть представление в виде дерева, для которого необходим более сложный способ обеспечения данными, чем предлагаемый классами QAbstractTableModel и QAbstractListModel. Поэтому в качестве базового класса модели в InterView используется QAbsractItemModel, для которого необходимо реализовать функции index() и parent().
В стандартных одноуровневых представлениях отдельные элементы нумеруются по строке и столбцу. В таких формах отображения как дерево, каждый элемент может хранить другой набор элементов, которые также нумеруются по строке и столбцу. Чтобы отличать элементы разных уровней друг от друга, кроме номеров строки и столбца необходимо также знать элемент, который находится на уровень выше в древовидной структуре, для этого каждому элементу присваивается уникальный индекс.
Описание модели
В отличие от других классов программы, Model создается пользователем. Основная трудоёмкость написания программы, подобной InterView, ложится на написание этого класса.
Обычно, при разработке на основе технологии модель/представление, данные хранятся вне модели, а модель оперирует указателями на данные. Но, поскольку программа InterView — является только примером, данные содержатся внутри объекта модели.
Хранилище данных состоит из двух элементов. Первый из них — контейнер QVector. Вектора удобно использовать совместно с архитектурой модель/представление, поскольку и там, и там для доступа к данным используются индексы. Второй элемент хранилища требуется из-за необходимости сложной организации структуры данных, требуемой для обеспечения данными такой формы отображения, как дерево. В качестве дерева использована структура Node. С её помощью генерируется массив данных, имитирующий файловую директорию.
Элементы хранилища взаимодействуют следующим образом: контейнер хранит массив структур, каждая структура содержит указатель на контейнер более низкого уровня (потомка) и на родительскую структуру. Для генерирования данных структура Node включает такие механизмы как конструктор и деструктор. Конструктор создает контейнер QWector с размерностью, равной количеству строк и инициализирует его объектами структуры Node.
Функция index() создает объекты класса QModelIndex, которые связывают между собой элементы отображения и элементы данных. В качестве операндов функция принимает значения строки, столбца и ссылку на индекс родительского элемента. Если элемент отображения является элементом верхнего уровня, в качестве родительского элемента передается недействительный модельный индекс. Первый условный оператор в теле функции index() проверяет, не выходят ли значения строки и столбца за пределы допустимых. С помощью операции internalPointer(), из объекта индекса извлекается указатель на родительскую структуру Node. Затем, вызовом функции node(), указателю childNode присваивается адрес нужного элемента данных. Функция node(), получающая на вход номер строки и указатель на родительский элемент, проверяет, существуют ли у этого родительского элемента потомки, и, в противном случае, создаёт, с помощью операции new, новый контейнер, инициализированный элементами типа Node. Адрес нового вектора присваивается переменной v, если в контейнер был передан недействительный модельный индекс, то переменной v присваивается адрес контейнера верхнего уровня. Функция at() извлекает из контейнера элемент, соответствующий номеру строки. Наконец, с помощью метода createIndex(), создаётся индекс элемента.
Функция data() предназначена для передачи запрашиваемых объектом отображения данных, которые соответствуют переданному индексу и роли. Программа InterView только имитирует файловую директорию, поэтому данные для каждого элемента отображения формируются из номера столбца и строки. Для расширения возможностей разработчика по оформлению представлений в архитектуре модель/представление для классификации данных введено понятие роли. Роль характеризует назначение данных. Кроме основного типа данных (DisplayRole), предназначенного для отображения, можно поставлять отображению данные для оформления элементов (DecorationRole), для отображения в статус баре, для реализации всплывающей подсказки и другие. В ItemView кроме основной роли, в функции data(), использована также DecorationRole, которая поставляет пиктограммы для обозначения элементов. Объект класса QFileIconProvider обеспечивает представление набором стандартных пиктограмм файловой директории.
В функции hederData(), обеспечивающей представление данными для заголовков, тоже использована DecorationRole, но для предоставления изображений используется не объект класса QFileIconProvider, а объект класса QIcon, изображения для которого загружаются в конструкторе модели.
Также для реализации иерархической модели необходимо переопределить функцию parent(), которая возвращает индекс предка нужного элемента. Так же как в случае функции index(), с помощью метода internalPointer(), из переданного в аргументах индекса извлекается указатель на элемент. Указатель на предка определяется с помощью ещё одной функции parent(), отличие которой в том, что она оперирует указателями на Node. В ней происходит проверка на то, не является ли данная структура элементом верхнего уровня и, в зависимости от этого, возвращается либо индекс предка, либо ноль. Если получено не нулевое значение, то с помощью createIndex(), создается объект индекса, но перед этим необходимо вычислить номер строки, что осуществляется с помощью функции row().
Алгоритм вычисления номера строки следующий:
Сначала определяется элемент ещё более высокого уровня иерархии, то есть относительно самого первого объекта данных, переданного на вход внешней функции parent(), он уже будет дедом;
Из этого элемента извлекается адрес потомка, который будет являться и адресом всего контейнера;
Из контейнера извлекается указатель на элемент первой строки; полученный указатель вычитается из указателя предка, переданного во внутреннюю функцию parent().
Разность указателей и будет равна номеру строки. Если обнаруживается, что элемента ещё более высокого уровня иерархии нет, тогда указатель на элемент первой строки извлекается из переменной three.
Описание main.cpp
В файле main.cpp происходит создание объектов визуальных элементов приложения и объекта модели, настройка свойств визуальных элементов, соединение объектов визуальных элементов с моделью, компоновка визуальных элементов, вывод приложения на экран и запуск цикла обработки событий QCoreApplication::exec(). Поскольку файл main.cpp описывает линейную последовательность действий, его этапы удобно отобразить с помощью диаграммы деятельности (рис. 5). Разделение потока на три параллельных означает, что данные деятельности независимы, и могут выполняться в произвольном порядке.
Рис. 5. Диаграмма деятельности.
Этапы создания отображения в форме таблицы показаны на рис. 6.
Рис. 6. Этапы создания отображения.
Соотношение методов, вызываемых в main.cpp, с классами, к которым они принадлежат, показано на рис. 7.
Рис. 7. Соотношение методов и классов.
Для виджета окна просмотра представлений таблицы и дерева с помощью метода setAttribute() устанавливается значение WA_StaticContents параметра Qt::WidgetAttribute, которое указывает, что содержимое виджета статично и привязано к левому верхнему углу. Установка этого параметра означает, что контент виджета не требует перерисовки при изменении размеров. Исключение лишних событий перерисовки производится в целях оптимизации.
Этапы создания отображения в форме дерева показаны на рис. 8.
Рис. 8. Этапы создания отображения (дерево)
Свойство setUniformRowHeights() устанавливает единое значение высоты всех строк. В качестве значения высоты устанавливается значение высоты первой строки. После применения метода setStretchLastSection() последний столбец будет растягиваться на все свободное пространство в виджете. Атрибут Qt::WA_MacShowFocusRect, устанавливаемый для представления дерева в значение false, указывает, что виджет не выделяется рамкой при фокусе.
Этапы создания отображения в форме списка показаны на рис. 9. Метод setViewMode() устанавливает режим IconMode — элементы располагаются слева направо, в виде значков, и могут перемещаться. Значение переменной selectionMode устанавливает режим выделения.
Рис. 9. Этапы создания отображения (список)
Таким образом, на примере приложения InterView возможно проведение исследования гибкости и масштабируемости каркаса модель/представление с помощью представления бесконечно глубокой структуры данных, используя модель и три разных типа представления.
Литература:
- Шлее М. Qt 5.3 Профессиональное программирование на C++. — СПб.: БХВ-Петербург, 2015.
- Саммерфилд М. Qt. Профессиональное программирование. Разработка кроссплатформенных приложений на С++. — СПб.: Символ-Плюс, 2011.
- Ж. Бланшет, М. Саммерфилд. Qt 4: Программирование GUI на C++. 2-е дополненное издание. — М.: Кудиц-пресс, 2008.
- Чеботарев А. Библиотека Qt 4. Создание прикладных приложений в среде Linux. — М.: Диалектика, 2006.
- Алексеев Е. Р., Злобин Г. Г., Костюк Д. А., Чеснокова О. В., Чмыхало А. С. Программирование на языке C++ в среде Qt Creator. — М.: ALT Linux, 2015.