源码地址
gitee,github
下面会尽可能地列举框架的所有功能,并介绍每个功能的用法。这里并不会讲解它是怎么实现的,但不用担心,它们的源码都很简单。
正文
以下样例都在QuickBootExample中可以找到。
编译
编译环境: Qt5.15(Qt6还未适配,低版本可能需要调整一下部分API)、C++17
整个框架的编译其实非常简单,因为我把一些依赖的第三方库源码也直接传到代码仓了,所以下载最新tag的源码,用QtCreator打开就能直接编译,但是有一些注意点,以下编译选项都在common.pri文件中:
- 日志库有依赖一个第三方库quazip,这个库的开源协议为lgpl,所以按道理来说不应该直接源码链接此库,但我为了方便,还是将此库的源码链接进来了,但这个库的用途非常的小,可以添加MC_DISABLE_QUAZIP宏去除掉此库。
- QuickBoot框架可以加上QScxml模块的依赖,后面会说到这个功能,如果你需要的话可以添加MC_ENABLE_QSCXML宏。
- QuickBoot默认依赖了QLocalServer、QQuick、QML以及QQuickWidget,如果你想只用轻量级的启动方案的话(不使用McQuickBoot.h、McSingleApplication.h以及McSingleCoreApplication.h),你可以添加MC_TINY_QUICK_BOOT宏来去除掉这些依赖。
初始化
框架的初始化有两个方法(如果框架有机会升级到2.x版本,初始化方案可能会有所调整,但不用担心,我们会尽量减少外部接口的调整),这取决于我们的开发是使用QML还是QWidget做界面。
#include <McBoot/McQuickBoot.h>
int main(int argc, char *argv[])
{
McQuickBoot::setPreInitFunc([](QCoreApplication *) {
qDebug() << QStringLiteral(u"此代码将在QGuiApplication构造完成后,框架初始化之前调用");
});
McQuickBoot::setAfterInitFunc([](QCoreApplication *, QQmlApplicationEngine *) {
qDebug() << QStringLiteral(u"此代码将在QGuiApplication构造完成后并且框架初始化之后调用");
});
return McQuickBoot::run(argc, argv, QLatin1String("qrc:/main.qml"));
}
这种启动方式主要是为QML提供支持的,它甚至于只需要return McQuickBoot::run(argc, argv, QLatin1String("qrc:/main.qml")); 这一行代码就能完成初始化。当然,既然是全量启动,那这种方式其实是可以QML和QWidget联合使用的,但用途较少,这里不过多介绍,我们姑且认为它只是给QML提供。
#include <QApplication>
#include <McBoot/McQuickBootSimple.h>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
McQuickBootSimple::init();
return app.exec();
}
这种方式取消了QML相关的注入,如果只是用QWidget,那么可以使用这种方式,他需要在外面手动构造一个QCoreApplication或其衍射类(暂时不打算像第一种方式那样提供run方法)。
配置式界面
这里针对McQuickBootSimple的方式提供一个界面配置功能,我们可以分模块实现各个零散的界面,通过配置文件将他们链接在一起。就如同上面只是启动了一个Application,现在我们为其添加QWidget界面。
添加MainWindow.h,注意构造函数一定要被Q_INVOKABLE标记
#pragma once
#include <QMainWindow>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
Q_INVOKABLE explicit MainWindow(QWidget *parent = nullptr);
};
MainWindow.cpp
#include "MainWindow.h"
#include <McWidgetIoc/McWidgetGlobal.h>
MC_STATIC()
qRegisterMetaType<MainWindow *>();
MC_STATIC_END
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {}
添加widgetioc.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean name="mainWindow" class="MainWindow">
</bean>
</beans>
并在exe的运行目录下添加application.yml,注意xmlPaths指向的路径为上一步添加的xml文件
boot:
application:
widget:
xmlPaths: ["../../Examples/QuickBootExample/widgetioc.xml"]
mainWindowName: "mainWindow"
再次编译运行,就能显示出我们的界面了。这里其实也只是把整个界面的依赖关系移交到了widgetioc.xml这个配置文件里面,框架负责拿到名为mainWindow 的对象,并显示出来。至于MC_STATIC宏,可以参考代码仓的wiki静态代码块
让界面更复杂一点
上面我们引入了widgetioc.xml和application.yml两个配置文件来构造界面,先说一说这两个文件的作用。
application.yml - 框架启动配置文件(至于如何映射这个文件里的内容,后面会单独说到,它是可以扩展到外部使用的)。框架在启动的时候会有一个默认配置文件加载列表: {runDir}/config/application.yml, {runDir}/application.yml。{runDir}为exe的运行目录,还可以调用McAbstractQuickBoot::addConfigPath方法在此列表末尾拼接配置文件的路径。框架启动时,会从前往后挨个查找这些配置文件,如果某一个配置文件存在,则直接使用这个文件为启动配置,并停止查找。如果文件都不存在,框架仍然正常启动,但某些功能将使用默认配置。如同上面的配置文件,表示要使用../../Examples/QuickBootExample/widgetioc.xml 路径下的xml文件构造界面,并查找名为mainWindow 的对象为主窗口并显示出来。相对路径为exe的运行目录。
widgetioc.xml - 这个文件是WidgetIoc容器的配置文件,配置文件中的内容和普通IOC一样。它表示声明一个对象名为mainWindow 的对象,这个对象是类MainWindow 的实例。
现在我们在主界面上再添加两个子界面,添加方法有很多种,正常情况下你应该在MainWindow 做好布局,配置文件传入两个子界面即可(也许这两个子界面也仅仅只需要做好布局,更具体的功能还在它的子界面)。但是这个配置文件是很灵活的,你完全可以在里面完成布局,然而我们并不建议你这么做,因为这个配置文件是运行时、通过反射加载的,它会影响效率,而且框架提出这个配置文件仅仅只是想更灵活的配置我们的功能界面,并不希望连布局都放在这里面去做。最后,QT有一个.ui文件,这个文件也是一个xml文件,他就是QT可视化布局的工具,而且QT的uic编译器还会把这个文件编译成.cpp文件,最后编译成二进制码直接链接到我们主程序里面,它的效率快得多。
所以,我们这里在MainWindow 里面做好布局,并添加两个子界面,通过配置文件将子界面放到主界面里面(注: 你可以借助.ui文件来完成布局,我这为了能把代码贴出来 ,所以手动用代码写的)。
MainWindow.h
#pragma once
#include <QMainWindow>
class MainWindow : public QMainWindow
{
Q_OBJECT
Q_PROPERTY(QList<QWidget *> widgets READ widgets WRITE setWidgets)
public:
Q_INVOKABLE explicit MainWindow(QWidget *parent = nullptr);
QList<QWidget *> widgets() const;
void setWidgets(const QList<QWidget *> &val);
};
MainWindow.cpp
#include "MainWindow.h"
#include <QHBoxLayout>
#include <McWidgetIoc/McWidgetGlobal.h>
MC_STATIC()
qRegisterMetaType<MainWindow *>();
//! WidgetIoc内部已经对QList<QWidget *>进行了注册,所以这里不再需要注册
//! MC_REGISTER_LIST_CONVERTER(QList<QWidget *>)
MC_STATIC_END
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
setCentralWidget(new QWidget(this));
auto layout = new QHBoxLayout(centralWidget());
centralWidget()->setLayout(layout);
}
QList<QWidget *> MainWindow::widgets() const
{
QList<QWidget *> ws;
auto count = centralWidget()->layout()->count();
for (int i = 0; i < count; ++i) {
auto item = centralWidget()->layout()->itemAt(i);
if (item->widget() == nullptr) {
continue;
}
ws.append(item->widget());
}
return ws;
}
void MainWindow::setWidgets(const QList<QWidget *> &val)
{
for (auto w : val) {
centralWidget()->layout()->addWidget(w);
}
}
添加第一个子窗口ChildWidget1.h
#pragma once
#include <QWidget>
class ChildWidget1 : public QWidget
{
Q_OBJECT
public:
Q_INVOKABLE explicit ChildWidget1(QWidget *parent = nullptr);
};
ChildWidget1.cpp
#include "ChildWidget1.h"
#include <QLabel>
#include <McWidgetIoc/McWidgetGlobal.h>
MC_STATIC()
qRegisterMetaType<ChildWidget1 *>();
MC_STATIC_END
ChildWidget1::ChildWidget1(QWidget *parent) : QWidget(parent)
{
new QLabel("ChildWidget1", this);
}
添加第二个子窗口ChildWidget2.h
#pragma once
#include <QWidget>
class ChildWidget2 : public QWidget
{
Q_OBJECT
public:
Q_INVOKABLE explicit ChildWidget2(QWidget *parent = nullptr);
};
ChildWidget2.cpp
#include "ChildWidget2.h"
#include <QLabel>
#include <McWidgetIoc/McWidgetGlobal.h>
MC_STATIC()
qRegisterMetaType<ChildWidget2 *>();
MC_STATIC_END
ChildWidget2::ChildWidget2(QWidget *parent) : QWidget(parent)
{
new QLabel("ChildWidget2", this);
}
修改widgetioc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean name="mainWindow" class="MainWindow">
<property name="widgets">
<list>
<ref bean="childWidget1" />
<ref bean="childWidget2" />
</list>
</property>
</bean>
<bean name="childWidget1" class="ChildWidget1" />
<bean name="childWidget2" class="ChildWidget2" />
</beans>
运行如下:
我们在整点额外功能
现在我们有一个新的需求,我们要实现第二个软件,它和第一个软件相比仅仅只是第一个界面不一样而已。因为第一个软件还需要使用,所以它不能修改,那么我们添加第三个界面ChildWidget3.h
#pragma once
#include <QWidget>
class ChildWidget3 : public QWidget
{
Q_OBJECT
public:
Q_INVOKABLE explicit ChildWidget3(QWidget *parent = nullptr);
};
ChildWidget3.cpp
#include "ChildWidget3.h"
#include <QLabel>
#include <McWidgetIoc/McWidgetGlobal.h>
MC_STATIC()
qRegisterMetaType<ChildWidget3 *>();
MC_STATIC_END
ChildWidget3::ChildWidget3(QWidget *parent) : QWidget(parent)
{
new QLabel("ChildWidget3", this);
}
把widgetioc.xml修改一下
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean name="mainWindow" class="MainWindow">
<property name="widgets">
<list>
<!-- <ref bean="childWidget1" />-->
<ref bean="childWidget3" />
<ref bean="childWidget2" />
</list>
</property>
</bean>
<!-- <bean name="childWidget1" class="ChildWidget1" />-->
<bean name="childWidget2" class="ChildWidget2" />
<bean name="childWidget3" class="ChildWidget3" />
</beans>
运行结果如下:
到现在我们其实就应该能知道WidgetIoc到底是干嘛了的,我们可以把所有的功能分成一个个独立的模块实现出来,最后使用配置文件灵活的组装这些模块,使其可以再不重新编译的情况下组装成多个软件。
最后提一个额外的功能,如同上面MainWindow 类,它的构造函数是被Q_INVOKABLE 宏标记,它才能在xml文件声明并构造,同时它的widgets 属性是用Q_PROPERTY 宏声明之后,xml文件才可以直接注入,那么对于一些内置类型,如QStackedWidget 一样,它的构造函数没有被Q_INVOKABLE 宏标记标记,也没有什么可用的属性被Q_PROPERTY 宏声明,这时如果想要在xml文件中使用的话,最直接的方法就是继承至这个类,并完成自己想要的功能。WidgetIoc还提供了另一个额外的功能,可以直接注册,如下:
auto func = [](QStackedWidget *w, const QString &name, const QVariant &val) {
if (name == "widgets") {
auto widgets = val.value<QList<QWidget *>>();
for (auto widget : widgets) {
w->addWidget(widget);
}
}
};
McBuiltInBeanContainer::addBuilderFactory<QStackedWidget>(func);
这段代码的意思就是注册QStackedWidget 类型,使其可以在xml文件中声明构造,同时为其添加一个属性widgets ,xml中可以为这个属性赋值。
待续…
|