QGroundControl Source Code Learning Series - 3
QGCApplication
QGCApplication 继承自 QGuiApplication,而不是 QApplication。QGroundControl 界面是使用 QML 及 QtQuick 组件完成的,所以在 QGCApplication 里需要管理一个 QQmlApplicationEngine 引擎。当然它还有别的作用,比如维护了一个 QGCToolbox 对象(可用于访问程序中用到的其他管理器),设定系统语言等等功能。
在应用程序中,通常只需要一个 Application 对象,能够完成一些全局的操作。QGCApplication 提供了类似单例模式的方式获取 QGCApplication 对象:
/// @brief Returns the QGCApplication object singleton.
QGCApplication* qgcApp(void);
/// @brief Returns the QGCApplication object singleton.
QGCApplication* qgcApp(void)
{
return QGCApplication::_app;
}
static QGCApplication* _app; ///< Our own singleton. Should be reference directly by qgcApp
调用 qgcApp
函数就可以访问全局的 应用对象
了,这里的 QGCApplication::_app 是公有的(因此才能在 qgcApp 函数中访问),但命名方式却写成私有的(有 _
前缀),因此尽量避免直接调用该指针。
以下为代码中的注释: Although public, these methods are internal and should only be called by UnitTest code
在我开发 Qt 应用的过程中并没有研究 QApplication
的作用,就像大部分示例那样,创建一个 QApplication
对象,然后执行事件循环。对于小项目,程序执行流程简单的话按照示例来没什么问题,但是当程序执行流程复杂之后,事情就没那么简单了。比如一个简单的例子,在 app.exec()
后执行一些自定义的操作,那么我们需要改变代码的流程,在 main 函数里不是简单的 return app.exec();
,我们可以这样做:
int main(int argc, char *argv[])
{
Something *something;
...
int ret = app.exec();
delete something;
return ret;
}
当应用程序更复杂后,通过继承 QApplication 并通过单例模式提供对外的接口,可以简化其他类的操作,比如将某些全局唯一的变量组合到自定义的 Application 中,然后通过 app 获取对象。这样就可以避免过度使用单例模式。
其实 Qt 封装的这个类能够很好的解决我们在开发桌面应用的一些比较麻烦的地方,它提供了一些方法使我们能够拿到整个应用中的 Widget 子类:QWidgetList allWidgets()
,返回桌面组件:QDesktopWidget *desktop()
等等,如果使用 QWidget 组件开发一个桌面应用,显然用 QApplication
更好。
曾经我为了能够更好的访问程序中的某些窗口部件,用单例模式构造了一个窗口管理器,在这个管理器里面用 QMap 存储所有构造过的窗口对象,然后通过 QMap 的方法来访问这个窗口指针。但是很麻烦的一点在于:每构建一个窗口对象,我都得加一行代码向窗口管理器注册这个对象,非常繁琐(有的是在构造函数中向窗口管理器进行注册,但也非常麻烦)。后来当需求发生变化后,需要添加新的功能/界面时,发现这样的模式并不适合开发,最终被弃用了。显然,如果要访问应用程序中已有的窗口部件,用
QApplication
提供的方法更加方便。
QGCApplication 中还有很多其他的功能,但我不打算逐行代码式的阅读了,说实话这样的效率太低,效果可能也不好。我会挑一些之前没见过的代码写法或用法来记录,这些内容你可能已经知道或者你想知道的在我的笔记里没有记录,但这些内容对我来说已经足够学习了。如果你想要深入了解大型软件项目的开发,建议根据自己的实际情况去学习 QGroundControl 的源码。还有一点值得一提,QGroundControl 已经有些年份了,所以其中有些代码可能已经弃用了。我在尝试使用
CMake
构建项目的时候,甚至出现了无法构建项目的情况,向官方确认了是 CMakeLists.txt 中的内容已经过时了,因此只能使用QMake
进行构建。
QGCLoggingCategory
- 这一部分和上一节的 AppMessages 非常相关。
AppMessages 中我讲了 QGroundControl 中是如何实现自定义的消息处理器,如何将日志导出到窗口部件以及如何保存日志的。在项目比较小,结构简单的时候,这样使用自定义的消息处理器完全没问题,毕竟要输出的东西就那么多。但项目一复杂,模块组件一多的时候,要从千万条日志中找出感兴趣的部分就太难了。因此查看日志时需要有过滤的功能。
QGCLoggingCategory 中的内容不多,包含两个类:QGCLoggingCategory
和 QGCLoggingCategoryRegister
,前者非常简单:
class QGCLoggingCategory
{
public:
QGCLoggingCategory(const char* category) { QGCLoggingCategoryRegister::instance()->registerCategory(category); }
};
它仅有一个构造函数,在构造函数里向 QGCLoggingCategoryRegister
进行注册日志类别。QGCLoggingCategoryRegister
也比较简单,这是一个注册器管理器,负责维护当前应用程序中的所有日志类别(logging category),在这个类里实现了对日志的级别控制和过滤(底层使用的还是 Qt 的 LoggingCategory 的 FilterRules)。
在使用 QLoggingCategory 时,会经常用到两个很重要的宏:Q_DECLARE_LOGGING_CATEGORY
和 Q_LOGGING_CATEGORY
,一个用于声明宏,一个用于定义宏,下面是这两个宏的宏定义:
// qloggingcategory.h Qt5.12
#define Q_DECLARE_LOGGING_CATEGORY(name)
extern const QLoggingCategory &name();
#define Q_LOGGING_CATEGORY(name, ...)
const QLoggingCategory &name()
{
static const QLoggingCategory category(__VA_ARGS__);
return category;
}
将宏展开后就能发现,这两个宏实际上分别是函数的声明和定义,因此一定要按照官方说明的那样,在头文件中使用 Q_DECLARE_LOGGING_CATEGORY
,在对应的源文件中使用 Q_LOGGING_CATEGORY
。
另外还需要注意的一点是,如果在动态链接库中使用
QLoggingCategory
,并且想要把这个 LoggingCategory 暴露给外部使用,需要在声明的时候添加宏DLL_EXPORT
,一般来说,在构建 DLL 时,该宏设为Q_DECL_EXPORT
;而在引用该 DLL 时,该宏一般为Q_DECL_IMPORT
。这个原理就跟暴露 DLL 内部的类是一样的。不过一般来说,CPP 编译器在编译时会修改 c 函数的函数名,如果使用的编译器版本不一样会不会导致 DLL 导出的 loggingCategory 无法在外部使用呢?有兴趣的话可以去了解一下。
在 QGCLoggingCategory
中还有一个扩展用的宏:
// QGCLoggingCategory.h Stable_v3.5.1
/// @def QGC_LOGGING_CATEGORY
/// This is a QGC specific replacement for Q_LOGGING_CATEGORY. It will register the category name into a
/// global list. It's usage is the same as Q_LOGGING_CATEOGRY.
#define QGC_LOGGING_CATEGORY(name, ...)
static QGCLoggingCategory qgcCategory ## name (__VA_ARGS__);
Q_LOGGING_CATEGORY(name, __VA_ARGS__)
注释中说明这个宏的作用与 Q_LOGGING_CATEGORY
相同,但对于不熟悉 C/C++ 宏处理器的来说,static QGCLoggingCategory qgcCategory ## name (__VA_ARGS__);
这部分看上去有点奇怪,最奇怪的部分就是 ##
。
其中的 ##
其实就是个 Token,没有含义,预处理之后会被去掉。在 token-pasting-operator 中有几个实例:
#define type i##nt
type a; // same as int a; since i##nt pastes together to "int"
想要查看预处理器处理宏定义后的结果,可以使用 cpp file.c
或者 gcc -E file.c
,我在文件中输入以下内容:
// testmacro.h
#define QGC_LOGGING_CATEGORY(name, ...)
static QGCLoggingCategory qgcCategory ## name (__VA_ARGS__);
#define type i##nt
type a; // same as int a; since i##nt pastes together to "int"
#define DECLARE_AND_SET(type, varname, value) type varname = value; type orig_##varname = varname;
DECLARE_AND_SET( int, area, 2 * 6 );
class QGCLoggingCategory {}
QGC_LOGGING_CATEGORY(FirmwareUpgradeLog, "FirmwareUpgradeLog")
然后用 cpp 处理看看结果 cpp testmacro.h
:
# 1 "testmacro.h"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "testmacro.h"
int a;
int area = 2 * 6; int orig_area = area;;
class QGCLoggingCategory {}
static QGCLoggingCategory qgcCategoryFirmwareUpgradeLog ("FirmwareUpgradeLog");
可以看到,处理后,宏定义被展开成具体的字符,而宏定义也消失了。在宏定义中的 ##
符号展开后就被去除了(连同左右的空格)。而下面这个宏的作用就容易理解了,它就是用来构造 QGCLoggingCategory 的实例的,并且给每个实例前面加上了 qgcCategory
前缀。
#define QGC_LOGGING_CATEGORY(name, ...)
static QGCLoggingCategory qgcCategory ## name (__VA_ARGS__);
接下来看看 QGCLoggingCategoryRegister
的实现,QGCLoggingCategoryRegister
使用的单例模式,通过 instance()
方法对外提供唯一的对象实例(因此该类的构造函数也同时被声明为了 private
修饰)。值得注意的是,在 instance()
方法中,构造了对象实例 _instance = new QGCLoggingCategoryRegister();
后,还使用了 Q_CHECK_PTR(_instance);
来进行了检查(防止内存分配失败)。
QGCLoggingCategoryRegister
对于各个模块的日志的过滤规则是通过 QSettings 进行保存和恢复的:
void QGCLoggingCategoryRegister::setCategoryLoggingOn(const QString& category, bool enable)
{
QSettings settings;
settings.beginGroup(_filterRulesSettingsGroup);
settings.setValue(category, enable);
}
bool QGCLoggingCategoryRegister::categoryLoggingOn(const QString& category)
{
QSettings settings;
settings.beginGroup(_filterRulesSettingsGroup);
return settings.value(category, false).toBool();
}
注意,上面两个函数中构造的
QSettings
对象所对应的域是相同的,对于不带参数的QSettings
构造的对象而言,Qt 官方的文档中是这样说的:Constructs a QSettings object for accessing settings of the application and organization set previously with a call to QCoreApplication::setOrganizationName(), QCoreApplication::setOrganizationDomain(), and QCoreApplication::setApplicationName().
而在QGCApplication
的构造函数中定义了 OrganizationName 和 OrganizationDomain,因此该程序中的 QSettings 对应的域都是相同的。
不过这里面有一点不好:beginGroup
之后没有调用 endGroup
方法来关闭分组。这里面的 settings
对象是堆中分配的,函数返回时自动销毁,可能就不会出现问题。但如果你在自己的应用程序中通过一个全局的 settings 对象来保存程序设置,调用 beginGroup
之后却忘了调用 endGroup
可能会出现很大的问题。因为 Groups can be nested,这可能将导致你上一次存储的值下一次没法正常读出来了。