【Qt6】嵌套 QWindow

在上個世紀的文章中,老周簡單介紹了 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> 對象來指定頂點列表。上面代碼中是兩個不規則圖形並集後,再與窗口的矩形區域取交集。最後把這個區域外的內容都設置成了透明。

運行效果如下。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章