Qt实用技能2-用好qmake



  • 简介

    本文是《Qt实用技能》系列文章的第二篇,涛哥将教大家,一些qmake的实用技巧。部分地方也会说一下原理,让大家知其然,知其所以然。

    工欲善其事,必先利其器。

    这个系列,全是干货!

    注:文章主要发布在涛哥的博客知乎专栏-涛哥的Qt进阶之路

    qmake简介

    qmake是Qt的构建工具,主要作用是解析pro格式的项目文件、生成编译规则(Makefiles或其它)。

    qmake是一个比较古老的工具,很多功能使用perl脚本实现,涛哥在其它地方就没怎么见过使用perl脚本的代码/项目。

    Qt官方之前开发的Qbs,后来又宣布不再更新,现在又大力支持CMake。。。

    在这样的背景下,qmake依然是当下主要的构建工具,所以qmake的一些技巧还是有必要掌握的。

    qmake本身作为一个可执行程序,也是有一些参数的,但这不是本文的重点,本文的重点都在pro文件里。

    pro文件中,除了常规的组织项目结构外,还可以做很多事情, 比如 指定编译选项、链接选项、制定目标生成规则、扩展编译规则 等等。

    pro文件中的qmake语法,包括 变量声明和使用、内建变量、替换函数、测试函数等,帮助文档都有详细的介绍。

    搜索关键词为qmake, 或者和普通的类查看帮助文档方式一样,光标放在pro文件要查看的变量上,按F1就能看到相应的说明。

    预览

    涛哥就不赘述了,后面用到的会单独说明。

    添加第三方库

    c++开发,使用第三方库也是家常便饭了,这是一个必备的技能。

    这里首选的方法,是使用QtCreator提供的添加库UI。在pro文件里(或者项目文件夹), 鼠标右键->添加库,然后根据自己的需要下一步、下一步点一下即可。

    预览

    熟练的人也可以直接按pro语法(perl语法)写,给LIBS变量赋值。

    下面给几个示例,至于动态库/静态库的差异,大家自己实践吧。

    示例1 - 直接链接库的全路径

      LIBS += c:/mylibs/math.lib
    

    我们都知道windows系统默认的路径分割符是'',但在qmake中要写成'\'才行。qmake也支持写成'/',其它unix系统又都是'/',

    所以干脆都写成'/',方便处理。

    示例2 - 路径中包含空格等特殊字符,用引号括起来。

      LIBS += "C:/mylibs/extra libs/extra.lib"
    

    示例3 - 分别指定路径和库

    LIBS += "C:/mylibs/extra libs" -lextra
    

    这里的LIBS指定要链接的库,'-L'是指定链接库的路径,'-l'指定要链接的库名称

    名称可以省略lib前缀和 扩展名后缀,Qt会自动处理。 后缀包括 '.so' '.dll' '.dylib' 等。

    示例4 - 分平台条件链接

      win32:LIBS += "C:/mylibs/extra libs/extra.lib"
      unix:LIBS += "-L/home/user/extra libs" -lextra
    

    条件链接可以很方便地实现不同平台链接不同的库。

    这里的 win32 unix 是在选择了不同的编译器环境时,qmake分别预置的变量。

    (比如win32平台相关的变量,可以参考msvc的配置文件: [QTDIR]/mkspecs/win32-msvc/qmake.conf 和 [QTDIR]/mkspecs/common/msvc-desktop.conf)

    原理

    Qt内置了一些perl脚本,在执行qmake解析时会包含这些脚本。其中一些脚本会来处理这个LIBS变量,将其转换成编译器/链接器的参数。

    内置的脚本路径在[QTDIR]/mkspecs/features文件夹下,扩展名为prf。

    预览

    后续的很多变量,也是一样的原理, 只是处理方式各不相同。

    很多pro文件的语法、功能实现,都可以参考这些prf来实现。

    (注意:不熟悉的同学,不要乱改prf,容易改坏)

    Qt程序员都知道的一件事:有时候修改了信号/槽相关的代码,不能正常运行,要重新qmake一下,才会生效。

    本质上就是在重新触发[QTDIR]/mkspecs/features/moc.prf这个脚本。

    (多少年了,都没有修好Moc生成问题,可见qmake的古老...)

    影子构建

    影子构建,就是编译生成的产物和源代码在不同的文件夹。这样可以防止源代码文件夹被污染。

    QtCreator默认导入pro工程时,就会生成一个影子构建路径。比如这样:

    预览

    F:\Dev\Qt\build-HelloTaoQuick-Desktop_Qt_5_12_3_MSVC2017_64bit-Debug
    

    之后编译项目时生成的中间文件及目标文件,都在这个文件夹中。

    这个路径很长,而且编译器或者编译选项不同时都有可能不一样。

    有时候要做一些特定的操作 比如目标exe生成到特定目录、拷贝资源文件等等,直接用这个路径会不太方便/不太可靠,我们需要一些定制。

    指定目标路径

    DESTDIR = $$PWD/bin
    

    通过给DESTDIR变量赋值, 可以指定生成的lib/exe放在哪个目录下

    'PWD'是qmake内置变量,'$$'是内置变量取值的写法。'/bin'是字符串拼接在变量后面。

    更多内置变量可以参考qmake帮助文档,以及这篇文档隐藏的qmake文档

    当然也可以参考那一堆prf和conf文件。

    指定中间件生成路径

    可以通过这几个变量指定中间件生成的路径

    config(debug, debug|release) {
        OBJECTS_DIR = build/debug/obj
        MOC_DIR = build/debug/moc
        RCC_DIR = build/debug/rcc
        UI_DIR = build/debug/uic
    } else {
        OBJECTS_DIR = build/release/obj
        MOC_DIR = build/release/moc
        RCC_DIR = build/release/rcc
        UI_DIR = build/release/uic
    }
    
    

    config(debug, debug|release) 是一个条件表达式,可以理解为

    if (debug === true) {
    
    } else if (release == true) {
    
    }
    

    注意: 按照perl语法,那个左大括号'{'不能换行,要和前面的表达式在同一行。(有人自作聪明换行,被坑了呢😄)

    上面这种指定中间件路径的方式,在QtCreator中有默认路径所以没有太大意义,不过在命令行编译时这样写却很有用。

    拷贝资源

    pro可以实现,在编译代码时,拷贝一些文件到指定的路径下

    拷贝资源示例

    这里以TaoQuick为例,来说明:

    我在TaoQuick库目录下,有个叫qmldir的文件,需要在编译代码时自动拷贝到bin目录下。(先别管这个文件干嘛的,下一篇文章会说)

    预览

    关键目录结构如下:

    TaoQuick
        TaoQuick.pro
        - bin
            -TaoQuick
        - TaoQuickCore
            TaoQuickCore.pro
            - Qml
                qmldir
    

    那么我在TaoQuickCore.pro文件中的写法如下:

    !equals(_PRO_FILE_PWD_, $$DESTDIR) {
        copy_qmldir.target = $$DESTDIR/qmldir
        copy_qmldir.depends = $$_PRO_FILE_PWD_/qmldir
        win32 {
            copy_qmldir.target ~= s,/,\\\\,g
            copy_qmldir.depends ~= s,/,\\\\,g
        }
        copy_qmldir.commands = $${QMAKE_COPY_FILE} $${copy_qmldir.depends} $${copy_qmldir.target}
        QMAKE_EXTRA_TARGETS += copy_qmldir
    }
    
    

    ‘!equals(PRO_FILE_PWD, $$DESTDIR)’ 这一句是执行条件,即: 目标路径不等于pro文件所在路径时 执行下面的操作。

    剩下的事情就是在创建一个"编译目标"(Target),将这个编译目标添加到QMAKE_EXTRA_TARGETS变量中就行了。

    熟悉MakeFiles的同学应该都清楚什么是"目标"。不懂MakeFiles也没关系,这里的目标就理解为自己声明的一个变量即可。

    这个变量有三个很重要的"子变量":

    copy_qmldir.target 指定目标文件所在的路径 (这里理解成要拷贝到哪去)
    copy_qmldir.depends 指定依赖文件所在的路径 (这里理解成从哪里拷贝)
    copy_qmldir.commands 指定拷贝操作的执行命令 (就是怎么拷贝)

    QMAKE_COPY_FILE 这个变量来自前面说过的隐藏的qmake文档

    qmake会在解析pro文件时,自动替换成平台相应的拷贝命令。 windows 平台就是 copy /y

    注意windows的copy指令,路径分隔符得写成 '\'才行。所以有了下面的特殊处理:

        win32 {
            copy_qmldir.target ~= s,/,\\\\,g
            copy_qmldir.depends ~= s,/,\\\\,g
        }
    

    ‘s,/,\\,g’ 是一个正则表达式,作用是把‘/’替换成‘\’ 。s表示开头,g表示结尾。

    VAR~= REGEXP 是对变量VAR执行REGEXP这个正则表达式

    编译前拷贝

    如果想在编译之前,先把资源拷贝完成,只需要前面的基础上,添加一句

        PRE_TARGETDEPS += $$copy_qmldir.target
    

    也就是把"目标"加到 PRE_TARGETDEPS变量

    !equals(_PRO_FILE_PWD_, $$DESTDIR) {
        copy_qmldir.target = $$DESTDIR/qmldir
        copy_qmldir.depends = $$_PRO_FILE_PWD_/qmldir
        win32 {
            copy_qmldir.target ~= s,/,\\\\,g
            copy_qmldir.depends ~= s,/,\\\\,g
        }
        copy_qmldir.commands = $${QMAKE_COPY_FILE} $${copy_qmldir.depends} $${copy_qmldir.target}
        QMAKE_EXTRA_TARGETS += copy_qmldir
        PRE_TARGETDEPS += $$copy_qmldir.target
    }
    
    

    安装

    pro中还有一种INSTALL功能,可以执行文件拷贝。

    和编译期拷贝 类似,INSTALL用起来更简单无脑一些,而且INSTALL只在执行make install指令时,才会拷贝资源。

    还是以TaoQuick为例, 我有一堆文件,需要在make install时,安装到Qt的Qml路径中

    预览

    如上图所示所有的文件, 除了TaoQuickDesigner.pri, 都要按照这个结构拷贝。

    (这个pri文件是pro文件的一小部分,可以直接在pro中通过include引入。

    pri和pro语法一样,但是qmake不直接识别pri,只识别pro

    pri一般用来写一些公用的部分,让多个pro公用)

    拷贝整个文件夹是一种做法, 当然为了精确地控制要拷贝的内容,可以写成下面这样:

    taoquick_designer.files = $$PWD/designer/TaoQuick.metainfo
    taoquick_designer.path = $$[QT_INSTALL_QML]/$${uri}/designer
    
    toaquick_qmldir.files = $$PWD/qmldir
    toaquick_qmldir.path = $$[QT_INSTALL_QML]/$${uri}
    
    taoquick_qml_buttons.files = $$PWD/BasicComponent/Button/*.qml
    taoquick_qml_buttons.path = $$[QT_INSTALL_QML]/$${uri}/BasicComponent/Button
    
    taoquick_qml_mouse.files = $$PWD/BasicComponent/Mouse/*.qml
    taoquick_qml_mouse.path = $$[QT_INSTALL_QML]/$${uri}/BasicComponent/Mouse
    
    taoquick_qml_others.files = $$PWD/BasicComponent/Others/*.qml
    taoquick_qml_others.path = $$[QT_INSTALL_QML]/$${uri}/BasicComponent/Others
    
    taoquick_qml_progress.files = $$PWD/BasicComponent/Progress/*.qml
    taoquick_qml_progress.path = $$[QT_INSTALL_QML]/$${uri}/BasicComponent/Progress
    
    taoquick_degisner_images.files = $$PWD/designer/images/*.png
    taoquick_degisner_images.path = $$[QT_INSTALL_QML]/$${uri}/designer/images
    
    INSTALLS  += taoquick_designer toaquick_qmldir taoquick_qml_buttons taoquick_qml_mouse taoquick_qml_others taoquick_qml_progress taoquick_degisner_images
    
    

    自定义一个变量,然后其子变量files指定要拷贝的文件,子变量path指定目标路径。

    把自定义变量加入INSTALLS变量就行了。

    QT_INSTALL_QML也是一个内置变量,默认值为[QTDIR]/qml。

    之后只要执行以下命令,就能完成资源拷贝。

    qmake
    make 
    make install
    

    当然QtCreator中也能执行make install

    如下图所示:

    预览

    任意编译器kit都可以,项目->构建步骤->添加build步骤->Make,添加之后在make参数中输入install。最后重新构建工程,即可完成安装。

    结束语

    以上案例,大部分都在TaoQuick项目中实践过,可以去Github参考。

    TaoQuick

    转载声明

    文章出自涛哥的博客

    文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作 © 涛哥

    联系方式


    作者 武威的涛哥
    开发理念 弘扬鲁班文化,传承工匠精神
    博客 https://jaredtao.github.io
    github https://github.com/jaredtao
    知乎 https://www.zhihu.com/people/wentao-jia
    邮箱 jared2020@163.com
    微信 xsd2410421
    QQ 759378563

    请放心联系我,乐于提供咨询服务,也可洽谈商务合作相关事宜。

    打赏

    weixin
    zhifubao


    如果觉得涛哥写的还不错,还请为涛哥打个赏,您的赞赏是涛哥持续创作的源泉。



Log in to reply
 

走马观花

最近的回复

  • C

    Qt for MCU需要商业授权的

    read more
  • Qt for MCUs

    搭建Qt for MCUs PC端开发环境。qt for mcus提供了一个完整的图形框架和工具包,包含了在MCUs上设计、开发和部署gui所需的一切。它允许您在裸机或实时操作系统上运行应用程序。

    先决条件

    开发主机环境支持仅限于Windows 10

    MSVC compiler v19.16 (Visual Studio 2017 15.9.9 or newer) x64

    CMake v3.13 or newer (you can install it using the Qt Online installer) x64

    使用Qt联机安装程序安装Qt for MCUs,该安装程序可通过Qt帐户下载

    安装Qt 5.14和Qt Creator 4.11 or higher

    安装链接

    › Qt: https://account.qt.io/downloads
    › CMake: https://cmake.org/download/
    › Python 2.7 32-bit: https://www.python.org/downloads/release/python-2716/
    › Arm GCC: https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnutoolchain/gnu-rm/downloads
    › J-Link Software Pack: https://www.segger.com/downloads/jlink/JLink_Windows.exe
    › J-Link OpenSDA Firmware: https://www.segger.com/downloads/jlink/OpenSDA_MIMXRT1050-EVKHyperflash
    › STM32CubeProgrammer: https://www.st.com/en/development-tools/stm32cubeprog.html
    › STM32 ST-LINK Utility: https://www.st.com/en/development-tools/stsw-link004.html​​​​​​​

    Qt Creator设置 启用Qt Creator插件 选择“帮助>关于插件”,然后从列表中选择“MCU支持(实验性)”插件,重新启动Qt Creator以应用更改
    替代文字 为MCU创建Qt工具包

    选择工具>选项>设备>MCU

    选择Qt for MCUs-Desktop 32bpp作为目标

    如果尚未设置,请提供Qt for MCUs安装目录的路径。

    单击Apply应用。

    替代文字

    替代文字
    替代文字

    注意:

    编译器要选X64,Qt版本要选64bit,CMake Tool选x64

    打开恒温器项目demo

    选择文件>打开文件或项目。。。

    打开CMakefiles.txt文件来自thermo文件夹的文件。

    选择Qt作为MCU-桌面32bpp套件。

    单击“配置项目”以完成。

    替代文字

    问题

    开发主机环境支持仅限于Windows 10

    C++编译失败,文本大字体.pixelSize.

    文本类型无法正确呈现需要复杂文本布局的unicode序列。对复杂文本使用StaticText

    read more
  • H

    hi 有问题请教你,方便加个联系方式吗

    read more
  • boost.asio是一个很棒的网络库,这回儿我也开始系统地学习起来了。想想当年接触boost,也有八年多了。这次开始接触boost,觉得既熟悉又陌生。熟悉的是小写字母+下划线的命名方式、晦涩的模板、很慢的编译速度以及较大的程序体积,陌生的是asio的各种概念:io服务、接收器、套接字等等:我之前对网络编程不是非常了解。

    于是根据我的理解,参考《Boost.Asio C++网络编程》实现了这样一个简单的客户端和服务端通信的例子,例子非常简单,还不完善,但是幸运的是,可以在本机上互通了。
    下面是客户端的代码:

    #include <iostream> #include <boost/asio.hpp> #include <boost/proto/detail/ignore_unused.hpp> using namespace std; using namespace boost::asio; using namespace boost::system; using namespace boost::proto::detail;// 提供ignore_unused方法 void writeHandler( const boost::system::error_code& ec, size_t bytesTransferred ) { if ( ec ) { cout << "Write data error, code: " << ec.value( ) << "transferred: " << bytesTransferred << endl; } else { cout << "OK! " << bytesTransferred << "bytes written. " << endl; } } int main(int argc, char *argv[]) { ignore_unused( argc ); ignore_unused( argv ); io_service service; ip::tcp::socket sock( service ); ip::tcp::endpoint ep( ip::address::from_string( "127.0.0.1" ), 6545 ); boost::system::error_code ec; sock.connect( ep, ec ); if ( ec ) { cout << "Connect error, code: " << ec.value( ) << ", We will exit." << endl; return ec.value( ); } else { char buf[1024] = "Hello world!"; sock.async_write_some( buffer( buf ), writeHandler ); sock.close( ); } return service.run( ); }

    下面是服务端的代码:

    #include <iostream> #include <boost/asio.hpp> #include <boost/proto/detail/ignore_unused.hpp> using namespace std; using namespace boost::asio; using namespace boost::system; using namespace boost::proto::detail;// 提供ignore_unused方法 void acceptHandle( const boost::system::error_code& code ) { cout << "Accepted." << endl; } int main(int argc, char *argv[]) { ignore_unused( argc ); ignore_unused( argv ); io_service service; ip::tcp::endpoint ep( ip::address::from_string( "127.0.0.1" ), 6545 ); boost::system::error_code ec; ip::tcp::socket sock( service ); ip::tcp::acceptor acceptor( service, ep ); acceptor.async_accept( sock, acceptHandle ); if ( ec ) { cout << "There is an error in server. code: " << ec.value( ) << endl; } return service.run( );// 阻塞运行 }

    运行结果是这样的:
    78448d7b-b3ae-42fc-9e2e-4dd2fbdac2c2-image.png

    我对boost.asio中几个概念的理解:

    io_service,这就是一个类似事件循环的东西,它为io设备提供服务,故名。不管是套接字、文件还是串口设备,都要使用它的服务。它的run()函数相当于启动了一个事件循环。一旦有消息了,即进行响应。这也是实现异步编程的重要基础。 socket,这个类则是套接字,可以处理TCP或者是UDP请求。有同步以及异步的处理方式,也有带异常以及不带异常的处理方式。 acceptor,接收器,仅仅是服务端使用。相当于其余框架中的listener,作接收用的。

    比较浅显,如果有不当之处,敬请指正。

    read more

关注我们

微博
QQ群