文章目录
- 1. 创建共享库
- 2. 使用共享库
- 2.1 共享库的调用方式
- 2.2 隐式链接调用共享库
- 2.3 显式链接调用共享库
1. 创建共享库
除了静态库,Qt 还可以创建共享库,也就是 Windows 平台上的动态链接库
。动态链接库项目编译后生成 DLL 文件,DLL 文件在 windows 平台上应用广泛。DLL 文件是在应用程序运行时加载的,不像静态库那样在编译期间就连编到应用程序里。若更新了 DLL 文件版本,只要接口未变,应用程序依然可以调用。
创建共享库项目,单击Qt Creator 的“File”->“New File or Project”菜单项,在 New File orProject 对话框中选择 Projects 组里的 Library,在右侧的具体类别中再选择 C++ Library,单击“Choose*·.”按钮后出现如下图 所示的向导对话框。
在此对话框的 Type 下拉列表框里选择 Shared Library,并给项目命名,例如 mySharedLib,再选择项目保存目录。单击“Next”按钮后选择编译器,下一步选择需要包含的 Qt 模块,再下一步是类定义页面,在其中输入类的名称,这里仍然输入类名称为QWDialogPen,再下一步结束即可。
由向导生成的 mySharedLib项目包含文件 mySharedLib.pro、qwdialogpen.h 和qwdialogpen.cpp。此外还有一个特殊的头文件 mysharedlib global.h。结构如下图所示:
mysharedlib global.h文件的内容如下:
#ifndef MYSHAREDLIB_GLOBAL_H
#define MYSHAREDLIB_GLOBAL_H
#include <QtCore/qglobal.h>
#if defined(MYSHAREDLIB_LIBRARY)
# define MYSHAREDLIBSHARED_EXPORT Q_DECL_EXPORT
#else
# define MYSHAREDLIBSHARED_EXPORT Q_DECL_IMPORT
#endif
#endif // MYSHAREDLIB_GLOBAL_H
这里定义了符号MYSHAREDLIBSHARED_EXPORT用于替代Qt的宏Q_DECL_EXPORT或Q_DECL_IMPORT。
一个共享库导出给用户使用的类、符号、函数等都需要用宏Q_DECL_EXPORT来定义导出,一个使用共享库的应用程序需要通过Q_DECL_IMPORT导入共享库里的可用对象。
在mySharedLib.pro文件中增加了符号MYSHAREDLIB_LIBRARY的定义,下面是mySharedLib.pro文件的主要内容:
QT += widgets
TARGET = mySharedLib
TEMPLATE = lib
DEFINES += MYSHAREDLIB_LIBRARY
DEFINES += QT_DEPRECATED_WARNINGS
自动生成的 qwdialogpen.h 文件里的内容是对 QWDialogPen 类的定义,在类名称前使用了宏MYSHAREDLIBSHARED_EXPORT,定义QWDialogPen 为一个导出的类。
#ifndef QWDIALOGPEN_H
#define QWDIALOGPEN_H
#include <QDialog>
#include <QPen>
#include "mysharedlib_global.h"
namespace Ui {
class QWDialogPen;
}
class MYSHAREDLIBSHARED_EXPORT QWDialogPen : public QDialog
{ //QPen属性设置对话框
Q_OBJECT
private:
QPen m_pen; //成员变量
public:
explicit QWDialogPen(QWidget *parent = 0);
~QWDialogPen();
void setPen(QPen pen); //设置QPen,用于对话框的界面显示
QPen getPen(); //获取对话框设置的QPen的属性
static QPen getPen(QPen iniPen, bool &ok); //静态函数
private slots:
void on_btnColor_clicked();
private:
Ui::QWDialogPen *ui;
};
#endif // QWDIALOGPEN_H
将 12.3 节静态库项目里的文件 qwdialogpen.h、qwdialogpen.cpp 和 qwdialogpen.ui 复制到本项目目录下,覆盖自动生成的初始文件,但是修改文件 qwdialogpen.h 里的类的定义,在类名称前增加MYSHAREDLIBSHARED_EXPORT 宏,并加入mysharedlib global.h 的包含语句。
项目的文件准备好之后就可以编译生成 DLL 文件,根据使用的编译器不同,生成的文件有些区别。
- 若使用 MSVC 编译,编译后会生成 mySharedLib.dll 和 mySharedLib.lib 两个文件,
mySharedLib.dll 在运行应用程序时调用,mySharedLib.lib 在应用程序隐式调用动态链接库时使用
。 - 若使用 MinGW 编译,编译后会生成 mySharedLib.dll 和 libmySharedLib.a 两个文件,mySharedLib.dll 在运行应用程序时调用,libmySharedLib.a 在应用程序隐式调用动态链接库时使用。
采用 debug 和release 不同模式生成的文件只能当应用程序在 debug 或release 模式下编译或调用。
由于动态库的代码和上篇静态库的基本一样,只是多了mysharedlib global.h文件,这里就不再赘述了。
2. 使用共享库
2.1 共享库的调用方式
调用动态链接库有两种形式,隐式链接 (implicit linking)调用和显式链接 (explicit linking)调用。
-
隐式链接调用是在编译应用程序时,有动态库的 lib 文件(或a 文件)和 h 头文件,知道 DLL中有哪些接口类和函数,编译时就隐式地生成必要的链接信息,使用 DLL 中的类或函数时根据h头文件中的定义使用即可。应用程序运行时将自动加载 DLL 文件。隐式链接调用主要用于同一种编程软件(如 Qt)生成的代码的共享。
-
显式链接调用是只有 DLL 文件,知道 DLL 里的函数原型,使用 QLibrary 类对象在应用程序里动态加载 DLL 文件,声明函数原型,并使用 DLL 里的函数。这种方式需要在应用程序里声明函数原型,并解析 DLL 里的函数。
2.2 隐式链接调用共享库
创建一个基于QMainWindow 的应用程序 shareLibUser,程序功能与 12.3 节的 LibUser 项目一样,将LibUser 项目的 mainwindow 相关3 个文件mainwindow.h、mainwindow.cpp 和mainwindow.ui复制到 shareLibUser 项目下,替换自动生成的文件。
在 shareLibUser 项目文件目录下新建一个 include 目录,将 mySharedLib 项目的两个头文件qwdialogpen.h 和 mysharedlib_global.h 复制到此目录下。若使用 MSVC 编译器,则将 release 版本的mySharedLib.lib 复制到此目录下,debug 版本的 mySharedLib.lib 更名为 mySharedLibd.lib 复制到此目录下;若使用 MinGW 编译器,则复制 release 版本的 libmySharedLib.a,debug 版本的libmySharedLib.a 更名为 libmySharedLibd.a 复制到此目录下。
为应用程序增加动态链接库,右键单击 shareLibUser 项目节点,在快捷菜单里单击“Add Library···”菜单项,在出现的向导对话框里首先选择添加的库类型为“Extermal Library”,在向导第二步设置导入的动态库文件(见下图)。
在上图中,选择项目 include 目录下的 mySharedLib.lib 文件或libmySharedLib.a 作为库文其他设置如上图所示。
完成后在shareLibUser.pro 文件中自动增加项目设置的语句如下:(目的也是为了下面的程序,也可以不通过UI操作,自己写相应的代码)
win32:CONFIG(release, debug|release): LIBS += -L$$PWD/include/ -lmySharedLib
else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/include/ -lmySharedLibd
INCLUDEPATH += $$PWD/include
DEPENDPATH += $$PWD/include
项目编译时,会根据当前是 release 还是 debug 模式,自动添加相应的库文件。这里添加库文件只是使用了动态库的导出定义,而不是将库的实现代码连接到应用程序的可执行文件里。主窗体类 MainWindow 的功能与上篇静态库 的程序完全一致,调用共享库里的类OWDialogPen也无需特别说明,只需包含头文件 qwdialogpen.h 即可。
注意:必须将动态链接库文件 mySharedLib.dll复制到可执行文件的目录下,程序才可以正常运行mySharedLib.dll 的 debug 和 release 版本必须分别用于应用程序的 debug 和 relcase 版本,否则运行时出错。
使用动态链接库可以很方便地扩展应用程序的功能,但是 DLL 文件需要随应用程序一起发布,并且编译 DLL和应用程序的 Qt 版本最好保持一致,否则需要考虑二进制兼容问题。关于二进制兼容可以参考:Qt源代码中二进制兼容及d、q指针的理解
2.3 显式链接调用共享库
显式链接调用共享库是在应用程序运行时才加载共享库文件,并调用库里的函数的。应用程序编译时无需共享库的任何文件,只需知道函数名和函数的原型即可。所以,这种方式可以调用其他语言编写的 DLL 文件
,例如用 Delphi 生成的一个DLL 文件。
显式链接调用共享库是通过 QLibrary 类实现的。QLibrary 是与平台无关的,用于在运行时载入共享库,一个 QLibrary 对象只对一个共享库进行操作。
一般在 QLibrary 的构造函数中传递一个文件名,可以是带路径的绝对文件名,也可以是不带后缀的单独文件名。QLibrary 会根据运行的平台自动查找不同后缀的共享库文件,例如 Unix 上是.so”,Mac上是“.dylib”,Windows 上是“.dll”。
作为示例,用 Delphi编写一个 DLL 项目,生成一个 DelphiDLL.dll文件,这个文件里只有一个函数,函数的原型为:
function triple(N;integer):integer;
它会计算传递参数N的3倍值并返回。
在Qt Creator 里创建一个基于 QMainWindow 的应用程序DelphiDLLUser,设计一个简单的界面,运行时下图所示。单击按钮时将根据输入,调用动态链接库 DelphiDLL.d11里的triple()函数,计算结果并显示在输出编辑框里。
按钮的槽函数代码如下:
void MainWindow::on_pushButton_clicked()
{
QLibrary myLib("DelphiDLL");
if (myLib.isLoaded())
QMessageBox::information(this,"信息","DelphiDLL.DLL已经被载入,第1处");
typedef int (*FunDef)(int); //函数原定定义
FunDef myTriple = (FunDef) myLib.resolve("triple"); //解析DLL中的函数
int V=myTriple(ui->spinInput->value()); //调用函数
ui->spinOutput->setValue(V);
if (myLib.isLoaded())
QMessageBox::information(this,"信息","DelphiDLL.DLL已经被载入,第2处");
}
在定义QLibrary对象实例 myLib 时传递了共享库文件名“DelphiDLL”,这里不需要给出后缀名。DelphiDLL.dll 文件必须在应用程序同一目录、系统目录或可搜索目录下。
QLibrary 有几个函数用于 DLL文件的载入与卸载:
-
load()用于手动载入 DLL 文件到内存里,一般无需手工调用此函数,在DLL里的函数第一次被使用时 QLibrary 会自动调用此函数;
-
isLoaded()用于判断 DLL是否已经被载入内存;
-
unload()用于将DLL从内存中卸载。
一个动态链接库在内存里只能有一个实例,也就是即使有多处调用了这个动态链接库里的函数,它也只会被载入一次,如果不是所有的实例都使用 unload()卸载它,那么它会在应用程序退出时才卸载。
在槽函数on_pushButton_clicked()的代码里,有两处QMessageBox 显示信息。在运行应用程序,第一次单击按钮时,只有第 2 处信息框显示,说明声明了 QLibrary 对象后,动态链接库没有立即被载入内存;第二次单击按钮时,两处信息框会先后显示,说明动态链接库上次载入内存后还在内存里。
显式调用动态链接库里的函数,需要声明函数原型的类型,即:
typedef int (*FunDef)(int); //函数原定定义
然后使用QLibrary的resolve()函数解析需要调用的函数。
FunDef myTriple = (FunDef) myLib.resolve("triple"); //解析DLL中的函数
这样就定义了一个函数 myTriple,用于实现 DLL文件里的函数"triple"的功能,当然重新声明的函数名称可以和 DLL 里的函数名称完全相同。
如果 DelphiDLL.dll 文件没有复制到应用程序目录下,则编译和启动应用程序都不会出错,只有单击按钮调用 DLL 里的函数时才会出错。所以,要使应用程序正常运行,需要将 DelphiDLL.dIl文件复制到应用程序目录下。(实际操作时发现即使复制进去,点击后会显示“程序异常结束”,后期实践时再测试)