鉴于 Qt6 已经选择了CMake作为基本的源码编译工具,看来我们不得不好好熟悉一下CMake的用法了。从网上和Qt新建工程的模板出发,花了3天时间,基本熟悉了CMake的语法和原理,并成功用笔者的一个OpenStreetMap客户端工程作为实验,为其添加了CMake支持。本文主要介绍一下迁移的基本过程,以及注意事项,最后,会进行一个小的总结。
该工程在我的博文中有详细描述,完成迁移的项目可以参考源码https://gitcode.net/coloreaglestdio/qplanetosm/-/blob/master
之所以考虑选择此工程,是因为其既具有很小的身材,又涉及不少的功能:
- 总规模很小,但含有多个子项目;
- 主界面工程同时导出widget, 既一个文件夹多个生成目标;
- 原本的.pro格式项目在Arm、Windows 、Linux上均可以运行,因此对比pro和cmake,便于大家迅速学习;
- 含有Qt自定义插件的编译;
- 含有QtDesigner插件的编译;
- 含有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
)
从上面的比较,我们发现:
- 二者都是用文本文件描述工程(废话)
- QMake一般是一个pro文件一个目标(Target),实在要多目标,通过config和QMAKE命令行指定。cMake是一个文件描述一个或者多个目标(add)。
- 由于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 编译前准备
- 如果是VC编译器,则需要把Qt的bin文件夹设置到PATH里,否则,注册的时候过不去。
- 如果是MingW编译,或者是MSYS2环境,则需要参考我以前写的文章,首先解决midl的调用问题。widl是不完善的,ActiveX还是要VC工具链命令行的参与。文章只看第三节即可,链接:https://blog.csdn.net/goldenhawking/article/details/51125604
- 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签出。
|