QThread 官方文档中介绍了的两种用法:

  • worker-object
  • subclass

worker-object

引用官方文档中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// worker 类
// 声明了一个信号,一个槽
class Worker : public QObject
{
Q_OBJECT

public slots:
void doWork(const QString &parameter) {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}

signals:
void resultReady(const QString &result);
};

// 控制器类
// 运行中主线程,用于管理 worker 对象和 QThread 对象
// 也声明了一个信号,一个槽
class Controller : public QObject
{
Q_OBJECT
QThread workerThread;
public:
Controller() {
Worker *worker = new Worker;
worker->moveToThread(&workerThread);
connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
connect(this, &Controller::operate, worker, &Worker::doWork);
connect(worker, &Worker::resultReady, this, &Controller::handleResults);
workerThread.start();
}
~Controller() {
workerThread.quit();
workerThread.wait();
}
public slots:
void handleResults(const QString &);
signals:
void operate(const QString &);
};

在 worker 类中的槽函数是真正在子线程中工作的内容,而 worker 那个信号被发出时则表示工作完成,控制器中与之连接的槽函数的作用就是处理 worker 对象在完成工作之后使用其信号发出来的数据,控制器中的那个信号被发出时则是命令 worker 对象开始工作。因此需要使用下面的代码让 worker 对象在线程中开始工作:

1
2
3
4
5
// Note: 不要在主线程中以调用 worker->doWork() 函数的方式让 worker 开始工作
// 否则将会导致主线程阻塞在这个调用上,只能使用信号-槽的方式:

Controller *controller = new Controller;
emit controller->operate("some string");

看起来 worker 对象和 QThread 的对象并没有什么关系,那么 worker 对象是怎么在后台工作的呢?关键的一句代码是:

1
worker->moveToThread(&workerThread);

其中moveToThread方法是在 QObject 类中定义的,这意味着所有继承了 QObject 类的对象都可以调用这个方法来改变与自身关联的线程,使用下面的代码可以将 worker 对象再转移回主线程中:

阅读全文 »

最近公司需要补充一些项目的文档,我负责的几个项目中比较有实用价值的是这个 dde-dock 插件的开发入门教程,这里是转载,原发在 dde-dock 项目源码中。

插件的工作原理

插件是一种在不需要改动并重新编译主程序本身的情况下去扩展主程序功能的一种机制。
dde-dock 插件是根据 Qt 插件标准所开发的共享库文件(so),通过实现 Qt 的插件标准和 dde-dock 提供的接口,共同完成 dde-dock 的功能扩展。
可以通过以下链接查看关于 Qt 插件更详细的介绍:

https://wiki.qt.io/Plugins
https://doc.qt.io/qt-5/plugins-howto.html

dde-dock 插件加载流程

在 dde-dock 启动时会跑一个线程去检测目录/usr/lib/dde-dock/plugins下的所有文件,并检测是否是一个正常的动态库文件,如果是则尝试加载。尝试加载即检测库文件的元数据,插件的元数据定义在一个 JSON 文件中,这个后文会介绍,如果元数据检测通过就开始检查插件是否实现了 dde-dock 指定的接口,这一步也通过之后就会开始初始化插件,获取插件提供的控件,进而将控件显示在任务栏上。

接口列表

这里先列出 dde-dock 都提供了哪些接口,可作为一个手册查看,注意,为 dde-dock 编写插件并不是要实现所有接口,这些接口提供了 dde-dock 允许各种可能的功能,插件开发者可以根据自己的需求去实现自己需要的接口。后续的插件示例也将会用到这里列出的部分接口。

接口定义的文件一般在系统的如下位置:

1
2
/usr/include/dde-dock/pluginproxyinterface.h
/usr/include/dde-dock/pluginsiteminterface.h
阅读全文 »

在 deepin linux 系统下编译 qtcreator 4.8.0 版本,可是按照官方 README 却始终编译不通过,遇到了以下几个问题,并列出了相关解决方案。当然官方的 README 上所说的编译依赖还是要装上的。

问题0

这个可能不是必须的

不要在源码目录下建立 build/build-debug/build-release 之类的构建目录, 否则会出现一些奇奇怪怪的问题, 导致编译失败

只需要直接在源码根目录下执行:

1
2
3
4
# 仅供参考
qmake
make
make install

问题1

1
2
3
4
g++ -Wl,-z,origin '-Wl,-rpath,$ORIGIN:$ORIGIN/..:$ORIGIN/../lib/qtcreator' -Wl,--no-undefined -Wl,-z,origin -Wl,-rpath,/usr/lib/llvm-7/lib -Wl,--exclude-libs,ALL -Wl,-O1 -shared -Wl,-soname,libClangFormat.so -o libClangFormat.so .obj/release-shared/clangformatconfigwidget.o .obj/release-shared/clangformatindenter.o .obj/release-shared/clangformatplugin.o .obj/release-shared/clangformatutils.o .obj/release-shared/moc_clangformatconfigwidget.o .obj/release-shared/moc_clangformatplugin.o  -L/home/ri/coding/qt-creator/lib/qtcreator -L/home/ri/coding/qt-creator/lib/qtcreator/plugins -lCppTools -lProjectExplorer -lTextEditor -lCore -lCPlusPlus -lQtcSsh -lAggregation -lExtensionSystem -lUtils -L/usr/lib/llvm-7/lib -lclangFormat -lclangToolingInclusions -lclangToolingCore -lclangRewrite -lclangLex -lclangBasic -lLLVM-7 -lQt5Widgets -lQt5Gui -lQt5Concurrent -lQt5Network -lQt5Core -lGL -lpthread  
/usr/bin/ld: 找不到 -lclangToolingInclusions
collect2: error: ld returned 1 exit status
make[3]: *** [Makefile:286:../../../lib/qtcreator/plugins/libClangFormat.so] 错误 1

报错里提到找不到 clangToolingInclusions 这个库文件, 根据 -L/usr/lib/llvm-7/lib 可知构建系统要在这个目录下找, 尝试了以下三个方法:

  1. 手动进入此目录搜索的确没有找到
  2. 故而又在 /usr/lib 目录搜索依然没有
  3. 接着使用 apt-file search clangToolingInclusions 命令搜索看是不是因为某个包没装, 结果依然没有
阅读全文 »

在 Qt 中实现动画的一种方便的做法就是使用 QPropertyAnimation 类, 构造 QPropertyAnimation 时设置目标 widget 和 property, 然后设置一下初始和结束的 property 值剩下的 Qt 就会帮我们做了.

常用的一个动画属性就是 “geometry”, 这个属性包含了 widget 的位置以及形状(矩形), 所以通过设置这个属性可以实现 widget 的位置和大小动画.

只是使用这个属性实现大小动画时要留意, widget 不能被设置固定的大小, 即下面这类函数不能调用, 否则 QPropertyAnimation 将无法调整目标 widget 的大小, 其中缘由细细想一下便可知道:

  • setFixedSize
  • setFixedWidth
  • setFixedHeight

但如果目的 widget 不得不设置一个初始大小的话可以调用如下这些函数:

  • setMinimumSize
  • setMaxmumSize
  • setGeometry

QPointer

QPointer在用法上跟普通的指针没有什么区别, 可以将它当做是一个普通指针一样使用. 例如:

1
2
3
4
5
6
7
8
9
void barFunc(QLabel *label) {
...
}

QPointer<QLabel> pointer;
pointer = new QLabel;

// 直接将pointer作为QLabel类型的指针传入barFunc函数作为参数
barFunc(pointer);

主要作用:

QPointer的主要功能是避免悬空指针的出现, 悬空指针是指: 指针不为空, 但是其指向的对象已经不在了. 也就是说当对象在其他地方被delete了, 而我们所持有的指向这个对象的指针依然指向那块内存地址, 而没有被置为空, 此时如果使用这个指针就会出错. 下面的QSharedPointer也有避免悬空指针的功能.

使用场景:

在多个不同地方的指针指向同一个对象, 当一个地方delete了这个对象后, 其他地方依然会使用指向这个对象的指针, 此时如果没有使用QPointer封装, if (pointer)返回的是true, 而如果使用QPointer封装, QPointer检测到对象被销毁那么if (pointer)返回的是false.

场景举例:

  • 我需要将我的一个对象以指针的形式暴露出去, 而且我会在某些情况下delete这个对象, 那我暴露出去的指针就应该使用QPointer封装一下.
  • (上面情况的另一个视角)我接收了一个指针, 但指针指向的对象会在别的地方被销毁, 那我接收这个指针时就应该使用QPointer封装一下.

QSharedPointer

阅读全文 »

在 qt 中要翻译一个字符串很简单,只需要使用 tr 函数包裹住字符串即可。后来发现这一方法对于静态变量无效,经同事提醒原来静态数据初始化时,程序还没有加载翻译数据,也就是一般写在 main 函数中的 QTranslator::load() / app.installTranslator() 类似代码。

经过一番搜索和阅读 qt 文档,找到了解决方法,原来 qt 早已有应对之策:QT_TR_NOOP 这个宏,或者说这类宏,因为类似的还有 QT_TR_NOOP_UTF8 QT_TRANSLATE_NOOP,这里只是简要说一下其大概用法,想要了解更多可以查阅 qt 相关文档。下面只介绍 QT_TR_NOOP 这个宏的用法:

这个宏跟 tr 函数的原理不同,tr 函数中的字符串会在程序加载时被替换为翻译后的文本,可以理解为这是一种类似宏展开的过程,因此这是一种静态的翻译,而 QT_TR_NOOP 这个宏则是动态翻译,传给这个宏的字符串将会被标记为需要动态翻译的内容,何为动态翻译,也就是说在程序加载时不会被修改/替换字符串( tr() 函数的过程),而是在真正使用时再进行翻译,拿 qt 文档中的例子来看:

1
2
3
4
5
6
7
8
QString FriendlyConversation::greeting(int type)
{
static const char *greeting_strings[] = {
QT_TR_NOOP("Hello"),
QT_TR_NOOP("Goodbye")
};
return tr(greeting_strings[type]);
}

上述代码中数组 greeting_strings 的成员是两个被标记为需要动态翻译的字符串,下面 return 就是真正要使用的地方,同样是用到了 tr 函数,将数据的成员传入即可。

如果静态字符串数据是一个 QMap 或者 QList 那使用时就要将成员转化为 c 风格的字符串或byte数组,还是上面的代码,稍加修改为 QList 类型的:

1
2
3
4
5
6
7
8
9
QString FriendlyConversation::greeting(int type)
{
static const QList *greeting_string_list = {
QT_TR_NOOP("Hello"),
QT_TR_NOOP("Goodbye")
};
return tr(greeting_string_list[type].toUtf8());
// 或者:greeting_string_list[type].toLocal8Bit() 之类的
}

qtcreator 的 fakevim 不支持原生 vim 的插件,所以就用不了 fcitx.vim 这个好用的插件了,所以只能自食其力喽。根据 fcitx 支持 dbus 通信的原理,实现了在 INSERT 模式下按 ESC 键回到 NORMAL 模式时自动切换到英文状态。
由于 fakevim 支持的特性过于简陋,目前只实现了这个,并不能像 fcitx.vim 插件一样记住 INSERT 模式下的中英文状态,等下次进入 INSERT 模式时再自动切回去,不过这样也舒服了很多了,将下面的配置放到 qtcreator fakevim 插件的配置文件中即可:

1
2
" 从insert模式按esc回到normal模式时自动关闭小企鹅输入法
inoremap <ESC> <ESC>:!dbus-send --type=method_call --dest=org.fcitx.Fcitx /inputmethod org.fcitx.Fcitx.InputMethod.InactivateIM<CR>

在 qtcreator 的设置里有 fakevim 设置项,里面有一项是设置 fakevim 启动时要加载的配置文件的路径。

如果没有使用 qtcreator 调试 qt 程序,而是手动或利用其他 IDE 使用 gdb 调试,会发现 qt 类型的数据如 QString QList QMap 等不能直接查看其值。其实 gdb 是提供了关于打印数据的接口的,叫做 Pretty-Printer,这是一种利用 python 代码更好的输出变量数据的机制,感兴趣的可以搜索下相关内容。qtcreator 之所以可以愉快的打印出 qt 相应的数据也是因为它使用了这种机制,下面的命令可以查看 qtcreator 的安装目录中的 python 文件,命令在 debian 系的 linux 系统上有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# apt-file show qtcreator-data | grep py
qtcreator-data: /usr/share/doc/qtcreator-data/copyright
qtcreator-data: /usr/share/qtcreator/debugger/boosttypes.py
qtcreator-data: /usr/share/qtcreator/debugger/cdbbridge.py
qtcreator-data: /usr/share/qtcreator/debugger/creatortypes.py
qtcreator-data: /usr/share/qtcreator/debugger/dumper.py
qtcreator-data: /usr/share/qtcreator/debugger/gdbbridge.py
qtcreator-data: /usr/share/qtcreator/debugger/lldbbridge.py
qtcreator-data: /usr/share/qtcreator/debugger/misctypes.py
qtcreator-data: /usr/share/qtcreator/debugger/opencvtypes.py
qtcreator-data: /usr/share/qtcreator/debugger/pdbbridge.py
qtcreator-data: /usr/share/qtcreator/debugger/personaltypes.py
qtcreator-data: /usr/share/qtcreator/debugger/qttypes.py
qtcreator-data: /usr/share/qtcreator/debugger/stdtypes.py
qtcreator-data: /usr/share/qtcreator/qml/qmlpuppet/qml2puppet/instances/nodeinstancesignalspy.cpp
qtcreator-data: /usr/share/qtcreator/qml/qmlpuppet/qml2puppet/instances/nodeinstancesignalspy.h
qtcreator-data: /usr/share/qtcreator/templates/wizards/classes/python/file.py
qtcreator-data: /usr/share/qtcreator/templates/wizards/classes/python/wizard.json
qtcreator-data: /usr/share/qtcreator/templates/wizards/files/python/file.py
qtcreator-data: /usr/share/qtcreator/templates/wizards/files/python/wizard.json

可以看到 /usr/share/qtcreator/debugger/ 目录下关于 qtcreator 对 debug 所做的优化,不过不知道为什么直接使用 qtcreator 提供的 qt 类型相关的脚本没有成功(有成功的可以在下面回复方法指点一下)。因此又在 github 上搜索相关内容找到了另一个 qt 相关的 Pretty-Print 脚本,项目地址为:https://github.com/Lekensteyn/qt5printers,在 linux 系统下具体使用方法如下:

1
2
# 克隆项目到~/.git/qt5printers目录下
git clone [email protected]:Lekensteyn/qt5printers.git ~/.git/qt5printers

复制以下内容到~/.gdbinit文件中,若文件不存在则手动创建:

1
2
3
4
5
6
python
import sys, os.path
sys.path.insert(0, os.path.expanduser('~/.gdb'))
import qt5printers
qt5printers.register_printers(gdb.current_objfile())
end

做完之后就可以 debug 一个 qt 程序测试了

重载父类的同名虚函数时会出现hides overloaded virtual function编译警告。
从字面上的意思其实就可以理解:重载的虚函数被隐藏了。

三个关键点:

  • 重载
  • 虚函数
  • 隐藏

这个编译警告之所以出现,是因为上面三个关键点,首先是发生了重载,子类重载了父类的函数,其次被重载的是虚函数,这时这个被重载的父类的虚函数将会被隐藏。
何为隐藏呢,应该是不能使用子类实例直接调用父类被隐藏的函数,调用时必须指定父类命名空间,往深了说也就是编译器如果在子类中发现了要使用的函数的名字,注意,是名字不包含函数签名,只要名字匹配上,就不会再去父类中去寻找这个名字的函数,即便子类中的函数参数不匹配,也不会再去父类中寻找。

其实去掉虚函数这个关键点,在c++中当重载了父类的函数时,隐藏同样会发生,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A {
public:
void foo() {
cout << "foo of A" << endl;
}
};

class B : public A{
public:
void foo(int i) {
cout << "foo of B" << endl;
}
};

int main(void)
{
B *b = new B();
b->foo(); //编译报错
b->A::foo(); //应该指定命名空间A::
b->foo(1);
return 0;
}

当重载的父类函数为虚函数时,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A {
public:
virtual void foo() { //将父类函数声明为虚函数
cout << "foo of A" << endl;
}
};

class B : public A{
public:
void foo(int i) { //此处会有编译警告:"'B::foo' hides overloaded virtual function"
cout << "foo of B" << endl;
}
};

int main(void)
{
B *b = new B();
b->foo(); //编译报错依旧
b->A::foo(); //应该指定命名空间A::
b->foo(1);
return 0;
}

编译错误依旧,但多了一个编译警告,关于这个编译警告,有种解释说是为了避免书写错误,这就要说到多态,上面的例子没有应用多态有些不合适,修改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BookA; //改动处
class BookB; //改动处

class A {
public:
virtual void foo(BookA *) { //改动处
cout << "foo of A" << endl;
}
};

class B : public A{
public:
void foo(BookB *) { //改动处
cout << "foo of B" << endl;
}
};

int main(void)
{
A *b = new B(); //改动处
b->foo(new BookB());
return 0;
}
阅读全文 »

手残总是难免的,有时候不小心删掉了一个还没有合并或者push的分支,等反应过来似乎已经找不到这个分支在仓库中存在的痕迹了。

这时git reflog命令就体现出了它的价值,其实git会把我们在仓库里的所有操作都记录下来,
git reflog命令的输出就可以看到,就算是从分支A切换到分支B也会被记录下来,那么假设,分支B被误删,
执行git reflog命令后就可以找到分支B的痕迹: “commit id”,不管是从其他分支切换到分支B,还是分支B的一个提交记录,
都会看到其”commit id”,复制这个”commit id”然后切换过去,此时仓库处于分离头指针状态,其实现在已经是在分支B了,
只需要从当前状态创建一个分支,那么分支B也就复活了: git checkout -b B-reborn
这个分支B-reborn就是先前删除的分支B。