1.前言:
个人笔记,欢迎探讨。
2.背景:
本人实际接触qt没多久,但很多方面已经喜欢上了qt。微软的全家桶用起来更顺手,但qt的某些方面也很独到。
本次遇到的问题,都源于dll。之前封装了一些代码,仅仅是h和cpp,复制过去就行了(其实跟lib静态链接库差不多意思,是否编译过的问题),然而心里总觉得不爽,弄成个dll多好。于是操练。
网上看了很多文章,看似简单,其实大家也是摸索着来的,要善于总结。有些细节不耽误用的可以暂且放下,一定要培养成就感。很多时候先实现,再研究效果会更好。但是只管实现不考虑的做法,路会很短。
3.制作dll:
之前曾经照猫画虎实现过一次demo,这次想直接把我做的封装代码改成dll。需要改pro文件,不知道格式没关系,直接新建一个lib项目,qt就自己生成了,可以参考着来。
我尽量用最剪短的方式说明。
3.1.pro文件有关的:
QT += core gui sql
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
如上这两句不用说了,用到什么加什么就行了。接着:
TEMPLATE = lib
DEFINES += QTHQ_BASE_LIBRARY
其实创建dll项目的时候qt会自动生成。
第一句说明这是个库项目,不是输出exe。
第二句是定义一个宏,后面有用。
继续:
HEADERS += \
qt-hq_base_global.h
qt会自动生成一个声明了宏的头文件,并包含进来。当然如果整个项目规模很小,比如就可以公用一个头文件的话,直接把里面的宏定义复制过来就可以了。
我的做法是,把这个自动生成的头文件稍作修改,并且直接和dll的头文件放在一起,这样以后include的时候一并复制过去,省事。至于如何修改它,看下面。
3.2.qt-hq_base_global.h文件:
#include <QtCore/qglobal.h>
#if defined(QTHQ_BASE_LIBRARY)
# define QTHQ_BASESHARED_EXPORT Q_DECL_EXPORT
#else
# define QTHQ_BASESHARED_EXPORT Q_DECL_IMPORT
#endif
就这么点东西而已,主要是声明了两个宏。
它是根据QTHQ_BASE_LIBRARY是否定义,来进一步定义QTHQ_BASESHARED_EXPORT的作用,是用于导出还是导入。
所以前面说为什么qt会定义一个宏QTHQ_BASE_LIBRARY。在制作dll的项目里有这个宏定义,QTHQ_BASESHARED_EXPORT的作用就是导出。相应的,在exe调用dll的pro文件里,没有定义宏QTHQ_BASE_LIBRARY,QTHQ_BASESHARED_EXPORT的作用就是导入。这样就实现了效果:制作dll的时候,类声明为导出,exe调用dll时没有定义宏,类声明为导入。
我修改为这样:
/*************************************************************************
** Class name: Macro defining
** Author: Henrick.Nie
** Created date: 2022-12-26
** Used for: The purpose of the header files of 'hq_base'
**
** Used for 'cpp':
**
** Add the code 'DEFINES += QTHQ_BASE_SOURCE' in the .pro file. Then
** the macro 'QTHQ_BASESHARED_EXPORT' means nothing. So the header
** files of 'hq_base' will be used for normal 'cpp' source codes.
**
** Used for 'dll' developing:
**
** Add the code 'DEFINES += QTHQ_BASE_LIBRARY' in the .pro file. Then
** the macro 'QTHQ_BASESHARED_EXPORT' equals 'Q_DECL_EXPORT', it means
** dll exporting. So the header files of 'hq_base' will be used for the
** 'dll' project, such as 'Qt-HQ_Base-dll'.
**
** Used for 'dll' calling:
**
** Do not add any other codes of macro defining in the .pro file. Then
** the macro 'QTHQ_BASESHARED_EXPORT' equals 'Q_DECL_IMPORT', it means
** dll importing. So the header files of 'hq_base' will be used for the
** 'dll' calling project, such as 'Qt-HQ_Base-exe'.
**
** In the header files:
**
** This header file including is required.
** Class defining using macro 'QTHQ_BASESHARED_EXPORT' is required.
** Such as below:
**
** #include "qt-hq_base_global.h"
**
** class QTHQ_BASESHARED_EXPORT MyClass {...};
**
*************************************************************************/
#ifndef QTHQ_BASE_GLOBAL_H
#define QTHQ_BASE_GLOBAL_H
#include <QtCore/qglobal.h>
#if defined(QTHQ_BASE_SOURCE)
# define QTHQ_BASESHARED_EXPORT //Used for normal 'cpp'.
#elif defined(QTHQ_BASE_LIBRARY)
# define QTHQ_BASESHARED_EXPORT Q_DECL_EXPORT //Used for 'dll' developing.
#else
# define QTHQ_BASESHARED_EXPORT Q_DECL_IMPORT //Used for 'dll' calling.
#endif
#endif // QTHQ_BASE_GLOBAL_H
我详细加了说明。虽然英文不好,但每次我都是复制到翻译中看看是否意思正确。
最终的目的是,在这里自动实现dll头文件的用途:
3.2.1.如果不想编译成dll,可以把.h和.cpp直接复制到项目中使用:
需要在项目的.pro文件中加入:
DEFINES += QTHQ_BASE_SOURCE
这样因为有了QTHQ_BASE_SOURCE的宏定义,则宏QTHQ_BASESHARED_EXPORT会是空定义,也就是什么都没有,所以所有的class都会跟用于cpp的一样,没有影响。
3.2.2.如果要编译成dll:
需要在项目的.pro文件中加入:
DEFINES += QTHQ_BASE_LIBRARY
因为没有QTHQ_BASE_SOURCE的宏定义,但是有QTHQ_BASE_LIBRARY的定义,则宏QTHQ_BASESHARED_EXPORT会定义为Q_DECL_EXPORT(导出),刚好用于dll工程。
3.2.3.如果这些.h文件是用于调用dll的程序:
则不需要在.pro文件中加相关宏定义。所以既没有QTHQ_BASE_SOURCE的宏定义,也没有QTHQ_BASE_LIBRARY的宏定义,因此宏QTHQ_BASESHARED_EXPORT被定义为Q_DECL_IMPORT(导入),刚好用于调用dll的工程。
所以实现了dll的头文件多用途。继续:
3.3.所有自己写的头文件:
头文件里当然主要内容是类定义,针对类定义一定要加上之前的宏:
class QTHQ_BASESHARED_EXPORT MyClass
{...}
以后dll的头文件要分发给调用者,所以头文件里的类,要根据使用环境不同而声明为导出/导入。显然,制作dll的时候,应该声明为导出。exe调用dll的时候,应该声明为导入。这就是前面提到的宏定义的作用。
然后就可以编译输出了,编译器设置就不赘述了,可以选择生成Debug或Release版本。
3.4.关于32bit和64bit:
在.pro文件的开头指定TARGET的地方,做一些修改:
#TARGET = Qt-HQ_Base-exe
contains(QT_ARCH, i386) {
TARGET = Qt-HQ_Base-exe
} else {
TARGET = Qt-HQ_Base-exe64
}
TARGET指明工程的目标名称,就拿dll来说,无论这个dll文件是什么名字,最终被调用的时候,都会按这里指定的名字去找它的接口。这是每一个qt工程的身份象征。
这里添加了对平台位数的判断,直接从名字上区分,32位的不用动,64位的加上‘64’后缀。一旦这里指定了目标名,最后生成的文件名也跟这里一样。比如像上面这样,它会生成Qt-hq_base.dll和Qt-hq_base64.dll。
4.调用dll:
之前不调用dll时,都是各种.h和.cpp源码文件。从使用角度可以这样认为,调用dll跟源码道理上是一样的,只不过dll是编译成二进制再调用而已。当然事实上,所谓动态链接的调用是运行时发生的。
4.1.复制文件:
一般dll编译完之后,会有:***.dll, lib***.a两个文件,dll都明白,主要是.a文件,如果编译器是msvc会有.lib文件,这种文件说是链接用的,我觉得可以粗暴地认为跟.h作用类似,就是告诉编译器dll里有什么。至于具体意义,可以看看这篇:
关于MinGW下.dll.a文件的作用_cibiren2011的博客-CSDN博客_.dll.a
把dll和a文件复制到需要它们的工程目录下,目录层级自己决定,我新建了一个目录叫include。
还要把dll相关的头文件.h,以及上面制作dll时,qt生成的qt-hq_base_global.h,都复制过来放到include目录下。
其实后期我就只用到了dll和h文件,其它没用。
4.2.修改pro文件:
4.2.1.可以指明包含路径,就是刚才复制的文件位置。如下这句就够了。
INCLUDEPATH +=\
$$PWD/include
4.2.2.还可以写成下面的方式去找头文件我认为也可以,只是没必要:
HEADERS += \
***.h
4.2.4.但是一定要注意,千万别用pri方式:
include($$PWD/include/include.pri)
否则会报重复声明的警告,但是能编译通过,也能用,但就是不爽。
redeclared without dllimport attribute: previous dllimport ignored
4.2.5.然后是大名鼎鼎的LIBS +=:
LIBS += -L$$PWD/include/ -lMyDll
#DEPENDPATH += $$PWD/include/
那个依赖路径我没遇到,感兴趣可以去查。网上说,它就是在头文件修改以后,是否重新编译源文件的意思,既然生成了,我就用上了,其它没细究。
主要是LIBS +=的语法。等号后面可以直接写绝对路径,也可以用这种通配符。语法格式特别像linux下的命令参数。“-L”后面紧跟着是dll所在路径,不能有空格。“-l”后面是那个.a的文件名,不能有空格。但是.a文件一般为lib***.a,这里写的时候,去掉lib前缀和扩展名。
4.2.6.让LIB +=支持32bit和64bit:
再升级一下,改成这样:
contains(QT_ARCH, i386) {
#message("32-bit")
CONFIG(debug,debug|release){
LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
}
else{
LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
}
} else {
#message("64-bit")
CONFIG(debug,debug|release){
LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base64
}
else{
LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base64
}
}
对应前文提到的关于32bit和64bit的内容。这里LIBS += 后面虽然涉及到了路径,但最后应该只是文件的名字,这就是上面说生成dll的工程.pro文件中的TARGET=语句那里,为什么要指定好。这里去找dll文件,不仅仅要文件名对,还要库名也对。这里所谓“库名”是我自己定义的,不知是否准确,就是指dll工程的.pro文件中的TARGET=指定的名字。一定要和这里LIBS +=后面的名字一致。否则,它会按照LIBS指定的名字去找dll文件,但这个dll文件中的库的名字,也要是dll工程中TARGET指定的才可以。
就好比我做了一个名为Qt-hq_base的动态库,虽然编译出来一个Qt-hq_base.dll文件,如果非要把文件重命名为abc.dll,调用它时,LIBS+=那里指定abc.dll可以找到它。找到又如何?其实这个库叫Qt-hq_base,‘abc’只是文件名、如果对不上暗号还得报错。因为我一开始傻乎乎以为按文件名找到就可以了,结果不行。
这里要提一句,上面我已经处理好了版本交叉问题。下面有说明。
然后就over了,可以直接开始运行调试。
4.2.7.到此为止.pro文件的关键内容大致如下:
...
#TARGET = Qt-HQ_Base-exe
contains(QT_ARCH, i386) {
TARGET = Qt-HQ_Base-exe
} else {
TARGET = Qt-HQ_Base-exe64
}
...
include($$PWD/_db/_db.pri)
include($$PWD/_modbus/_modbus.pri)
include($$PWD/_encrypt/_encrypt.pri)
#include($$PWD/include/include.pri) 这里不要写,不要写,不要写
INCLUDEPATH +=\
$$PWD/_db\
$$PWD/_modbus\
$$PWD/_encrypt\
$$PWD/include
DEPENDPATH += $$PWD/include
contains(QT_ARCH, i386) {
#message("32-bit")
CONFIG(debug,debug|release){
LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
}
else{
LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
}
} else {
#message("64-bit")
CONFIG(debug,debug|release){
LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base64
}
else{
LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base64
}
}
...
再次注意:头文件包含不要用的pri目录方式。
5.版本交叉问题:
5.1.报错:
刚才其实挺简单的,但中途遇到问题:
Debugging starts
QWidget: Must construct a QApplication before a QWidget
Debugging has finished
5.2.参考:
看到这篇:
QWidget: Must construct a QApplication before a QWidget_ronal7do的博客-CSDN博客
其实就是Debug/Release版本混淆造成的。
“QWidget: Must construct a QApplication before a QWidget 出现这个问题是调用的dll库不对应, debug版本要用debug版本的dll, release版本要用release版本的dll.”
作者可能把文章改过。上面这段引用是搜索引擎的简介上看到的。
调试程序当然是debug,于是把debug版本的dll复制过来,再试,ok了。
5.3.分析:
网上查过,debug和release对堆内存的管理方式不一样,看来不只是release优化调试信息与否的问题,从具体实现方式上这要深究原理了。有兴趣的朋友可以单独研究。
于是考虑pro文件中能否有灵活的设置,最好能自动识别编译模式并指定相应的dll文件。于是查到了这篇:
QTpro文件详解 - 百度文库
LIBS +=那里,可以用Debug:或Release:来指定,如果是vs做的dll,名字不一样就可以这样用,但是qt生成的dll名字都一样,没戏。
5.4.解决:
咋整?要么改编译方式时,复制相应版本的dll过来,要么把不同版本的dll分开放在不同目录就得了。所以早期我把pro文件写成了这样:
Debug: LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
Release: LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
直接分成debug和release目录存放,真正发布的时候,记着对应即可。版本不要交叉使用就可以了。
后来发现放在ubuntu下编译,又报错了。于是这个部分又改为以下样式:
#Debug: LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
#Release: LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
CONFIG(debug,debug|release){
LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
}
else{
LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
}
这里要注意。else后面紧跟“{”,换行会报错。
5.5.最终.pro文件的全貌:
#-------------------------------------------------
#
# Project created by QtCreator 2022-05-17T08:55:54
#
#-------------------------------------------------
QT += core gui sql serialbus
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
#TARGET = Qt-HQ_Base-exe
contains(QT_ARCH, i386) {
TARGET = Qt-HQ_Base-exe
} else {
TARGET = Qt-HQ_Base-exe64
}
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
# any feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
include($$PWD/_db/_db.pri)
include($$PWD/_modbus/_modbus.pri)
include($$PWD/_encrypt/_encrypt.pri)
#include($$PWD/include/include.pri)
INCLUDEPATH +=\
$$PWD/_db\
$$PWD/_modbus\
$$PWD/_encrypt\
$$PWD/include
DEPENDPATH += $$PWD/include
contains(QT_ARCH, i386) {
#message("32-bit")
CONFIG(debug,debug|release){
LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
}
else{
LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
}
} else {
#message("64-bit")
CONFIG(debug,debug|release){
LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base64
}
else{
LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base64
}
}
SOURCES += \
main.cpp \
mainwindow.cpp
HEADERS += \
mainwindow.h
FORMS += \
mainwindow.ui
QMAKE_CXXFLAGS += -Wno-unused-parameter
RC_FILE = logo.rc
6.所有文件与目录的组成:
主要是指调用dll的exe工程。按照上述.pro文件的定义,主要就是那个include目录。
6.1.../include/*.h:
这里是制作dll时,所有的.h文件。主要是告诉exe工程,这个dll库里面都有哪些定义和声明。以后这个exe工程在编译的时候,会按照这些.h头文件去构建规则,这样最后生成的exe可执行程序,只要能找到相应的dll文件,就可以运行了。
6.2.../include/Debug/:
这里面放入之前制作dll的工程生成的两个debug版本的dll文件,Qt-HQ_Base.dll和Qt-HQ_Base64.dll。
上面提到过构建dll的工程的.pro文件中,有个TARGET选项,分别指定了32位和64位。所以它会按照指定的名字生成dll文件。使用32bit编译器生成Qt-HQ_Base.dll,使用64位编译器生成Qt-HQ_Base64.dll。
6.3.../include/Release/:
这里面放入之前制作dll的工程生成的两个release版本的dll文件,Qt-HQ_Base.dll和Qt-HQ_Base64.dll。
具体如何生成上一条刚说过,不再赘述。
6.4.文件总结:
上面所有的文件结构就是:
.../include/*.h
.../include/Debug/Qt-HQ_Base.dll
.../include/Debug/Qt-HQ_Base64.dll
.../include/Release/Qt-HQ_Base.dll
.../include/Release/Qt-HQ_Base64.dll
同理,也可以指定调用dll的exe工程的.pro文件的TARGET,让它分别生成32位和64位的程序。就如上面完整的.pro文件中描述的那样,为了便于识别,这个exe工程我命名为了Qt-HQ_Base-exe。所以使用32位和64位编译器,会分别生成Qt-HQ_Base-exe.exe和Qt-HQ_Base-exe64.exe。
看起来名字蒙圈?我就是故意这样的。因为生成dll的时候TARGET为Qt-HQ_Base,所以调用dll的时候,TARGET就为Qt-HQ_Base-exe。无他,就为了容易识别。总得有个记号吧。
6.5.发布:
为了测试运行效果,我建了独立的目录:
.../Qt-HQ_Base-exe_debug/
.../Qt-HQ_Base-exe_release/
里面放的文件名都是一样的,只不过debug和release版本的不同,最大区别就是文件大小。这些文件分别是:
config.ini
qss.css
Qt-HQ_Base.db
Qt-HQ_Base.dll
Qt-HQ_Base64.dll
Qt-HQ_Base-exe.exe
Qt-HQ_Base-exe64.exe
其中.ini,.css,.db这个三个不用管,是我做的dll需要用到的,以及控制界面风格用的。主要是dll和exe文件,将来发布打包的时候就这样一并复制过去就可以了。
就像很多成品程序一样,为什么里面有两个exe文件,有一个带64后缀,一个不带。就是这个意思。
关于文件和路径,又是另外一个话题,下面提到。
7.路径问题:
以上所有路径,仅仅是针对于开发环境而言,在qt环境中,点击不带爬虫那个绿三角可以运行,因为qt会根据pro文件中的路径去查找。
真正发布的时候,把release目录中的exe文件拷贝出来,它如果找不到相关文件是不行的。所以比如配置文件,数据库,dll等,一并复制出来放在可找到的目录。当然一般就是跟exe一样的当前目录。这跟操作系统的查找路径有关,path环境变量定义的地方理论上都可以,具体怎么玩看自己。
我的另外一篇博客有提及:
从PowerBuilder+wiseinstaller程序发布看windows的system32目录共享_大橘的博客-CSDN博客
8.dll包含ui问题:
我就没把这个当回事。我写的dll中,包含一个分页控件的widget,ui也是个类,后期调用无非就是“提升为”这个类,改怎样就怎样,不用特殊处理。
9.本文完。
版权归原作者 大橘 所有, 如有侵权,请联系我们删除。