DTK列表控件原理与API详解

为什么要重新造一个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 表示所有列都可以隐藏

具体的效果如下图:


隐藏列
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容