【Qt6】QWidgetAction 的使用

在開始主題前,先看一個 C++ 例子:

#include <iostream>

struct Data
{
    int a;
    int b;
};

// 注意這裏
struct Data *s;

void doSome()
{
    Data k;
    k.a = 100;
    k.b = 300;
    // 注意這裏,會出大事
    s = &k;
}

int main()
{
    // 先調用了函數
    doSome();
    // 再輸出 Data 結構體的內容
    std::cout << "a = " << s->a << '\n';
    std::cout << "b = " << s->b << '\n';
    return 0;
}

不要問這個例子的功能,問就是超能力。其實這個例子沒啥功能,純粹是爲了運行後出錯而寫的。有同學會疑惑:這程序好像沒啥問題。嗯,看着是沒啥問題,我們預期的情況是:a 的值是 100,b 的值是 300。

遺憾的是,運行結果是這樣的:

a = -858993460
b = -858993460

啥玩意兒?下面咱們就扒一下到底哪裏出事了。

這個例子先定義了一個結構體叫 Data,裏面有兩個字段 a、b。然後聲明 Data 類型的指針變量,在 doSome 函數中讓變量 s 引用了一個 Data 實例的實例。在 main 函數中,先調用 doSome 函數,然後再輸出 a、b 的值。這裏就出現一個問題了:s 引用的 k 是在 doSome 函數內創建的,而且它的數據分配在棧上,當 doSome 函數執行結束時,k 的生命週期也差不多了。當調用 doSome 函數之後訪問 s,此時 s 所指向的對象已經沒有了,所以 a、b 輸出的是一個“髒”的值。

若是把 k 改爲 static,那結果就不一樣了。

void doSome()
{
    static Data k;
    k.a = 100;
    k.b = 300;
    // 注意這裏,會出大事
    s = &k;
}

控制檯將輸出:

a = 100
b = 300

如果你不相信上述現象,也可以把例子改成這樣:

#include <iostream>

class Test
{
public:
    Test()
    {
        std::cout << "Test 構造函數 ..." << std::endl;
    }

    ~Test()
    {
        std::cout << "Test 析構函數 ..." << std::endl;
    }
    int a,b;
};

// 注意這裏
Test *s;

void doSome()
{
    Test k;
    k.a= 100;
    k.b = 300;
    // 注意這裏,會出大事
    s = &k;
}

int main()
{
    // 先調用了函數
    std::cout << "調用doSome函數前\n";
        doSome();
    std::cout << "調用doSome函數後\n";
    // 再輸出a、b的內容
    std::cout << "a = " << s->a << '\n';
    std::cout << "b = " << s->b << '\n';
    return 0;
}

運行上述代碼,得到的輸出爲:

Test 構造函數 ...
Test 析構函數 ...
調用doSome函數後
a = -858993460
b = -858993460

這樣就能清楚地知道,s 引用的對象在退出 doSome 函數之前就已經析構了。除了使用 static 關鍵字外,也可以讓 Test 對象分配在堆上。

void doSome()
{
    Test *k = new Test;
    k->a = 100;
    k->b = 300;
    // 複製的是地址,不是對象
    s = k;
}

把 k 賦值給 s,只是把指向的地址複製一遍罷了,對象實例並沒有複製。棧上的數據會因變量的生命週期而被回收,但堆上的東西需要 delete。所以,在調用完 doSome 函數後,堆上的東西還在,所以輸出的 a、b 值不會“髒”。按理說,s 用完了應該 delete 的,不過,我沒寫 delete 語句,畢竟這裏 main 函數馬上就執行完了,程序都結束了,堆上的東西早沒了,所以,這裏就偷偷懶吧,不必管它。

下面再來看一個 Qt 程序:

#include <QWidget>
#include <QApplication>
#include <QVBoxLayout>
#include <QPushButton>


int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    // 創建兩個按鈕
    QPushButton btnA("Yes");
    QPushButton btnB("No");
    // 創建頂層窗口
    QWidget window;
    
    // 構建對象樹
    btnA.setParent(&window);
    btnB.setParent(&window);
    // 設置按鈕在窗口中的位置
    btnA.move(28, 30);
    btnB.move(28, 75);

    // 顯示窗口
    window.show();

    return QApplication::exec();
}

上述程序也是一個有問題的程序,但它能運行,只是在關閉窗口時報錯。

Unhandled exception at 0x00007FFDD029C1F9 (ntdll.dll) in myapp.exe: 0xC0000374: 堆已損壞。 (parameters: 0x00007FFDD03118A0).

這個問題和第一個例子的有點像但又不完全一樣。這個 Qt 程序是一個經典錯誤,問題出在兩個 QPushButton 對象被析構了兩次。由於所有變量都是在棧上分配的,上述程序的壓入順序是 btnA - btnB - window。按照後進先出的規則,window 變量是最新定義的,它首先發生析構。由於 btnA、btnB 調用了 setParent 方法設置了對象樹關係,當 window 析構時會刪除 btnA、btnB。又因變量生命週期的原因,在 window 析構之後,btnA 和 btnB 又發生析構(可剛纔 window 讓它們析構過了)。

解決方法:1、調整聲明變量的順序,先聲明 window 變量,再聲明其他變量;2、用指針。

下面代碼改爲用指針類型。

#include <QWidget>
#include <QApplication>
#include <QVBoxLayout>
#include <QPushButton>


int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    // 創建兩個按鈕
    QPushButton *btnA = new QPushButton("Yes");
    QPushButton *btnB = new QPushButton("No");
    // 創建頂層窗口
    QWidget *window = new QWidget;
    
    // 構建對象樹
    btnA->setParent(window);
    btnB->setParent(window);
    // 設置按鈕在窗口中的位置
    btnA->move(28, 30);
    btnB->move(28, 75);

    // 顯示窗口
    window->show();

    return QApplication::exec();
}

這裏咱們也不需要 delete,畢竟窗口和兩個按鈕在應用程序運行期間它們都必須存在的,只到了程序退出時才銷燬,那就沒必要 delete 了。

所以說:

1、不是所有指針變量都要 delete 的,因爲它引用的可能不是堆上的對象,沒準是棧上的對象;

2、不是所有 new 出來的對象就非要 delete 不可,主要看它的生命週期是否該結束。如果是短暫使用的,在應用程序運行期間不需要一直存在的,用完就要 delete。有些 new 出來的對象可能要傳遞給其他對象用,並由它們負責釋放,那也不需要 delete,比如包裝剪貼板數據的 QMimeData 類。

==========================================================================

好了,以上一大段內容就當作科普,正片現在纔開始。本篇咱們看一下特殊的 QAction 類——QWidgetAction。看名字也可以聯想到,它是可以把一個 QWidget 用作 action 的類。這個有什麼用呢?作用就是你可以在菜單裏做些交互功能。

 QWidgetAction 類有兩種用法:

1、直接用,這是最簡單方法。實例化後調用 setDefaultWidget 方法設置一個 widget;

2、派生出子類,重寫 createWidget 方法,創建你需要的組件對象。

先看第一種用法,非常好辦,你想在菜單項上顯示什麼組件就創建它,然後調用 setDefaultWidget 方法就行了。

// 頭文件
#ifndef APP_H
#define APP_H

#include <QMainWindow>
#include <QWidget>
#include <QAction>
#include <QSpinBox>
#include <QMenu>
#include <QMenuBar>
#include <QWidgetAction>

class MyWindow : public QMainWindow
{
public:
    MyWindow();
};

#endif
/*---------------------------------------------*/
// 代碼文件
MyWindow::MyWindow()
    :QMainWindow((QWidget*)nullptr)
{
    // 創建菜單欄
    QMenuBar *menubar = this->menuBar();
    // 創建菜單
    QMenu *menu = menubar->addMenu("應用程序");
    // 添加兩個普通action,意思一下
    menu->addAction("打開文件");
    menu->addAction("關閉文件");
    // 下面纔是主角
    QWidgetAction *widgetAct = new QWidgetAction(menu);
    // 創建一個數字組件
    QSpinBox *spinbox = new QSpinBox;
    // 設置一下有效範圍
    spinbox->setRange(0, 1000);
    // 設置當前值
    spinbox->setValue(250);
    // 設置爲 QWidgetAction 的默認組件
    widgetAct->setDefaultWidget(spinbox);
    // 把action添加到菜單中
    menu->addAction(widgetAct);
}

應用程序窗口繼承了 QMainWindow 類,因爲這個類比較方便構建菜單欄、工具欄、狀態欄、停靠欄。咱們用它來創建一個菜單欄對象(QMenuBar),然後添加一個叫“應用程序”的菜單(QMenu)。

“應用程序”菜單的前兩個菜單項是普通的 action,第三個是 QWidgetAction 對象。在 new 出 QWidgetAction 後,先初始化一下 QSpinBox 組件,然後調用 setDefaultWidget 方法,這樣 QSpinBox 組件就能顯示在菜單項上了。

在 main 函數中顯示主窗口。

int main(int argc, char** argv)
{
    QApplication app(argc, argv);
    MyWindow *win = new MyWindow;
    win->setWindowTitle("自定義菜單項");
    win->resize(450, 400);
    win->show();
    return QApplication::exec();
}

好了,見證奇蹟的時候到了,看看效果。

 

另一種用法,就是從 QWidgetAction 類派生。然後重寫這個方法:

QWidget *createWidget(QWidget *parent);

parent 是父級對象,由調用者傳遞,這取決於這個自定義的 action 用在什麼容器上了,如果用在菜單上,就是 QMenu 對象。返回值就是創建的自定義組件了。

另外,如果在析構自定義組件時有特殊處理,還可以重寫 delete 方法。

void deleteWidget(QWidget *widget);

widget 參數是要被刪除的自定義組件實例。如果無其他要實現的需求,沒必要重寫它。

下面咱們來個示例:自定義組件做個帶三個滑塊的界面。組件名稱爲 CustWidget,基類是 QFrame。選擇 QFrame 作爲基類是方便設置邊框。

// 頭文件
#ifndef CUSTWIDGET_H
#define CUSTWIDGET_H
#include <QWidget>
#include <QFrame>

class CustWidget: public QFrame
{
public:
    CustWidget(QWidget* parent = nullptr);
private:
    void initUI();
};
#endif

// 代碼文件
#include "custWidget.h"
#include <QFormLayout>
#include <QSlider>

CustWidget::CustWidget(QWidget *parent)
    :QFrame::QFrame(parent)
{
    this->initUI();
}

void CustWidget::initUI()
{
    // 創建佈局
    QFormLayout* layout = new QFormLayout(this);
    // 創建三個滑條
    QSlider* slider1 = new QSlider;
    slider1->setRange(0,255);   // 有效範圍
    QSlider* slider2 = new QSlider;
    slider2->setRange(0,255);
    QSlider* slider3 = new QSlider;
    slider3->setRange(0,255);
    // 設置滑條的方向是水平方向
    slider1->setOrientation(Qt::Horizontal);
    slider2->setOrientation(Qt::Horizontal);
    slider3->setOrientation(Qt::Horizontal);
    // 把它們添加到佈局中
    layout->addRow("Red:", slider1);
    layout->addRow("Green:", slider2);
    layout->addRow("Blue:", slider3);
    // 設置邊框爲面板
    this->setFrameShape(QFrame::Panel);
}

滑塊條是 QSlider 組件,它默認的方向是垂直的,所以要將方向設定爲水平。自定義組件還用到了 QFormLayout 類,它是佈局類,類似 HTML Form 元素的佈局方式,即表單。一般分爲兩列,左列是字段標題,右列是字段內容。

CustWidget 組件定義好了,接下來就是 MyWidgetAction 類,派生自 QWidgetAction。

// 頭文件
#ifndef MYWIDGETACTION_H
#define MYWIDGETACTION_H

#include <QWidgetAction>
#include "custWidget.h"

class MyWidgetAction : public QWidgetAction
{
public:
    MyWidgetAction(QObject *parent);

protected:
    QWidget *createWidget(QWidget *parent) override;
};

#endif

// 代碼文件
#include "myWidgetAction.h"

MyWidgetAction::MyWidgetAction(QObject *parent)
    :QWidgetAction::QWidgetAction(parent)
{
}

QWidget *MyWidgetAction::createWidget(QWidget *parent)
{
    CustWidget* w = new CustWidget(parent);
    return w;
}

整體邏輯很簡單,就是返回 CustWidget 的實例。

 

然後咱們在前面 QWidgetAction 的示例上再添加一個菜單項,使用咱們剛定義的 MyWidgetAction。

MyWindow::MyWindow()
    :QMainWindow((QWidget*)nullptr)
{
    // 創建菜單欄
    QMenuBar *menubar = this->menuBar();
    // 創建菜單
    QMenu *menu = menubar->addMenu("應用程序");
    ……
    // 下面這個是自定義的
    MyWidgetAction *custAct = new MyWidgetAction(menu);
    menu->addAction(custAct);
}

最後,咱們來看看效果。

這效果不錯吧。

好了,今天就水到這裏了,有空咱們繼續聊。

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