Механика Interview Framework

 Содержание

В общих чертах схему стандартного приложения Interview Framework (в том числе, приложения баз данных) можно описать как «модель – делегат – представление»

Мы обычно работаем напрямую с моделями и представлениями, работа делегатов скрыта от нас, хотя они играют очень важную роль. Рассмотрим подробнее, как работает система «модель – делегат – представление». Мы рассматриваем простейший случай, когда объекту класса QTableView требуется заполнить ячейку данными из связанного с этим объектом объекта класса-потомка QAbstractItemModel (все классы моделей, такие как QSqlRelationalTableModel или QDirModel, являются потомками этого класса). Для этого объект QTableView вызывает метод paint() своего объекта-делегата. У каждого объект QTableView есть объект-делегат, даже если вы не создавали его явным образом. По умолчанию это объект класса QItemDelegate или, начиная с Qt 4.4, QStyledItemDelegate. Этот объект вполне справляется с отображением данных модели (если данные могут быть отображены в виде текстовой строки), и умеет редактировать стандартные типы данных, однако с нестандартными ситуациями, особенно когда дело касается редактирования, он справиться не способен. В этом случае нам требуется специальный класс делегата, как, например, QSqlRelationalDelegate. Обычно специальные классы делегатов связаны со специальными моделями (исключение из этого правила представляют делегаты, предназначенные для отдельных столбцов и строк, которые мы рассмотрим ниже). Так, например, QSqlRelationalDelegate связан с моделью QSqlRelationalTableModel. С другими моделями его использовать не следует (да, скорее всего, и не получится).

Когда объекту, графически представляющему таблицу, требуется заполнить ячейку, он вызывает метод paint() объекта-делегата. Помимо прочего, методу paint() передается объект класса QModelIndex. Это еще один очень важный элемент цепочки «модель – делегат – представление». Объект класса QModelIndex содержит информацию, с помощью которой модель может найти требуемую ячейку данных. Поскольку в рамках Interview Framework все данные представляются в виде двумерных таблиц (даже если это линейный список или дерево), объект QModelIndex содержит поля row (строка) и column (столбец). Древовидные структуры отличаются от табличных тем, что их индексы содержат ссылки на индексы родительских, дочерних и «братских» элементов. Но во внутреннем представлении данные этих структур все равно хранятся в виде таблиц. В зависимости от конкретной структуры данных один из этих элементов может не использоваться. Объект-делегат вызывает метод data() объекта-индекса, который, в свою очередь вызывает метод data() объекта-модели. При этом делегат может трансформировать переданный ему индекс, если структура данных в графическом отображении не соответствует их структуре в модели данных.

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

Все методы data() класса-модели возвращают значение типа QVariant. Если стандартный объект-делегат запрашивает содержательные данные модели для отображения, он просто конвертирует это значение в QString и отображает его в соответствии с «подсказками», которые он получил от объекта-модели (если таковые имели место быть).

Объекты, графически представляющие данные модели, будь то таблица, дерево или список, позволяют редактировать существующие данные модели, но у них отсутствуют средства, с помощью которых можно было бы добавлять или удалять элементы. Такие методы есть у классов, реализующих модели данных. Нельзя не заметить, что в результате возникает некоторая раздробленность. Возможность удалять или добавлять элементы модели должна быть реализована там же, где реализована и возможность редактировать эти элементы. Почему же возможности управления элементами моделей отсутствуют у графических виджетов, представляющих данные модели? Дело в том, что управление элементами моделей в значительной степени зависит от контекста приложения, так что разработчики Qt оставили нам в этом вопросе свободу выбора. Самое простое (и разумное), что мы можем сделать – это возложить управление элементами модели на объекты QAction, добавить эти объекты в список actions() виджета, отображающего данные, и затем, добавить в виджет, например, контекстное меню, в котором будут отображены соответствующие действия.

Пусть, например, у нас есть виджет tableView, отображающий данные некоторой табличной модели и есть объект actionAddRow класса QAction, добавляющий строку в модель. Тогда создание контекстного меню для виджета tableView может выглядеть так:

tableView->addAction(actionAddRow);
tableView->setContextMenuPolicy(Qt::ActionsContextMenu); 

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

Говоря о моделях, предназначенных для взаимодействия с базами данных, нельзя не упомянуть параметр EditStrategy, который имеется у класса QSqlTableModel и его потомков, и определяет, в какой момент изменения, внесенные в модель данных, будут передаваться в базу данных. Существует три режима: OnFieldChange, OnRowChange, OnManualSubmit. При выборе первого режима модель данных пытается зафиксировать любые изменения в базе данных сразу после того как она была изменена. Я не пользуюсь этим режимом, и вам не советую. Во-первых, он затрудняет отмену ошибочного ввода данных, поскольку данные сразу же сохраняются на сервере БД. Во-вторых, он может просто не работать, когда пользователь добавляет в таблицу новые строки. Если для ряда полей таблицы установлены ограничения NOT NULL, строку невозможно будет добавить в таблицу до тех пор, пока все эти поля не будут заполнены. Модель же будет пытаться добавить строку после каждого изменения, что будет приводить к сообщениям сервера об ошибке. Еще один аргумент против использования режима OnFieldChange при работе с базами данных заключается в том, что приложению, как правило, желательно проверить корректность введенных данных, прежде чем передавать их на сервер БД. Некоторые проверки могут быть встроены в сами виджеты, с помощью масок ввода и объектов-валидаторов, но иногда перед отправкой данных требуются более сложные проверки.

В режиме OnRowChange модель будет пытаться внести изменения в базу данных после завершения пользователем редактирования данной строки. Момент, когда пользователь завершает редактирование строки, определяется сменой текущего индекса (например, вызовом метода setCurrentIndex() класса QDataWidgetMapper). Этот режим передачи изменений на сервер определенно лучше предыдущего и хорошо подходит для многих ситуаций.

В режиме OnManualSubmit мы сами определяем, когда изменения, внесенные в модель, будут переданы на сервер. Этот режим лучше всего подходит для сложных стратегий редактирования. При этом следует учитывать, что если в работе программы (или в работе сервера, или в соединении между программой и сервером) произойдет сбой, в режиме OnManualSubmit могут быть потеряны большие объемы введенных пользователем данных. В режиме OnRowChange мы рискуем потерять максимум одну строку, а в режиме OnFieldChange – и того меньше – одну ячейку.

Создание собственного делегата

Классы-делегаты Interview Framework ведут свой род от класса QAbstractItemDelegate. Как указывает имя этого класса, QAbstractItemDelegate является абстрактным классом, то есть, формирует базовый интерфейс для всех классов-делегатов, но сам делает не так уж много полезного. Это значит, что как правило, вы не захотите объявлять собственные классы-делегаты непосредственно наследниками QAbstractItemDelegate (если, конечно, вы не хотите воспроизвести всю необходимую функциональность делегата самостоятельно). Гораздо лучше на роль базового класса для специализированных делегатов подходит класс QStyledItemDelegate, который является потомком класса QAbstractItemDelegate и реализует функциональность стандартного делегата.

В соответствии с документацией, класс-делегат, наследующий классу QStyledItemDelegate, должен перекрыть четыре метода последнего: createEditor(), setEditorData(), updateEditorGeometry() и setModelData(). Первый из этих методов выполняет самую "волшебную" операцию – создает редактирующий виджет, тип которого зависит от типа редактируемых данных. Метод setEditorData() участвует во взаимодействии между делегатом и представлением данных, а именно, с помощью этого метода редактирующий виджет заполняется текущими данными, которые необходимо изменить. Метод updateEditorGeometry() позволяет изменить геометрию редактирующего виджета в зависимости от изменений геометрии объекта, представляющего данные. Метод setModelData() передает отредактированные данные объекту-модели.

Вообще говоря, перечисленный в документации список методов, которые необходимо перекрыть, не является ни полным, ни обязательным. Например, не обязательно перекрывать метод updateEditorGeometry(), так как базовый метод класса QStyledItemDelegate хорошо справляется с соответствующей задачей для большинства виджетов. Обратите внимание так же на то, что в перечисленном в документации списке методов, которые следует перекрыть, присутствуют только методы, участвующие в процессе редактирования данных, в то время как делегаты используются и для отображения. Возможно, это объясняется тем, что методы базовых классов-делегатов успешно справляются с отображением данных большинства типов, так что вам, как правило, просто не нужно перекрывать эти методы. Если же вам все-таки требуется делегат с нестандартными функциями отображения данных, вам может понадобиться перекрыть метод paint() базового класса (например, QStyledItemDelegate). Реализация метода paint() может быть довольно сложной, если вы захотите реализовать полное стандартное поведение этого метода (что, в принципе, не обязательно). Тем не менее, даже минимальный метод paint() дает вам большую свободу в управлении параметрами отображения.

void MyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const 
{ 
	QStyleOptionViewItemV4 opt = QStyleOptionViewItemV4(option); 
	initStyleOption(&opt, index); 
	const QWidget *widget = opt.widget;
	QStyle *style = widget ? widget->style() : QApplication::style();
	painter->save(); 
	style->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
	painter->restore();
} 

Класс QStyleOptionViewItemV4 предназначен для описания различных параметров отображения значения в ячейке таблицы. Помимо прочего, этот класс должен содержать строку текста, которая будет выведена. Метод initStyleOption() заполняет объект класса QStyleOptionViewItemV4 значениями, переданными методу paint() в параметре index (шрифт, стиль начертания, параметры кисти и так далее). Помимо прочего, при вызове этого метода свойству text объекта opt присваивается значение текста, который требуется вывести в ячейке графической таблицы. Если вы хотите заменить или модифицировать этот текст, вы можете присвоить этому свойству другое значение, но делать это нужно после вызова initStyleOption().

Сам вывод данных выполняется методом drawControl() объекта style класса QStyle. В данном случае этот метод выполняет стандартный вывод текстового содержимого ячейки. Однако у класса QStyle есть и другие методы (drawItemText(), drawItemPixmap()) применение которых позволяет получить больший контроль над стилем отображения информации. Можно также использовать методы объекта painter, переданного функции paint(). Вот как, например, выполняется вывод текста с помощб метода painter->drawText():

void MyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const 
{
	QString text = index.model()->data(index).toString();
	painter->save();
	QPoint bottomLeft = option.rect.bottomLeft();
	int size = option.rect.height()/2; 
	QFont font = painter->font(); 
	font.setPixelSize(size); 
	painter->setFont(font); 
	painter->setPen(QPen(QColor("black"))); 
	painter->drawText(bottomLeft, text); 
	painter->restore();

Для того чтобы нарисовать что-то в ячейке вывода, надо, как минимум, знать ее геометрию. Геометрию ячейки можно получить с помощью объекта option.rect. Для создания нестандартного вывода данных, приводимых к текстовому типу, можно перекрывать не метод paint(), а метод displayText(), который используется делегатом для получения строки дынных для отображения. Задача этого метода – вернуть строку текста, которую он может преобразовать из переданного ему значения типа QVariant. Очевидно, что этот метод проще в реализации, чем метод panint(). Стоит учитывать, что основное предназначение метода displayText() заключается в том, чтобы перекодировать строку текста, если это требуется. Поэтому метод displayText() вызывается стандртными делегатами и их птомками только дляя  тех ячеек, содержимое которых может быть в принципе представлено в виде строки текста, тогда как метод paint() вызывается для всех ячеек.

Документация Qt подробно описывает, как создать нестандартные редактирующие виджеты для определенных типов данных. Практика показывает, что зачастую важнее другая задача – настройка параметров стандартных виджетов в зависимости от смысла (а не от типа) редактируемых данных. Пусть, например, один из столбцов редактируемой таблицы должен содержать телефонные номера в формате +7 (XXX) XXX-XXXX. Допустим, мы хотим, чтобы при редактировании этого столбца виджет-редактор использовал маску ввода. Тут возникает одна проблема. С точки зрения Interview Framework эти данные имеют тип QString, такой же, как например, данные об имени и фамилии обладателя телефонного номера. Если мы просто прикажем объекту делегату создавать виджет с маской ввода для всех полей типа QString, то результат будет совсем не похож на то, чего мы хотели. В нашем случае нам нужны разные типы виджетов в зависимости от того, в каком столбце расположена редактируемая ячейка. Но и это не все. Нет смысла создавать класс-потомок виджета QLineEdit для того, чтобы реализовать маскированный ввод. Класс QLineEdit уже обладает всем необходимым для этого. Нужно только настроить его соответствующим образом. Это лишь один из примеров, когда нам может потребоваться настраивать параметры виджетов-редакторов в зависимости от того, для каких данных они вызываются.

Указанную проблему можно решить путем создания специализированных делегатов для каждой модели данных. Однако разработчики Qt позаботились о том, чтобы написав код делегата один раз, мы могли использовать его в самых разных моделях. Система Interview Framework позволяет связывать делегаты не только с целыми моделями, но и с отдельными столбцами моделей. Например, для ввода номеров телефонов можно написать специальный делегат PhoneNumberDelegate.

class PhoneNumberDelegate : public QStyledItemDelegate 
{ 
	Q_OBJECT 
public: 
	QWidget * createEditor ( QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index ) const 
	{ 
		QWidget * w = QStyledItemDelegate::createEditor(parent, option, index);
		QLineEdit * lineEdit = qobject_cast<QLineEdit *>(w);
		if (lineEdit) lineEdit->setInputMask("+7 (999) 999-9999"); 
		return w; 
	} 
}; 

Класс PhoneNumberDelegate наследует классу QStyledItemDelegate и перекрывает только один метод – createEditor(). В перекрытом методе сначала вызывается метод базового класса, который создает сам виджет. Затем указатель на QWidget приводится к типу «указатель на QLineEdit». Функция-шаблон qobject_cast<QLineEdit *> выполняет интеллектуальное приведение типов. Если объект, указатель на который передан ему в качестве аргумента, может быть приведен к типу QLineEdit *, функтор возвращает указатель на QLineEdit. В противном случае возвращается 0. нам остается только установить маску телефонных номеров с помощью метода setInputMask(). Теперь мы можем установить сразу два делегаты – один для всей таблицы и второй – специально для столбца телефонных номеров:

ui->tableView->setItemDelegate(new QStyledItemDelegate());
ui->tableView->setItemDelegateForColumn(phoneNumberColumn, new PhoneNumberDelegate()); 

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

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

class MoneyDelegate : public QStyledItemDelegate 
{ 
	Q_OBJECT 
public: 
	QWidget * createEditor ( QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index ) const 
	{ 
		QWidget * w = QStyledItemDelegate::createEditor(parent, option, index);
		QDoubleSpinBox * sb = qobject_cast(w); 
		if (sb) { 
			sb->setDecimals(2); 
			sb->setSuffix(QString::fromUtf8(" руб.")); 
		} 
		return w; 
	} 

	QString displayText ( const QVariant & value, const QLocale & locale ) const 
	{
		QString val = QStyledItemDelegate::displayText(value, locale);
		double d = val.toDouble(); 
		QString s = QString::number(d, 'f', 2); 
		s = s + QString::fromUtf8("\tруб."); 
		return s; 
	}
}; 

Применение этого делегата к столбцу, содержащему значения денежных сумм, изменяет и редактирование, и отображение столбца. Вместо того чтобы перекрыть метод paint(), как было описано выше, мы перекрываем метод displayText(), который возвращает текст для отображения в виде строки QString. Это боле простой способ отформатировать строку, нежели перекрытие метода paint(), а ничего другого нам в нашем делегате и не требуется. Манипуляции со значением double, которые мы выполняем в методе displayText(), необходимы для того чтобы превратить стандартное представление чисел с плавающей точкой, наподобие 7.5e+6 в формат, принятый для представления денежных значений – 7500000.00.

Обратите внимание на то, что помимо параметра value метод displayText() получает параметр locale, класса QLocale. Поскольку источник данных для объекта-модели может передавать текст в различных кодировках, этот текст должен быть преобразован в строку с учетом локали источника, (что он и делает по умолчанию с помощью метода locale.toSting()). В некоторых случаях может потребоваться более сложное перекодирование строки, или создание текстового эквивалента данных, которые, сами по себе, не являются текстовыми. Например, если ячейка модели содержит двоичные данные, в представлении данных их можно заменить текстовыми описаниями. Именно для этих целей разработчики Qt и предоставили нам метод displayText(), что, конечно, не мешает нам использовать его и в других целях.

Главное преимущество делегатов, создаваемых для отдельных столбцов, заключается в их универсальности. Например, если вы используете модели QSqlTableModel или QSqlRelationalTableModel, вы можете применять, соответственно, делегаты QStyledItemDelegate, QSqlRelationalDelegate и им подобные к моделям в целом, и делегаты типа PhoneNumberDelegate и MoneyDelegate к отдельным столбцам этих моделей.

Еще один полезный делегат расширяет возможности раскрывающихся списков реляционных делегатов (таких как QSqlRelationalDelegate). Как вы, конечно, заметили, раскрывающийся список, который реляционные делегаты создают по умолчанию, позволяет только выбирать значения из выпадающего перечня. Это удобно, когда значений в перечне немного. Если же нам приходится выбирать из большого числа элементов, удобнее сделать так, чтобы первые буквы искомого элемента можно было набрать в строке раскрывающегося списка. Все, что нужно для этого сделать – передать значение true методу setEditable() объекта QComboBox. Но для этого нам тоже понадобится делегат.

Мы, конечно, могли бы создать классы-потомки делегатов типа QSqlRelationalDelegate, перекрыть в них методы createEditor(), но такой подход противоречил бы принципу повторного использования кода (что, если мы захотим использовать эту функциональность с каким-то другим делегатом?). Мы пойдем по уже знакомому пути и создадим делегат для отдельного столбца. Но этот делегат будет отличаться от всех рассмотренных выше. В рассмотренных примерах, когда нам требовалось изменить стандартный виджет-редактор, мы создавали свой виджет сами, а затем вносили в него изменения. Виджет QComboBox отличается от других редактирующих виджетов тем, что перед его использованием его нужно настроить в соответствии со структурой модели данных, а о том, как это сделать, «знает» только делегат, предназначенный для работы именно с этой моделью. Тем не менее, мы можем сделать наш делегат EditableComboBoxDelegate универсальным. Для этого достаточно вспомнить, что делегаты для отдельных столбцов назначаются совместно с делегатами, предназначенными для модели в целом. Если столбцу назначен специальный делегат, делегат для модели в целом для этого столбца уже не вызывается. Однако ничто не мешает нам вызвать этот «базовый» делегат из своего делегата. Первый параметр конструктора EditableComboBoxDelegate является указателем на общий делегат модели данных (подразумевается, что этот делегат умеет создавать виджеты-редакторы для всех столбцов модели, в том числе и виджеты типа QComboBox). Когда метод createEditor() делегата EditableComboBoxDelegate вызывается для некоторого столбца таблицы, он, в свою очередь вызывает метод общего делегата для этого же столбца и получает виджет класса QComboBox, настроенный на работу с соответствующей моделью данных. Все, что остается сделать методу createEditor() класса EditableComboBoxDelegate – вызывать метод setEditable() виджета QComboBox с аргументом true.

class EditableComboBoxDelegate : public QStyledItemDelegate 
{
public: 
	EditableComboBoxDelegate(QAbstractItemDelegate * itemDelegate, QObject * parent = 0):QStyledItemDelegate(parent) 
	{
		delegate = itemDelegate;
	} 
	QWidget *createEditor(QWidget *aParent, const QStyleOptionViewItem &option, const QModelIndex &index) const 
	{ 
		QWidget * w = delegate->createEditor(aParent, option, index);
		QComboBox * combo = qobject_cast(w);
		if (combo) { 
			combo->setEditable(true);
		} 
		return w; 
	} 
private: 
	QAbstractItemDelegate * delegate;
}; 

Назначение делегата EditableComboBoxDelegate объекту QTableView может выглядеть так:

ui->tableView->setItemDelegate(new QSqlRelationalDelegate());
ui->tableView->setItemDelegateForColumn(relationalCol, new EditableComboBoxDelegate(ui->tableView->itemDelegate())); 

 Содержание

Понравился контент? Нажми:



© 2011  Андрей Боровский anb@symmetrica.net

На главную