> 文档中心 > 快速搭建一个简洁高效的跨平台Qt项目工程

快速搭建一个简洁高效的跨平台Qt项目工程

在搭建简单的Qt Demo的时候,我们可以将所有的代码写在一个工程里面,这样操作起来比较简单。但是,如果在一个有很多个开发者参与的大型项目中,这样做肯定就不行了,这时候我们需要对项目进行拆分,拆分成几个可以独立并行开发的工程模块。这里介绍的就是如何对项目进行拆分。

首先采用简单的MVC结构对项目进行拆分,将UI显示和业务逻辑拆分开发,这样设计工程师就可以专注于UI的设计,同时开发工程师专注于业务逻辑的开发。前端设计不用关注实际的业务逻辑,而业务开发工程也不用关注前端的UI显示。拆分开之后,桌面端和移动端的界面可以共用一套业务逻辑代码,避免了重复开发和维护。

近些年TDD(Test Driven Development)越来越火了,为了提升项目的持续集成和测试的效率,我们可以引入Qt项目的测试框架对所写的模块进行测试。

通过这样的拆分,之前一个工程就可以拆分成三个工程,分别是UI工程,业务逻辑工程、测试工程。工程的架构图如下图所示:

项目工程的创建

首先创建一个子项目目录用来包含多个子项目:

在子项目目录ime中新建一个名称为ime-ui的Qt Quick Application工程,该工程是负责前端显示的工程。

在子项目目录ime中新建一个名称为ime-lib的C++库工程,该工程负责项目的业务逻辑处理。

 

在子项目目录ime中新建一个名称为ime-test的测试工程,该工程负责对项目进行单元测试。

搭建完成之后的项目结构如下图所示:

 

业务库的项目配置

ime-lib是项目的核心,负责业务逻辑的处理,在ime-lib目录下新建一个子目录src负责存放项目的的源码。ime-lib.pro文件的内容如下:

#库文件不适用gui库QT-= gui#库类型和库名称TARGET = ime-libTEMPLATE = lib#使用该宏进行符号导出DEFINES += IMELIB_LIBRARY#使用C++14的特性CONFIG += c++14#包含src目录路径INCLUDEPATH += src# 如果使用了Qt抛弃的特性,会发出对应的警告DEFINES += QT_DEPRECATED_WARNINGS#cpp文件SOURCES += \ src/imelib.cpp#头文件HEADERS += \ src/imelib.h \ src/ime-lib_global.h#linux下的安装目录unix {    target.path = /usr/lib    INSTALLS += target}
//ime-lib_global.h文件中定义了符号的导出宏//通过导出宏我们可以导出我们需要的符号和变量#ifndef IMELIB_GLOBAL_H#define IMELIB_GLOBAL_H#include #if defined(IMELIB_LIBRARY)#  define IMELIBSHARED_EXPORT Q_DECL_EXPORT#else#  define IMELIBSHARED_EXPORT Q_DECL_IMPORT#endif#endif // IMELIB_GLOBAL_H

如果说项目工程比较大的话,我们还可以通过命名空间来划分不同类型的导出符号。

单元测试项目配置

在单元测试ime-test目录下新建src目录用来存放单元测试的源码文件,然后单元测试的项目工程配置如下所示:

#添加测试库,移除gui库QT+= testlibQT-= gui#目标类型和配置TARGET = tst_ime_testtestCONFIG   += consoleCONFIG   -= app_bundleTEMPLATE = app#宏配置DEFINES += QT_DEPRECATED_WARNINGS#源码文件SOURCES += \ src/tst_ime_testtest.cppDEFINES += SRCDIR=\\\"$$PWD/\\\"

UI项目配置

在ime-ui目录下新建两个子目录,子目录src用来存放源码,views用来存放UI文件。工程配置文件的内容如下:

#指定项目类型和配置QT +=  qml quickCONFIG += c++11TEMPLATE = app#添加源码目录INCLUDEPATH += srcCONFIG -= qml_debug#警告宏DEFINES += QT_DEPRECATED_WARNINGSDEFINES+=QT_QML_DEBUG_NO_WARNING#添加源码SOURCES += src/main.cppRESOURCES += qml.qrc

修改qml.qrc的文件内容将子目录下的qml文件添加进去。

     views/main.qml views/MainForm.ui.qml    

修改了资源的目录地址之后,在项目的入口函数中我们就可以通过修改后的路径加载对应的资源了。

int main(int argc, char *argv[]){    QGuiApplication app(argc, argv);    QQmlApplicationEngine engine;    engine.load(QUrl(QStringLiteral("qrc:/views/main.qml")));    if (engine.rootObjects().isEmpty()) return -1;    return app.exec();}

 QObjec类型和QML界面交互

QObject携带的元数据允许一定程度的类型检查,这是和QML交互的基础,QML可以通过事件订阅机制与QObject类型进行交互。在事件订阅机制当中,事件发送称为信号,事件订阅称为槽。由于这个机制的存在,我们通过继承QObject实现的自定义的类型可以与QML界面进行交互。

在ime-ui项目中导入对应的动态库和目录文件,这样我们就可以在ime-ui库中使用对应的业务逻辑类了。

//pro文件中引入对应的库#添加源码目录INCLUDEPATH += src \     ../ime-lib/src# -L说明指代的是目录 -l说明指代的是库文件# 引入库的时候不需要指定库文件的前缀和后缀(后缀:.so,.dll,前缀lib)LIBS += -L$$PWD/../../build-ime-ming_gw-Debug/ime-lib/debug -lime-lib

在ime-ui项目中引入了对应的业务逻辑库之后,我们就可以在qml项目中注册并使用对应的业务逻辑类了,注册的业务逻辑如下。

int main(int argc, char *argv[]){    QGuiApplication app(argc, argv);    //在engine声明之前注册C++类型    //@1:类在qml中别名 @2:版本主版本号 @3:版本的次版本号 @4类的名称    qmlRegisterType("ime",1,0,"Imelib");    //声明ime_lib类并将其注入到qml引擎当中    //此操作应该在加载qml之前执行    Imelib ime_lib;    QQmlApplicationEngine engine;    engine.rootContext()->setContextProperty("ime_lib",&ime_lib);    engine.load(QUrl(QStringLiteral("qrc:/views/main.qml")));    if (engine.rootObjects().isEmpty()) return -1;    return app.exec();}

注册完成之后,假设我们需要动态的获取C++类的一个字符串属性作为QML显示的内容。使用之前,我们先在对应的类中声明QML访问的属性别名,和访问权限。

//imelib.h#ifndef IMELIB_H#define IMELIB_H#include "ime-lib_global.h"#include class IMELIBSHARED_EXPORT Imelib : public QObject{    Q_OBJECT    // 声明在Q_OBJECT之后,第一个public之前    //ui_tiltle_content是在QML使用的别名,m_title_content是对应的变量名称    //CONSTANT说明是只读的    Q_PROPERTY(QString ui_title_content MEMBER m_title_content CONSTANT)    //声明QML中使用某个变量的方法,包括读方法和写方法,以及变量发生变化之后的通知信号    //这样修改了对应字段的值之后,对应的QML中显示也会发生变化    Q_PROPERTY(int lib_state READ state WRITE setState     NOTIFY stateChanged)    Q_PROPERTY(QString edit_content READ edit_content WRITE setEdit_content     NOTIFY editstateChanged)public:    Imelib(QObject* parent = nullptr);public:    //声明QML中可以调用的函数    Q_INVOKABLE void changeEditContent(QString inputContent);public:    int state() const;    void setState(int state);    int m_state = 20;    QString edit_content() const;    void setEdit_content(const QString &edit_content);    QString m_edit_content = "edit content";    signals:    void stateChanged();    void editstateChanged();public:    QString m_title_content = "title_content";};#endif // IMELIB_H

 

//imelib.cpp#include "imelib.h"Imelib::Imelib(QObject* parent): QObject(parent){}void Imelib::changeEditContent(QString inputContent){    setEdit_content(inputContent);}int Imelib::state() const{    return m_state;}void Imelib::setState(int state){    m_state = state;    emit stateChanged();}QString Imelib::edit_content() const{    return m_edit_content;}void Imelib::setEdit_content(const QString &edit_content){    m_edit_content = edit_content;    emit editstateChanged();}

在声明了对应的QML访问属性之后,编译一下对应的业务库。之后我们就可以在QML中访问对应的属性字段了。

import QtQuick 2.6import QtQuick.Window 2.2Window {    visible: true    width: 640    height: 480    title: ime_lib.ui_title_content    MainForm { anchors.fill: parent mouseArea.onClicked: {     console.log(qsTr('Clicked Text: "' + ime_lib.edit_content + '"'))     ime_lib.changeEditContent("hello") }    }}

通过这种方式我们就可以实现QML界面和业务库中的QObject类型之间的相互调用了,实现了QML和C++的混合编程。在基于QWidget的项目中我们也可以通过QQuickWidget控件,实现类似的功能。

Imelib ime_lib;QQuickWidget* simple_quick = new QQuickWidget();simple_quick->rootContext()->setContextProperty("ime", (QObject*)&ime_lib);simple_quick->setSource(QUrl("qrc:/views/main.qml"));

项目输出路径管理

项目系统的默认输出文件夹的名称如下所示:

//build+项目名称+构建套件+构建类型build-ime-ming_gw-Debug

为了让构建项目输出文件夹层次更加清晰,我们可以对输出文件夹进行分层显示,层次关系如下:

操作系统类型 >> 构建套件(mingw/msvc) >> 处理器架构 >> 构建类型(debug/release)

由于这个构建配置是所有项目共用的,为了避免重复配置,我们将所有的修改放到一个公共配置文件中。

在qmkae-target-platform.pri中我们添加对于构建系统进行各种配置的宏,通过各种各样的宏,我们就知道了当前系统的构建类型。

//qmake-target-platform.pri#win32下的构建配置win32 {CONFIG += PLATFORM_WINmessage(PLATFORM_WIN)win32-g++ {CONFIG += COMPILER_GCCmessage(COMPILER_GCC)}win32-msvc2015 {CONFIG += COMPILER_MSVC2015message(COMPILER_MSVC2015)win32-msvc2015:QMAKE_TARGET.arch = x86_64}}#linux下的构建配置linux {CONFIG += PLATFORM_LINUXmessage(PLATFORM_LINUX)!contains(QT_ARCH, x86_64){QMAKE_TARGET.arch = x86} else {QMAKE_TARGET.arch = x86_64}linux-g++{CONFIG += COMPILER_GCCmessage(COMPILER_GCC)}}#mac下的构建配置macx {CONFIG += PLATFORM_OSXmessage(PLATFORM_OSX)macx-clang {CONFIG += COMPILER_CLANGmessage(COMPILER_CLANG)QMAKE_TARGET.arch = x86_64}macx-clang-32{CONFIG += COMPILER_CLANGmessage(COMPILER_CLANG)QMAKE_TARGET.arch = x86}}#CPU的架构contains(QMAKE_TARGET.arch, x86_64) {CONFIG += PROCESSOR_x64message(PROCESSOR_x64)} else {CONFIG += PROCESSOR_x86message(PROCESSOR_x86)}#构建对应的配置项CONFIG(debug, release|debug) {CONFIG += BUILD_DEBUGmessage(BUILD_DEBUG)} else {CONFIG += BUILD_RELEASEmessage(BUILD_RELEASE)}

依据在qmkae-target-platform.pri中我们添加的各种宏,我们就可以重新定义构建项目的输出路径,将输出路径的定义放到qmake-dest-path.pri文件中,文件内容如下

//qmake-dest-path.priplatform_path = unknown-platformcompiler_path = unknown-compilerprocessor_path = unknown-processorbuild_path = unknown-buildPLATFORM_WIN {platform_path = windows}PLATFORM_OSX {platform_path = osx}PLATFORM_LINUX {platform_path = linux}COMPILER_GCC {compiler_path = gcc}COMPILER_MSVC2017 {compiler_path = msvc2017}COMPILER_CLANG {compiler_path = clang}PROCESSOR_x64 {processor_path = x64}PROCESSOR_x86 {processor_path = x86}BUILD_DEBUG {build_path = debug} else {build_path = release}#指定输出路径的结构DESTINATION_PATH = $$platform_path/$$compiler_path/$$processor_path/$$build_pathmessage(Dest path: $${DESTINATION_PATH})

完善了输出路径的配置之后,我们就可以在各个项目中引入对应的配置了。引入方法如下,通过对应的配置我们就可以将执行文件和中间文件分开了,这样项目结构就更加清晰了。

#引入配置文件include(../qmake-target-platform.pri)include(../qmake-dest-path.pri)#指定可用文件的输出目录DESTDIR = $$PWD/../binaries/$$DESTINATION_PATH#指定中间文件的输出目录OBJECTS_DIR = $$PWD/build/$$DESTINATION_PATH/.objMOC_DIR = $$PWD/build/$$DESTINATION_PATH/.mocRCC_DIR = $$PWD/build/$$DESTINATION_PATH/.qrcUI_DIR = $$PWD/build/$$DESTINATION_PATH/.ui

修改了库的输出路径之后,我们就可以通过新的路径引入ime-lib库了,引入方法如下:

LIBS += -L$$PWD/../binaries/$$DESTINATION_PATH -lime-lib

修改完成之后ime-ui.pro的配置如下:

#指定项目类型和配置QT +=  qml quickCONFIG += c++11TEMPLATE = app#添加源码目录INCLUDEPATH += src \     ../ime-lib/srcCONFIG -= qml_debuginclude(../qmake-target-platform.pri)include(../qmake-dest-path.pri)#指定生成目录DESTDIR = $$PWD/../binaries/$$DESTINATION_PATH#指定中间文件的输出目录OBJECTS_DIR = $$PWD/build/$$DESTINATION_PATH/.objMOC_DIR = $$PWD/build/$$DESTINATION_PATH/.mocRCC_DIR = $$PWD/build/$$DESTINATION_PATH/.qrcUI_DIR = $$PWD/build/$$DESTINATION_PATH/.ui# -L说明指代的是目录 -l说明指代的是库文件# 引入库的时候不需要指定库文件的前缀和后缀(后缀:.so,.dll,前缀lib)LIBS += -L$$PWD/../binaries/$$DESTINATION_PATH -lime-lib#警告宏DEFINES += QT_DEPRECATED_WARNINGSDEFINES+=QT_QML_DEBUG_NO_WARNING#添加源码SOURCES += src/main.cppRESOURCES += qml.qrc

修改完毕之后ime-lib.pro文件的配置如下:

QT-= gui#库类型和库名称TARGET = ime-libTEMPLATE = libDEFINES += IMELIB_LIBRARY#使用C++14的特性CONFIG += c++14#包含src目录路径INCLUDEPATH += srcinclude(../qmake-target-platform.pri)include(../qmake-dest-path.pri)#指定生成目录DESTDIR = $$PWD/../binaries/$$DESTINATION_PATH#指定中间文件的输出目录OBJECTS_DIR = $$PWD/build/$$DESTINATION_PATH/.objMOC_DIR = $$PWD/build/$$DESTINATION_PATH/.mocRCC_DIR = $$PWD/build/$$DESTINATION_PATH/.qrcUI_DIR = $$PWD/build/$$DESTINATION_PATH/.ui# 如果使用了Qt抛弃的特性,会发出对应的警告DEFINES += QT_DEPRECATED_WARNINGS#cpp文件SOURCES += \ src/imelib.cpp#头文件HEADERS += \ src/imelib.h \ src/ime-lib_global.h#linux下的安装目录unix {    target.path = /usr/lib    INSTALLS += target}

整个项目的目录的层级结构如下图:

#└── ime#    ├── binaries#    │   └── windows#    │└── gcc#    │    └── x86#    │ └── debug #    ├── ime-lib#    │    ├── src#    │    └── ime-lib.pro#   │── ime-test#   │     ├── src#   │     └── ime-test.pro#   │── ime-ui #   │     ├── src#   │     └── ime-ui.pro      #   │── ime.pro #   │── qmake-dest-path.pri#   │── qmake-target-platform.pri    #