☰
q2c工具详解:qmake工程一键转CMake的实操与避坑指南
2026/10/13 5:52:56 网站建设 项目流程

简介:q2c是一款面向Qt开发者的构建系统转换工具,专注于在qmake与CMake之间自动互转项目配置,解决Qt项目迁移与多构建系统维护的痛点。这份源码包仅14KB,共含13个文件,核心代码以C++实现,包括6个源程序文件与5个头文件,另附1个qmake工程文件和1份说明文档,结构小巧完整。包内代码按功能划分了项目解析、配置读取、终端参数处理与日志记录等模块,便于跟踪q2c如何解析.pro文件、生成CMakeLists.txt,也清晰展示反向转换时对参数与配置项的处理逻辑。整个资源可编译运行,参考说明文档即可完成构建安装,验证双向转换效果,复杂项目中的特殊配置也可通过阅读源码进行手动调整。已有3205人浏览学习,适合需要在Qt项目中灵活切换构建体系、或希望深入理解两种构建系统差异的开发者收藏使用。

1. q2c是干嘛的:qmake工程一键转CMake,先看清值不值得用

手头攒了十几个.pro文件的项目不在少数,Qt6全面拥抱CMake之后,老工程的构建脚本还停在qmake时代,迁不迁都难受。手动重写CMakeLists.txt不是不行,是漏一个头文件、丢一个链接参数就要排查半天。q2c就是干这个的转换工具——把qmake的.pro工程文件解析成CMakeLists.txt,把QT模块、源文件、头文件、UI表单、资源文件、宏定义和include路径一次性映射过去。它不是万能的,复杂的条件分支和自定义函数依然要手工处理,但对常规的app、lib工程,转换结果能直接进入构建流程,省下最机械的那部分工作量。

适合两类人:一是手里有存量qmake工程、准备迁到CMake的Qt开发者;二是新项目不想从零写CMakeLists,先用qmake画草稿再转的人。下面所有步骤都按我拆过的工程来写,照着走一遍就能落地。

2. qmake和CMake的映射关系:q2c的转换规则和三个关键配置

2.1 .pro里到底有什么:q2c真正要解析的东西

qmake的.pro文件本质上是一个声明式构建脚本:QT += widgets声明模块依赖,SOURCES += main.cpp罗列编译单元,CONFIG += c++17决定编译标准,DESTDIR、TEMPLATE控制产物布局。q2c做的第一件事就是逐行扫描这份文本,把每一类变量分拣出来,再映射到CMake的target属性上。一个典型的.pro是这样:

QT += core gui widgets TEMPLATE = app TARGET = myapp CONFIG += c++17 SOURCES += main.cpp MainWindow.cpp HEADERS += MainWindow.h FORMS += MainWindow.ui RESOURCES += resources.qrc DEFINES += APP_NAME=\"myapp\" INCLUDEPATH += include

逐行拆解映射关系:QT +=决定CMake里find_package的内容和target_link_libraries的链接库;TEMPLATE决定最终生成add_executable还是add_library;TARGET对应CMake的target名字;CONFIG += c++17对应CMAKE_CXX_STANDARD;SOURCES、HEADERS、FORMS、RESOURCES直接进target的源码列表;DEFINES变成target_compile_definitions;INCLUDEPATH变成target_include_directories。理解这一层之后,你才具备逐一核对转换结果的能力。

qmake变量是叠加的:QT += core和QT += widgets会合并成core gui widgets。q2c在解析时会把同名字段的增量合并后再输出,所以生成的CMakeLists里不会出现两份find_package调用。这个合并逻辑对SOURCES同样有效,哪怕.pro里在不同作用域中追加源文件,转换结果仍然是完整的源文件列表。

2.2 模块映射:QT += 到底对应CMake的什么

这是转换结果里最核心的对应关系,它决定工程能不能在CMake世界里正确找到Qt的库和头文件。qmake模块名到CMake组件的映射关系如下,这也是检查q2c输出是否正常的对照表:

qmake 模块find_package 组件链接目标
coreCoreQt6::Core
guiGuiQt6::Gui
widgetsWidgetsQt6::Widgets
networkNetworkQt6::Network
sqlSqlQt6::Sql
xmlXmlQt6::Xml
multimediaMultimediaQt6::Multimedia
chartsChartsQt6::Charts
quickQuickQt6::Quick
qmlQmlQt6::Qml
uitoolsUiToolsQt6::UiTools
testlibTestQt6::Test

以最常用的QT += core gui widgets为例,q2c生成的CMakeLists.txt里会包含find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED),后面跟着target_link_libraries(app PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets)。有个容易忽视的点:core和gui在qmake里是默认加载的模块,很多老工程并不会显式写QT += core,但CMake没有默认模块,所以q2c必须兜底补齐。我检查转换文件时,第一件事就是看find_package那一行有没有把Core和Gui补上,缺了后面链接必然报错。

如果你不确定某个Qt库对应CMake的什么组件名,有个比查文档更快的方法:打开Qt安装目录下的lib/cmake文件夹,里面的子目录名就是组件名,比如Qt6Charts对应Charts。这个规则在Qt5和Qt6下都适用。如果.pro里还写了greaterThan(QT_MAJOR_VERSION, 5)这种版本判断,q2c只能把它当作静态文本跳过,不会执行条件。常见做法是转换前确定目标Qt大版本,把版本分支删掉或统一,再跑q2c,否则find_package(Qt6 ...)和find_package(Qt5 ...)可能同时出现在生成文件里,配置阶段直接乱套。

2.3 三个自动化宏:AUTOMOC、AUTOUIC、AUTORCC

qmake时代有个隐藏的便利:.ui文件自动生成ui_xxx.h,带Q_OBJECT的头文件自动跑moc,.qrc资源文件自动编进二进制。这三件事在qmake里是默认行为,到了CMake却要显式开启,否则工程能编译,但链接期会报一堆undefined reference to vtable。q2c生成的CMakeLists.txt顶部一般会这样设置:

set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON)

三个宏的分工很明确:AUTOMOC扫描target里所有源文件,对包含Q_OBJECT的头文件自动生成moc源文件并参与编译;AUTOUIC负责把.ui文件转换成ui_*.h,转换时使用target的include路径作为搜索目录;AUTORCC处理.qrc里列出的资源,资源内容会被编译进生成的qrc_*.cpp。这三个开关对应qmake里CONFIG += automoc的旧写法,但q2c的输出里不再写CONFIG,直接交给CMake宏接管。

一个容易被忽略的细节:AUTOMOC只处理出现在target源文件列表里的文件。如果.pro里的HEADERS没写全,某个头文件虽然被.cpp间接包含,但没写进CMakeLists的target列表,moc就不会触发,结果就是那种“编译全过、链接报槽函数找不到”的玄学问题。如果你需要给moc额外传参数,用set_property(TARGET demo PROPERTY AUTOMOC_MOC_OPTIONS -i),这些参数在qmake时代是写进.pro的QMAKE_MOC +=或CONFIG里的,转换后不会自动迁移,需要手工补。

3. q2c实操:从.pro到CMakeLists.txt的完整复现流程

3.1 拿到q2c:源码编译与版本问题

q2c是个命令行工具,常见获取方式是从源码仓库克隆下来自己编译。它本身也是Qt工程,所以机器上先得有一套能编译Qt项目的环境。编译方式不复杂,用qmake或CMake都能构建。我一般习惯让构建q2c的Qt版本和后续转换目标版本保持一致,避免工具自带规则和实际链接的Qt版本不匹配,后面转换出来的CMakeLists还要二次修改。

编译命令大致是这样的:

git clone <你找到的q2c仓库地址> cd q2c qmake q2c.pro make -j4

编译完成后,工程目录下会生成q2c可执行文件。如果机器上装了多个Qt版本,编译前先跑qmake -v确认当前用的是哪一套。工具本身面向Qt5和Qt6都兼容,但编译器版本和Qt版本混搭时容易出现标准库头文件冲突,这是所有Qt源码项目编译的通用问题,q2c也不例外。第一次跑的时候,我建议先在临时目录里转一个能编译的小工程,确认工具链没问题,再去碰真工程。这样即使转换结果有问题,排查范围也小得多。

3.2 最小复现:带窗口的.pro转成可构建的CMake

从头复现一遍才能看清q2c的脾气。先写一个最常规的Qt Widgets工程,目录结构如下:

demo/ ├── demo.pro ├── main.cpp ├── MainWindow.h ├── MainWindow.cpp ├── MainWindow.ui └── resources.qrc

demo.pro内容:

QT += core gui widgets TARGET = demo TEMPLATE = app CONFIG += c++17 SOURCES += main.cpp MainWindow.cpp HEADERS += MainWindow.h FORMS += MainWindow.ui RESOURCES += resources.qrc

在demo/目录下执行转换:

q2c demo.pro

命令执行完,当前目录下会生成一个CMakeLists.txt。不同版本的q2c在指定输出文件名上有差异,有些支持第二个参数指定输出路径,有些只生成默认文件名,具体以q2c --help的输出为准。生成的CMakeLists.txt大致长这样:

cmake_minimum_required(VERSION 3.16) project(demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED) add_executable(demo main.cpp MainWindow.cpp MainWindow.h MainWindow.ui resources.qrc ) target_include_directories(demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(demo PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets)

这里解释几个转换细节。CONFIG += c++17被翻译成CMAKE_CXX_STANDARD 17,这个宏从CMake 3.1开始支持,所以cmake_minimum_required至少要设在3.10以上,q2c默认给的3.16算是个保守且合理的值。MainWindow.ui和resources.qrc被原样写进target源码列表,AUTOUIC和AUTORCC会在configure阶段自动生成对应的源文件参与编译,CMakeLists里不需要手动写qt6_add_ui和qt6_add_resources。这是新手和老手写法的分水岭:前者依赖自动化宏,后者显式调用Qt6的CMake函数,二者效果等价,但AUTOUIC的写法更干净,也更贴近qmake时代的行为。

转换后的构建命令:

cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.5.0/gcc_64 cmake --build build -j4

-DCMAKE_PREFIX_PATH是CMake寻找Qt库的关键参数。系统里装了多个Qt版本时,这行参数决定你最终链接的是哪一套。不加这个参数,CMake只会去默认路径找Qt,Qt装在自定义目录的机器上会直接报Could not find a package configuration file provided by "Qt6"。从qmake环境迁过来的开发者最容易在这一步卡住:qmake时代用QTDIR环境变量或qmake -query,CMake完全不读这一套,它走自己的路径发现机制。迁移之后,这个参数会陪伴你很久,建议写进后续要讲的CMakePresets.json里。

3.3 生成结果不急着用:先对照检查六个位置

q2c的输出不能无脑信任,每次转换后我都会按固定顺序检查六个位置。第一个是find_package,确认模块列表和.pro里的QT +=一致;第二个是target类型,TEMPLATE = app对应add_executable,TEMPLATE = lib对应add_library;第三个是源文件列表,核对SOURCES和HEADERS是否完整,HEADERS缺失是q2c转换里比较常见的疏漏;第四个是target_include_directories,检查INCLUDEPATH里的路径有没有漏掉;第五个是宏定义,DEFINES里的内容必须转成target_compile_definitions,字符串值要保留引号;第六个是链接库,LIBS里的-l参数和CMake的写法有区别,后面会细说。

六个位置和失败症状的对应关系:

检查位置对应.pro写法缺失时的典型报错
find_packageQT += modulesQt6 not found
target类型TEMPLATE产物类型不对
源码列表SOURCES / HEADERSvtable未定义、符号找不到
include路径INCLUDEPATH头文件No such file
宏定义DEFINES行为不一致、条件编译分支不对
链接库LIBScannot find -lxxx

这六个位置是我第一次用q2c转换后踩坑踩出来的checklist。工具有一个通病:它不告诉你哪些内容没有理解,遇到解析不了的变量就跳过,跳完之后照样生成一份CMakeLists.txt。所以转换后做这份对照检查,比纠结工具本身生成的代码更值得花时间。等构建报错再回头改,定位成本会翻好几倍。

3.4 实际工程常遇到的两种模板:lib和subdirs

上面demo是TEMPLATE = app,单target最简单的情况。实际工程里还有两类很常见:动态库和子目录工程。qmake里TEMPLATE = lib通常配合CONFIG += shared或CONFIG += staticlib,对应CMake的add_library及其STATIC/SHARED属性;CONFIG += plugin则对应MODULE。q2c能把TEMPLATE = lib映射成add_library,但STATIC和SHARED的判断经常不准,常见做法是转换后手动确认一次。

TEMPLATE = lib TARGET = core CONFIG += shared QT -= gui SOURCES += core.cpp HEADERS += core.h VERSION = 1.2.0

转换后需要手工修正的关键点:

add_library(core SHARED core.cpp core.h ) set_target_properties(core PROPERTIES VERSION 1.2.0 SOVERSION 1 )

QT -= gui在qmake里表示从默认模块中移除gui,q2c对这种减法操作的处理也不总是可靠,结果里可能仍然带着Qt6::Gui,需要手动删掉。.pro里的VERSION也不会主动映射到CMake的VERSION和SOVERSION属性,这些都要自己补。

TEMPLATE = subdirs的工程在q2c这里基本只能手动处理。父级.pro里的SUBDIRS += core app,q2c通常只会解析出子目录列表,不会自动生成add_subdirectory。我一般的做法是:每个子工程分别用q2c生成CMakeLists.txt,父目录手工写一个最简CMakeLists:

cmake_minimum_required(VERSION 3.16) project(workspace LANGUAGES CXX) add_subdirectory(core) add_subdirectory(app)

子工程之间的依赖关系,比如app依赖core,需要在app的CMakeLists.txt里补target_link_libraries(app PRIVATE core),q2c不解析LIBS += -lcore这种变量和target之间的对应关系。这一步没有捷径,手工操作时注意链接顺序,静态库依赖要放在后面。

4. 必踩的五个转换坑:条件语法、路径变量和AUTOMOC失效

4.1 qmake作用域条件被静默丢弃

现象:转换后CMakeLists里所有源文件都被堆在一起,原来在win32:{}、unix:{}里单独添加的文件、库,要么全部丢失,要么全被保留,构建时还没有任何warning。

原因:qmake的作用域语法是预处理级别的条件判断,win32:SOURCES += winmain.cpp只有在Windows平台上才生效。q2c按静态文本解析,看到win32:这个token但不会执行它背后的逻辑,最终就把条件分支吞掉或合并错了。

解决:转换前先确定目标平台,把对应平台的分支手动合并到主作用域。常见做法是复制一份临时的pro文件,把条件分支里的SOURCES +=、LIBS +=挪到顶层,再跑q2c。比如win32:LIBS += user32.lib可以直接改成顶层LIBS += user32.lib,前提是确认工程不会在非Windows平台编译。这是转换工作的第一步预处理,不是工具的错,是静态解析工具的固有边界。

4.2 $$PWD和系统调用展开不出真值

现象:生成的CMakeLists里出现$$PWD/../third_party这类字面字符串,或者$$system(...)被原样保留,include路径和库路径全部失效。

原因:$$PWD在qmake里展开为工程文件所在目录,$$system()会调用外部命令并把输出拼进变量。q2c不执行系统命令,也不展开这类变量,只把看到的文本原样输出。CMake拿到一个$$PWD开头的路径,自然找不到目录。

解决:转换前先把.pro里的$$PWD替换成实际相对路径。以INCLUDEPATH += $$PWD/third_party/include为例,交给q2c的输入应该写成INCLUDEPATH += third_party/include,或者转换后用${CMAKE_CURRENT_SOURCE_DIR}/third_party/include补进target_include_directories。$$system(...)没有通用解法,建议先跑一遍qmake拿到实际展开的值,替换成字面量后再转换。这是最花时间的一步,但偷懒跳过的话,后面每个路径错误都要单独排查。

4.3 AUTOMOC失效:头文件不在target列表里

现象:编译一切正常,链接时报undefined reference to vtable for MainWindow,或者编译日志里根本找不到moc_MainWindow.cpp。

原因:AUTOMOC只扫描target的源文件列表。.pro里如果HEADERS漏写了MainWindow.h,或者只有SOURCES += MainWindow.cpp却没有HEADERS += MainWindow.h,q2c按pro内容生成CMakeLists时自然不会包含这个头文件。但头文件里写了Q_OBJECT,moc不触发,虚表就没有定义。这里有个qmake和CMake的隐性差异:qmake时代即使头文件没列进HEADERS,某些场景下还能靠include链碰巧生成moc文件,AUTOMOC的规则更严格,漏了就真的漏了。

解决:转换后检查add_executable或add_library的源码列表,确认所有带Q_OBJECT的头文件都在列表里。我用一个固定命令快速排查:

grep -rl "Q_OBJECT" --include="*.h" . | sort

把输出结果和CMakeLists里的源码列表对照,漏一个补一个。从那之后我养成了习惯:任何工具转换完,先做这个grep对照,再进入构建流程,基本杜绝了这类vtable报错。

4.4 UI文件路径和INCLUDEPATH不匹配

现象:构建时报uic: Cannot open file 'forms/MainWindow.ui',或者uic能打开文件却报找不到ui_xxx.h内部引用的自定义头文件。

原因:qmake把FORMS += forms/MainWindow.ui的相对路径基于.pro所在目录解析,AUTOUIC的基本规则相同,但uic在生成ui_MainWindow.h时,如果.ui文件内部引用了自定义widget或头文件,搜索路径来自target的include目录列表。q2c把INCLUDEPATH转成target_include_directories后,如果.pro里原本用$$PWD拼接的路径没有正确展开,uic的搜索目录就不完整。

解决:先按4.2节把$$PWD处理干净,再确认CMakeLists里target_include_directories有没有包含UI文件所在目录的上一级。另一个容易漏的点是.ui里<header>customwidget.h</header>这类自定义控件声明,AUTOUIC的搜索目录必须包含该头文件所在路径。排查时打开生成的ui_MainWindow.h,看它的#include语句指向哪个目录,就知道缺了哪条路径。这个坑在qmake时代几乎不会暴露,因为qmake会把pro文件所在目录自动加进搜索路径。

4.5 静默跳过:q2c不会提示它丢失了什么

现象:转换后构建报某个源文件不存在、某个宏没定义,回到CMakeLists一查,对应内容压根没被生成。

原因:这是q2c和所有静态转换工具的共性问题:它不报告解析失败或跳过的地方,只输出生成结果。比如.pro里写contains(QT_CONFIG, private) {}这种内置函数调用,q2c解析不了就静默跳过,CMakeLists生成后完全看不出这里曾经有过内容。相比编译器报错,这种静默缺失的排查成本更大,因为你不知道问题出在转换前还是转换后。

解决:转换前先手动清理.pro里的函数调用和模板变量,把contains、message()、system()改成可转换的静态文本。更稳妥的流程是先用qmake -Wall跑一遍原工程,确认.pro本身能正确解析;再用qmake par或查看生成的Makefile拿到展开后的内容,拿展开结果和q2c输出对照。这个对照工作量大,但对复杂工程有必要,尤其是准备删掉.pro正式切CMake之前。我自己的标准是:转换结果能通过第5章的验证闭环,才允许动删除的念头。

5. 转换后的验证闭环:编译对照、运行对照和双轨并行

验证这步很多教程都不提,但我觉得它是整个迁移里最不能省的事。CMake工程编译通过,不等于行为和原工程一致。我自己的验证分三层:编译层、运行层、配置一致性层。

编译层验证最简单,先确认机器上同时保留qmake和CMake两套构建产物,分别编译同一份源码:

# 原qmake构建 mkdir build_qmake && cd build_qmake qmake ../demo/demo.pro && make -j4 cd .. # CMake构建 cmake -S demo -B build_cmake -DCMAKE_PREFIX_PATH=/path/to/Qt/6.5.0/gcc_64 cmake --build build_cmake -j4

两套二进制都出来后,对比文件大小、动态链接依赖和运行结果。ldd build_qmake/demo和ldd build_cmake/demo输出里的Qt库路径应该完全一致,这个检查能暴露CMAKE_PREFIX_PATH指错版本的问题。然后是运行层:同时打开两个demo,把窗口尺寸、资源图标、中文乱码、信号槽响应逐一对比。qmake和CMake对翻译文件、资源压缩格式的处理差异不大,但Q_OBJECT类如果没正确触发moc,运行期会表现成槽函数不响应,这类问题静态编译阶段根本发现不了。

配置一致性层有个实用技巧:把两套构建的关键编译参数导出来对比。qmake构建用make V=1能打印完整编译命令;CMake侧在配置时加-DCMAKE_EXPORT_COMPILE_COMMANDS=ON,会在build目录生成compile_commands.json,把两边的-D参数、-I参数放一起比对,能精确看出哪个宏定义或include路径在转换过程中丢了。这个对比我一般用脚本:

jq -r '.[] | select(.file | contains("MainWindow")) | .command' build_cmake/compile_commands.json

把输出的编译命令和qmake打印的编译命令放在一起diff,差异项就是转换的遗漏点。

我第一回用q2c迁移一个带二十个源文件的中型工程时,编译一次过,跑起来主窗口的工具栏图标全部消失。排查到最后是.qrc里的资源别名在CMake侧解析不一致,个别:/icon.png引用失效。从那以后,我每次转换都强制走一遍“源码列表grep对照 → compile_commands.json对比 → 运行期资源检查”的流程,再没翻过车。

还有一个值得养成的习惯:切CMake之后的两个星期内不要删qmake的.pro,保持双轨并行。q2c只代表把构建脚本翻译了一遍,不代表工程已经完成迁移。新需求到来时要加文件时,先两边同步改一遍,确认CMake侧行为一致再让.pro退休。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询