做優(yōu)惠卷網(wǎng)站倒閉了多少錢剪輯培訓(xùn)班一般學(xué)費多少
0 環(huán)境
- Windows 11
- Qt 5.15.2 MinGW x64
1 系列文章
簡介:本系列文章,是以純代碼方式實現(xiàn) Qt 控件的重構(gòu),盡量不使用 Qss 方式。
《[Qt]QListView 重繪實例之一:背景重繪》
《[Qt]QListView 重繪實例之二:列表項覆蓋的問題處理》
《[Qt]QListView 重繪實例之三:滾動條覆蓋的問題處理》
《[Qt]QListView 重繪實例之四:效果一講解》
《[Qt]QListView 重繪實例之五:效果二講解》
2 開始
自定義 Qt 控件,無外乎兩個主要目的:
- 實現(xiàn)更漂亮的樣式;
- 實現(xiàn)更強大的/更合適的功能;
要實現(xiàn)以上兩個主要目的,基本上都需要對 Qt 原生控件進行一定的重繪,以適應(yīng)需求。
本節(jié)中,主要講解 QListView
的背景繪制。
(之所以單獨寫一文,是因為自己動手實現(xiàn)時才發(fā)現(xiàn):雖然最后的實現(xiàn)代碼并不多,但要弄懂這些,還是要花費很多精力的。)
→ 解決方案直達 ←
3 paintEvent
重繪與問題
通常,重構(gòu)一個新控件,基本上都是直接重寫 void paintEvent(QPaintEvent *event)
方法。
void PListView::paintEvent(QPaintEvent *event)
{Q_UNUSED(event)QPainter painter(this); // Errorpainter.setRenderHint(QPainter::Antialiasing);painter.setPen(QPen(Qt::red));painter.setBrush(QBrush(Qt::white));painter.drawRoundedRect(rect(), 5, 5);
}
3.1 問題 1 —— 繪圖對象
通常,進行重繪時,新建 QPainter
對象都是以父控件為對象,意即在父控件中進行繪制。
但是,如果這樣直接對 QListView
進行重繪,是會出錯的:
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::setRenderHint: Painter must be active to set rendering hints
QPainter::setPen: Painter not active
QPainter::setBrush: Painter not active
(猜測)原因大致應(yīng)該是:QListView
是由多個子控件組成,實際負責(zé)顯示內(nèi)容的只是其中的一個子控件,所以繪制對象需要具體指定到負責(zé)顯示的對象。
QListView
繼承樹如下:
而一個默認 QListView
對象包含的子控件如下:
(QWidget(0x1eb4600, name = "qt_scrollarea_viewport"),
QStyledItemDelegate(0x1eb1840),
QItemSelectionModel(0x1eb1ba0),
QWidget(0x1eb1010, name = "qt_scrollarea_hcontainer"),
QWidget(0x1eb1150, name = "qt_scrollarea_vcontainer"))
其中,實際顯示內(nèi)容的對象就是 “qt_scrollarea_viewport”,也就是 QListView
的視口(viewport)。這樣做的主要原因,是要實現(xiàn)對 QListView
內(nèi)容的滾動顯示(顯示部分內(nèi)容)。
所以,對于 QListView
重繪,必須要針對視口 viewport()
。
void PListView::paintEvent(QPaintEvent *event)
{Q_UNUSED(event)QPainter painter(viewport());painter.setRenderHint(QPainter::Antialiasing);painter.setPen(QPen(Qt::red));painter.setBrush(QBrush(Qt::white));painter.drawRoundedRect(rect(), 5, 5);
}
效果如下圖示:
3.2 問題 2 —— 外邊線框
從上圖看,這次倒是繪制出背景框。但首先注意到的問題是 QListView
默認外連線框,非常顯眼。因此,首要目的是要去掉這個外連線框。
上文實現(xiàn)代碼中的重繪過程,僅做了兩件事:
- 繪制了一個圓角矩形;
- 阻止了
QListView
的其它默認繪制;
因此 ,基本可以肯定,外連線框并不是由 paintEvent()
繪制過程中引起的。看來原因得到 QListView
里層查找。
原因查找的具體過程略過不述,QListView
的外邊線框其實就是其父類 QFrame
的邊框(可以理解為一個底層,其它內(nèi)容都繪制在這個底層之上,畢竟 QListView
是 UI 控件)。
只需要對 QListView
進行如下設(shè)置,改變一下 QFrame
樣式即可去掉外邊線框:
PListVeiw::PListView(QWidget *parent) : QListView(parent)
{setFrameStyle(QFrame::NoFrame);
}
效果如下:
強制隱藏/關(guān)閉垂直滾動條,效果如下:
3.3 問題 3 —— 繪制區(qū)域
從上圖可知,繪制的背景效果基本出來了。但是,也被垂直滾動條擋住了一部分。
再回來看一看繪圖代碼,其中有一行如下:
painter.drawRoundedRect(rect(), 5, 5);
此時,指定的繪圖區(qū)域為 rect()
,即針對控件的整個顯示區(qū)域。而我們指定的繪圖對象是 QListView
的視口,原則上為了保證一致性,在什么上繪圖,就應(yīng)該在該對象的區(qū)域內(nèi)進行繪制。所以,修改以上那行的代碼:
painter.drawRoundedRect(viewport()->rect(), 5, 5);
效果如下:
這種效果,也還可以。一些樣式也確實是將滾動條置于控件之外的。
本文不針對此樣式進行講解,主要考慮滾動條內(nèi)含在列表內(nèi)的樣式。
滾動條的問題,先按下不提,具體詳見本系列后文說明。參考《[Qt]QListView 重繪實例之三:滾動條覆蓋的問題處理》。
3.4 問題 4 —— 滾動時殘留
先前為了重點顯示 QListView
的背景繪制效果,所以沒有繪制 QListView
的內(nèi)容。
現(xiàn)在,加上內(nèi)容的繪制代碼:
void PListView::paintEvent(QPaintEvent *event)
{QPainter painter(viewport());painter.setRenderHint(QPainter::Antialiasing);painter.setPen(QPen(Qt::red));painter.setBrush(QBrush(Qt::white));painter.drawRoundedRect(rect(), 5, 5); // 理解為視口占據(jù)整個控件區(qū)域QListView::paintEvent(event);
}
說明:繪制順序是有要求的。應(yīng)該先繪制背景,然后繪制列表內(nèi)容(即前景)。
效果圖如下:
但是,如果我們使用鼠標(biāo)滾輪滾動或拖動滾動條,滾動 QListView
的內(nèi)容,卻出現(xiàn)了如下效果:
這顯然不是想要的效果。
具體原因未深究,暫時未知,猜測應(yīng)該是底層代碼的原因。因為,上文中的重繪代碼其實很簡單,并未做多余的動作。
但這種殘留效果,顯然不可接受。
因此,至少到目前,這種方式繪制 QListView
的背景是不可行的。
(考慮到添加委托會對列表項進行繪制,可能會影響到這個殘留問題。嘗試過添加委托,但這個殘留問題依然存在。)
4 解決方案
從上文得知,采用 paintEvent()
對 QListView
背景進行繪制的方案不可行。
另,考慮到后來的 Qt 版本對于 Qss 的性能問題,本系列也不考慮 Qss 方案。
于是,已知可行的方案只剩使用 QProxyStyle
代理樣式定制了。
(之前也沒有實際使用過代理樣式,通過學(xué)習(xí)/練習(xí)/測試得出了合適的效果。)
關(guān)于 QProxyStyle
的具體內(nèi)容,查找資料的過程中有發(fā)現(xiàn),有不少介紹的好博文,請酌情參考(文末參考資料有鏈接),本文不另述。
4.1 定義背景繪制樣式
/* .h */
class PListViewStyle : public QProxyStyle
{
public:PListViewStyle();void drawControl(QStyle::ControlElement element,const QStyleOption *option,QPainter *painter,const QWidget *widget = nullptr) const override;
};/* .cpp */
PListViewStyle::PListViewStyle()
{
}
void PListViewStyle::drawControl(QStyle::ControlElement element,const QStyleOption *option,QPainter *painter,const QWidget *widget) const
{switch(element){case QStyle::CE_ShapedFrame:{const QStyleOptionFrame *opt = qstyleoption_cast<const QStyleOptionFrame *>(option);if(nullptr == opt) { return; }painter->save();painter->setRenderHint(QPainter::Antialiasing);painter->setPen(QPen(Qt::red));painter->setBrush(QBrush(Qt::white));painter->drawRoundedRect(opt->rect, 5, 5);painter->restore();return;}default:break;}QProxyStyle::drawControl(element, option, painter, widget);
}
4.2 使用代理樣式
PListVeiw::PListView(QWidget *parent) : QListView(parent)
{// setFrameStyle(QFrame::NoFrame); // Must delete or comment itsetStyle(new PListViewStyle);
}
注意:
- 需要刪除重寫函數(shù)
void paintEvent(QPaintEvent *event)
,否則可能覆蓋效果。 - 需要刪除對
QFrame
的樣式設(shè)置,不能再設(shè)置為QFrame::NoFrame
。因為代理樣式實際是對QFrame
進行繪制的,如果設(shè)置了QFrame::NoFrame
,則繪制的樣式根本就不會顯示。
效果如下:
至少看上去,基本達到了預(yù)期的效果。
但是,
但是,
但是,總有但是,哈哈。
將背景的圓角矩形圓角半徑加大一下,再來看看效果圖:
從上圖可以看出有幾個問題:
- 列表項在背景的上層,即背景繪制先于列表項。而列表項也是有背景的(以及高亮/選中背景),可以理解為列表項就是一個個小矩形(默認沒有圓角)。由上可以看出,視口的最上/最下一行,都有矩形直角覆蓋了背景(圓角矩形),因此破壞了背景的效果;
- 同理,滾動條也在背景上層,滾動條也是一個直角矩形,矩形直角覆蓋了背景,因此也破壞了背景的效果;
其中:
- 對于列表項產(chǎn)生的覆蓋問題,可以通過使用委托,控制列表項背景(默認背景/高亮背景/選中背景)的繪制,使繪制視口最上/最下一行時,繪制合適的圓角效果。
- 對于滾動條的問題,就復(fù)雜得多,具體詳見本系列后文內(nèi)容。參考《[Qt]QListView 重繪實例之三:滾動條覆蓋的問題處理》。
5 參考資料
- 《C++ GUI Qt 4編程(第二版)》,第 19 章,19.2 子類化 QStyle
- QStyle類用法總結(jié)(一)
- 繪制自定義QSlider