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