在搭建简单的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-lib
TEMPLATE = 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 += testlib
QT -= gui
#目标类型和配置
TARGET = tst_ime_testtest
CONFIG += console
CONFIG -= app_bundle
TEMPLATE = app
#宏配置
DEFINES += QT_DEPRECATED_WARNINGS
#源码文件
SOURCES += \
src/tst_ime_testtest.cpp
DEFINES += SRCDIR=\\\"$$PWD/\\\"
在ime-ui目录下新建两个子目录,子目录src用来存放源码,views用来存放UI文件。工程配置文件的内容如下:
#指定项目类型和配置
QT += qml quick
CONFIG += c++11
TEMPLATE = app
#添加源码目录
INCLUDEPATH += src
CONFIG -= qml_debug
#警告宏
DEFINES += QT_DEPRECATED_WARNINGS
DEFINES+=QT_QML_DEBUG_NO_WARNING
#添加源码
SOURCES += src/main.cpp
RESOURCES += 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();
}
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.6
import QtQuick.Window 2.2
Window {
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_WIN
message(PLATFORM_WIN)
win32-g++ {
CONFIG += COMPILER_GCC
message(COMPILER_GCC)
}
win32-msvc2015 {
CONFIG += COMPILER_MSVC2015
message(COMPILER_MSVC2015)
win32-msvc2015:QMAKE_TARGET.arch = x86_64
}
}
#linux下的构建配置
linux {
CONFIG += PLATFORM_LINUX
message(PLATFORM_LINUX)
!contains(QT_ARCH, x86_64){
QMAKE_TARGET.arch = x86
} else {
QMAKE_TARGET.arch = x86_64
}
linux-g++{
CONFIG += COMPILER_GCC
message(COMPILER_GCC)
}
}
#mac下的构建配置
macx {
CONFIG += PLATFORM_OSX
message(PLATFORM_OSX)
macx-clang {
CONFIG += COMPILER_CLANG
message(COMPILER_CLANG)
QMAKE_TARGET.arch = x86_64
}
macx-clang-32{
CONFIG += COMPILER_CLANG
message(COMPILER_CLANG)
QMAKE_TARGET.arch = x86
}
}
#CPU的架构
contains(QMAKE_TARGET.arch, x86_64) {
CONFIG += PROCESSOR_x64
message(PROCESSOR_x64)
} else {
CONFIG += PROCESSOR_x86
message(PROCESSOR_x86)
}
#构建对应的配置项
CONFIG(debug, release|debug) {
CONFIG += BUILD_DEBUG
message(BUILD_DEBUG)
} else {
CONFIG += BUILD_RELEASE
message(BUILD_RELEASE)
}
依据在qmkae-target-platform.pri中我们添加的各种宏,我们就可以重新定义构建项目的输出路径,将输出路径的定义放到qmake-dest-path.pri文件中,文件内容如下
//qmake-dest-path.pri
platform_path = unknown-platform
compiler_path = unknown-compiler
processor_path = unknown-processor
build_path = unknown-build
PLATFORM_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_path
message(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/.obj
MOC_DIR = $$PWD/build/$$DESTINATION_PATH/.moc
RCC_DIR = $$PWD/build/$$DESTINATION_PATH/.qrc
UI_DIR = $$PWD/build/$$DESTINATION_PATH/.ui
修改了库的输出路径之后,我们就可以通过新的路径引入ime-lib库了,引入方法如下:
LIBS += -L$$PWD/../binaries/$$DESTINATION_PATH -lime-lib
修改完成之后ime-ui.pro的配置如下:
#指定项目类型和配置
QT += qml quick
CONFIG += c++11
TEMPLATE = app
#添加源码目录
INCLUDEPATH += src \
../ime-lib/src
CONFIG -= qml_debug
include(../qmake-target-platform.pri)
include(../qmake-dest-path.pri)
#指定生成目录
DESTDIR = $$PWD/../binaries/$$DESTINATION_PATH
#指定中间文件的输出目录
OBJECTS_DIR = $$PWD/build/$$DESTINATION_PATH/.obj
MOC_DIR = $$PWD/build/$$DESTINATION_PATH/.moc
RCC_DIR = $$PWD/build/$$DESTINATION_PATH/.qrc
UI_DIR = $$PWD/build/$$DESTINATION_PATH/.ui
# -L说明指代的是目录 -l说明指代的是库文件
# 引入库的时候不需要指定库文件的前缀和后缀(后缀:.so,.dll,前缀lib)
LIBS += -L$$PWD/../binaries/$$DESTINATION_PATH -lime-lib
#警告宏
DEFINES += QT_DEPRECATED_WARNINGS
DEFINES+=QT_QML_DEBUG_NO_WARNING
#添加源码
SOURCES += src/main.cpp
RESOURCES += qml.qrc
修改完毕之后ime-lib.pro文件的配置如下:
QT -= gui
#库类型和库名称
TARGET = ime-lib
TEMPLATE = lib
DEFINES += IMELIB_LIBRARY
#使用C++14的特性
CONFIG += c++14
#包含src目录路径
INCLUDEPATH += src
include(../qmake-target-platform.pri)
include(../qmake-dest-path.pri)
#指定生成目录
DESTDIR = $$PWD/../binaries/$$DESTINATION_PATH
#指定中间文件的输出目录
OBJECTS_DIR = $$PWD/build/$$DESTINATION_PATH/.obj
MOC_DIR = $$PWD/build/$$DESTINATION_PATH/.moc
RCC_DIR = $$PWD/build/$$DESTINATION_PATH/.qrc
UI_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
#