Не раз проскакивали сравнения сложности построения интерфейсов на Qt. В данной статье приведу пример, как можно сделать список в стиле списка модулей FireFox.
Для этого воспользуемся MVC подходом, который реализован в Qt. На выходе получим что-то вроде этого:
Весь процесс разделим на 3 части:
Общие принципы работы с моделями в Qt можно прочитать в документации, там все очень подробно расписано.
Итак, приступим. Первым делом определимся с базовой моделью. Так как список может содержать разделы, то нам необходима простейшая древовидная модель. В базовой комплектации такой не имеется, поэтому реализуем ее сами.
Для этого наследуемся от QAbstractItemModel и реализуем все абстрактные методы (это тот обязательный минимум, который нужен для построения модели).
Также для возможности изменения модели переопределим и метод:
Подробно модель расписывать не буду, полную реализацию можно будет посмотреть приложенных исходниках.
Самая интересная и важная часть находится именно здесь, т.к. этот класс отвечает за то, как наши данные будут выглядеть и взаимодействовать с пользователем.
И так, работа делегата будет заключаться в отображении элемента на вьюшке и в обработке нажатий на него (мы же хотим как у Firefox кнопочки всякие и тд).
Точка входя для этого находится в методе paint:
В зависимости от того, является ли отображаемый элемент заголовком или нет вызываем соответствующие методы. Если все укладывать в один, то получится страшная нечитабельная портянка, да и чем мельче/проще методы тем легче в них ориентироваться.
Тут все предельно просто. Получаем текст заготовка, выводим его с выравниванием по высоте и левому краю. Описание, содержащие количество элементов и другую информацию выравниваем по правому краю.
Для начала ограничим область рисования и установим clipping, чтобы не залезть на соседей случайно.
Вторым шагом сдвигаемся в точку относительно которой будем выводить содержимое:
Вторая строка необходима чтобы учесть высоту информационного сообщения, о нем будет ниже. Далее выводим иконку, название, описание и версию. При выводе расписания так же учтем и то, что текст не должен залазить под кнопки и другие элементы. Поэтому рассчитаем максимальную допустимую ширину и обрежем его, чтобы выглядело более приятно.
Так как хотим чтобы элементы управления выглядели также как в системе, то для отрисовки воспользуемся стилем установленным в приложение и предоставляемыми примитивами.
Начнем с индикатора загрузки:
Основа данного фрагмента опять же была взята из исходников, а именно QProgressBar. Для вывода заполним неободимые значения в структуру QStyleOptionProgressBarV2 (полное описание смотрим в документации). Выставим началльное, макстмальное и текущие значения, а так же текст надписи, которая будет выведена поверх индикатора. После чего все это отправляется на обработку в стиль:
После индикатора приступим к кнопкам.
Каждый элемент списка может находится в разных состояних, в зависимости от которых может предоставлять различные допустимые операции. Для начала составим список тех действий, которые можем выполнить:
Заполним структуру QStyleOptionButton и для каждого элемента из спика действий выполним отрисовку кнопки:
И в конце, если необходимо выведем информационное сообщение.
На этом вывод выполнен, но толку от него, если не научили наши рисованные кнопки нажиматься. Поэтому перейдем к следующей части, обработке нажатий мыши.
Все события о нажатии на элементы нашей вьюшки приходят в метод:
Его то мы и будем переопределять:
Наши кнопки должны реагировать на нажатие, отпускание и наведение на них курсором.
При проверке нажатия на кнопку снова получим список доступных операций, так как порядок при выводе совпадает с тем что получен в этот момент, то легко установить соответствие пробегая по массиву и определяя в квадрат какой кнопки попала точка нажатия. После испускаем соответствующий сигнал с индексом элемента на котором было нажатие. Аналогично поступаем и для ссылки Detail.
Для представления нашей древовидной структуры воспользуемся QTreeView. Так.как нам не нужны отступы выставим соответствующие параметры в конструкторе. Основные операции будут происходить при установке модели, где установим связи между сигналами делегата и слотами модели.
В итоге построив модель, делегат и настроив все связи получим список элементов аля Firefox.
В ссылке ниже сможете скачать исходный код демонстрационного проекта, в котором показана работа списка и различные состояние элементов. Так как в своем проекте использую его для отображения списка доступных обновлений и модулей, то и в пример включил возможность загрузки файлов из сети ( в примере скачивается файл образ FreeBSD 8.2).
Конструктивная критика и замечания приветствуются.
Исходники: ajieks.ru/download/file_list.zip
DISCLAIMER
Приведенный пример не претендует на роль готового компонента, представлен в ознакомительных целях и при использовании требует адаптации. Взят из рабочего проекта, где полностью выполняет поставленные перед ним задачи.
Для этого воспользуемся MVC подходом, который реализован в Qt. На выходе получим что-то вроде этого:
Весь процесс разделим на 3 части:
- создание модели
- создание делегата
- создание представления
Создание модели
Общие принципы работы с моделями в Qt можно прочитать в документации, там все очень подробно расписано.
Итак, приступим. Первым делом определимся с базовой моделью. Так как список может содержать разделы, то нам необходима простейшая древовидная модель. В базовой комплектации такой не имеется, поэтому реализуем ее сами.
Для этого наследуемся от QAbstractItemModel и реализуем все абстрактные методы (это тот обязательный минимум, который нужен для построения модели).
int columnCount ( const QModelIndex & parent = QModelIndex() ) const;
QVariant data ( const QModelIndex & index, int role = Qt::DisplayRole ) const;
QModelIndex index ( int row, int column, const QModelIndex & parent = QModelIndex() ) const;
QModelIndex parent ( const QModelIndex & index ) const;
int rowCount ( const QModelIndex & parent = QModelIndex() ) const;
Qt::ItemFlags flags ( const QModelIndex & index ) const;
Также для возможности изменения модели переопределим и метод:
bool setData ( const QModelIndex & index, const QVariant & value, int role = Qt::EditRole );
Подробно модель расписывать не буду, полную реализацию можно будет посмотреть приложенных исходниках.
Создание делегата
Самая интересная и важная часть находится именно здесь, т.к. этот класс отвечает за то, как наши данные будут выглядеть и взаимодействовать с пользователем.
На данную реализацию меня подтолкнуло то, как была реализована работа с CheckBox у штатного делегата QItemDelegate. Поэтому кроме чтение документации, полезно еще и в исходный код заглядывать, много там встречается полезных решений.
И так, работа делегата будет заключаться в отображении элемента на вьюшке и в обработке нажатий на него (мы же хотим как у Firefox кнопочки всякие и тд).
Отрисовка
Точка входя для этого находится в методе paint:
void QvObjectDelegate::paint( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const
{
painter->save();
if(index.parent().isValid())
{
if(needRestart(index))
{
drawItemBackground(painter, option, index);
paintObjectHeader(painter, option, index);
}
paintObject(painter, option, index);
} else
{
paintHeader(painter, option, index);
}
painter->restore();
painter->save();
painter->setPen(QColor(0xD7, 0xD7, 0xD7));
painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
painter->restore();
}
В зависимости от того, является ли отображаемый элемент заголовком или нет вызываем соответствующие методы. Если все укладывать в один, то получится страшная нечитабельная портянка, да и чем мельче/проще методы тем легче в них ориентироваться.
Отрисовка заголовка раздела
void QvObjectDelegate::paintHeader( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const
{
QPainter &p = *painter;
p.save();
p.setClipRect(option.rect);
p.setPen(QColor(77, 77, 77));
// рисуем текст
QRect tr;
QString name = index.data(Qt::DisplayRole).toString(),
desc = index.data(QvObjectModel::DetailRole).toString();
QFont f = option.font;
f.setPointSize(12);
f.setWeight(QFont::Bold);
QFontMetrics fm(f);
tr = fm.boundingRect(name);
p.setFont(f);
p.drawText(option.rect, Qt::AlignVCenter | Qt::AlignLeft, name);
f = option.font;
f.setWeight(QFont::DemiBold);
p.setFont(f);
p.drawText(option.rect, Qt::AlignVCenter | Qt::AlignRight, desc);
p.restore();
}
Тут все предельно просто. Получаем текст заготовка, выводим его с выравниванием по высоте и левому краю. Описание, содержащие количество элементов и другую информацию выравниваем по правому краю.
Отрисовка тела элемента
void QvObjectDelegate::paintObject(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const
{
QRect tr;
QString name = index.data(Qt::DisplayRole).toString(),
description = index.data(QvObjectModel::DescriptionRole).toString();
QPainter &p = *painter;
p.setClipRect(option.rect);
p.setPen(QColor(210, 210, 210));
p.setBrush(QColor(240, 240, 240));
p.setPen(QColor(77, 77, 77));
p.translate(option.rect.topLeft());
p.translate(0, sizeHint(option, index).height() - ITEM_HEIGHT);
p.translate(OFFSET_H, OFFSET_H);
QImage img = index.data(Qt::DecorationRole).value<QImage>();
if(!img.isNull())
{
p.drawImage(0,0, img);
} else {
p.drawImage(0,0, defaultIcon_);
}
p.translate(ICON_SIZE + OFFSET_H, 0); // отступили от иконки на 10px
// рисуем текст
QFont f = option.font;
f.setPointSize(10);
f.setWeight(QFont::Bold);
QFontMetrics fm(f);
tr = fm.boundingRect(name);
p.setFont(f);
p.drawText(0, tr.height()-5, name);
// рисуем описание
p.setFont(option.font);
fm = QFontMetrics(option.font);
QDate date_ = index.data(QvObjectModel::DateRole).toDate();
int version_ = index.data(QvObjectModel::VersionRole).toInt();
QString versionStr_;
if(!date_.isNull())
{
versionStr_ = date_.toString("dd MMMM yyyy");
} else if(version_ > 1000000000){
int ver_min = 0;
int ver = version_ / 1000000000;
ver_min = version_ % 1000000000;
int major = ver_min / 10000000;
ver_min = ver_min % 10000000;
int minor = ver_min / 100000;
ver_min = ver_min % 100000;
versionStr_ = QCoreApplication::translate("list", "%1.%2.%3.%4", "Version in list")
.arg(ver).arg(major, 2, 10, QLatin1Char('0') )
.arg(minor, 2, 10, QLatin1Char('0')).arg(ver_min);
}
if(!versionStr_.isEmpty())
{
tr = fm.boundingRect(versionStr_);
tr.moveTo(option.rect.width() - ICON_SIZE - 2*OFFSET_H - tr.width() - OFFSET_BUTTON, 0 );
painter->drawText(tr, Qt::TextSingleLine, versionStr_);
}
int maxWidth(option.rect.width() - widthButtonGroup(index) - ICON_SIZE - OFFSET_H - DETAIL_OFFSET );
if(!index.data(QvObjectModel::DetailRole).toString().isEmpty())
{
maxWidth -= fm.boundingRect(QCoreApplication::translate("list", "Detail")).width();
}
description = fm.elidedText(description, Qt::ElideRight, maxWidth);
p.translate(0, ICON_SIZE / 2);
tr = fm.boundingRect(description);
p.drawText(0, tr.height(), description);
if(!index.data(QvObjectModel::DetailRole).toString().isEmpty())
{
paintDetail(painter, option, index);
}
if(index.flags().testFlag(Qt::ItemFlag(QvAbstractListItem::Downloading)))
{
paintObjectProgress(painter, option, index);
}
paintObjectBtn(painter, option, index);
}
Для начала ограничим область рисования и установим clipping, чтобы не залезть на соседей случайно.
Вторым шагом сдвигаемся в точку относительно которой будем выводить содержимое:
p.translate(option.rect.topLeft());
p.translate(0, sizeHint(option, index).height() - ITEM_HEIGHT);
p.translate(OFFSET_H, OFFSET_H);
Вторая строка необходима чтобы учесть высоту информационного сообщения, о нем будет ниже. Далее выводим иконку, название, описание и версию. При выводе расписания так же учтем и то, что текст не должен залазить под кнопки и другие элементы. Поэтому рассчитаем максимальную допустимую ширину и обрежем его, чтобы выглядело более приятно.
Отрисовка кнопок и полосы загрузки
Так как хотим чтобы элементы управления выглядели также как в системе, то для отрисовки воспользуемся стилем установленным в приложение и предоставляемыми примитивами.
Начнем с индикатора загрузки:
void QvObjectDelegate::paintObjectProgress( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const
{
QStyleOptionProgressBarV2 opt;
initStyleProgressOption(&opt, index);
QStyle *style = QApplication::style();
opt.rect.setLeft(option.rect.width() - (CHECK_WIDTH + OFFSET_H) * 2 - ICON_SIZE - OFFSET_BUTTON - buttonRect.width() - OFFSET_BUTTON );
opt.rect.setTop( 4 );
opt.rect.setWidth(CHECK_WIDTH * 2);
opt.rect.setHeight(PROGRESS_HEIGHT);
painter->setPen(QColor(77, 77, 77));
style->drawControl(QStyle::CE_ProgressBar, &opt, painter);
}
void QvObjectDelegate::initStyleProgressOption( QStyleOptionProgressBar *option, const QModelIndex & index ) const
{
int value = index.data(QvObjectModel::ProgressRole).toInt();
if (!option)
return;
option->rect = QRect(100, 100, 100, 100);
option->state |= QStyle::State_Active;
option->state |= QStyle::State_Enabled;
option->state |= QStyle::State_Horizontal;
option->minimum = 0;
option->maximum = 100; //maximum?maximum:100;
option->progress = value;
option->textAlignment = Qt::AlignCenter;
option->textVisible = true;
option->text = QString("%1%").arg(value);
if (QStyleOptionProgressBarV2 *optionV2
= qstyleoption_cast<QStyleOptionProgressBarV2 *>(option)) {
optionV2->orientation = Qt::Horizontal ; // ### Qt 5: use State_Horizontal instead
optionV2->invertedAppearance = false;
optionV2->bottomToTop = true;
}
}
Основа данного фрагмента опять же была взята из исходников, а именно QProgressBar. Для вывода заполним неободимые значения в структуру QStyleOptionProgressBarV2 (полное описание смотрим в документации). Выставим началльное, макстмальное и текущие значения, а так же текст надписи, которая будет выведена поверх индикатора. После чего все это отправляется на обработку в стиль:
style->drawControl(QStyle::CE_ProgressBar, &opt, painter);
После индикатора приступим к кнопкам.
Каждый элемент списка может находится в разных состояних, в зависимости от которых может предоставлять различные допустимые операции. Для начала составим список тех действий, которые можем выполнить:
QVector<QvObjectDelegate::ButtonAction> QvObjectDelegate::getButtons( const QModelIndex &index ) const
{
QVector<ButtonAction> tags_;
if(needRestart(index) )
{
tags_ << baRestart << baCancel;
} else if( index.flags() & Qt::ItemFlag(QvAbstractListItem::Downloading) ) {
tags_ << baCancel;
} else {
bool installed = index.data(QvObjectModel::InstalledRole).toBool(),
enabled = index.data(QvObjectModel::EnabledRole).toBool(),
buildin = index.data(QvObjectModel::BuildInRole).toBool();
if (installed && (index.flags() & Qt::ItemFlag(QvAbstractListItem::hasUpdate)))
{ tags_ << baUpdate;}
if (installed && index.flags() & Qt::ItemFlag(QvAbstractListItem::canBeToggled))
{ tags_ << (enabled ? baDisable : baEnable);}
if (installed && !buildin)
{ tags_ << baRemove;}
if (!installed)
{ tags_ << baInstall; }
}
return tags_;
}
Заполним структуру QStyleOptionButton и для каждого элемента из спика действий выполним отрисовку кнопки:
void QvObjectDelegate::drawButton( QStyleOptionButton &o, const QPoint &p, QPainter * painter ) const
{
if(o.rect.contains(p))
o.state |= QStyle::State_Sunken;
QStyle * style = QApplication::style();
if(style)
style->drawControl(QStyle::CE_PushButton, &o, painter );
o.state &= ~QStyle::State_Sunken;
o.rect.translate(buttonRect.width() + OFFSET_BUTTON, 0);
}
И в конце, если необходимо выведем информационное сообщение.
На этом вывод выполнен, но толку от него, если не научили наши рисованные кнопки нажиматься. Поэтому перейдем к следующей части, обработке нажатий мыши.
Обработка нажатий
Все события о нажатии на элементы нашей вьюшки приходят в метод:
bool editorEvent( QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index );
Его то мы и будем переопределять:
Q_ASSERT(event);
Q_ASSERT(model);
// make sure that we have the right event type
if ((event->type() == QEvent::MouseButtonRelease)
|| (event->type() == QEvent::MouseMove)
|| (event->type() == QEvent::MouseButtonPress))
{
return validateLabel(index, option, event) || validateButton(index, option, event);
} else if (event->type() == QEvent::KeyPress) {
if (static_cast<QKeyEvent*>(event)->key() != Qt::Key_Space
&& static_cast<QKeyEvent*>(event)->key() != Qt::Key_Select)
return false;
} else {
return false;
}
return false;
Наши кнопки должны реагировать на нажатие, отпускание и наведение на них курсором.
При проверке нажатия на кнопку снова получим список доступных операций, так как порядок при выводе совпадает с тем что получен в этот момент, то легко установить соответствие пробегая по массиву и определяя в квадрат какой кнопки попала точка нажатия. После испускаем соответствующий сигнал с индексом элемента на котором было нажатие. Аналогично поступаем и для ссылки Detail.
Создание представления
Для представления нашей древовидной структуры воспользуемся QTreeView. Так.как нам не нужны отступы выставим соответствующие параметры в конструкторе. Основные операции будут происходить при установке модели, где установим связи между сигналами делегата и слотами модели.
В итоге построив модель, делегат и настроив все связи получим список элементов аля Firefox.
В ссылке ниже сможете скачать исходный код демонстрационного проекта, в котором показана работа списка и различные состояние элементов. Так как в своем проекте использую его для отображения списка доступных обновлений и модулей, то и в пример включил возможность загрузки файлов из сети ( в примере скачивается файл образ FreeBSD 8.2).
Конструктивная критика и замечания приветствуются.
Исходники: ajieks.ru/download/file_list.zip
DISCLAIMER
Приведенный пример не претендует на роль готового компонента, представлен в ознакомительных целях и при использовании требует адаптации. Взят из рабочего проекта, где полностью выполняет поставленные перед ним задачи.