Qt官方逐渐的放弃了qmake构建体系,切换到了cmake。我做了一些尝试把过去的qmake体系下的一些软件迁移到了cmake体系,这里记录一下,以备不时之需。

cmake的一般用法和其他的软件没有什么区别,比如说cmake_minimum_required、project、set这些常用命令。但是涉及到Qt会有一些额外的命令。

自动预处理

Qt有3个重要的预处理命令:moc、uic、rcc

moc用于处理包含Q_OBJECT宏的类,uic用于处理QtDesigner设计的ui,rcc用于处理资源文件。

为了保证预处理命令的运行,在CMakeLists.txt中要加入一些内容

对于单一编译目标的项目,在CMakeListx.txt中加入

# auto MOC, UIC, RCC
set_target_properties(${PROJECT_NAME} PROPERTIES
    AUTOMOC ON
    AUTOUIC ON
    AUTORCC ON
)

对于多编译目标的项目,可以在每个需要单独处理的submodule的CMakeLists.txt中加入

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/ui)

cmake编译Qt程序的基本的设定

二进制目标

一般的程序,cmake会这样设置二进制目标

cmake_minimum_required(VERSION 3.14)
set(APP_NAME myName)
project(${APP_NAME})

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
add_executable(${APP_NAME } ${SRC_LIST})

对于Qt程序,则要稍作调整

cmake_minimum_required(VERSION 3.14)
set(APP_NAME myName)
project(${APP_NAME})

qt_standard_project_setup()
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
qt_add_executable(${APP_NAME} ${SRC_LIST})

qt_add_executable指令会增加一些Qt专属的设置。

要特别提醒的是cmake自带的add_executable命令会把第一个参数设定为APP_NAME变量,而qt_add_executable指令不会,所以最好是搭配set命令单独设置这个变量

链接库

Qt专用的链接库

target_link_libraries(${APP_NAME} PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets)

这是最常用的Qt组件库,如果需要其他的组件库,追加到后面就好

部署

如果在windows平台采用vcpkg作为依赖包的管理器,那么将会遇到一些插件的问题。

Qt默认自带的插件不是安装在bin目录的,在vcpkg中Qt的插件安装在Qt6/plugins目录下。

所以,当编译完成后,运行可执行程序的时候,会提示找不到插件。

把需要的插件复制到可执行程序的对应目录就可以了。但是手工处理的话很难搞清楚。

https://github.com/microsoft/vcpkg/issues/43668

这里的讨论提示在CMakeLists.txt中增加一条指令

add_custom_command(TARGET ${APP_NAME} POST_BUILD COMMAND Qt6::windeployqt --qmldir "${CMAKE_CURRENT_SOURCE_DIR}/qml" "$<TARGET_FILE:${APP_NAME}>" ) 

就可以自动把依赖的二进制文件复制到合适的目标目录

前面添加的qt_add_executable指令会自动处理插件的依赖关系,然后这步通过windeployqt来把需要的二进制文件复制到合适的位置。

最后清理

qt_finalize_project | Qt Core 6.8.2

在整个CMakeLists.txt文件的最后还要增加一条指令

qt_finalize_project()

这样就可以保证cmake在构建Qt程序的时候能够把需要处理的都处理掉。

按照Qt官方的说法,这个指令将来可能会变,到时候再说。

小结

总结起来CMakeLists.txt就像这样

cmake_minimum_required(VERSION 3.14)
set(APP_NAME myName)
project(${APP_NAME})

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/ui)

qt_standard_project_setup()
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
qt_add_executable(${APP_NAME} ${SRC_LIST})

target_link_libraries(${APP_NAME} PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets)

add_custom_command(TARGET ${APP_NAME} POST_BUILD COMMAND Qt6::windeployqt --qmldir "${CMAKE_CURRENT_SOURCE_DIR}/qml" "$<TARGET_FILE:${APP_NAME}>" ) 

qt_finalize_project()

一个简单的Qt程序的CMakeListx.txt就是这样,从qt_standard_project_setup()开始,到qt_finalize_project()结束。

但是实际开发中往往会遇到更多的需求,这里记录一下我遇到的需求在cmake构建系统中的处理方法。

去掉控制台

Qt主要用于开发图形界面程序,但是如果代码中含有一些控制台的函数,比如qDebug(),那么,Qt程序编译的时候就会带着控制台的相关代码。在运行的时候,除了主图形界面,还会附带一个控制台,并在控制台输出相关内容,有些甚至不是开发者自己写的代码,而是Qt自带的一些代码。

https://forum.qt.io/topic/121502/cmake-shows-console/3

CMakeLists.txt中加入这条指令,编译出来的Qt程序在运行的时候就不带控制台了

# hide the console
set_property(TARGET ${APP_NAME} PROPERTY WIN32_EXECUTABLE true)

多国语言

不需要多国语言的Qt软件CMakeLists.txt是这样设置的

qt_standard_project_setup()

如果需要多国语言,那么就需要调整一下设置,这样在编译后的软件中zh_CN的翻译部分会被嵌入到最终生成的二进制程序中。

qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES zh_CN)

qt_add_translations | Qt Linguist Manual

对于Qt多国语言程序,主要是在代码中涉及到多国语言的位置使用QString.tr()函数。Qt提供了一个linguist语言家程序可以扫描代码中的tr()函数,并汇总为一个扩展名为ts的文件,实质上这是一个xml格式的文件。Qt语言家可以方便的编辑这个ts文件,开发者可以把这里面的词句翻译为目标语言,当然如果不用Qt语言家来编辑,而是采用其他文本编辑器也是可以的。

在cmake构建系统内,Qt提供了自动调用linguits的功能,不过需要CMakeLists.txt中加入

find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets LinguistTools)

注意最后的LinguistTools,带上这一项才能保证cmake能调用linguist,按照开发者要求在构建过程中执行相应的指令。linguist是QtTools的子组件。

在cmake构建系统里,linguist提供了一些指令

CMake Commands in Qt6 LinguistTools | Qt Linguist Manual

lrelease命令是扫描所有源代码文件,找出里面的tr()函数,并汇总到一个ts文件里。当然,它不会自动翻译里面的内容,开发者需要自己翻译。

lupdate命令是在源代码文件有所变更的情况下,扫描源代码文件,并把变更更新到对应的ts文件里。如果是全新的ts文件,直接用lrelease重新生成就好了;如果是已经翻译好的文件,不能丢弃已经翻译好的部分,就要用到lupdate

add_translation计划要废弃

add_translations是把xml格式的ts文件编译成二进制的qm文件,这个qm文件可以嵌入到可执行程序里,将来这个可执行程序就可以显示对应的语言。在add_translations里,create_translation会被自动调用,我不用create_translation。

在CMakeLists.txt里添加

qt_add_translations(${APP_NAME}
    TS_FILE_BASE basename
    TS_FILE_DIR translation
    RESOURCE_PREFIX "/lang"
)

APP_NAME就是前面qt_add_executable生成的变量

TS_FILE_BASE是涉及到所有ts文件命名的参数。所有的ts文件应该按照<TS_FILE_BASE>_<LANG>.ts来命名。如果这个选项留空,那就会被自动设定为${PROJECT_NAME}

TS_FILE_DIR选项是指从CMakeLists.txt所在的目录出发,找到ts文件的相对路径。

RESOURCE_PREFIX是在源代码的QTranslator中调用时,需要设定的部分。比如说这里设定的RESOURCE_PREFIX是"/lang",那么在源代码中

    QApplication app(argc, argv);
    QTranslator translator;
    const QStringList uiLanguages = QLocale::system().uiLanguages();
    for (const QString &locale : uiLanguages)
    {
        const QString translationFileName = "basename_" + QLocale(locale).name(); // 6
        if (translator.load(":/lang/" + translationFileName))
        {
            app.installTranslator(&translator);
            break;
        }
    }

第6行的basename就是前面设定的TS_FILE_BASE

QLocale(locale).name()就是语言的代码,比如说zh_CN或者en_US

它们中间加一个下划线_组合在一起就是translationFileName

而在translator.load函数中":/lang/" + translationFileName会组合成一个字符串,这个字符串以冒号:开头表明这是一个资源,后面用/分段。第一段的/lang就是RESOURCE_PREFIX,第二段就是TS_FILE_BASE和语言代码组成的translationFileName。

意思就是说通过:判断出这是一个资源。因为CMakeLists.txt中qt_add_translations设置了lang是翻译资源的意思,在构建时源代码中遇到lang就识别为翻译资源。构建时在qt_add_translations里设置的TS_FILE_DIR目录下找到<TS_FILE_BASE>_<LANG>.ts也就是源代码中的translocationFileName找到这个翻译文件。交给qrc和编译器做最后的处理。

qt_add_translations的更多选项可以参考

qt_add_translations | Qt Linguist Manual

qt_add_lrelease和qt_add_lupdate虽然可以在cmake构建系统中自动执行,但是我觉得意义不大,因为就算生成了ts文件,还是要人来手动翻译才能完成,所以这步应该在编码翻译阶段用,而不是等到自动构建阶段再用。

linguist和QtCreator集成非常好,QtCreator是GPL和商业许可的软件,商用的话需要付费,linguist命令行工具是免费的。如果想用cmake调用linguist命令行来工作也不是不可以,这样可以实现一些自动批处理,可以调整一下cmake的target。这样的话,直接用命令行也可以。

资源的处理

cmake处理Qt资源有2种方法,一种是采用qrc定义资源文件的方法,另一种是在CMakeLists.txt中使用qt_add_resources指令的方法。

qrc定义资源文件

The Qt Resource System | Qt Core 6.8.2

QtCreator可以创建qrc资源文件并很方便的编辑,如果打开qrc文件就会发现,其实它是一个纯文本的文件,xml格式的。内部大概是这样

<RCC>
    <qresource prefix="/icon">
        <file>images/copy.png</file>
        <file alias="cut">images/cut.jpg</file>
        <file>images/new.png</file>
        <file>images/open.png</file>
        <file>images/paste.png</file>
        <file>images/save.png</file>
    </qresource>
    <qresource prefix="/icon" lang="fr">
        <file alias="images/cut.jpg">images/cut_fr.jpg</file>
    </qresource>
</RCC>

当Qt使用资源文件的时候,可以直接使用绝对路径,比如说源代码里

copy_icon = new QIcon("C:/project/icons/copy.png");

这种方法的缺点是显而易见的,一旦项目目录移动,绝对路径就失效了。如果采用以...的相对路径,也有类似的问题,因为不同的构建系统输出的应用程序的目录是不一致的。

如果采用资源文件的方法就可以避免类似问题。上面的源代码可以改为

copy_icon = new QIcon(":/icons/images/copy.png");

路径以冒号:开头代表这是一个资源路径,后面以/分割为几段。

第一段icons就是前缀,它和qrc文件中的prefix一致。实际上在qrc文件中,可以在不同的块设置不同的prefix,这样就可以把不同的资源分开。

第二段images/copy.png就是资源文件中的具体条目的内容。如果具体的条目设置有别名alias,那么在源代码中调用资源时就要用别名,比如说

cut_icon = new QIcon(":/icons/cut");

这里的cut就是images/cut.jpg的别名

同样的资源还可以设定不同的语言,当程序在不同语言运行时,会根据语言切换资源。比如说当程序运行在法语环境时,cut.jpg图标会自动切换为cut_fr.jpg的图像,但是开发者在编码中要把它alias到那个没有语言设定的资源的本名(不是别名),也就是images/cut.jpg

也就是说,在Qt的资源系统里,如果遇到了冒号:开头的就会识别为资源,然后紧跟着分析是否匹配prefix,匹配好以后根据别名alias去找对应的资源,如果没有别名就会去直接找相应的资源,资源放在哪里则是在qrc文件中定义好的,别名和实际资源名的对应关系是一一对应的。

在cmake构建系统中处理qrc资源文件的时候,要这样

set(CMAKE_AUTORCC ON) # 前面已经说过了,启用自动rcc

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
qt_add_executable(${APP_NAME} WIN32
    resource.qrc
    ${SRC_LIST}
)

先启动自动rcc,然后在qt_add_executable指令中,把qrc资源文件和源代码一起加入进去。在构建的时候,cmake就会自动调用rcc处理资源文件,然后在编译源代码的时候自动把资源加入进去。

在CMakeLists.txt中使用qt_add_resources指令

qt_target_qml_sources | Qt Qml 6.8.2

在CMakeLists.txt文件中,这样添加指令

set(CMAKE_AUTORCC ON) # 前面已经说过了,启用自动rcc

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
qt_add_executable(${APP_NAME} WIN32 ${SRC_LIST})
qt_add_resources(${APP_NAME} icons
    PREFIX "/icons"
    FILES images/copy.png images/cut.jpg images/new.png images/open.png images/paste.png images/save.png
)

set_source_files_properties(images/cut.jpg PROPERTIES
    QT_RESOURCE_ALIAS cut
)

qt_add_resources(${APP_NAME} icons_fr
    PREFIX "/icons"
    LANG fr
    FILES images/cut-fr.jpg
)

set_source_files_properties(images/cut-fr.jpg PROPERTIES
    QT_RESOURCE_ALIAS images/cut.jpg
)

qt_add_resources指令的前面两个参数分别是目标和资源名,如果要多次添加,那么资源名必须要不同。

后面的PREFIX和qrc中的prefix是一个意思,在调用的时候要紧跟在冒号后面。

LANG参数就是语言

FILES就是资源的文件名,这里写的是从CMakeLists.txt出发的相对路径名

但是qt_add_resources指令不能设定别名,虽然在一般资源的使用中可以不使用别名,而使用原始文件名,但是在涉及到语言选择时,别名是必须要有的。这个时候要set_source_files_properties命令来处理一下,如果需要处理的文件比较多的话,还是挺烦的

在源码使用资源时用法和qrc方案的用法完全一样

cut_icon = new QIcon(":/icons/cut");

以冒号:开头表明这是一个资源,然后用/分割几段。第一段是PREFIX,也就是icons;后面是别名

这个别名对应的images/cut.jpg在法语中还有一个替代品images/cut-fr.jpg,所以在语言切换到法语的时候,这个图标会自动切换到cut-fr.jpg

对比qrc方法和qt_add_resources方法,我还是更倾向于使用qrc的方案来管理资源

更多cmake相关设置可以参看

CMake Commands in Qt6 Core | Qt Core 6.8.2

软件自身图标

Setting the Application Icon | Qt 6.8

软件内部的图标按照一般资源来处理就可以了,但是在windows系统下,软件本身还有一个图标,这个图标不仅会显示在windows文件管理器里,在软件运行时还会显示在任务栏。这个图标没法按照一般资源的方式来处理。如果是命令行程序或者后台程序,没有图形界面,这个图标也就无所谓了。但是Qt一般用于开发GUI程序,所以图标是很重要的。

首先要创建一个rc文件,比如说title.rc文件。这是Visual Studio开发时使用的资源文件格式,可以通过Visual Studio创建。里面包含3列,前两列是固定格式,第3列是图标文件,是从CMakeLists.txt出发的相对路径

IDI_ICON1               ICON    "icons/myappico.ico"

然后把这个rc文件加入到qt_add_executable指令里。

set(CMAKE_AUTORCC ON) # 前面已经说过了,启用自动rcc

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
qt_add_executable(${APP_NAME} WIN32
    resource.qrc
    ${SRC_LIST}
    title.rc # 新增的这行就是软件本身的图标
)

编译出来的可执行程序就带了图标

Linux系统不需要这么处理,因为Linux系统自身不带图形界面,图形界面中涉及到图标的时候都是通过另外定义.desktop文件的,跟程序本身是否带图标无关。

qwt插件

Qwt User's Guide: Qwt - Qt Widgets for Technical Applications

qwt是Qt的绘图插件,功能十分强大,不过已经多年没有更新。当年qwt是qmake构建体系开发的,现在Qt整体切换到了cmake构建体系,所以qwt在cmake下使用有点困难。

vcpkg项目把qwt集成了进来,并且提供了一个cmake模块,感谢微软的贡献

https://githubissues.com/microsoft/vcpkg/35522

只要在CMakeLists.txt中添加

find_package(unofficial-qwt CONFIG REQUIRED)
target_link_libraries(${APP_NAME} PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets unofficial::qwt::qwt) # 如果有其他的库需要链接也要加上

注意最后的unofficial::qwt::qwt 

这样在使用cmake构建的时候就可以自动处理qwt插件了

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐