在上個世紀的文章中,老周簡單介紹了 QWindow 類的基本使用——包括從 QWindow 類派生和從 QRasterWindow 類派生。
其實,QWindow 類並不是只能充當主視窗用,它也可以巢狀到父級視窗中,變成子級物件。我們們一般稱之為【控制元件】。F 話不多講,下面我們們用實際案例來說明。
這個例子中老周定義了兩個類:
class MyControl : public QRasterWindow { Q_OBJECT public: MyControl(QWindow *parent = nullptr); private: // “開啟”狀態時的背景色 QColor _on_bgcolor; // “關閉”狀態時的背景色 QColor _off_bgcolor; // 當前狀態 bool _state; signals: // 訊號 void stateChanged(bool isOn); public: // 獲取狀態 inline bool state() const { return _state; } // 修改狀態 inline void setState(bool s) { _state = s; // 發出訊號 emit stateChanged(_state); } protected: // 重寫方法 void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent* evebt) override; };
在私有成員中,兩個 QColor 型別的變數分別表示控制元件處於【開】或【關】狀態時的背景色。_state 是一個布林值,true就是【開】,false就是【關】,用來儲存控制元件的當前狀態。
公共方法 state 獲取當前狀態,setState 方法用來修改當前狀態,同時會發出 stateChanged 訊號。訊號成員我們們先放一下,後文再敘。
兩個虛擬函式的重寫。paintEvent 負責畫出控制元件的模樣;mousePressEvent 當滑鼠左鍵按下時改變控制元件的狀態。實現點選一下開啟,再點選一下關閉的功能。
接下來是實現各個成員。先是建構函式。
MyControl::MyControl(QWindow* parent) :QRasterWindow::QRasterWindow(parent), _state(false), _on_bgcolor(QColor("red")), _off_bgcolor(QColor("gray")) { }
建構函式主要用來初始化幾個私有成員。下面程式碼實現滑鼠左鍵按下後更改狀態。
void MyControl::mousePressEvent(QMouseEvent *event) { if(! (event->buttons() & Qt::MouseButton::LeftButton)) return; // 如果按的不是左鍵就 PASS this->setState(!this->state()); update(); }
每次點選後控制元件的狀態都會取反(開變關,關變開),為了反映改變必須重新繪製控制元件,所以要呼叫 update 方法。
paintEvent 中實現繪製的過程。
void MyControl::paintEvent(QPaintEvent* event) { // 如果當前為不可見狀態,就不繪圖了 if(!isExposed()) return; // 要繪製的區域 QRect rect = event -> rect(); QPainter painter; painter.begin(this); // 根據狀態填充背景 QBrush bgbrush; bgbrush.setStyle(Qt::SolidPattern); if(_state) { bgbrush.setColor(_on_bgcolor); } else { bgbrush.setColor(_off_bgcolor); } painter.fillRect(rect, bgbrush); QRect rectSq; // 如果是“開”的狀態,綠色矩形在右側 if(_state) { rectSq.setX(rect.width() / 3 * 2); rectSq.setWidth(rect.width() / 3); } // 如果為“關”的狀態,綠色矩形在左側 else{ rectSq.setWidth(rect.width() / 3); } rectSq.setHeight(rect.height()); painter.fillRect(rectSq, QColor("gold")); painter.end(); }
繪製分兩步走。第一步是填充背景矩形,如果【開】就填充紅色,如果【關】就填充灰色。此處用到了 QBrush 物件。根據不同顏色呼叫 setColor 方法來設定。這裡要注意 QBrush 物件要呼叫 setStyle 方法設定畫刷樣式為 SolidPattern。這是因為 QBrush 類預設的 style 是 NoBrush,因此要手動設定一下,不然看不到繪製。
第二步是繪製小方塊。當控制元件狀態為【開】時方塊在右邊,狀態為【關】時方塊在左邊。小方塊的寬度是控制元件寬度的 1/3,高度與控制元件相同。要顯式呼叫 setHeight 方法設定矩形高度(因為它預設為0)。記得小方塊的左上角的 X 座標也要調整的。
下面是視窗 MyWindow 的成員。
class MyWindow : public QRasterWindow { Q_OBJECT public: MyWindow(QWindow *parent = nullptr); private: MyControl *_control; void initUI(); protected: void paintEvent(QPaintEvent *event) override; };
重寫的 paintEvent 方法負責畫視窗的背景。私有成員 initUI 用於初始化子視窗(MyControl物件)。initUI 方法在建構函式中呼叫。
MyWindow::MyWindow(QWindow *parent) :QRasterWindow::QRasterWindow(parent) { initUI(); } void MyWindow::initUI() { // 初始化視窗 setTitle("示例程式"); resize(550, 450); setMinimumSize(QSize(340, 200)); _control = new MyControl(this); // 初始化子視窗 _control->setPosition(35, 40); _control->resize(120, 35); _control->setVisible(true); }
setVisible 方法使用控制元件變為可見,只有可見的物件才能被看到。
paintEvent 方法只負責畫視窗背景。
void MyWindow::paintEvent(QPaintEvent *event) { // 只填充視窗 QPainter painter(this); painter.fillRect(event->rect(), QColor("blue")); painter.end(); }
最後寫 main 函式。
int main(int argc, char** argv) { QGuiApplication app(argc, argv); MyWindow wind; wind.show(); return QGuiApplication::exec(); }
執行後,點一下子視窗,它就會改變顏色。
關於控制元件周圍有白邊(或黑邊)的問題:
這個問題比較不確定,不同平臺好像表現不一樣。Windows 下在控制元件周圍會多出白色區域(也可能是黑色);在 Debian 下沒有出現白邊。所以,為了儘可能地避免跨平臺差異導致的問題,可以用 mask 讓控制元件區域之外的地方變為透明,這樣白邊黑邊就會消失。
呼叫 setMask 方法要在重寫的 resizeEvent 方法中進行,這是為了響應控制元件大小改變後,及時調整要透明的區域。
void MyControl::resizeEvent(QResizeEvent *event) { setMask(QRect(QPoint(), event->size())); }
剛才,我們們 MyControl 類定義了 stateChanged 訊號,並在修改控制元件狀態後發出。接下來我們用一個 lambda 表示式來充當 slot,當控制元件狀態改變後輸出除錯資訊(使用 qDebug 宏)。
connect(_control, &MyControl::stateChanged, this, [](bool st){ qDebug() << "當前狀態:" << st; });
再次執行程式,每點選一下控制元件,控制檯就會輸出一條除錯資訊。
setMask 還有一個經典用途——製作透明視窗。下面我們們順便討論一下如何做不規則形狀的視窗。
a、呼叫 setFlags 方法設定 WindowType::FramelessWindowHint 標誌,去掉視窗的邊框;
b、呼叫 setMask 方法時需要傳遞一個 QRegion 物件,它表示一個形狀區域。這個區域可以是矩形,也可以是圓形,當然也可以是多邊形。設定 mask 後,指定區域外的內容變成透明,並且不會響應使用者的輸入事件(滑鼠、鍵盤等)。QRegion 物件可以進行 and、or 等運算,多個區域可以進行交集或並集處理,形成各種不規則圖形。
我們位做個例子。
#ifndef APP_H #define APP_H #include <QColor> #include <QSize> #include <QWindow> #include <QRasterWindow> #include <QPainter> #include <QRect> #include <QPaintEvent> #include <QRegion> #include <QList> #include <QPolygon> #include <QPoint> #include <QResizeEvent> class CustWindow : public QRasterWindow { Q_OBJECT public: CustWindow(QWindow *parent = nullptr); protected: void paintEvent(QPaintEvent* event) override; void resizeEvent(QResizeEvent* event) override; // 視窗沒了邊框無法用常規操作,於是實現雙擊關閉視窗 void mouseDoubleClickEvent(QMouseEvent *event) override; }; #endif
CustWindow::CustWindow(QWindow* parent) :QRasterWindow::QRasterWindow(parent) { setFlags(Qt::WindowType::FramelessWindowHint|Qt::WindowType::BypassWindowManagerHint); // 視窗位置和大小 setGeometry(600, 500, 400, 400); } void CustWindow::paintEvent(QPaintEvent* event) { QPainter p(this); p.fillRect(event->rect(), QColor("deeppink")); p.end(); } void CustWindow::resizeEvent(QResizeEvent *event) { QRegion rg0(QRect(QPoint(), event->size())); QList<QPoint> pt1 = { {15,16}, {180, 300}, {320, 300}, {150, 57} }; QPolygon pl1(pt1); QRegion rg1(pl1); QList<QPoint> pt2 = { {315, 20}, {25, 160}, {160, 320}, {240, 150}, {330, 30} }; QPolygon pl2(pt2); QRegion rg2(pl2); QRegion rg3 = rg0 & (rg1 | rg2); setMask(rg3); } void CustWindow::mouseDoubleClickEvent(QMouseEvent *event) { if(event->button() == Qt::LeftButton) { this->close(); // 關閉視窗 } }
呼叫setFlags方法時,用到了 WindowType::BypassWindowManagerHint 值。Windows 下沒有影響,主要是針對 X11,去除第三方主題產生的視窗陰影。
在 resize 事件處理程式碼中,QPolygon 類用來構建多邊形,透過 QList<QPoint> 物件來指定頂點列表。上面程式碼中是兩個不規則圖形並集後,再與視窗的矩形區域取交集。最後把這個區域外的內容都設定成了透明。
執行效果如下。