DTK列表控件原理與API詳解

為什么要重新造一個ListView控件夕吻?

在開發(fā)應(yīng)用程序的過程中惶洲,經(jīng)常會使用到列表來展現(xiàn)內(nèi)容(比如音樂播放器的播放列表和系統(tǒng)監(jiān)視器的進程列表)罩旋,而制作列表內(nèi)容不能像傳統(tǒng)的VBoxLayout來添加子控件菱阵,因為每個子控件都代表一個 XWindow他爸, 當成百上千的子控件堆砌在一起的時候就會造成巨大的性能瓶頸。

開發(fā)了很多Gtk+和Qt的程序策彤,對于Gtk+和Qt內(nèi)置的ListView的控件易用性非常不滿意栓袖,因為當開發(fā)者初次學(xué)習(xí)這些控件時,會被Gtk+/Qt的MVC模型和各種API繞暈店诗,不是說MVC的模型不容易理解裹刮,而是在理解MVC模型后,要通過查看API就可以快速開發(fā)出符合產(chǎn)品要求的ListView非常非常的困難庞瘸,經(jīng)常要看現(xiàn)有的例子捧弃,然后把所有接口的細節(jié)都小心翼翼的組裝才能正常工作,因為Gtk+/Qt的ListView的API設(shè)計的非常復(fù)雜擦囊,如果每一行還是復(fù)雜的自定義渲染內(nèi)容時违霞,實現(xiàn)會更加復(fù)雜難懂。

所以瞬场,我在寫深度系統(tǒng)監(jiān)視器的時候买鸽,大部分的工作都在創(chuàng)造 DTK Simple ListView, 希望ListView在設(shè)計上不但要滿足極高的渲染性能,還要能夠繪制各種復(fù)雜的自繪內(nèi)容贯被,最后要求創(chuàng)造控件的開發(fā)難度降到最低眼五,做到一看就懂妆艘,一通百通。

DTK Simple ListView 設(shè)計理念

DTK Simple ListView的設(shè)計理念是看幼,MV模型:

  • DSimpleListView 提供列表行高度和列寬度的控制批旺、列表滾動位置和選擇狀態(tài)的維護和傳遞 QPainter 給 DSimpleListItem, DSimpleListView 本身不進行任何行內(nèi)容的繪制,它只是把所有 DSimpleListItem 繪制的內(nèi)容整合在一起進行管理
  • DSimpleListItem 得到 DSimpleListView 傳遞過來的 QPainter诵姜、列信息汽煮、表格矩形數(shù)據(jù)后,由開發(fā)者完全控制行內(nèi)容的繪制

這樣設(shè)計的好處是棚唆,開發(fā)者只要懂得怎么使用 QPainter 進行圖形繪制暇赤,開發(fā)者就可以在 DSimpleListItem 中繪制任意行內(nèi)容,包括文本瑟俭、圖片翎卓、任意控件甚至每行都可以畫一個小電影,而代碼的復(fù)雜度不會隨著繪制行內(nèi)容而發(fā)生變化摆寄,所有的行內(nèi)容都源于怎么使用 QPainter失暴。

一旦理解了DSimpleListView/DSimpleItem的設(shè)計理念,看了兩個小例子微饥,任何復(fù)雜的產(chǎn)品列表需求都可以快速滿足逗扒。

安裝開發(fā)版本 DTK

在講解代碼用例之前,需要先安裝 DTK 開發(fā)版本欠橘,Deepin用戶可以直接從 DTK Deb包 下載 libdtkwidget-dev_.deb 和 libdtkwidget2_.deb 兩個包矩肩。

其他Linux發(fā)行版的開發(fā)者需要自行從源碼進行編譯: DTK源碼編譯手冊

DTK Simple ListView 用例講解

單列列表

入門例子:做一個最簡單的例子,顯示只有一列的文本肃续。

首先黍檩,得基于 DSimpleListView/DSimpleListItem 創(chuàng)建兩個子類, ListView很簡單始锚,直接繼承 DSimpleListView 就可以了刽酱, ListItem 只要實現(xiàn)三個非常簡單的接口函數(shù) (sameAs, drawBackground, drawForeground)即可:

// singlelistview.h
#ifndef SINGLELISTVIEW_H
#define SINGLELISTVIEW_H

#include <DSimpleListView> 

DWIDGET_USE_NAMESPACE  // 這句話主要強調(diào)使用 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 接口函數(shù)瞧捌,用于區(qū)分兩個Item是否是同一個Item?
    bool sameAs(DSimpleListItem *item);

    // 繪制Item背景的接口函數(shù)棵里,參數(shù)依次為表格矩形、繪制QPainter對象姐呐、行索引殿怜、當前行是否選中?
    void drawBackground(QRect rect, QPainter *painter, int index, bool isSelect);

    // 繪制Item前景的接口函數(shù)曙砂,參數(shù)依次為表格矩形头谜、繪制QPainter對象、行索引鸠澈、當前行是否選中柱告?
    void drawForeground(QRect rect, QPainter *painter, int column, int index, bool isSelect);
    
    // 名字屬性砖织,這里用于繪制文本列的內(nèi)容
    QString name;
};  

#endif

其次,實現(xiàn) singlelistview.cpp, 看看超級簡單吧末荐? 只需根據(jù) QString 創(chuàng)建一個 DSimpleListItem ,然后通過函數(shù)添加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 設(shè)計理念是新锈,開發(fā)者只需要把所有精力專注于 DSimpleListItem 的接口函數(shù)上甲脏,就可以實現(xiàn)任意復(fù)雜的界面效果, DSimpleListView 不用過多關(guān)心妹笆,開發(fā)者的附加門檻非常非常低块请。

下面我們就看一下實現(xiàn)上圖中的單列列表的 DSimpleListItem 的實現(xiàn)細節(jié):

// singlelistitem.cpp
#include "singlelistitem.h"
#include <QColor>

DWIDGET_USE_NAMESPACE

SingleListItem::SingleListItem(QString itemName)
{
    // 初始化文本屬性
    name = itemName;
}

bool SingleListItem::sameAs(DSimpleListItem *item)
{
    // 根據(jù)兩個Item的屬性來判斷兩個Item是否是相同的?
    // DSimpleListView 內(nèi)部都是按照 DSimpleListItem 類型來處理的拳缠,sameAS 中需要用 static_cast 進行一下類型轉(zhuǎn)換
    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表示當前列自動撐開徙菠,其他數(shù)字表示固定像素值讯沈,一個列表只允許有一個自動撐開的列
    QList<int> widths;
    widths << 100 << -1 << 20;

    // 設(shè)置列表的標題、寬度和標題欄的高度
    setColumnTitleInfo(titles, widths, 36);

    addItems(items);
}

多列的 ListView 也非常簡單婿奔,唯一多了 setColumnTitleInfo 函數(shù)缺狠,因為列表有多個列,需要告訴 DSimpleListView 每一列的標題萍摊、寬度和最終標題欄的高度挤茄,如果不想顯示標題欄,可以把標題欄的高度設(shè)置0像素即可记餐。

multilistviewitem.cpp 的實現(xiàn)非常類似單列列表的Item實現(xiàn):

// 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 參數(shù)片酝,根據(jù)不同的列索引囚衔,繪制不同的列文字,最終的效果圖如下:


多列列表

是不是很簡單雕沿?
更復(fù)雜的自繪內(nèi)容练湿,只需使用 QPainter 進行不同的內(nèi)容繪制即可,代碼復(fù)雜度不會增加审轮,原理都一樣:

  • 繪制圖標時肥哎,把 painter->drawText 替換成 painter->drawPixmap
  • 繪制進度條時辽俗,把 painter->drawText 替換成 painter->drawRect
  • ...

設(shè)置邊框和圓角

有時候設(shè)計師更青睞對列表有一個圓角的邊線,以更加優(yōu)雅的顯示界面細節(jié), 直接在DSimpleListView子類中調(diào)用下面兩行代碼即可實現(xiàn):

    // 設(shè)置為true時繪制邊框
    setFrame(true);

    // 設(shè)置邊框的圓角是 8像素
    setClipRadius(8);

如果要控制邊線的顏色和邊線透明度篡诽,也非常簡單:

    setFrame(true, QColor("#FF0000"), 0.5);
圓角邊框效果

彈出右鍵菜單

當用戶在列表中右鍵時往往希望彈出右鍵菜單崖飘,連接信號 rightClickItems 即可。

void rightClickItems(QPoint pos, QList<DSimpleListItem*> items);
  • 參數(shù) pos 表示用戶右鍵點擊的位置
  • 參數(shù) items 表示所有選中的 items

以上面的多列列表為例杈女,右鍵菜單響應(yīng)的實例代碼如下:

// 在 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)
{
    // 構(gòu)建菜單朱浴,為了便于演示,只取選中的第一個 item达椰,用于菜單內(nèi)容展示
    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);
}
彈出右鍵菜單

設(shè)置列的排序算法

多列列表中最常用的操作就是排序翰蠢,在 DSimpleListView 實現(xiàn)排序非常簡單。
首先在 DSimpleListItem 的子類中實現(xiàn)靜態(tài)的排序函數(shù)啰劲,以上面的 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;
}

上面三個靜態(tài)排序函數(shù)分別對 artist梁沧、song、length三列提供排序算法蝇裤, 參數(shù) descendingSort 表示排序是否是升序還是降序廷支。

然后在 DSimpleListView 的子類中調(diào)用 setColumnSortingAlgorithms 函數(shù)即可:

    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 列排序接口的參數(shù)依次表示:

  • algorithms 列對應(yīng)的靜態(tài)排序函數(shù),長度必須和列的數(shù)量保持一致
  • sortColumn 默認排序的列栓辜,設(shè)置成 0 表示第一列
  • descendingSort 是否是降序排列酥泞?

最終的排序效果如下圖:


列表排序

搜索列表

搜索列表的實現(xiàn)原理,現(xiàn)在 DSimpleListItem 子類構(gòu)建搜索函數(shù):

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);
}

然后在調(diào)用 DSimpleListView 子類的setSearchAlgorithm 函數(shù)即可設(shè)置列表的搜索函數(shù)啃憎,注意芝囤,DTK Simple ListView 所有干活的函數(shù)其實都是 DSimpleListItem 各種接口去實現(xiàn)的, DSimpleListView 只提供框架實現(xiàn)

setSearchAlgorithm(&MultiListItem::search);

最后辛萍,每次在 DSimpleListView 調(diào)用 search 函數(shù)的時候悯姊,DSimpleListView 自動會根據(jù) setSearchAlgorithm 設(shè)置的搜索算法對列表的行進行過濾顯示:

void search(QString searchContent);

搜索效果如下圖(盜用深度監(jiān)視器的效果):


搜索列表

隱藏指定列

DSimpleListView 的 setColumnHideFlags 接口可以用于控制列表中置頂列的是否顯示

void setColumnHideFlags(QList<bool> toggleHideFlags, int alwaysVisibleColumn=-1);
  • 參數(shù) toggleHideFlags 表示對應(yīng)列的隱藏狀態(tài), true 表示顯示贩毕, false 表示隱藏
  • 參數(shù) alwaysVisibleColumn 表示永遠顯示的一列悯许,默認 -1 表示所有列都可以隱藏

具體的效果如下圖:


隱藏列
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市辉阶,隨后出現(xiàn)的幾起案子先壕,更是在濱河造成了極大的恐慌,老刑警劉巖谆甜,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垃僚,死亡現(xiàn)場離奇詭異,居然都是意外死亡规辱,警方通過查閱死者的電腦和手機谆棺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來罕袋,“玉大人改淑,你說我怎么就攤上這事碍岔。” “怎么了朵夏?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵蔼啦,是天一觀的道長。 經(jīng)常有香客問我仰猖,道長询吴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任亮元,我火速辦了婚禮,結(jié)果婚禮上唠摹,老公的妹妹穿的比我還像新娘爆捞。我一直安慰自己,他們只是感情好勾拉,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布煮甥。 她就那樣靜靜地躺著,像睡著了一般藕赞。 火紅的嫁衣襯著肌膚如雪成肘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天斧蜕,我揣著相機與錄音双霍,去河邊找鬼。 笑死批销,一個胖子當著我的面吹牛洒闸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播均芽,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼丘逸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了掀宋?” 一聲冷哼從身側(cè)響起深纲,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎劲妙,沒想到半個月后湃鹊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡镣奋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年涛舍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唆途。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡富雅,死狀恐怖掸驱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情没佑,我是刑警寧澤毕贼,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站蛤奢,受9級特大地震影響鬼癣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜啤贩,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一待秃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧痹屹,春花似錦章郁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至楼肪,卻和暖如春培廓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背春叫。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工肩钠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人暂殖。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓蔬将,卻偏偏與公主長得像,于是被迫代替她去往敵國和親央星。 傳聞我的和親對象是個殘疾皇子霞怀,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容