为什么要重新造一个ListView控件?
在开发应用程序的过程中,经常会使用到列表来展现内容(比如音乐播放器的播放列表和系统监视器的进程列表),而制作列表内容不能像传统的VBoxLayout来添加子控件,因为每个子控件都代表一个 XWindow, 当成百上千的子控件堆砌在一起的时候就会造成巨大的性能瓶颈。
开发了很多Gtk+和Qt的程序,对于Gtk+和Qt内置的ListView的控件易用性非常不满意,因为当开发者初次学习这些控件时,会被Gtk+/Qt的MVC模型和各种API绕晕,不是说MVC的模型不容易理解,而是在理解MVC模型后,要通过查看API就可以快速开发出符合产品要求的ListView非常非常的困难,经常要看现有的例子,然后把所有接口的细节都小心翼翼的组装才能正常工作,因为Gtk+/Qt的ListView的API设计的非常复杂,如果每一行还是复杂的自定义渲染内容时,实现会更加复杂难懂。
所以,我在写深度系统监视器的时候,大部分的工作都在创造 DTK Simple ListView, 希望ListView在设计上不但要满足极高的渲染性能,还要能够绘制各种复杂的自绘内容,最后要求创造控件的开发难度降到最低,做到一看就懂,一通百通。
DTK Simple ListView 设计理念
DTK Simple ListView的设计理念是,MV模型:
- DSimpleListView 提供列表行高度和列宽度的控制、列表滚动位置和选择状态的维护和传递 QPainter 给 DSimpleListItem, DSimpleListView 本身不进行任何行内容的绘制,它只是把所有 DSimpleListItem 绘制的内容整合在一起进行管理
- DSimpleListItem 得到 DSimpleListView 传递过来的 QPainter、列信息、表格矩形数据后,由开发者完全控制行内容的绘制
这样设计的好处是,开发者只要懂得怎么使用 QPainter 进行图形绘制,开发者就可以在 DSimpleListItem 中绘制任意行内容,包括文本、图片、任意控件甚至每行都可以画一个小电影,而代码的复杂度不会随着绘制行内容而发生变化,所有的行内容都源于怎么使用 QPainter。
一旦理解了DSimpleListView/DSimpleItem的设计理念,看了两个小例子,任何复杂的产品列表需求都可以快速满足。
安装开发版本 DTK
在讲解代码用例之前,需要先安装 DTK 开发版本,Deepin用户可以直接从 DTK Deb包 下载 libdtkwidget-dev_.deb 和 libdtkwidget2_.deb 两个包。
其他Linux发行版的开发者需要自行从源码进行编译: DTK源码编译手册
DTK Simple ListView 用例讲解
单列列表
入门例子:做一个最简单的例子,显示只有一列的文本。
首先,得基于 DSimpleListView/DSimpleListItem 创建两个子类, ListView很简单,直接继承 DSimpleListView 就可以了, ListItem 只要实现三个非常简单的接口函数 (sameAs, drawBackground, drawForeground)即可:
// singlelistview.h
#ifndef SINGLELISTVIEW_H
#define SINGLELISTVIEW_H
#include <DSimpleListView>
DWIDGET_USE_NAMESPACE // 这句话主要强调使用 dtkwidget 的命名空间,以使用其控件
class SingleListView : public DSimpleListView
{
Q_OBJECT
public:
SingleListView(DSimpleListView *parent=0);
};
#endif
// singlelistitem.h
#ifndef SINGLELISTITEM_H
#define SINGLELISTITEM_H
#include <DSimpleListItem>
DWIDGET_USE_NAMESPACE
class SingleListItem : public DSimpleListItem
{
Q_OBJECT
public:
SingleListItem(QString itemName);
// DSimpleListItem 接口函数,用于区分两个Item是否是同一个Item?
bool sameAs(DSimpleListItem *item);
// 绘制Item背景的接口函数,参数依次为表格矩形、绘制QPainter对象、行索引、当前行是否选中?
void drawBackground(QRect rect, QPainter *painter, int index, bool isSelect);
// 绘制Item前景的接口函数,参数依次为表格矩形、绘制QPainter对象、行索引、当前行是否选中?
void drawForeground(QRect rect, QPainter *painter, int column, int index, bool isSelect);
// 名字属性,这里用于绘制文本列的内容
QString name;
};
#endif
其次,实现 singlelistview.cpp, 看看超级简单吧? 只需根据 QString 创建一个 DSimpleListItem ,然后通过函数添加Item到ListView即可:
// singlelistview.cpp
#include "singlelistview.h"
#include "singlelistitem.h"
DWIDGET_USE_NAMESPACE
SingleListView::SingleListView(DSimpleListView *parent) : DSimpleListView(parent)
{
QStringList rockStars;
rockStars << "Bob Dylan" << "Neil Young" << "Eric Clapton" << "John Lennon";
QList<DSimpleListItem*> items;
for (auto rockStarName : rockStars){
SingleListItem *item = new SingleListItem(rockStarName);
items << item;
}
addItems(items);
}
DTK Simple ListView 设计理念是,开发者只需要把所有精力专注于 DSimpleListItem 的接口函数上,就可以实现任意复杂的界面效果, DSimpleListView 不用过多关心,开发者的附加门槛非常非常低。
下面我们就看一下实现上图中的单列列表的 DSimpleListItem 的实现细节:
// singlelistitem.cpp
#include "singlelistitem.h"
#include <QColor>
DWIDGET_USE_NAMESPACE
SingleListItem::SingleListItem(QString itemName)
{
// 初始化文本属性
name = itemName;
}
bool SingleListItem::sameAs(DSimpleListItem *item)
{
// 根据两个Item的属性来判断两个Item是否是相同的?
// DSimpleListView 内部都是按照 DSimpleListItem 类型来处理的,sameAS 中需要用 static_cast 进行一下类型转换
return name == (static_cast<SingleListItem*>(item))->name;
}
void SingleListItem::drawBackground(QRect rect, QPainter *painter, int index, bool isSelect)
{
// 初始化绘制背景所需的行矩形对象
QPainterPath path;
path.addRect(QRectF(rect));
// 当行选中时绘制蓝色背景,没有选中时绘制灰色背景
painter->setOpacity(1);
if (isSelect) {
painter->fillPath(path, QColor("#2CA7F8"));
} else if (index % 2 == 1) {
painter->fillPath(path, QColor("#D8D8D8"));
}
}
void SingleListItem::drawForeground(QRect rect, QPainter *painter, int column, int index, bool isSelect)
{
// 当行选中时使用白色文字,没有选中时使用黑色文字
painter->setOpacity(1);
if (isSelect) {
painter->setPen(QPen(QColor("#FFFFFF")));
} else {
painter->setPen(QPen(QColor("#000000")));
}
// 绘制文字,左对齐,纵向居中对齐,文字左边留10像素的空白
int padding = 10;
painter->drawText(QRect(rect.x() + padding, rect.y(), rect.width() - padding * 2, rect.height()), Qt::AlignLeft | Qt::AlignVCenter, name);
}
是不是非常非常的简单? 最终效果图如下:
多列列表
多列列表的原理也非常简单,直接看代码:
// multilistview.cpp
#include "multilistview.h"
#include "multilistitem.h"
DWIDGET_USE_NAMESPACE
MultiListView::MultiListView(DSimpleListView *parent) : DSimpleListView(parent)
{
QList<DSimpleListItem*> items;
MultiListItem *item1 = new MultiListItem("Bob Dylan", "Like A Rolling Stone", "5:56");
MultiListItem *item2 = new MultiListItem("Neil Young", "Old Man", "4:08");
MultiListItem *item3 = new MultiListItem("Eric Clapton", "Tears In Heaven", "4:34");
MultiListItem *item4 = new MultiListItem("John Lennon", "Imagine", "3:56");
items << item1;
items << item2;
items << item3;
items << item4;
// 初始化标题列的名字
QList<QString> titles;
titles << "Artist" << "Song" << "Length";
// 初始化每一列的宽度,-1表示当前列自动撑开,其他数字表示固定像素值,一个列表只允许有一个自动撑开的列
QList<int> widths;
widths << 100 << -1 << 20;
// 设置列表的标题、宽度和标题栏的高度
setColumnTitleInfo(titles, widths, 36);
addItems(items);
}
多列的 ListView 也非常简单,唯一多了 setColumnTitleInfo 函数,因为列表有多个列,需要告诉 DSimpleListView 每一列的标题、宽度和最终标题栏的高度,如果不想显示标题栏,可以把标题栏的高度设置0像素即可。
multilistviewitem.cpp 的实现非常类似单列列表的Item实现:
// multilistitem.cpp
#include "multilistitem.h"
#include <QColor>
DWIDGET_USE_NAMESPACE
MultiListItem::MultiListItem(QString artistName, QString songName, QString songLength)
{
artist = artistName;
song = songName;
length = songLength;
}
bool MultiListItem::sameAs(DSimpleListItem *item)
{
return artist == (static_cast<MultiListItem*>(item))->artist && song == (static_cast<MultiListItem*>(item))->song && length == (static_cast<MultiListItem*>(item))->length;
}
void MultiListItem::drawBackground(QRect rect, QPainter *painter, int index, bool isSelect)
{
QPainterPath path;
path.addRect(QRectF(rect));
painter->setOpacity(1);
if (isSelect) {
painter->fillPath(path, QColor("#2CA7F8"));
} else if (index % 2 == 1) {
painter->fillPath(path, QColor("#D8D8D8"));
}
}
void MultiListItem::drawForeground(QRect rect, QPainter *painter, int column, int index, bool isSelect)
{
int padding = 10;
painter->setOpacity(1);
if (isSelect) {
painter->setPen(QPen(QColor("#FFFFFF")));
} else {
painter->setPen(QPen(QColor("#000000")));
}
if (column == 0) {
painter->drawText(QRect(rect.x() + padding, rect.y(), rect.width() - padding * 2, rect.height()), Qt::AlignLeft | Qt::AlignVCenter, artist);
} else if (column == 1) {
painter->drawText(QRect(rect.x() + padding, rect.y(), rect.width() - padding * 2, rect.height()), Qt::AlignLeft | Qt::AlignVCenter, song);
} else if (column == 2) {
painter->drawText(QRect(rect.x() + padding, rect.y(), rect.width() - padding * 2, rect.height()), Qt::AlignRight | Qt::AlignVCenter, length);
}
}
唯一的变化,就是 drawForeground 的时候,利用了 column 参数,根据不同的列索引,绘制不同的列文字,最终的效果图如下:
是不是很简单? 更复杂的自绘内容,只需使用 QPainter 进行不同的内容绘制即可,代码复杂度不会增加,原理都一样:
- 绘制图标时,把 painter->drawText 替换成 painter->drawPixmap
- 绘制进度条时,把 painter->drawText 替换成 painter->drawRect
- …
设置边框和圆角
有时候设计师更青睐对列表有一个圆角的边线,以更加优雅的显示界面细节, 直接在DSimpleListView子类中调用下面两行代码即可实现:
// 设置为true时绘制边框
setFrame(true);
// 设置边框的圆角是 8像素
setClipRadius(8);
如果要控制边线的颜色和边线透明度,也非常简单:
setFrame(true, QColor("#FF0000"), 0.5);
弹出右键菜单
当用户在列表中右键时往往希望弹出右键菜单,连接信号 rightClickItems 即可。
void rightClickItems(QPoint pos, QList<DSimpleListItem*> items);
- 参数 pos 表示用户右键点击的位置
- 参数 items 表示所有选中的 items
以上面的多列列表为例,右键菜单响应的实例代码如下:
// 在 multilistview.h 中声明 popupMenu slots 用于处理 rightClickItems 信号
public slots:
void popupMenu(QPoint pos, QList<DSimpleListItem*> items);
...
// 连接信号 rightClickItems 到 popupMenu 槽
connect(this, &MultiListView::rightClickItems, this, &MultiListView::popupMenu, Qt::QueuedConnection);
...
void MultiListView::popupMenu(QPoint pos, QList<DSimpleListItem*> items)
{
// 构建菜单,为了便于演示,只取选中的第一个 item,用于菜单内容展示
QMenu *menu = new QMenu();
MultiListItem *item = static_cast<MultiListItem*>(items[0]);
QAction *artistAction = new QAction(item->artist, this);
QAction *songAction = new QAction(item->song, this);
QAction *lengthAction = new QAction(item->length, this);
menu->addAction(artistAction);
menu->addAction(songAction);
menu->addAction(lengthAction);
// 在用户右键的坐标弹出菜单
menu->exec(pos);
}
设置列的排序算法
多列列表中最常用的操作就是排序,在 DSimpleListView 实现排序非常简单。 首先在 DSimpleListItem 的子类中实现静态的排序函数,以上面的 multilistitem.h 为例:
// multilistview.h
static bool sortByArtist(const DSimpleListItem *item1, const DSimpleListItem *item2, bool descendingSort);
static bool sortBySong(const DSimpleListItem *item1, const DSimpleListItem *item2, bool descendingSort);
static bool sortByLength(const DSimpleListItem *item1, const DSimpleListItem *item2, bool descendingSort);
// multilistview.cpp
bool MultiListItem::sortByArtist(const DSimpleListItem *item1, const DSimpleListItem *item2, bool descendingSort)
{
// Init.
QString artist1 = (static_cast<const MultiListItem*>(item1))->artist;
QString artist2 = (static_cast<const MultiListItem*>(item2))->artist;
bool sortOrder = artist1 > artist2;
return descendingSort ? sortOrder : !sortOrder;
}
bool MultiListItem::sortBySong(const DSimpleListItem *item1, const DSimpleListItem *item2, bool descendingSort)
{
// Init.
QString song1 = (static_cast<const MultiListItem*>(item1))->song;
QString song2 = (static_cast<const MultiListItem*>(item2))->song;
bool sortOrder = song1 > song2;
return descendingSort ? sortOrder : !sortOrder;
}
bool MultiListItem::sortByLength(const DSimpleListItem *item1, const DSimpleListItem *item2, bool descendingSort)
{
// Init.
QString length1 = (static_cast<const MultiListItem*>(item1))->length;
QString length2 = (static_cast<const MultiListItem*>(item2))->length;
bool sortOrder = length1 > length2;
return descendingSort ? sortOrder : !sortOrder;
}
上面三个静态排序函数分别对 artist、song、length三列提供排序算法, 参数 descendingSort 表示排序是否是升序还是降序。
然后在 DSimpleListView 的子类中调用 setColumnSortingAlgorithms 函数即可:
QList<SortAlgorithm> *alorithms = new QList<SortAlgorithm>();
alorithms->append(&MultiListItem::sortByArtist);
alorithms->append(&MultiListItem::sortBySong);
alorithms->append(&MultiListItem::sortByLength);
setColumnSortingAlgorithms(alorithms, 0, true);
void setColumnSortingAlgorithms(QList<SortAlgorithm> *algorithms, int sortColumn=-1, bool descendingSort=false);
setColumnSortingAlgorithms 列排序接口的参数依次表示:
- algorithms 列对应的静态排序函数,长度必须和列的数量保持一致
- sortColumn 默认排序的列,设置成 0 表示第一列
- descendingSort 是否是降序排列?
最终的排序效果如下图:
搜索列表
搜索列表的实现原理,现在 DSimpleListItem 子类构建搜索函数:
static bool search(const DSimpleListItem *item, QString searchContent);
bool MultiListItem::search(const DSimpleListItem *item, QString searchContent)
{
const MultiListItem *item = static_cast<const MultiListItem*>(item);
return item->artist.contains(searchContent) || item->song.contains(searchContent) || item->length.contains(searchContent);
}
然后在调用 DSimpleListView 子类的setSearchAlgorithm 函数即可设置列表的搜索函数,注意,DTK Simple ListView 所有干活的函数其实都是 DSimpleListItem 各种接口去实现的, DSimpleListView 只提供框架实现
setSearchAlgorithm(&MultiListItem::search);
最后,每次在 DSimpleListView 调用 search 函数的时候,DSimpleListView 自动会根据 setSearchAlgorithm 设置的搜索算法对列表的行进行过滤显示:
void search(QString searchContent);
搜索效果如下图(盗用深度监视器的效果):
隐藏指定列
DSimpleListView 的 setColumnHideFlags 接口可以用于控制列表中置顶列的是否显示
void setColumnHideFlags(QList<bool> toggleHideFlags, int alwaysVisibleColumn=-1);
- 参数 toggleHideFlags 表示对应列的隐藏状态, true 表示显示, false 表示隐藏
- 参数 alwaysVisibleColumn 表示永远显示的一列,默认 -1 表示所有列都可以隐藏
具体的效果如下图: