在開始主題前,先看一個 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); }
最後,咱們來看看效果。
這效果不錯吧。
好了,今天就水到這裡了,有空咱們繼續聊。