【Qt6】QWidgetAction 的使用

来源:https://www.cnblogs.com/tcjiaan/archive/2023/08/07/17608491.html
-Advertisement-
Play Games

在開始主題前,先看一個 C++ 例子: #include <iostream> struct Data { int a; int b; }; // 註意這裡 struct Data *s; void doSome() { Data k; k.a = 100; k.b = 300; // 註意這裡,會 ...


在開始主題前,先看一個 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);
}

最後,咱們來看看效果。

這效果不錯吧。

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ## 前言 假設一個場景,服務端部署在內網,客戶端需要通過暴露在公網的nginx與服務端進行通信。為了避免在公網進行 http 明文通信造成的信息泄露,nginx與客戶端之間的通信應當使用 https 協議,並且nginx也要驗證客戶端的身份,也就是mTLS雙向加密認證通信。 這條通信鏈路有三個角色 ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本篇是《quarkus依賴註入》系列的第 ...
  • ## Mybatis ### 舉個小慄子 mybatis配置文件(XML配置文件) ```java ``` user.xml(實現增刪改查的sql語句) ```xml insert into user values (#{userId},#{username},#{password}) delete ...
  • [toc] > 技術和工具「!喜新厭舊」 # 一、背景 最近在一個輕量級的服務中,嘗試了最新的技術和工具選型; 即`SpringBoot3`,`JDK17`,`IDEA2023`,`Navicat16`,雖然新的技術和工具都更加強大和高效,但是適應採坑的過程總是枯燥的; 【環境一覽】 ![](htt ...
  • 多態是Java語言極為重要的一個特性,可以說是Java語言動態性的根本,那麼線程執行一個方法時到底在記憶體中經歷了什麼,JVM又是如何確定方法執行版本的呢? ...
  • ## 教程簡介 Ext JS是一個流行的JavaScript框架,它為使用跨瀏覽器功能構建Web應用程式提供了豐富的UI。 Ext JS基本上用於創建桌面應用程式它支持所有現代瀏覽器,如IE6 +,FF,Chrome,safari 6+ 等。Ext JS基於MVC / MVVM架構。 最新版本的Ex ...
  • 本文翻譯自國外論壇 medium,原文地址:https://medium.com/@raviyasas/spring-boot-best-practices-for-developers-3f3bdffa0090 Spring Boot 是一種廣泛使用且非常流行的企業級高性能框架。以下是一些最佳實踐 ...
  • 對於個人建站來說,WordPress相信很多讀者都知道了。但WordPress很多時候我們還是用來建立自主發佈內容的站點為主,適用於個人博客、企業主站等。雖然有的主題可以把WordPress變為論壇,但效果並不是很好。 所以,今天給大家推薦一個開源的論壇項目: [**vanilla**](https ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...