• 【Qt6】QWidgetAction 的使用


    在开始主题前,先看一个 C++ 例子:

    复制代码
    #include 
    
    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 
    
    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 
    #include 
    #include 
    #include 
    
    
    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 
    #include 
    #include 
    #include 
    
    
    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 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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 
    #include 
    
    class CustWidget: public QFrame
    {
    public:
        CustWidget(QWidget* parent = nullptr);
    private:
        void initUI();
    };
    #endif
    
    // 代码文件
    #include "custWidget.h"
    #include 
    #include 
    
    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 
    #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);
    }
    复制代码

    最后,咱们来看看效果。

    这效果不错吧。

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

  • 相关阅读:
    LeetCode单周赛第320场 && AcWing周赛第78场总结
    【脉冲通信】用于空间应用的飞秒脉冲通信的符号误码率模型研究(Matlab代码实现)
    C语言实现根据用户输入的整数求和(两种方法)
    树莓派(七)文件系统及其目录结构、虚拟文件系统
    spring boot集成MockMvc进行集成测试小案例
    vue基础 —— 单网页版的Vue学习 基础
    java版工程管理系统Spring Cloud+Spring Boot+Mybatis实现工程管理系统源码
    Ubuntu18.04安装redis与启动
    如何解决maven依赖冲突?
    内网安全学习
  • 原文地址:https://www.cnblogs.com/tcjiaan/p/17608491.html