• Qt制作dll并调用,以及解决QWidget: Must construct a QApplication before a QWidget


    1.前言:

    个人笔记,欢迎探讨。本次记录的,仅仅是一种为了减轻exe文件体量的封装,如果要作为正式发行的dll,还需要考虑二进制兼容。参考qxlsx。还有这个文章:

    QT宏: Q_DECLARE_PRIVATE与Q_DECLARE_PUBLIC(d指针 \ p指针) 及其优化使用Q_Q宏和Q_D宏_rainbow_lucky0106的博客-CSDN博客

    我写的类库是自己用的,按照文章提及的试过,没发现改动类以后的不兼容现象。不知何故,暂且放下,有空再研究。

    2.背景:

    本人实际接触qt没多久,但很多方面已经喜欢上了qt。微软的全家桶用起来更顺手,但qt的某些方面也很独到。

    本次遇到的问题,都源于dll。之前封装了一些代码,仅仅是h和cpp,复制过去就行了(其实跟lib静态链接库差不多意思,是否编译过的问题),然而心里总觉得不爽,弄成个dll多好。于是操练。

    网上看了很多文章,看似简单,其实大家也是摸索着来的,要善于总结。有些细节不耽误用的可以暂且放下,一定要培养成就感。很多时候先实现,再研究效果会更好。但是只管实现不考虑的做法,路会很短。

    3.制作dll:

    之前曾经照猫画虎实现过一次demo,这次想直接把我做的封装代码改成dll。需要改pro文件,不知道格式没关系,直接新建一个lib项目,qt就自己生成了,可以参考着来。

    我尽量用最剪短的方式说明。

    3.1.pro文件有关的:

    1. QT += core gui sql
    2. greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

    如上这两句不用说了,用到什么加什么就行了。接着:

    1. TEMPLATE = lib
    2. DEFINES += QTHQ_BASE_LIBRARY

    其实创建dll项目的时候qt会自动生成。

    第一句说明这是个库项目,不是输出exe。

    第二句是定义一个宏,后面有用。

    继续:

    1. HEADERS += \
    2. qt-hq_base_global.h

    qt会自动生成一个声明了宏的头文件,并包含进来。当然如果整个项目规模很小,比如就可以公用一个头文件的话,直接把里面的宏定义复制过来就可以了。

    我的做法是,把这个自动生成的头文件稍作修改,并且直接和dll的头文件放在一起,这样以后include的时候一并复制过去,省事。至于如何修改它,看下面。

    3.2.qt-hq_base_global.h文件:

    1. #include
    2. #if defined(QTHQ_BASE_LIBRARY)
    3. # define QTHQ_BASESHARED_EXPORT Q_DECL_EXPORT
    4. #else
    5. # define QTHQ_BASESHARED_EXPORT Q_DECL_IMPORT
    6. #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时没有定义宏,类声明为导入。

    我修改为这样:

    1. /*************************************************************************
    2. ** Class name: Macro defining
    3. ** Author: Henrick.Nie
    4. ** Created date: 2022-12-26
    5. ** Used for: The purpose of the header files of 'hq_base'
    6. **
    7. ** Used for 'cpp':
    8. **
    9. ** Add the code 'DEFINES += QTHQ_BASE_SOURCE' in the .pro file. Then
    10. ** the macro 'QTHQ_BASESHARED_EXPORT' means nothing. So the header
    11. ** files of 'hq_base' will be used for normal 'cpp' source codes.
    12. **
    13. ** Used for 'dll' developing:
    14. **
    15. ** Add the code 'DEFINES += QTHQ_BASE_LIBRARY' in the .pro file. Then
    16. ** the macro 'QTHQ_BASESHARED_EXPORT' equals 'Q_DECL_EXPORT', it means
    17. ** dll exporting. So the header files of 'hq_base' will be used for the
    18. ** 'dll' project, such as 'Qt-HQ_Base-dll'.
    19. **
    20. ** Used for 'dll' calling:
    21. **
    22. ** Do not add any other codes of macro defining in the .pro file. Then
    23. ** the macro 'QTHQ_BASESHARED_EXPORT' equals 'Q_DECL_IMPORT', it means
    24. ** dll importing. So the header files of 'hq_base' will be used for the
    25. ** 'dll' calling project, such as 'Qt-HQ_Base-exe'.
    26. **
    27. ** In the header files:
    28. **
    29. ** This header file including is required.
    30. ** Class defining using macro 'QTHQ_BASESHARED_EXPORT' is required.
    31. ** Such as below:
    32. **
    33. ** #include "qt-hq_base_global.h"
    34. **
    35. ** class QTHQ_BASESHARED_EXPORT MyClass {...};
    36. **
    37. *************************************************************************/
    38. #ifndef QTHQ_BASE_GLOBAL_H
    39. #define QTHQ_BASE_GLOBAL_H
    40. #include
    41. #if defined(QTHQ_BASE_SOURCE)
    42. # define QTHQ_BASESHARED_EXPORT //Used for normal 'cpp'.
    43. #elif defined(QTHQ_BASE_LIBRARY)
    44. # define QTHQ_BASESHARED_EXPORT Q_DECL_EXPORT //Used for 'dll' developing.
    45. #else
    46. # define QTHQ_BASESHARED_EXPORT Q_DECL_IMPORT //Used for 'dll' calling.
    47. #endif
    48. #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.所有自己写的头文件:

    头文件里当然主要内容是类定义,针对类定义一定要加上之前的宏:

    1. class QTHQ_BASESHARED_EXPORT MyClass
    2. {...}

    以后dll的头文件要分发给调用者,所以头文件里的类,要根据使用环境不同而声明为导出/导入。显然,制作dll的时候,应该声明为导出。exe调用dll的时候,应该声明为导入。这就是前面提到的宏定义的作用。

    然后就可以编译输出了,编译器设置就不赘述了,可以选择生成Debug或Release版本。

    3.4.关于32bit和64bit:

    在.pro文件的开头指定TARGET的地方,做一些修改:

    1. #TARGET = Qt-HQ_Base-exe
    2. contains(QT_ARCH, i386) {
    3. TARGET = Qt-HQ_Base-exe
    4. } else {
    5. TARGET = Qt-HQ_Base-exe64
    6. }

    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.可以指明包含路径,就是刚才复制的文件位置。如下这句就够了。

    1. INCLUDEPATH +=\
    2. $$PWD/include

    4.2.2.还可以写成下面的方式去找头文件我认为也可以,只是没必要:

    1. HEADERS += \
    2. ***.h

    4.2.4.但是一定要注意,千万别用pri方式:

    include($$PWD/include/include.pri)

    否则会报重复声明的警告,但是能编译通过,也能用,但就是不爽。

    redeclared without dllimport attribute: previous dllimport ignored

    4.2.5.然后是大名鼎鼎的LIBS +=:

    1. LIBS += -L$$PWD/include/ -lMyDll
    2. #DEPENDPATH += $$PWD/include/

    那个依赖路径我没遇到,感兴趣可以去查。网上说,它就是在头文件修改以后,是否重新编译源文件的意思,既然生成了,我就用上了,其它没细究。

    主要是LIBS +=的语法。等号后面可以直接写绝对路径,也可以用这种通配符。语法格式特别像linux下的命令参数。“-L”后面紧跟着是dll所在路径,不能有空格。“-l”后面是那个.a的文件名,不能有空格。但是.a文件一般为lib***.a,这里写的时候,去掉lib前缀和扩展名。

    4.2.6.让LIB +=支持32bit和64bit:

    再升级一下,改成这样:

    1. contains(QT_ARCH, i386) {
    2. #message("32-bit")
    3. CONFIG(debug,debug|release){
    4. LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
    5. }
    6. else{
    7. LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
    8. }
    9. } else {
    10. #message("64-bit")
    11. CONFIG(debug,debug|release){
    12. LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base64
    13. }
    14. else{
    15. LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base64
    16. }
    17. }

    对应前文提到的关于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文件的关键内容大致如下:

    1. ...
    2. #TARGET = Qt-HQ_Base-exe
    3. contains(QT_ARCH, i386) {
    4. TARGET = Qt-HQ_Base-exe
    5. } else {
    6. TARGET = Qt-HQ_Base-exe64
    7. }
    8. ...
    9. include($$PWD/_db/_db.pri)
    10. include($$PWD/_modbus/_modbus.pri)
    11. include($$PWD/_encrypt/_encrypt.pri)
    12. #include($$PWD/include/include.pri) 这里不要写,不要写,不要写
    13. INCLUDEPATH +=\
    14. $$PWD/_db\
    15. $$PWD/_modbus\
    16. $$PWD/_encrypt\
    17. $$PWD/include
    18. DEPENDPATH += $$PWD/include
    19. contains(QT_ARCH, i386) {
    20. #message("32-bit")
    21. CONFIG(debug,debug|release){
    22. LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
    23. }
    24. else{
    25. LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
    26. }
    27. } else {
    28. #message("64-bit")
    29. CONFIG(debug,debug|release){
    30. LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base64
    31. }
    32. else{
    33. LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base64
    34. }
    35. }
    36. ...

    再次注意:头文件包含不要用的pri目录方式。

    5.版本交叉问题:

    5.1.报错:

    刚才其实挺简单的,但中途遇到问题:

    1. Debugging starts
    2. QWidget: Must construct a QApplication before a QWidget
    3. 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文件写成了这样:

    1. Debug: LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
    2. Release: LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base

    直接分成debug和release目录存放,真正发布的时候,记着对应即可。版本不要交叉使用就可以了。

    后来发现放在ubuntu下编译,又报错了。于是这个部分又改为以下样式:

    1. #Debug: LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
    2. #Release: LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
    3. CONFIG(debug,debug|release){
    4. LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
    5. }
    6. else{
    7. LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
    8. }

    这里要注意。else后面紧跟“{”,换行会报错。

    5.5.最终.pro文件的全貌:

    1. #-------------------------------------------------
    2. #
    3. # Project created by QtCreator 2022-05-17T08:55:54
    4. #
    5. #-------------------------------------------------
    6. QT += core gui sql serialbus
    7. greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
    8. #TARGET = Qt-HQ_Base-exe
    9. contains(QT_ARCH, i386) {
    10. TARGET = Qt-HQ_Base-exe
    11. } else {
    12. TARGET = Qt-HQ_Base-exe64
    13. }
    14. TEMPLATE = app
    15. # The following define makes your compiler emit warnings if you use
    16. # any feature of Qt which has been marked as deprecated (the exact warnings
    17. # depend on your compiler). Please consult the documentation of the
    18. # deprecated API in order to know how to port your code away from it.
    19. DEFINES += QT_DEPRECATED_WARNINGS
    20. # You can also make your code fail to compile if you use deprecated APIs.
    21. # In order to do so, uncomment the following line.
    22. # You can also select to disable deprecated APIs only up to a certain version of Qt.
    23. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
    24. include($$PWD/_db/_db.pri)
    25. include($$PWD/_modbus/_modbus.pri)
    26. include($$PWD/_encrypt/_encrypt.pri)
    27. #include($$PWD/include/include.pri)
    28. INCLUDEPATH +=\
    29. $$PWD/_db\
    30. $$PWD/_modbus\
    31. $$PWD/_encrypt\
    32. $$PWD/include
    33. DEPENDPATH += $$PWD/include
    34. contains(QT_ARCH, i386) {
    35. #message("32-bit")
    36. CONFIG(debug,debug|release){
    37. LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base
    38. }
    39. else{
    40. LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base
    41. }
    42. } else {
    43. #message("64-bit")
    44. CONFIG(debug,debug|release){
    45. LIBS += -L$$PWD/include/Debug/ -lQt-HQ_Base64
    46. }
    47. else{
    48. LIBS += -L$$PWD/include/Release/ -lQt-HQ_Base64
    49. }
    50. }
    51. SOURCES += \
    52. main.cpp \
    53. mainwindow.cpp
    54. HEADERS += \
    55. mainwindow.h
    56. FORMS += \
    57. mainwindow.ui
    58. QMAKE_CXXFLAGS += -Wno-unused-parameter
    59. 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.本文完。

  • 相关阅读:
    Linux文件-内存映射mmap
    大型网站系统架构演化实例_6.使用分布式文件系统和分布式数据库系统
    Python万圣节蝙蝠
    ESP32/8266使用painlessMesh库实现mesh
    网页制作基础大二dw作业HTML+CSS+JavaScript云南我的家乡旅游景点
    centos安装Redis
    c++的ThreadPool,OpenHarmony源码实现版赏析和使用
    HTML+CSS+JavaScript七夕情人节表白网页【樱花雨3D相册】超好看
    一文弄懂JUnit5相关注解
    面试题总结
  • 原文地址:https://blog.csdn.net/u012999461/article/details/126440052