从QMake到CMake-带有插件与ActiveX的Qt .pro工程向MSYS2-CMake移植实验

贺子昂
2023-12-01

鉴于 Qt6 已经选择了CMake作为基本的源码编译工具,看来我们不得不好好熟悉一下CMake的用法了。从网上和Qt新建工程的模板出发,花了3天时间,基本熟悉了CMake的语法和原理,并成功用笔者的一个OpenStreetMap客户端工程作为实验,为其添加了CMake支持。本文主要介绍一下迁移的基本过程,以及注意事项,最后,会进行一个小的总结。

该工程在我的博文中有详细描述,完成迁移的项目可以参考源码https://gitcode.net/coloreaglestdio/qplanetosm/-/blob/master

之所以考虑选择此工程,是因为其既具有很小的身材,又涉及不少的功能:

  1. 总规模很小,但含有多个子项目;
  2. 主界面工程同时导出widget, 既一个文件夹多个生成目标;
  3. 原本的.pro格式项目在Arm、Windows 、Linux上均可以运行,因此对比pro和cmake,便于大家迅速学习;
  4. 含有Qt自定义插件的编译;
  5. 含有QtDesigner插件的编译;
  6. 含有ActiveX控件的编译后处理,如提取IDL, 嵌入TLB等。

一旦我们成功在此项目上应用CMake,就能够基本在掌握CMake的主要功能,为后续更大的项目学习做好准备。

1. 为什么要有CMake?VC不香吗?

目前我们学校教学使用的还是VC2010, 同学们习惯于直接一路Next建立工程。即使是使用Qt的同学,主要以Qt的VS扩展,或者QtCreator QMake为主。QtCreator对待QMake Pro文件,就和VS对待.vcprojx一样,开发小的程序,都是基本不需要人为动工程文件的。

1.1 CMake的意义

包括笔者,平时基本也不会手工修改工程文件,只喜欢右键点击,甚至一路Next。有同学问我,为什么放着IDE不用,还要学这个——QtCreator里CMake添加文件都是不支持的,只能打开CMakeLists.txt写代码。这特莫不等于又学了一种语言?!我想主要的原因很多大佬已经说的很客观了,我总结起来,就是无奈的选择,瘸子里拔将军。

因为C++跨ABI带来的邪门的问题罄竹难书,在很久以前,没有VC和Qt的时候,就已经把上一代爷爷辈的大佬搞得焦头烂额。相对其他命令行下的跨平台编译环境而言,CMake可以较为简单的Hold住超大项目的编译,使得本来不可能完成的任务,通过996可以完成。

1.2 关于复杂性的题外话

当系统遇到的变数太多,复杂性一定会呈指数级别增长——不管用什么语言。

先看VC。实际上,如果项目的依赖性太多,VC也是很烦的。要不停点击鼠标,配置一堆的外部Include,Lib文件夹。尤其是库开发者,要记住ANSI/Unicode、 Debug/Release 、 32/64 的组合,2x2x2就是8组配置,一不小心就出错。想做一个跨平台的通用库,是非常累的事情。因为库的作者无法确定用户的具体环境,不能像丁老师实验课要求大家都把openCV放在D:\,而后,直接把工程拷贝过去,并要求在32位编译器,ANSI字符集运行。

推广而来,即使是当代的神器Python, 遇到pip的兼容性问题也是头大。前面AI课程,我们推迟了1个礼拜,就是因为我手贱,滚动更新了Anaconda后,导致spider和tensorflow对python的小版本要求不同,spider环境里面老是无法调用tensorflow。生态系统发展到今天,神仙也头大了——各个包的依赖性不是简单的一对一的,而是图状的,一旦某个团队修改了特性,别的团队没有及时更近,就呵呵了。只能不停update,或者退回去。

说Python简单,也是一种营销。在7x24小时环境中,把涉及多个依赖的应用稳定跑起来是一种境界,能够迅速部署到不同的环境中是另一种境界,真实生产环境下,使用Python和C++的代价基本差不多。可以看看PostgreSQL的官方客户端 pgAdminIII 和 pgAdmin4的开发日志,就明白无论哪种框架,都免不得反复迭代。实际上就pgAdmin 4那样的Plot界面,用Qt也是可以很快实现的,比如tableau.

2. CMake项目的基本结构

CMake的基本原理和QMake一样。通过扫描各个文件夹的CMakeLists.txt, 获取要生成哪些目标。生成目标用到的参数,是从很多变量里获得的。变量有的是系统自己定义的,有的是用户配置的。最终,系统会输出用于真正编译生成目标的Make文件,也就是MakeFile.

我们看最简单的一个DLL工程:

#开启对Qt等特性的支持
QT       += widgets
TARGET = qtvplugin_grid
TEMPLATE = lib
#设置预编译宏 PLANETOSM_EXPORT_DLL, 好在源代码里判断DLL函数是导出还是导入
DEFINES += QTVPLUGIN_GRID_LIBRARY
#C++版本特性支持
QMAKE_CXXFLAGS += -std=c++17
#列举文件
SOURCES += \
    qtvplugin_grid.cpp
HEADERS +=\
    ../qtviewer_planetosm/osmtiles/layer_interface.h \
    ../qtviewer_planetosm/osmtiles/viewer_interface.h \
    qtvplugin_grid.h
FORMS += \
    qtvplugin_grid.ui
TRANSLATIONS += qtvplugin_grid_zh_CN.ts
RESOURCES += \
    resources.qrc

这个QMake的工程主要描述了Qt的依赖库,目标的名称,类型。通过DESTDIR设置输出的文件夹,通过DEFINES加入编译器预先宏定义。由于Qt本身是为自己的API量身定制的(其实除了QMake,还有Qbs),所以非常简洁。基本按照上述结构,替换为CMake:

#指定CMake的最低版本要求
cmake_minimum_required(VERSION 3.5)

#指定工程的信息
project(qtvplugin_grid VERSION 1.0 LANGUAGES CXX)

#开启对Qt等特性的支持, CMake与QMake相比,要额外告诉它,引入Qt.尽管已经做了很大简化,但比QMake还是要多几行。
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOUIC ON) #自动对UI文件执行uic
set(CMAKE_AUTOMOC ON) #自动对h/cpp文件执行moc
set(CMAKE_AUTORCC ON) #自动对qrc文件执行rcc

#C++版本特性支持
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

#引入必须的Qt模块,找到的库会有 库名_FOUND的变量被定义,以便判断。
#同时会有各类文件夹对应的变量被创建find_package是CMake最关键的特性之一
find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED)

#创建编译目标,是一个DLL(SHARED library),由列举的文件生成
add_library(qtvplugin_grid SHARED
    ../qtviewer_planetosm/osmtiles/layer_interface.h
    ../qtviewer_planetosm/osmtiles/viewer_interface.h
    qtvplugin_grid.h
    qtvplugin_grid.cpp
    qtvplugin_grid.ui
    resources.qrc
    qtvplugin_grid_zh_CN.ts
)
#设置预编译宏 PLANETOSM_EXPORT_DLL, 好在源代码里判断DLL函数是导出还是导入
target_compile_definitions(qtvplugin_grid PRIVATE PLANETOSM_EXPORT_DLL)
#链接库
target_link_libraries(qtvplugin_grid PRIVATE
    Qt${QT_VERSION_MAJOR}::Widgets
)

从上面的比较,我们发现:

  1. 二者都是用文本文件描述工程(废话)
  2. QMake一般是一个pro文件一个目标(Target),实在要多目标,通过config和QMAKE命令行指定。cMake是一个文件描述一个或者多个目标(add)。
  3. 由于CMake是通用的工具,想让它认识Qt,还要多写几行代码。

CMake与其他类似工具比较,find_package是比较重要的特性。关于这个知识可以参考这里

3. 多层文件夹

CMake也可以支持类似QMake的subdir开关,支持多层文件夹。我们来看顶层结构:

cmake_minimum_required(VERSION 3.5)
project(qtv.planet VERSION 1.0 LANGUAGES CXX)
#设置整体工程的输出文件夹
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)

#设置整体工程的编译开关,可以通过该开关开启/关闭ActiveX控件编译。
option(QTV_ACTIVEX "BUILD Active X control for the map." ON)
#配置开关的合法性
if (NOT WIN32 AND QTV_ACTIVEX)
    SET(QTV_ACTIVEX OFF)
endif()
#子工程文件夹注册
add_subdirectory(qtviewer_planetosm)
add_subdirectory(qtvplugin_geomarker)
add_subdirectory(qtvplugin_grid)
add_subdirectory(qtwidget_planetosm_designer)
add_subdirectory(test_container)

这里比较有意思的特点是,CMake的add_subdirectory加入的文件夹名字,不需要类似QMake一样,默认与pro的名字一致。因为CMake的工程文件永远叫CMakeLists.txt。具体的输出参数、工程名字是在内部明确指定的。

4. 多目标

一个CMakeLists.txt可以按需编译出好多个目标,比如既想生成EXE,又想生成Qt Widget控件,则可以既add_executable 又 add_library。相比CMake,QMake的多目标,以前主要用多个pro文件设置。QMake的config开关设置多目标也是可以的,但不常用。

#上层QMake
TEMPLATE = subdirs
DEFINES += BUILD_ACTIVEX_OSM
SUBDIRS += \
	qtwidget_planetosm \
	qtwidget_planetosm_designer \
	qtviewer_planetosm 
qtwidget_planetosm.file = qtviewer_planetosm/qtwidget_planetosm.pro
qtaxviewer_planetosm.file = qtviewer_planetosm/qtaxviewer_planetosm.pro
#目标1  qtwidget_planetosm.pro
...
#目标2 qtwidget_planetosm.pro
...
#目标3 qtaxviewer_planetosm.pro

而CMake在文件里指定多目标,则直接顺序添加多个目标即可:

cmake_minimum_required(VERSION 3.5)
project(qtv_mainframe VERSION 1.0 LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
#省略..
find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Network AxServer REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Network AxServer REQUIRED)
#省略..
set(PRJ_HEADERS
    osm_frame_widget.h
    osmtiles/cProjectionMercator.h
    ...
    )
set(PRJ_SOURCES
    osmtiles/layer_tiles_page.cpp
    ...
    )
# ========================Exe========================
#省略..
add_executable(qtviewer_planetosm
	main.cpp
	${PRJ_HEADERS}
	${PRJ_SOURCES}
    )
endif()
#省略..
# ========================Widget Library========================
add_library(qplanetosm_widget SHARED
    qtwidget_planetosm.h
    qtwidget_planetosm.cpp
    ${PRJ_HEADERS}
    ${PRJ_SOURCES}
)
#==========================ActiveX==========================
#省略..
add_library(axplanetosm SHARED
    qtaxviewer_planetosm.def
    qtaxviewer_planetosm.h
    qtaxviewer_planetosm.cpp
    ${PRJ_HEADERS}
    ${PRJ_SOURCES}
)

#省略..

5 ActiveX编译

Qt里,ActiveQt是最乖戾的存在。在Qt的QMake版本中,编译和调用ActiveX都是非常容易出错的事情。在CMake里,更是要注意啦。

5.1 编译前准备

  1. 如果是VC编译器,则需要把Qt的bin文件夹设置到PATH里,否则,注册的时候过不去。
  2. 如果是MingW编译,或者是MSYS2环境,则需要参考我以前写的文章,首先解决midl的调用问题。widl是不完善的,ActiveX还是要VC工具链命令行的参与。文章只看第三节即可,链接:https://blog.csdn.net/goldenhawking/article/details/51125604
  3. win10以上,默认普通用户是不能用/regserver注册,要用/regserverperuser, 因此要木用管理员启动,调用regserver,要木就用普通用户,调用regserverperuser.

5.2 酌情使用Qt6的AxServer CMake便利函数

最新的Qt 6.2是有一个自动提取idl,植入tlb并注册控件的脚本,但是目前不建议使用。要使用的话,就把自动注册关闭,否则它默认用管理员去注册去了。

#关键就是NO_AX_SERVER_REGISTRATION
qt6_add_axserver_library(axplanetosm SHARED NO_AX_SERVER_REGISTRATION
	    qtaxviewer_planetosm.def
	    qtaxviewer_planetosm.h
	    qtaxviewer_planetosm.cpp
	    ${PRJ_HEADERS}
	    ${PRJ_SOURCES}
	    ${PRJ_FORMS}
	    ${PRJ_RESOURCES}
	)
add_custo$<TARGET_FILE:axplanetosm>\m_command(TARGET axplanetosm
	     POST_BUILD
	     COMMAND echo If you want to reg server, please set Qt BIN PATH first
	     COMMAND idc.exe \"$<TARGET_FILE:axplanetosm>\" /regserverperuser
	     #COMMAND regsvr32 \"$<TARGET_FILE:axplanetosm>\"
	     #COMMAND idc.exe \"$<TARGET_FILE:axplanetosm>\" /regserver
	     )

5.3 调用自定义命令完成注册

一般来说,我们都是通过自定义命令完成注册:

add_custom_command(TARGET axplanetosm
		 POST_BUILD
		 COMMAND idc.exe \"$<TARGET_FILE:axplanetosm>\" /idl \"$<TARGET_FILE:axplanetosm>.idl\" -version 1.0
		 COMMAND midl.exe \"$<TARGET_FILE:axplanetosm>.idl\" /nologo /tlb  \"$<TARGET_FILE:axplanetosm>.tlb\"
		 COMMAND idc.exe \"$<TARGET_FILE:axplanetosm>\" /tlb \"$<TARGET_FILE:axplanetosm>.tlb\"
		 COMMAND idc.exe \"$<TARGET_FILE:axplanetosm>\" /regserverperuser
		 #COMMAND idc.exe \"$<TARGET_FILE:axplanetosm>\" /regserver
		 )

7 总结与建议

通过近2天的阅读,和1天的调试,我们顺利把一个含有多种属性的工程从qmake迁移到cmake. 注意,使用CMake的工程,目前无法获得QMake一样的右键便捷性。这个特性上,显著降低了CMake的易用性。 如果项目是封闭的,建议还是使用QMake。QMake与Qt的结合是最紧密的。

相关工程请从https://gitcode.net/coloreaglestdio/qplanetosm/-/blob/master签出。

 类似资料: