5

【VS Code 与 Qt6】运用事件过滤器批量操作子级组件 - 东邪独孤

 1 year ago
source link: https://www.cnblogs.com/tcjiaan/p/17471085.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【VS Code 与 Qt6】运用事件过滤器批量操作子级组件

如果某个派生自 QObject 的类重写 eventFilter 方法,那它就成了事件过滤器(Event Filter)。该方法的声明如下:

virtual bool eventFilter(QObject *watched, QEvent *event);

watched 参数是监听事件的对象,即事件的接收者;event 参数当然就是待处理的事件了。事件过滤器(也可以翻译为“筛选器”)可在接收者之前拦截事件,处理完毕后还可以决定是否把事件转发给接收者。如果不想转发给事件接收者,就返回 true;若还想让事件继续传播就返回 false。

这玩意儿最有益的用途就是:你的顶层窗口上有 K 个子级组件(正常情形是 QWidget 的子类),如果组件没有定义你想用的信号,只能通过处理事件的途径解决,可你又不想只为了处理一个事件就派生一个类(比如,QLabel组件在鼠标悬浮时做点事情),就可以用上事件过滤器了。顶层窗口类重写 eventFilter 方法,拦截发往子组件的事件(如mouseMove)直接处理,这样能节省 N 百行代码。

重写了 eventFilter 方法的类就成了事件的过滤者,而调用 installEventFilter 方法安装过滤器的类才是事件的原始接收者。就拿上文咱们举的 QLabel 组件的例,假设顶层窗口的类名是 DuckWindow,那么,DuckWindow 重写 eventFilter 方法,它就是事件的拦截者;而 QLabel 组件就是事件的原始接收者,所以,调用 installEventFilter 方法的是它。即 QLabel::installEventFilter( DuckWindow )。

不知道老周这样说大伙伴们能否理解。就是负责过滤事件的对象重写 eventFilter 方法;被别人过滤的对象才调用 installEventFilter 方法。

我们用示例说事。下面咱们要做的练习是这样的:

我定义了一个类叫 MyWindow,继承 QWidget 类,作为顶层窗口。然后在窗口里,我用一个 QHBoxLayout 布局,让窗口内的子级组件水平排列。但每个子组件的颜色不同。常规做法是写个自定义组件类,从构造函数或通过成员函数传一个 QColor 对象过去,然后重写 paintEvent 方法绘图。这种做法肯定没问题的。但是!我要是不想写自定义类呢,那就得考虑事件过滤器了,把 paintEvent 事件过滤,直接用某颜色给子组件画个背景就行了。

头文件声明 MyWindow 类。

#ifndef MYWIN
#define MYWIN

#include <QWidget>
#include <QHBoxLayout>
#include <QPainter>
#include <QEvent>
#include <QColor>
#include <QRect>

class MyWindow : public QWidget
{
    Q_OBJECT
public:
    MyWindow(QWidget* parent=nullptr);
    bool eventFilter(QObject *obj, QEvent *event) override;
private:
    // 私有成员,画痘痘用的
    void paintSomething(QPainter *p, const QColor &color, const QRect &paintRect);
    // 布局
    QHBoxLayout *layout;
    // 三个子级组件
    QWidget *w1, *w2, *w3;
}; 

#endif

这里提一下这个 eventFilter 方法,这厮声明为 public 和 protected 都是可行的。老周这里就声明为 public,与基类的声明一致。

paintSomething 是私有方法,自定义用来画东西的。有伙伴们会问:QPainter 的 paintDevice 不是可以获取到绘图设置(这里指窗口或组件)的大小的矩形区域吗,为啥要从参数传个 QRect?因为这个 rect 来自 QPaintEvent 对象的事件参数,它指的可不一定窗口/组件的整个矩形区域。如果是局部重绘,这个矩形可能就是其中一小部分区域。所以,咱们用事件传递过来的矩形区域绘图。

窗口布局用的是 QHBoxLayout,非常简单的布局方式,子级组件在窗口上水平排列。

下面代码实现构造函数,初始化各个对象。

MyWindow::MyWindow(QWidget *parent)
    : QWidget(parent)
{
    // 初始化
    layout = new QHBoxLayout;
    this->setLayout(layout);
    w1 = new QWidget(this);
    w2 = new QWidget(this);
    w3 = new QWidget(this);
    layout->addWidget(w1);
    layout->addWidget(w2);
    layout->addWidget(w3);
    // 安装事件过滤器
    w1->installEventFilter(this);
    w2->installEventFilter(this);
    w3->installEventFilter(this);
}

只有在被拦截的对象上调用 installEventFilter 方法绑定过滤器后,事件过滤器才会生效。此处,由于 MyWindow 类重写了 eventFilter 方法,所以过滤器就是 this。

下面是 eventFilter 方法的实现代码,只过滤 paint 事件即可,其他传给基类自己去玩。

bool MyWindow::eventFilter(QObject *obj, QEvent *event)
{
    // 如果是paint事件
    // 这里“与”判断事件接收者是不是在那三个子组件中
    // 防止有其他意外对象出现
    // 不过这里不会发生,因为只有install了过滤器的对象才会被拦截事件
    if(event->type() == QEvent::Paint
        && (obj==w1 || obj==w2 || obj==w3))
    {
        QPaintEvent* pe = static_cast<QPaintEvent*>(event);
        QWidget* uiobj = static_cast<QWidget*>(obj);
        QPainter painter;
        // 注意这里,绘图设备不是this了,而是接收绘图事件的对象
        // 由于它要求的类型是QPaintDevcie*,所以要进行类型转换
        // 转换后的uiobj变量的类型是QWidget*,传参没问题
        painter.begin(uiobj);
        if(w1 == uiobj)
        {
            // 红色
            paintSomething(&painter, QColor("red"), pe->rect());
        }
        if(w2 == uiobj)
        {
            // 橙色
            paintSomething(&painter, QColor("orange"), pe->rect());
        }
        if(w3 == uiobj)
        {
            // 紫色
            paintSomething(&painter, QColor("purple"), pe->rect());
        }
        painter.end();
        return true;
    }
    return QWidget::eventFilter(obj, event);
}

拦截并处理了 paint 事件后,记得返回 true,这样事件就不会传给目标对象了(咱们帮它处理了,不必再重复处理,毕竟 QWidget 类默认的 paint 事件是啥也不做)。

下面代码是 paintSomething 方法。只是画了颗巨型青春痘……哦不,是一个椭圆。

void MyWindow::paintSomething(QPainter *p, const QColor &color, const QRect &paintRect)
{
    // 设置画刷
    p->setBrush(QBrush(color));
    // 无轮廓
    p->setPen(Qt::NoPen);
    // 画椭圆
    p->drawEllipse(paintRect);
}

setPen中设定 NoPen 是为了在绘制圆时去掉轮廓,默认会画上轮廓线的。

最后,该到 main 函数了。

int main(int argc, char **argv)
{
    QApplication app(argc,argv);
    MyWindow wind;
    // 窗口标题
    wind.setWindowTitle("干点杂活");
    // 调整窗口大小
    wind.resize(321, 266);
    wind.show();
    return QApplication::exec();
}

运行一下,看,横躺着三颗痘痘,多好看。

367389-20230611121204009-1867696947.png

再来一例,这次咱们拦截的是窗口的 close 事件,当窗口要关闭的时候,咱们输出一条调试信息。

#ifndef 奶牛
#define 奶牛

#include <QObject>
#include <QEvent>

class MyFilter : public QObject
{
protected:
    bool eventFilter(QObject *obj, QEvent *e) override;
};

#endif

这次我们不从任何可视化类型派生,而是直接派生自 QObject 类。这里只是重写 eventFilter 方法,没有用到信号和 cao,所以,可以不加 Q_OBJECT 宏。也就是说咱们这个过滤器是独立用的,不打算加入到 Qt 的对象树中。

下面是实现代码:

bool MyFilter::eventFilter(QObject *obj, QEvent *e)
{
    if(e->type() == QEvent::Close)
    {
        // 此处要类型转换
        QWidget* window = qobject_cast<QWidget*>(obj);
        // 看看这货是不是窗口(有可能是控件)
        if(window->windowFlags() & Qt::Window)
        {
            // 获取这个窗口的标题
            QString title = window->windowTitle();
            // 输出调试信息
            qDebug() << "正在关闭的窗口:" << title;
        }
    }
    // 事件继续传递
    return false;
}

最好返回 false,把事件继续传递给窗口,毕竟窗口可能在关闭时要做一些重要的事,比如保存打开的文件。QWidget 的 WindowFlags 如果包含 Window 值,表明它是一个窗口。

下面直接写main函数。

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    MyFilter *filter = new MyFilter;
    // 弄三个窗口试试
    QWidget *win1 = new QWidget;
    win1->setWindowTitle("狗头");
    win1->installEventFilter(filter);
    win1->show();

    QWidget *win2 = new QWidget;
    win2->setWindowTitle("鸡头");
    win2->installEventFilter(filter);
    win2->show();

    QWidget *win3 = new QWidget;
    win3->setWindowTitle("鼠头");
    win3->installEventFilter(filter);
    win3->show();

    return QApplication::exec();
    // 可选
    delete filter;
    filter = nullptr;
}

filter 是指针类型,它没有添加到 Qt 对象树中,不会自动清理,在exec返回后用 delete 解决它。在清理时有个好习惯,就是 del 之后把指针变量重设为 null,这样下次再引用变量时不容易产生错误,只要 if(! filter) 就能测出它是空的。

反正程序都退出了,所以此处你也可以让它泄漏一下也无妨。程序挂了后进程空间会被系统收回。

当然,用Qt专供的“作用域”指针也不错,超出作用域自动XX掉。

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    QScopedPointer<MyFilter> filter(new MyFilter);
    // 弄三个窗口试试
    QWidget *win1 = new QWidget;
    win1->setWindowTitle("狗头");
    win1->installEventFilter(filter.data());
    win1->show();

    QWidget *win2 = new QWidget;
    win2->setWindowTitle("鸡头");
    win2->installEventFilter(filter.data());
    win2->show();

    QWidget *win3 = new QWidget;
    win3->setWindowTitle("鼠头");
    win3->installEventFilter(filter.data());
    win3->show();

    return QApplication::exec();
}

QScopedPointer 通过构造函数引用要封装的对象,要访问被封装的指针对象,可以使用 data 成员。

运行之后,会出现三个窗口。逐个关闭,会输出以下调试信息:

367389-20230611164503208-1572955891.png

---------------------------------------------------------------------------------------

最后老周扯点别的。

咱们知道,Qt官方推出 Python for Qt,名曰 PySide。五月份的时候,老周遇到一个问题:PySide6 无法加载 QML 文件,报的错误是加载 dll 失败,找不到指定的模块。

网上的方法都是不行的,首先,Qt 在版本号相同(均为 6.5.1)的情况下,C++是可以正常加载 QML 文件的。不管是生成资源文件还是直接访问文件均可。但 Python 是报错的。这至少说明我的机器上不缺某些 .dll,不然C++代码应该也报错。

接着,老周想是不是Qt官方编译的有问题,于是,我把自己编译的Qt动态库替换 PySide6 里面的动态链接库。报错依旧,那就排除编译的差异性。

那么,老周就想到,就是 Python 的问题了,3.7 到 3.10 几个版本测试也报错;用不同路径建的虚拟环境也报错;更换 Qt 版本(6.0 到 6.5)同样报错。

这时可以直接肯定就是 Python 的问题了。不是版本号的问题,是 Windows 商店安装的 Python 就会报错,非 Windows 商店安装的就正常。

不过,还得再加一句话:想把 Qt 用得 666 还是用 C++ 吧,用 Python 仅适合初学和娱乐。由于 Rust 可以调用 C/C++ 代码,所以你是可以尝试用 Rust 的。Rust 也不是什么鬼自动内存管理,要 GC 用 .NET 就完事了。Rust 的重点是内存安全。看似挺诱人,官方也把牛吹得入木四分。可用了之后(和用 Go 一样的感觉),是真的没 C++ 好用。C++ 能背负上这么多的历史包袱也不是靠吹的。当然 C++ 内存泄漏也没你想的那么恐怖。养成好习惯,作用域短,存放数据不多的对象就直接栈分配就行了;要在不同代码上下文传递对象,或分配的数据较大的,用指针。指针类型的变量,在不要的时候坚决干掉,然后记得设置变量为 nullptr。养成这些好习惯基本没多大问题。

一般代码你写惯了是不会忘记 delete 的,容易遗漏的是庞大复杂的代码之间会共享某些对象,在很多地方会引用到某对象。于是,码着码着就头晕了,就不记得销毁了。

会被多处引用的对象,可以写上注释提醒自己或别人要清理它,或者加个书签。写完代码后去看看书签列表,就会想起有哪些对象还没销毁。代码写复杂了会容易混,经常会访问已清理的对象。于是,不妨在访问指针变量前 if 语句一下,if (ptr),在 bool 表达式中,若指针类型的变量是空会得到 false,非空为 true。这样就可以避免许多低级错误。

哪怕是不常用指针的语言也不见得不出事。C# 里面你要是访问 null 的变量(VB 是 Nothing)也会报那个很经典的错误:“未将对象引用设置到对象的实例”,就是 NullReferenceException。在.NET 代码中你只要看到这货就得明白肯定有某个为 null 的对象被访问了。

C++ 里面,这样写就能实例化 MyClass 类,只是分配在栈上。

MyClass x;

但在 C# 中,初始值是 null,即未初始化的,初始化你还得 new。哦,顺便想起个事,C# 中数据的隐式基类是 Array,所以它是引用类型,初始值也是 null 的,就算你数据组里面的元素是值类型,但数组自身是引用类型。委托也是引用类型。有的刚入门的同学会以为委托是值类型。

C++函数按“引用”传值的话,一般会用到指针、引用参数,如 int *p、const int &a、const char *w(不能改)等,C# 中如果是引用类型,直接声明就行了,如 MyClass x,值类型可以用 ref 关键字,ref int v。

C# 中 int?、double? 等可以让其成为引用类型,你可以类比 C 中的 int* 等。


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK