Qt制作dll(带ui)并调用,兼容32位和64位

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文件有关的:

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.本文完。

  • 9
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在使用Qt生成界面的动态链接库(DLL)供C/C++调用时,可以按照以下步骤进行: 1. 创建Qt工程:首先,使用Qt Creator创建一个新的Qt工程。选择Qt Widgets应用程序模板,并选择导入外部库,并选择动态创建库。在项目设置中确保选择“构建为库”选项,以便生成DLL文件。 2. 在Qt工程中编写界面代码:使用Qt提供的UI设计器在Qt工程中编写相应的界面代码。可以使用QWidget、QMainWindow等类创建需要的窗口,并通过布局管理器来安排部件的位置。 3. 实现界面逻辑:在Qt工程中,根据需要实现相应的界面逻辑。可以通过信号和槽机制实现界面部件之间的交互,也可以在所需的界面类中编写相关函数进行处理。 4. 生成DLL文件:在Qt Creator中,选择构建菜单中的“构建”选项,编译和生成Qt工程。生成的DLL文件将会保存在指定的构建目录中。 5. 将DLL文件供C/C++调用:在C/C++项目中,通过导入生成的DLL文件进行调用。在C/C++代码中,使用动态链接库相关的函数和结构体来加载DLL,并调用其中的函数。 需要注意的是,在导出函数时,需要使用`__declspec(dllexport)`将要导出的函数标记为可导出的,以便在DLL中被C/C++代码调用。 总结起来,生成界面的DLL给C/C++调用的关键步骤为:创建Qt工程、编写界面代码和实现界面逻辑、生成DLL文件,以及将DLL文件供C/C++代码调用。通过这些步骤,就可以成功生成界面的DLL给C/C++调用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值