為什么要重新造一個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 表示所有列都可以隱藏
具體的效果如下圖: