在开始主题前,先看一个 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);
}

最后,咱们来看看效果。

这效果不错吧。

好了,今天就水到这里了,有空咱们继续聊。

【Qt6】QWidgetAction 的使用的更多相关文章

  1. QT6 源码杂记

    菜鸡一个,随便写写,勿喷.好记性不如烂笔头. 了解qt,第一个绕不过的坎便是qt的元对象系统 QMetaObject. 1 class Object : public QObject 2 { 3 Q_ ...

  2. 用VS Code搞Qt6:编译源代码与基本配置

    先说明一下,本水文老周仅讨论新版的 Qt 6,旧版的 Qt 不讨论. 尽管 Qt 有自己的开发环境,但老周必须说句不装逼的话:真的不好用.说起写代码,当然了,用记事本也能写.但是,有个高逼格的工具,写 ...

  3. 用VS Code搞Qt6:编译附加模块

    上一次水文中,老周所介绍的是编译 Qt 的基础模块-- qtbase.一次性编译所有代码可以一劳永逸,但体积相当大,编译时间较长,CPU负载大发热大,风扇转得猛,电费交得多.因此老周更喜欢分开来编译. ...

  4. 用VS Code搞Qt6:至简窗口部件——QWidget

    在正题开始之前,老周照例扯点别的.嗯,咱们扯一下在 VS 2022 下结合 CMake 开发 Qt6 时的环境变量设置问题.在VS Code 中,通够通过 CMake Tools 扩展的配置来设置环境 ...

  5. 用 VS Code 搞 Qt6:信号、槽,以及QObject

    Qt 里面的信号(Signal)和槽(Slot)虽然看着像事件,但它实际上是用来在两个对象之间进行通信的.既然是通信,就会有发送者和接收者. 1.信号是发送者,触发时通过特有的关键字"emi ...

  6. 用 VS Code 搞 Qt6:让信号和槽自动建立连接

    Qt 具备让某个对象的信号与符合要求的槽函数自动建立连接.弄起来也很简单,只要调用这个静态方法即可: QMetaObject::connectSlotsByName(...); connectSlot ...

  7. Qt6.2 在Ubuntu20下提示 C++ 和 CMake 错误

    Qt6.2 在Ubuntu20下提示 CMake No CMake configuration found apt install libgl-dev 即可! 先是C++提示没有找到C++编译器,需要 ...

  8. 【原创】2022年linux环境下QT6不支持中文输入法解决方案

    1.配置环境 export PATH="~/目录/Qt/6.x.x/gcc_64/bin":$PATH export PATH="~/目录/Qt/Tools/Cmake/ ...

  9. vs2019 配置 qt6

    1.下载qt6 我的目录C:\Qt\6.3.1\msvc2019_64\bin C:\Qt\6.3.1\msvc2019_64\include C:\Qt\6.3.1\msvc2019_64\lib ...

  10. 【Qt6】QWindow类可以做什么

    原来的水文标题是"用 VS Code 搞 Qt6",想想还是直接改为"Qt6",反正这个用不用 VS Code 也能搞.虽然我知道大伙伴们都很讨厌 CMake, ...

随机推荐

  1. 用go设计开发一个自己的轻量级登录库/框架吧(项目维护篇)

    用go设计开发一个自己的轻量级登录库/框架吧(项目维护篇) 本篇将开始讲讲开发库/框架的最开始阶段,也就是搭建一个项目 源码:weloe/token-go: a light login library ...

  2. 2022-10-31:以下go语言代码输出什么?A:map[];B:nil;C:Panic;D:编译错误。 package main import “fmt“ func main() {

    2022-10-31:以下go语言代码输出什么?A:map[]:B:nil:C:Panic:D:编译错误. package main import "fmt" func main( ...

  3. Redis实战解读-初识Redis&Redis基本数据类型

    Redis实战解读 一.初识Redis 1.什么是Redis ​ Redis是一个速度非常快的非关系型数据库(non-relational database),它可以存储键(key)与五种不同类型的值 ...

  4. drf-spectacular

    介绍 drf-spectacular是为Django REST Framework生成合理灵活的OpenAPI 3.0模式.它可以自动帮我们提取接口中的信息,从而形成接口文档,而且内容十分详细,再也不 ...

  5. MMCM and PLL Dynamic Reconfiguration

    Reconfiguration is performed through the DRP. The DRP provides access to the configuration bits that ...

  6. 图数据库 NebulaGraph 的内存管理实践之 Memory Tracker

    数据库的内存管理是数据库内核设计中的重要模块,内存的可度量.可管控是数据库稳定性的重要保障.同样的,内存管理对图数据库 NebulaGraph 也至关重要. 图数据库的多度关联查询特性,往往使图数据库 ...

  7. 【Java】连接MySQL问题总结

    前言 最近在学习Java的数据库相关操作,在看视频时自己找资源而产生的一些问题,在此做个总结. 正文 在刚开始学习的时候,你可能跟着老师这样写代码,虽然某些地方已经冒出了红色的波浪线,但你半信半疑的继 ...

  8. PHP反序列化常用魔术方法

    PHP反序列化 php序列化(serialize):是将变量转换为可保存或传输的字符串的过程 php反序列化(unserialize):就是在适当的时候把这个字符串再转化成原来的变量使用 PHP反序列 ...

  9. 代码随想录算法训练营Day50 动态规划

    代码随想录算法训练营 代码随想录算法训练营Day50 动态规划| 123.买卖股票的最佳时机III 188.买卖股票的最佳时机IV 123.买卖股票的最佳时机III 题目链接:123.买卖股票的最佳时 ...

  10. dockder 学习第一篇

    1 docker安装 1 yum包的更新到最新 yum update 2 安装需要软件包,yum-util [root@localhost ~]# yum install -y yum-utils d ...