7. 工程管理

Qt 工程文件 pro pri 多项目工程,子项目,跨平台管理

7.1. 经验之谈

通用规则

除了极小的微型demo级别项目外,其余项目建议用pri分门别类不同文件夹存放代码文件,方便统一管理和查找。 同类型功能的类建议统一放在一起,如果该目录下代码文件数量过多,也建议拆分多个目录存放。 比如就3-5个界面的项目,统一搞个form.pri存放这些界面,而当项目越来越大,界面可能也需要按照功能划分,比如系统配置的窗体放在一个目录下,日志管理的窗体放在一个目录下。 很多通用功能,多个项目都会用到,可以考虑封装成pri形式的模块,俗称轮子,不断完善这些轮子,多个项目共享该模块,一旦遇到BUG修复,只需要更改一个地方就行。 项目如果还更大或者项目组人员分配不同功能,可以考虑插件形式,插件一般会用到两种,一种是普通动态库形式的插件,必须和主程序放在一起;一种是Qt机制的插件,放在指定的目录。

全局配置文件

全局配置文件管理类 appconfig.h 用来读写对应项目的配置文件。

格式可以是ini、xml、json等,小项目建议ini,怎么方便怎么来,相当于将配置文件的值映射到全局变量。 配置文件如果配置项较多建议分组存储方便查找,而不是全部放在一个大分组中。 读配置文件的时候可以判断配置文件是否存在、配置项是否缺失等情况,有问题则重新生成配置文件,避免恶意删除配置文件导致程序运行异常。 读配置文件的时候可以填入默认值(qt配置文件类QSettings的value方法的第二个参数,set.value(“Hardware”, App::Hardware)),避免初始时候读取不到节点而导致配置项值不符合预期值类型。 读配置文件完成后可以重新判断配置项的值是否符合要求,对值进行过滤和矫正,防止人为打开配置文件修改后填入了异常的值,比如定时器的间隔为0,要重新纠正设定为合法的值。 带中文的初始值用QString::fromUtf8包起来,比如QString::fromUtf8(“管理员”)。 带中文的配置项要设置配置文件编码为 utf-8,set.setIniCodec(“utf-8”)。

全局变量

全局变量管理类 appdata.h 用来设置项目中用到的所有全局变量。

比如当前用户/系统是否锁定等,这样可以在任意的编码位置使用该变量进行判断处理。 可以将UI界面中的导航栏宽高、按钮大小、图标大小等变量放在这,系统启动后判断分辨率等来设定不同的值。

全局事件中转处理

全局事件中转处理类 appevent.h 用来中转系统中各种跨多个UI以及多个类的事件。

此类必须是全局单例类,便于全局统一使用。 比如类a的父类是b,类b的父类是c,现在有个信号要发给类d,在没有事件中转处理的情况下的做法是将a信号发给b,b再发给c,c再发给d,如果父类嵌套层级越多越复杂,代码越难管理。 将类a的信号发给appevent类,然后类d直接关联appevent类进行处理就行。 项目越大,会越发现事件中转处理的必要性,代码清晰,管理方便。

全局程序初始化

全局程序初始化类 appinit.h 用来做一些程序启动后的初始化处理。

读取配置文件。 设置全局字体。 设置全局样式表,建议先读取通用的样式表,然后将额外的样式表内容加到后面一起设置。 设置项目编码。 设置翻译文件,可以加载多个,包括qt内置的qt_zh_CN.qm,用户自己的翻译文件等。 初始化随机数种子。 新建项目中需要的目录,防止没有目录无法保存文件到目录。 初始化数据库,包括打开数据库,载入基础数据比如用户表、设备表等。 启动日志输出类用来启动日志服务。 启动运行时间记录类用来记录每次软件运行开始时间和结束时间。 关联全局事件过滤器处理自定义无边框UI拖动、全局按键处理等。

全局通用类

调试日志输出类 savelog.h 用来启动日志服务,可以将日志输出到文件或者网络打印输出。 运行时间记录类 saveruntime.h 用来记录每次软件运行开始时间和结束时间。 图形字体类 iconfont.h 用来设置图形字体图标。

7.2. 工程目录

.pro :linenos:
├── xxxx.pro                        # Readme
├── .gitignore.md                   # 工程配置文件
├── README.md                       # git 可忽略的中间文件
├── api                             # 本工程特有的代码,函数,类
│   ├── api.pri                     # 代码模块配置文件
│   ├── *.cpp
│   └── *.h
├── common                          # 可复用移植的代码,函数,类 如 日志类,加密解密类
│   ├── common.pri
│   ├── *.cpp
│   ├── *.h
│   ├── resources
│   └── res.qrc
├── *                               # 本工程使用到其他模块 如OpenCV等等
├── ui                              # UI代码、文件
│   ├── base                        # 可复用移植的的界面,控件
│   │   ├── base.pri                # 控件设计/效果
│   │   ├── *.cpp
│   │   └── *.h
│   ├── ui.pri                      # UI设计/效果
│   ├── *.cpp
│   ├── *.h
│   ├── *.rp
│   └── source                      # 资源文件
│       └── *
├── app
│   ├── app.pri
│   ├── main.cpp
│   ├── main.qrc
│   ├── mainwindow.cpp
│   ├── mainwindow.h
│   ├── mainwindow.ui
│   └── resources
│       ├── classcell.mmap
│       ├── classcell.vsdx
│       ├── css
│       │   ├── *.css
│       ├── qm
│       │   ├── language.en.qm
│       │   ├── language.en.ts
│       │   ├── language.zh.qm
│       │   └── language.zh.ts
│       ├── rc
│       │   ├── app.rc
│       │   └── main.ico
│       ├── question.txt
│       └── version.txt
├── build
│   └── classcell
│       └── *.o
├── demo
│   ├── ClassCell
│   ├── ClassCell.pdb
│   ├── conf
│   │   └── config.ini
│   ├── language
│   │   ├── language.en.qm
│   │   └── language.zh.qm
│   ├── log
│   │   ├── log_2021-08-20.txt
│   │   └── runtime_2021.txt
│   ├── res
│   │   └── fontawesome-webfont.ttf
│   ├── *.dll
│   ├── *.exe
│   └── *.lib
└── doc
   ├── doc.conf
   ├── html
   └── *

example

project001.png

Tip

  1. 使用 git 管理工程

  2. 使用不同目录管理不同的代码模块,方便测试、移植

  3. Qt 跨平台项目,将依赖库直接放入工程中管理,切换平台或电脑,不需要重新配置环境(嵌入式环境编译库比较麻烦),不足之处是需要对依赖库比较了解。

  4. 不使用 QtCreator 默认的构建目录,自定义 build 中间目录和demo目录,demo目录放入程序运行的依赖库,方便打包程序。

  5. 引入 Doxygen 来管理注释生成文档。

  6. common 和 ui 模块为可复用代码,多个工程可通用。也可以整理自己的通用库。

7.3. 工程结构

pro pri SUBDIRS

ClassCell.pro :linenos:
QT       += core gui

QT       += serialport

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11
CONFIG -= debug_and_release

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

include($$PWD/app/app.pri)
include($$PWD/common/common.pri)
include($$PWD/toupcam/toupcam.pri)
include($$PWD/ui/ui.pri)
include($$PWD/api/api.pri)
include($$PWD/opencv/opencv.pri)
include($$PWD/gsl/gsl.pri)

TARGET = ClassCell
TEMPLATE = app

DESTDIR         = $$PWD/demo
MOC_DIR         = $$PWD/build/classcell
UI_DIR          = $$PWD/build/classcell
RCC_DIR         = $$PWD/build/classcell
OBJECTS_DIR     = $$PWD/build/classcell

win32:RC_FILE = $$PWD/app/resources/rc/app.rc

TRANSLATIONS = \
    $$PWD/app/resources/qm/language.zh.ts $$PWD/app/resources/qm/language.en.ts

win32{
    # opencv
    INCLUDEPATH += $$PWD/opencv/includes/opencv2/
    LIBS += $$PWD/opencv/mingw/winlib/libopencv_*.a
}

#DEFINES += __ARM__
#DEFINES += __UBUNTU__

contains(DEFINES, __ARM__){
    # arm
    unix{
        # opencv
        LIBS += -L$$PWD/usr/local/lib -lopencv_calib3d -lopencv_core -lopencv_dnn
        LIBS += -L$$PWD/usr/local/lib -lopencv_features2d -lopencv_flann -lopencv_gapi
        LIBS += -L$$PWD/usr/local/lib -lopencv_highgui -lopencv_imgcodecs -lopencv_imgproc
        LIBS += -L$$PWD/usr/local/lib -lopencv_ml -lopencv_objdetect -lopencv_photo
        LIBS += -L$$PWD/usr/local/lib -lopencv_stitching -lopencv_video -lopencv_videoio
    }

}

contains(DEFINES, __UBUNTU__){
    # ubuntu
    unix{
        # opencv
        LIBS += -L$$PWD/opencv/mingw/linuxlib -lopencv_calib3d -lopencv_core -lopencv_dnn
        LIBS += -L$$PWD/opencv/mingw/linuxlib -lopencv_features2d -lopencv_flann -lopencv_gapi
        LIBS += -L$$PWD/opencv/mingw/linuxlib -lopencv_highgui -lopencv_imgcodecs -lopencv_imgproc
        LIBS += -L$$PWD/opencv/mingw/linuxlib -lopencv_ml -lopencv_objdetect -lopencv_photo
        LIBS += -L$$PWD/opencv/mingw/linuxlib -lopencv_stitching -lopencv_video -lopencv_videoio
    }
}

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

Tip

pro pri 关键字可参考 本工程目录下 pdf/QT中.pro变量解析.pdf

或访问官方有参考文档。

gsl 模块

gsl.pri :linenos:
DEFINES += GSL_DLL

INCLUDEPATH += $$PWD/includes

LIBS+= -L$$PWD/winlib/mingw81_32/lib -llibgsl
LIBS+= -L$$PWD/winlib/mingw81_32/lib -llibgslcblas

HEADERS += \
    $$PWD/includes/gsl/gsl_sf_bessel.h \
    $$PWD/includes/gsl/gsl_mode.h \
    $$PWD/includes/gsl/gsl_precision.h \
    $$PWD/includes/gsl/gsl_sf_result.h

模块测试

gsl.pro :linenos:
QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11
CONFIG -= debug_and_release

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

include($$PWD/gsl.pri)

TARGET = Gsltest
TEMPLATE = app

DESTDIR         = $$PWD/../demo
MOC_DIR         = $$PWD/../build/gsltest
UI_DIR          = $$PWD/../build/gsltest
RCC_DIR         = $$PWD/../build/gsltest
OBJECTS_DIR     = $$PWD/../build/gsltest

SOURCES += \
    $$PWD/main.cpp
main.pro :linenos:
#include <QCoreApplication>
//#include <gsl/gsl_sf_bessel.h>
#include <gsl/gsl_sf_bessel.h>
#include "math.h"
#include <iostream>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc,argv);
    double x=10;
    double y=gsl_sf_bessel_J0(x);
    std::cout<<"J0("<< x <<")= "<<y<<std::endl;
    return a.exec();
}

Tip

配置文件中添加注释 快捷键:Ctrl + /

7.4. 跨平台

工程配置库,不同平台下的库隔离

直接定义一个宏;用的时候可以直接判断,这样做不好的地方是编译前需要重新切换一下宏

win32{
    # opencv
    INCLUDEPATH += $$PWD/opencv/includes/opencv2/
    LIBS += $$PWD/opencv/mingw/winlib/libopencv_*.a
}

#DEFINES += __ARM__
#DEFINES += __UBUNTU__

contains(DEFINES, __ARM__){
    # arm
    unix{
        # opencv
        LIBS += -L$$PWD/usr/local/lib -lopencv_calib3d -lopencv_core -lopencv_dnn
        LIBS += -L$$PWD/usr/local/lib -lopencv_features2d -lopencv_flann -lopencv_gapi
        LIBS += -L$$PWD/usr/local/lib -lopencv_highgui -lopencv_imgcodecs -lopencv_imgproc
        LIBS += -L$$PWD/usr/local/lib -lopencv_ml -lopencv_objdetect -lopencv_photo
        LIBS += -L$$PWD/usr/local/lib -lopencv_stitching -lopencv_video -lopencv_videoio
    }

}

contains(DEFINES, __UBUNTU__){
    # ubuntu
    unix{
        # opencv
        LIBS += -L$$PWD/opencv/mingw/linuxlib -lopencv_calib3d -lopencv_core -lopencv_dnn
        LIBS += -L$$PWD/opencv/mingw/linuxlib -lopencv_features2d -lopencv_flann -lopencv_gapi
        LIBS += -L$$PWD/opencv/mingw/linuxlib -lopencv_highgui -lopencv_imgcodecs -lopencv_imgproc
        LIBS += -L$$PWD/opencv/mingw/linuxlib -lopencv_ml -lopencv_objdetect -lopencv_photo
        LIBS += -L$$PWD/opencv/mingw/linuxlib -lopencv_stitching -lopencv_video -lopencv_videoio
    }
}

代码中区分多平台,通过不同的宏来实现。

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

class QProcess;

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void openProcess();
    void readResult(int exitCode);

private:
    QProcess *p;
};

#endif // MAINWINDOW_H
#include <QProcess>
#include <QPushButton>
#include <QMessageBox>
#include <QTextCodec>

#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    p = new QProcess(this);
    QPushButton *bt = new QPushButton("display", this);
    connect(bt, SIGNAL(clicked()), this, SLOT(openProcess()));
}

MainWindow::~MainWindow()
{

}

void MainWindow::openProcess()
{
#if defined(Q_OS_WIN32)
    p->start("cmd.exe", QStringList() << "/c" << "dir");
#elif defined(Q_OS_LINUX)
    p->start("ls", QStringList() << "/home/usr_name");
#endif
    connect(p, SIGNAL(finished(int)), this, SLOT(readResult(int)));
}

void MainWindow::readResult(int exitCode)
{
    if(exitCode == 0) {
#if defined(Q_OS_WIN32)
        QTextCodec* gbkCodec = QTextCodec::codecForName("GBK");
        QString result = gbkCodec->toUnicode(p->readAll());
#elif defined(Q_OS_LINUX)
        QTextCodec* utfCodec = QTextCodec::codecForName("UTF-8");
        QString result = utfCodec->toUnicode(p->readAll());
#endif
        QMessageBox::information(this, "dir", result);
    }
}