Content Table

创建使用动态链接库

想一想大多数时候我们的项目是不是所有代码都会放在同一个工程中?人少的时候问题不大,但当项目越来越大,开发人员越来越多,会发觉开发、管理能让人窒息,大家都绞在一起,出问题时互相推诿责任,各自有理,这时如果按照功能模块进行分组各自开发,以库的形式提供给其他人使用,就能够最大限度的并行开发,提高工作效率,而且项目的模块也很清晰,责任一目了然,此外使用动态链接库后还能够按模块升级,编译的速度也更快。下面就介绍怎么在工程中创建和使用动态链接库。

Windows 中叫动态链接库 (Dynamic Link Library: .dll),Linux 中叫共享库 (Shared Library: .so),Mac 下后缀为 .dylib,这几种叫法实际指的是一种类型的库,这里都统称为动态链接库吧。

理解动态链接库需要理解符号的概念,符号包含函数、变量或者类,分为公有符号和私有符号:

  • 公有符号: 在其他程序或者库使用的符号,需要根据用途使用宏进行标记:

    • Q_DECL_EXPORT: 编译为动态链接库时符号要标记为 Q_DECL_EXPORT,表明是导出的符号
    • Q_DECL_IMPORT: 在调用动态链接库时符号要标记为 Q_DECL_IMPORT,表明是导入的符号
  • 私有符号: 除了公有符号外的其他符号,在此库之外不应该被访问,不需要进行标记

    建议: 不要在头文件中声明私有符号。

符号上的标记 Q_DECL_EXPORTQ_DECL_IMPORT 不能同时存在,为了在导出和导入时使用同一个头文件, 头文件中包含下面的宏,编译时根据条件使用不同的宏就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <QtCore/qglobal.h>

// 根据条件定义 LIB 为不同的宏
#if defined(BUILD_LIB)
#define LIB Q_DECL_EXPORT
#else
#define LIB Q_DECL_IMPORT
#endif

// 使用 LIB 修饰符号,编译时会根据编译条件替换为 Q_DECL_EXPORT 或者 Q_DECL_IMPORT
class LIB Calculator {
...
};

为了达到了在导出和导入时使用同一个头文件的目的:

  • 生成动态链接库工程的 pro 文件中添加 DEFINES += BUILD_LIB,在编译的时候 LIB 就会被替换为 Q_DECL_EXPORT
  • 使用动态链接库工程的 pro 文件中千万不要加 DEFINES += BUILD_LIB,编译的时候发现没有定义 BUILD_LIB,则 LIB 就会被替换为 Q_DECL_IMPORT

测试 Widget 的效率

Widget 的效率怎么样,来进行一个简单的测试,添加 1千,1万,2万个,……,10万个 QPushButton(修改程序中的 buttonsCount 即可),看看程序的创建好按钮,点击按钮执行槽函数,程序退出效果怎么样。

添加 1千个,1万个按钮的时候窗口显示的速度非常快。
添加 2万个的时候就需要几秒窗口才显示出来。
添加的越多窗口显示需要的时间越长,添加 10万个需要等很久。
按钮越多,程序退出的时间就越长,不过即使是 10万个按钮,退出也就是多了几秒,因为释放的内存多,这倒是没什么。
当窗口显示出来后,不管添加了多少个按钮,点击按钮,它的槽函数都是瞬间就被执行。
在实际应用中,添加上百个 widgets 在窗口上的见过,但有谁会添加上万个 widgets 到窗口上?不担心被产品经理揍的可以试试!

测试 Graphics View 的效率

Qt 说 Graphics View Framework 效率很高,到底有多高呢?来进行一个简单的测试,向 scene 中添加 10万,50万,100万个 items(修改程序中的 rowCount 和 colCount 即可),进行缩放、旋转看看效率怎么样。

在可视区域内的 items 少的话,不管 scene 里有多少个 items,10万个和 100万个的区别不大,效率都是非常高的,但可视区内 items 越多的话,越多效率越低。Qt 使用 Binary-Space-Partitioning 算法管理 items,能够快速的找出可视区内的 items 进行绘制(100万个 items 中可能只需要绘制 100 个 items),不会绘制所有的 items,绘制操作是非常耗时的,这也就是为什么影响效率最大的因素是可视区内的 items。

实际项目中添加 10万个 items 的效率和此处测试的 10万个 items 的效率是有些微区别的,绘制的消耗由其 paint() 函数决定。

Qt 全局快捷键

全局快捷键: 按下快捷键后,不管程序是否当前正在使用的程序,它都能得到此快捷键的事件通知,例如 Windows 里按下 Ctrl + Shift + A 就可以使用 QQ 截图一样。

Qt SDK 没有自带设置全局快捷键的功能,需要自己实现,在 Github 上也有人开源了全局快捷键的库,例如 QHotKey,可以直接在项目中使用。

QHotkey 有很多特点,能够满足绝大多数的需求:

  • Works on Windows, Mac and X11
  • Easy to use, can use QKeySequence for easy shortcut input
  • Supports almost all common keys (Depends on OS & Keyboard-Layout)
  • Allows direct input of Key/Modifier-Combinations
  • Supports multiple QHotkey-instances for the same shortcut (with optimisations)
  • Thread-Safe - Can be used on all threads (See section Thread safety)
  • Allows usage of native keycodes and modifiers, if needed

下面就写个 HelloWorld 级别的程序,定义全局快捷键 Alt+P 唤出程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <QHotkey>
#include <QApplication>

Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);

QHotkey *hotkey = new QHotkey(QKeySequence("Alt+P"), true); // Alt 和 P 之间不能有空格
qDebug() << "Is Registered: " << hotkey->isRegistered();

connect(hotkey, &QHotkey::activated, [this](){
this->show();
this->raise();
this->activateWindow();
this->raise();
QApplication::setActiveWindow(this);
this->raise();
});
}

继承 QThread 实现多线程

Qt 中使用多线程,最简单直观的方法就是继承 QThread,重写 run() 方法,需要使用多线程执行的代码放在 run() 函数中,调用 start() 函数启动线程,线程正在运行时 isRunning() 返回 true,结束运行后发出信号 finished()

实现线程

仍以读取文本显示到 QTextEdit 为例,类 ReadingThread 继承 QThread,在 run() 方法中读取文件并添加到 QTextEdit。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 文件名: ReadingThread.h

#ifndef READINGTHREAD_H
#define READINGTHREAD_H

#include <QThread>

class QTextEdit;

class ReadingThread : public QThread {
public:
ReadingThread(QTextEdit *textEdit, QObject *parent = NULL);

protected:
void run() Q_DECL_OVERRIDE;

private:
QTextEdit *textEdit;
};

#endif // READINGTHREAD_H
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
// 文件名: ReadingThread.cpp

#include "ReadingThread.h"
#include <QFile>
#include <QTextStream>
#include <QTextEdit>
#include <QMetaObject>

ReadingThread::ReadingThread(QTextEdit *textEdit, QObject *parent) : QThread(parent), textEdit(textEdit) {

}

void ReadingThread::run() {
QFile file("/Users/Biao/Desktop/data.txt");

if (!file.open(QIODevice::Text | QIODevice::ReadOnly)) {
return;
}

QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine();
textEdit->append(line);
}
}

多线程编程

读取文件显示到 text edit 中,一个非常简单的需求,啥也不说了,撸起袖子,打开 Qt Creator 开干。

先设计 UI 如下,中间是 QTextEdit,底部是按钮 QPushButton:

点击按钮,按行读取文件,然后添加到 QTextEdit,对我们来说也是分分钟的事(Lambda Lambda Lambda):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ReadingWidget::ReadingWidget(QWidget *parent) : QWidget(parent), ui(new Ui::ReadingWidget) {
ui->setupUi(this);

// 点击按钮,按行读取文件,添加到 text edit 中显示出来
connect(ui->pushButton, &QPushButton::clicked, [this] {
QFile file("/Users/Biao/Desktop/data.txt"); // 文件路径自己修改一下啊

if (!file.open(QIODevice::Text | QIODevice::ReadOnly)) {
return;
}

QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine(); // 读取一行
ui->textEdit->append(line); // 添加到 text edit
}
});
}

分布式 ID 生成算法 Snowflake

分布式系统中,有一些需要使用全局唯一 ID 的场景,这种时候为了防止 ID 冲突可以使用 36 个字符的 UUID,但是 UUID 有一些缺点,首先他相对比较长,另外 UUID 一般是无序的字符串。

有些时候我们希望能使用简单一些的 ID,并且希望 ID 能够按照时间有序生成,为了解决这个问题,Twitter 发明了 SnowFlake 算法,不依赖第三方介质例如 Redis、数据库,本地程序生成分布式自增 ID,这个 ID 只能保证在工作组中的机器生成的 ID 唯一,不能像 UUID 那样保证时空唯一。

Snowflake 把时间戳、工作组 ID、工作机器 ID、自增序列号组合在一起,生成一个 64bits 的整数 ID,能够使用 70 年,每台机器每秒可产生约 400 万个 ID (2^12*1000,每毫秒理论最多生成 2^12 个 ID)。

Snowflake 也有自己的缺点,虽然不同 workId 的机器生成的 ID 永远不会相同,但是同一台机器当把时间往后回拨后,生成的 ID 就会重复,所以需要保持时间是网络同步的。

Snowflake 生成的 ID 的 bit 结构如下:

VS2013 使用 dll

Qt 使用 curl 一文中介绍了怎么编译 curl 并且在 Qt 项目中使用,那么在 VS 项目中应该怎么使用 curl 的 dll 呢?

动态库的使用分为隐式链接和显示链接两种方式:

  • 显示链接: 只需要 .dll 动态库文件,代码中使用 LoadLibrary + GetProcAddress 加载函数后需要自己进行函数类型转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 函数类型定义
    typedef void (*DLLFunc)(int);

    // 加载 dll 中的函数
    HINSTANCE hInstLibrary = LoadLibrary("DLLSample.dll");
    DLLFunc dllFunc = (DLLFunc)GetProcAddress(hInstLibrary, "TestDLL");

    // 执行函数
    dllFunc(123);
  • 隐式链接: 需要 .h 头文件、.lib 库导入文件和 .dll 动态库文件,代码中直接使用库的函数即可

    推荐使用隐式链接,更省事,可参考 LIB 和 DLL 的区别与使用

VS2013 中隐式链接使用 dll 一般有两种方法:

  • 使用 #pragma 引入 lib

  • 设置 项目属性 引入 lib

Qt 使用 curl

Qt 已经提供了 QNetworkAccessManager 用于 Http 访问,Qt 访问网络的 HttpClient 对其进行了简单封装,如下就可以进行 GET 请求:

1
2
3
HttpClient("http://localhost:8080/device").get([](const QString &response) {
qDebug() << response;
});

但是,在非 Qt 项目中就不能使用 QNetworkAccessManager 了,还有就是因为 curl 成熟、强大、跨平台,可能有些项目更希望使用 curl,所以在此以 Windows MinGW 的 Qt 项目为例,介绍 curl 的集成使用。

Qt 安装

对于想学习 Qt 的同学来说,下载、安装 Qt 还真不是一件容易的事,但这也是学习 Qt 的基石,否则连个 Qt Hello World 都不能在电脑上编写,还玩什么呢?

下载

下载 Qt,首先想到的是到官网 https://www.qt.io 下载,悲催的是,别说对于新手,就算对于我这种业余爱好 Qt 好多年的伪骨灰,进去后都是懵逼状态,别说还要注册、登录,估计连找下载的地方都困难,大多数人在这估计就有放弃的想法了,不能从大门进,还不能直捣黄龙么,访问 http://download.qt.io/archive/qt/ 就可以无障碍的挑选想要的 Qt 版本了。