Pull to refresh

Работа с моделями и делегатами на примере создания списка аля FireFox

Reading time8 min
Views26K
Не раз проскакивали сравнения сложности построения интерфейсов на Qt. В данной статье приведу пример, как можно сделать список в стиле списка модулей FireFox.



Для этого воспользуемся MVC подходом, который реализован в Qt. На выходе получим что-то вроде этого:


Весь процесс разделим на 3 части:
  1. создание модели
  2. создание делегата
  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
Приведенный пример не претендует на роль готового компонента, представлен в ознакомительных целях и при использовании требует адаптации. Взят из рабочего проекта, где полностью выполняет поставленные перед ним задачи.
Tags:
Hubs:
Total votes 54: ↑47 and ↓7+40
Comments12

Articles