【Qt6】巢狀 QWindow

東邪獨孤發表於2023-05-02

在上個世紀的文章中,老周簡單介紹了 QWindow 類的基本使用——包括從 QWindow 類派生和從 QRasterWindow 類派生。

其實,QWindow 類並不是只能充當主視窗用,它也可以巢狀到父級視窗中,變成子級物件。我們們一般稱之為【控制元件】。F 話不多講,下面我們們用實際案例來說明。

這個例子中老周定義了兩個類:

MyControl:子視窗物件,充當控制元件角色。這裡實現一個類似開關的控制元件。【關閉】狀態下,控制元件的背景呈現為灰色,金色方塊位於最左側;當控制元件處於【開啟】狀態下,控制元件背景為紅色,金色方塊位於最右側。
MyWindow:作為視窗使用,裡面包含 MyControl 物件。
先看 MyControl 類。
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> 物件來指定頂點列表。上面程式碼中是兩個不規則圖形並集後,再與視窗的矩形區域取交集。最後把這個區域外的內容都設定成了透明。

執行效果如下。

 

相關文章