DTK列表控件原理与API详解
Deepin
字数10761 2017-11-04

为什么要重新造一个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模型:

这样设计的好处是,开发者只要懂得怎么使用 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 进行不同的内容绘制即可,代码复杂度不会增加,原理都一样:

设置边框和圆角

有时候设计师更青睐对列表有一个圆角的边线,以更加优雅的显示界面细节, 直接在DSimpleListView子类中调用下面两行代码即可实现:

    // 设置为true时绘制边框
    setFrame(true);

    // 设置边框的圆角是 8像素
    setClipRadius(8);

如果要控制边线的颜色和边线透明度,也非常简单:

    setFrame(true, QColor("#FF0000"), 0.5);

圆角边框效果

弹出右键菜单

当用户在列表中右键时往往希望弹出右键菜单,连接信号 rightClickItems 即可。

void rightClickItems(QPoint pos, QList<DSimpleListItem*> 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 列排序接口的参数依次表示:

最终的排序效果如下图: 列表排序

搜索列表

搜索列表的实现原理,现在 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);

具体的效果如下图: 隐藏列