Qt 笔记

看到什么特别的东西就在这里随便记一下。

信号与槽

语法

#define signals public
#define slots
#define emit

从上面的定义可以看出,signals 是当 public 用的,slots 是加在 public/protected/private 之后的,emit 是加在调用(发射)信号之前的。

用法

emit 一个 signal 去触发 slots,slots 就是回调。 signal 只能返回 void,接受的参数会传给绑定触发的 slots。

connect

connect 函数用于绑定 signal 和 slot,有很多重载(原型)可以选用。我个人更倾向不使用 Qt 提供的宏(SIGNAL 和 SLOT)的版本,主要有如下两种:

// 取成员函数地址
QObject::connect(&ma, &A::s, &mb, &B::x);
// 用 lambda 捕获
QObject::connect(&ma, &A::s, [&]() { mb.x(); });

disconnect

disconnect 与 connect 形式上相仿,用于断开 connect 的链接。

Ref

对象树模型

QObject 对象之间是用对象树的形式组织起来的,这样的好处简单说就是当父节点 destroy 的时候,会递归再去 destory 子节点。于是即使是在堆上创建的 QObject,只要正确管理父子结构(比如关系图中不能有环),所有的内存都会被正确回收;相对地,如果是在栈上建的对象可能反而会出问题,比如:

A a;
B b;
// b.B::~B();
// a.A::~A();

如果是先创建的 a 再创建的 b,那么栈上对象的析构函数的调用顺序会正好相反——先析构 b 再析构 a——即先进后出。这里如果 b 是 a 的父对象,b 会被先析构,并 destroy 其子节点 a,而后 a 又会再次调用其析构,导致二次释放。 ref: https://blog.csdn.net/m0_65635427/article/details/130780280

Qt 的组件/部件整理

QMainWindow 的部件

QMenuBar:菜单栏
QMainWindow::addToolBar():创建工具栏
QStatusBar:状态栏
QDockWidget:停靠部件
核心部件:中心显示的部件都可以作为核心部件

这些栏可以挂具体的内容,比如菜单栏可以添加 Action,也就是菜单栏上的菜单项:

QAction *actionBEV = bar->addAction("BEV");
QAction *actionDSP = bar->addAction("DSP");

常用控件/组件/我也不知道怎么说

QLabel:标签。这个标签不仅可以添加文字,还可以放图片,显示动画
QMessageBox:message box
QDialog:对话框
QTimer:计时器(类似游戏引擎中的那种,参考 Unreal/Godot)
……待补充

特殊设施

QImage:图像
QRect:矩形
QColor:颜色
qDebug() << :debug 输出

智能指针

QPointer<T>:同 T*,必须是 QObject 的子类。与 T* 不同的是多个引用其中一个被销毁时其它的指针不会变成 dangling reference,而是也被销毁
QSharedPointer<T>:同 std::shared_ptr<T>,应该也是要求 T 是 QObject 的子类。同理有附属 QWeakPointer<T> 等价于 std::weak_ptr<T>
QScopedPointer<T>:应该是同 std::unique_ptr<T>,禁用复制构造只能移动(没有考证,但是如果不禁复制就没有意义了),由 RAII 利用 scope 去管理堆内存

注意!与对象树本身的维护机制冲突。

ref: https://www.jianshu.com/p/675878b386e7

Qt 的容器

QString: std::string
QMap: std::map
(所以大概还有 QSet)
QList: std::list
QVector:std::vector

这些类模板的参数都与后面标注的 STL 对应容器相同(除了 allocator 的部分)。

注:在 Qt6 中 QList 就是 QVector 的别名,不再使用链表结构。ref: https://doc.qt.io/qt-6/containers.html#algorithmic-complexity / https://www.qt.io/blog/qlist-changes-in-qt-6

QString

与 std::string 的相互转换

QString q_str = /* ... */;
// to std::string
std::string std_str = q_str.toStdString();

std::string std_str = /* ... */;
// from std::string
QString q_str = QString::fromStdString(std_str);

国际化文本

用 tr 函数包括字符串字面量,可以从多语言的配置文件中(具体要怎么写我还不知道)获取不同语言的版本,这个版本可能是根据系统来的,可能是软件里设置的。

个人猜测 tr 就是 translate。

格式化字符串

直接参考下面的代码一看就懂:

QString("[%1, %2]").arg(x).arg(y);

QMap

contains

QMap 有 STL std::map 到 C++20 才有的 contains 函数,直接判断容器中是否存在 key,遥遥领先。

erase & remove

不同于 STL std::map 中的 erase 用的重载,可以删迭代器位置的元素也可以删给定的 key,QMap 拆出了删 key 的单独做了一个函数叫 remove,与其它语言(如 Java)相仿。 insert & operator[]

QMap 的 insert 直接接收两个参数,不用像 STL 那样需要构造 pair。同时,同样 QMap 也支持 operator[] 重载。

迭代器

STL std::map 的迭代器是 value_type 也就是 std::pair<K, V> 的引用,所以拿 key 和 value 要用 itr->first 和 itr->second,非常反直觉。QMap 的迭代器给的是 key() 和 value() 函数。

事件

QWidget

所有组件的父类 QWidget 有一些定义好常用事件:

keyPressEvent:键盘按键按下事件
keyReleaseEvent:键盘按键松开事件
mouseDoubleClickEvent:鼠标双击事件
mouseMoveEvent:鼠标移动事件
mousePressEvent:鼠标按键按下事件
mouseReleaseEvent:鼠标按键松开事件

只需对这些函数进行重载即可。

事件处理

TODO

事件过滤

TODO

QCPGraph

画图。

setName:设置名称
setPen:设置笔。参数 QPen 可以从 QColor 隐式转换
setLineStyle:设置线风格。接收 QCPGraph::lsXXX
setScatterStyle:设置离散点风格。接收 QCPScatterStyle,可以参考 QCPScatterStyle(QCPScatterStyle::ssDisc, 2) 进行构造

QPainter

异步

并发(QtConcurrent)

函数原型(QtConcurrent::run)

QFuture<T> QtConcurrent::run(Function function, ...);

// 若想要指定线程池,可以使用以下版本
QFuture<T> QtConcurrent::run(QThreadPool *pool, Function function, ...);

Function 可以兼容多种函数类型:其中当然包括 C 的函数指针和 C++11 的 lambda 表达式,其返回值期物的模板参数也可以通过多种重载进行推导。

(看了一下 VSCode 的代码提示,说是有 156 种重载,很夸张)

当然,任务会被先添加到线程池的等待队列里,线程池中有空闲的线程时才会执行。

QThreadPool::globalInstance() 可以获取全局线程池,即调用第二种接口时传递这个等价于第一种。

QFuture

功能和 STL 或其他语言的期物类似。QFuture 可以用来指代异步计算的结果。注意,可能会有 QFuture 这种奇怪的东西,此时可以理解成——“该异步计算结束”即为结果,所以即使是 void 也没关系,只要期物能够获取了即可表示这种已经完成的状态。

QFuture 的成员函数:

QFutureWatcher & QFutureSynchronizer

监视器和同步器。

Watcher 通过信号与槽实现对 future 对象的监控,比如当异步计算完成时触发某个槽:

MyClass myObject;
QFutureWatcher<int> watcher;
// 先完成信号与槽的 connect
connect(&watcher, SIGNAL(finished()), &myObject, SLOT(handleFinished()));

绑定信号与槽后再绑定期物即可完成监视:

QFuture<int> future = QtConcurrent::run(...);
watcher.setFuture(future);

Synchronizer 会在作用域结束前等待所有期物完成:

{
    QFutureSynchronizer<void> synchronizer;
    
    synchronizer.addFuture(QtConcurrent::run(anotherFunction));
    synchronizer.addFuture(QtConcurrent::map(list, mapFunction));

    // wait for all futures to finish
}

并发逻辑内操作 UI 的限制

比如我用 QtConcurrent::run,里面传一个 lambda 并捕获 this,那么也不能/不应该通过 this->ui 去修改 UI 的内容,可能会破坏 Qt 的底层的多线程或窗体更新逻辑,以及 GC(我们姑且把 Qt 的对象树模型当作 GC)。目前并不知道具体是什么原因,但是找了个方法绕过这个问题:

使用事件去触发 UI 更新。

在 lambda 中新建一个事件。

QEvent *e = new QEvent(QEvent::User);

后面的参数是 event 的 ID,这里的 QEvent::User 是 first user event id。

然后挂一个 postEvent。经过我的测试这个绑定要放在 lambda 的最后:

QCoreApplication::postEvent(this, e);

这样当该异步计算完成,会发起 ID 为 QEvent::User 的事件,此时需要在 event 函数里接收。对需要操作 UI 的类重载 event 函数(上面的代码中出现的 this 也是根据需求调整,我这里是当前类,所以就写 this 并在当前类里重载):

 protected:
   bool event(QEvent* e) override;

实现这个函数:

bool Evaluate::event(QEvent* e) {
    if (QEvent::User == e->type()) {
        // ...TODO
    }
    return QWidget::event(e);
}

然后将更新逻辑放在 QEvent::User 这个分支里即可。

实践

比如我的代码的 UI 里有个 textEdit,如果直接在异步的 lambda 里更新可能会报错,所以我就在类成员里加了一个 QString 用过存 textEdit 的内容,然后到 event 触发的时候一并更新。这样虽然不能做到实时在 textEdit 里更新,但是不会报错,不会阻塞主线程。

Ref

多线程(QThread)

TODO

这个暂时用不上,比 QtConcurrent 要强大,但是写起来非常繁琐,等能用上再总结。