AFSim_二次开发_wkf界面开发和插件扩展
wkf界面开发和插件扩展
1
目的
之前的文章《创建AFSim开发环境》介绍了怎么创建一个AFSim的开发环境并创建了一个WSF框架的插件Demo和VS项目模板,由于WSF是AFSim的核心框架,不带任何界面元素,因此只能创建出控制台的应用程序,但面向用户使用的时候需要界面来给用户可视化的呈现,这篇文章就来基于wkf框架创建界面元素。
2
基本的界面应用程序(exe)
通过查看源码获知wkf框架通过wkfEnv来创建界面环境,通过它可以获取到MainWindow的实例,调用show方法,即可打开界面。wizard、warlock、mystic都是基于此基础并扩展特定插件完成的,我们这里就来搭建一个属于自己可控的界面应用程序。后续在此框架中通过扩展插件完成我们自己想要的功能。
1)warlock界面框架
首先通过分析warlock的界面框架构建方式确定wkf框架的运行流程。其实大家通过阅读warlock或mystic的启动和加载的代码,大致就能清楚了。我这里基于warlock的代码将基本的启动流程和要注意的地方给大家说说,有些不重要的或对运行没有影响的内容或参数,就不展开了。
warlock的入口函数为warlock.cpp文件中的wkExecute函数,传入的参数是warlock程序自定义的Application。

此函数首先做了些初始配置,如Qt应用程序信息、处理了命令行参数等内容,这些都是次要内容,可以不管。初始配置完成后才是创建wkf界面的关键代码,如下:

前两个wkf::开头的是配置wkf环境的必须代码,后面warlock::是warlock程序相关的,我们的目的主要是创建自定义的wkf界面,因此只需要wkf::环境配置即可。这两个函数的原型如下:
void wkf::VtkEnvironment::Create(Factory* aFactoryPtr, const std::string& aScenarioType = "wkf");aFactoryPtr: wkf::Factory的指针,用于获取内置或自定义的窗口void wkf::Environment::Create(const QString& aApplicationName,const QString& aApplicationPrefix,const QString& aSettings = "",bool aImportSettings = false,const QString& aPermissionsFile = "");符
wkf::Environment::Create目前只需要传入前两个参数即可。后面三个参数,可以保持默认,不影响功能搭建,因为warlock本身传进去的参数也是无效的,但也没有影响warlock的功能^_^。因此在这里,我们要创建自己的基于wkf的程序就可以这样实现:
intmain(int argc, char* argv[]){QApplication a(argc, argv);// 初始化 VTK WKF 环境wkf::VtkEnvironment::Create(new wkf::Factory());wkf::Environment::Create("HSimulation", "HSimulation");// Todo,请继续下文return a.exec();}
上述代码就能够完成wkf界面框架的初始化配置,编译调试但没有界面显示出来。继续分析warlock,在初始化环境过后,warlock有三条执行路线:

第1条是运行场景仿真;第2条是显示使用提示,即命令行参数说明;第3条是将语法文件写到本地,是给wizard使用的方法。因此这里我们只分析第一条执行过程,并且这里面最重要的代码是StartUp。

warlock代码的执行逻辑是启动wkf后获取主窗口,再隐藏掉。是为了先显示出场景选择对话框(GetMainWindow->hide()后的代码)。其实这里也可以不隐藏主窗口的。注释掉wkfEnv.GetMainWindow()->hide(); 代码即可,不会有什么影响。因此我们在之前代码中只需添加一个StartUp调用,然后获取到MainWindow,然后show出来即可。如下:

这时编译调式,不出意外的话是无法运行的^_^,会报以下问题:

2)启动问题分析解决
点击堆栈跟踪发现Visibility::Plugin方法里的问题:

定位到这个方法发现空指针,所有才崩溃的:

那为什么是空指针呢?上图中通过vaEnv.GetFactory()->CreateDockWidget()方法返回进行赋值,但为空指针,那么问题就出在这个CreateDockWidget方法里面。在这里面打个断点,再次调试发现,这里的参数“VisibilityWidget”,在wkf::Factory的CreateDockWidget里面并没有处理,直接返回了nullptr,所以这里的mDockWidgetPtr才为空,造成崩溃。

而查看warlock::Factory的CreateDockWidget发现,它实现了VisibilityDockWidget的创建,所有warlock运行不会崩溃,但是wkf::Factory并没有实现创建过程,下图是warlock的实现。

找到了问题,本来想仿照warlock尝试修改源代码wkfFactory.cpp中的这个方法,让它支持VisibilityDockWidget的创建和返回,但发现在wkf工程的实现里面找不到VisibilityDockWidget。查看这个类的实现是放在wkf_common项目里面,而wkf框架工程并没有引用wkf_common也不应该由wkf来依赖wkf_common,这会违背依赖倒置的原则。所以需要采用另外一种办法来解决这个问题。我这里是在自己的界面程序项目中创建继承wkf::Factory的类来支持VisibilityDockWidget的创建和返回(如warlock::Factory的实现那样)。
// HSimFactory.h

// HSimFactory.cpp

创建后在main中调用wkf::VtkEnvironment::Create方法时,传入自定义的HSimFactory。

再次编译调试,就会进入到我们自定义的CreateDockWidget方法里面去创建VisibilityDockWidget了。

这时编译运行基于wkf库构建的最基础界面如下(只是个空架子):

注:如果显示的地球出现黑斑或不正常,请在main创建QApplication之前,开启Qt共享OpenGL模式。
QApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
上面的过程就是创建基于wkf框架的界面应用程序的过程。
3
界面插件开发和集成
AFSim的插件都是动态库形式的,因此先创建一个基于Qt的动态库工程,并将输出路径指向AFSim的bin/wkf_plugins目录,然后创建两个文件用于生成插件(每个界面插件,这两个文件的初始代码都是这样)。
// WkfDemoPlugin.h
#ifndef WkfDemoPlugin_h__#define WkfDemoPlugin_h__#include"WkfPlugin.hpp"class WkfDemoPlugin : public wkf::Plugin{Q_OBJECTpublic:WkfDemoPlugin(const QString& aPluginName, const size_t aUniqueId);virtual ~WkfDemoPlugin() override;};#endif// WkfDemoPlugin_h__
// WkfDemoPlugin.cpp
#pragma execution_character_set("utf-8")#include"WkfDemoPlugin.h"#include"WkfEnvironment.hpp"#include"WkfMainWindow.hpp"#include"WkfObserver.hpp"#include"WkfScenario.hpp"#include"WkfVtkEnvironment.hpp"WKF_PLUGIN_DEFINE_SYMBOLS(WkfDemoPlugin,"Wkf插件Demo", // 这个是插件的名称,在插件管理器中显示"创建Wkf插件的一个Demo示例,它包括ToolBar/Dockwidget/PreferencesWidget/右键菜单的创建示例", // 这是插件的描述信息,也在插件管理器中显示"all")WkfDemoPlugin::WkfDemoPlugin(const QString& aPluginName, const size_t aUniqueId): wkf::Plugin(aPluginName, aUniqueId){}WkfDemoPlugin::~WkfDemoPlugin(){}
将上面两个文件进行编译,会在wkf_plugin目录下生成一个动态库文件。运行上面创建的界面程序Demo,就可以在opentions/pluginmanager菜单下看到我们创建的插件了。

光有插件还不行,这只是第一步,我们需要通过插件来让wkf界面知道我们需要什么样的界面,让他在我们需要的时候帮我们打开,目前我了解到的界面类型包括:Toolbar、Dockwidget、Dialog、PreferencesWidget、右键菜单等。下面我们挨个来实现。
1)ToolBar
首先用Qt向导创建一个继承自QToolBar的DemoToolBar类,在界面中添加一个测试Action,界面如下:

在DemoToolBar的头文件和实现文件中添加:

最后在WkfDemoPlugin.cpp中创建DemoToolBar实例,并添加到wkfEnv的MainWindow中。

编译运行后,在工具栏上自动显示了我们创建的工具栏,点击后会执行自定义的代码,并且在View/Toolbars菜单中显示了工具栏名称,可以点击关闭或打开,工具栏右键菜单中也有我们的ToolBar选项。效果如下:

2)DockWidget
与创建ToolBar界面类似,也是使用Qt向导创建一个界面类DemoDockWidget并继承至QDockWidget。然后在WkfDemoPlugin.cpp中创建DemoDockWidget实例,并添加到wkfEnv的MainWindow中。这里AddDockWidget有三个方法,其中addDockWidget为QMainWindow的自身方法,AFSim建议不使用此方法来添加DockWidget,而是使用其他两个:

AddDockWidget的效果是首先将界面添加到指定的DockArea,同时在View菜单和右键菜单中有控制选项,这种添加方法是真正意义上的Qt停靠窗口。

AddDockWidgetToViewenu是将创建的DockWidget添加到View菜单或指定的View的子菜单下,并通过点击菜单弹出窗口。严格来讲不是停靠窗口,而只是将DockWidget以对话框的形式展示了出来,并不能在任何的DockArea中停靠。

3)Dialog
a界面创建与上述一样,就不多讲了。这里AddDialog有两个方法,其中AddDialogToViewMenu是被标记为DEPRECATED的,后续此方法会删除,因此不要使用此方法而是使用AddDialogToToolMenu。wkfEnv的MainWindow根据传入的actionName,在Tools菜单中添加菜单项,如果为空,则默认使用Dialog的类名作为菜单名称显示,所以建议一定要传入一个易懂的名称。

4)PreferencesWidget
这种界面在options/preferences菜单中,主要用于配置系统参数和样式偏好等内容,它支持修改配置和重置为默认配置。根据AFSim的实现机制,要实现在它提供的Preferences界面中添加我们自定义的界面时,需要创建三个类:PrefData、PrefObject和PrefWidget。
其中,PrefData是我们要配置的数据,PrefObject用于实现数据的读取、保存和应用,PrefWidget通过可编辑的界面来修改数据。
第一步:创建PrefData
为了仅展示流程,这里我只使用一个bool变量来做示例,创建的结构如下:
// 配置数据struct DemoPrefData{bool m_isOn = false;};// 这句代码时必须得,用于告知Qt元数据类型Q_DECLARE_METATYPE(DemoPrefData);
第二步:创建PrefObject
这里使用wkf提供的模板类,这也是AFSim推荐的方式,模板参数就是上面创建的PrefData。这个类需要实现父类的三个纯虚函数,AFSim会自动实现对这三个函数的调用,我们这里只需要关心数据怎么应用、怎么读取,怎么保存即可:
// 配置对象class DemoPrefObj : public wkf::PrefObjectT<DemoPrefData>{Q_OBJECTpublic:// 这里必须是默认构造函数,否则会编译报错,原因在wkf::PrefObjectT模板类说明中DemoPrefObj(QObject* parent = nullptr);~DemoPrefObj();protected:virtualvoidApply()override;virtual DemoPrefData ReadSettings(QSettings& aSettings)constoverride;virtualvoidSaveSettingsP(QSettings& aSettings)constoverride;};
对这三个函数的实现这里就简单的进行读取和保存。
voidDemoPrefObj::Apply(){mDefaultPrefs.m_isOn;mCurrentPrefs.m_isOn;// 这个方法是在Preference界面上点击Appaly或者点击Ok时触发的,这个时候的值已经通过PrefWidget改变了// 可用于将配置信息改变的消息发送到其他对象// 也可以使用AFSim的插件通信机制,将数据发送到其他插件去处理}DemoPrefData DemoPrefObj::ReadSettings(QSettings& aSettings)const{DemoPrefData pData;pData.m_isOn = aSettings.value("isOn", mDefaultPrefs.m_isOn).toBool();return pData;}voidDemoPrefObj::SaveSettingsP(QSettings& aSettings)const{aSettings.setValue("isOn", mCurrentPrefs.m_isOn);}
第三步:创建PrefWidget
PrefWidget的作用就是提供可视化的界面来对数据进行修改,这里也使用wkf提供的模板类,模板参数就是第二步的PrefObject,这个类必须实现父类的两个纯虚函数,我这里还实现了Category的两个方法,用于分组:
// 配置界面class DemoPrefWidget : public wkf::PrefWidgetT<DemoPrefObj>{Q_OBJECTpublic:DemoPrefWidget(QWidget *parent = nullptr);~DemoPrefWidget();// 此方法用于将本配置界面放到哪个分组下面virtual QString GetCategoryHint()const{ return "DemoPref"; }// 此方法用于指示当,点击分组名时,是否默认打开这里实现的配置界面,如分组下有多个都是true,则打开最近的那个virtualboolGetCategoryHintPriority()const{ return true; }private:Ui::DemoPrefWidgetClass *ui;protected:virtualvoidReadPreferenceData(const PrefDataType& aPrefData)override;virtualvoidWritePreferenceData(PrefDataType& aPrefData)override;};a
需要实现的两个虚函数主要用于从PrefData中取数据更新界面,或者从界面读取数据保存到PrefData中。
DemoPrefWidget::DemoPrefWidget(QWidget *parent): wkf::PrefWidgetT<DemoPrefObj>(parent), ui(new Ui::DemoPrefWidgetClass()){ui->setupUi(this);// 这个在Preference界面左边树形结构的显示this->setWindowTitle("Demo插件的配置");}// 这个方法是在界面上点击Cancel时触发的,用于将未改变的配置信息更新到界面// 查看PrefDataType的类型可知它就是 DemoPrefDatavoidDemoPrefWidget::ReadPreferenceData(const PrefDataType& aPrefData){ui->checkBox->setChecked(aPrefData.m_isOn);}// 这个方法用于将界面数据更新到配置对象DemoPrefDatavoidDemoPrefWidget::WritePreferenceData(PrefDataType& aPrefData){aPrefData.m_isOn = ui->checkBox->isChecked();}
最后一步:加载PrefWidget
这里只需要在我们创建的插件类中重写wkf::Plugin的GetPreferencesWidgets方法,返回我们的PrefWidget对象即可,AFSim在需要时会自动调用此方法用于构建Preferences界面。

编译生成后,在preferences界面中即可展示我们创建的PrefWidget。

5)右键菜单
这个比较简单,与创建DockWidget是有关系的,因为右键菜单都是在某一个DockWidget中右键出现的,需要注意的是要继承wkf::DockWidget并重载BuildContextMenu函数。

参数aMenu用于添加自己的菜单项Action,并处理点击此Action时的处理逻辑。

返回值为true和false的两种效果如下:

4
加载其他文件夹下的插件
前面生成的插件我们是将它放到AFSim自带的wkf_plugins目录下的,但有时我们不希望将插件放到此目录下,而是想保存到我们自定义的目录下,这样就可以很快捷的移除所有我们自己的插件,而不用到一堆wkf_plugins目录下的插件寻找我们开发的插件。(当然,如果可以从命名上很快找到,全放到wkf_plugis目录下也没问题!!),这里既然AFSim提供了从其他位置加载插件的能力,那这里就将它写出来怎么使用,具体用不用,各位自己决定即可。
在warlock的实现里,wkf::Environment::Create的第一个参数就是在指定wkf插件的加载路径。

它在后续代码中会自动加上_plugins,形成wkf_plugins目录。跟踪代码发现,在wkf::Environment创建的时候会创建PluginManager对象,同时将wkf::Environment::Create的第一个参数传入:

PluginManager使用这个参数在GetPluginDirectories方法中构建plugins目录,mFunctionTag就是创建PluginManager时传入的参数,也就是wkf::Environment::Create的第一个参数:

从这里发现不管传入的是什么参数,都会wkf_plugins这个目录加载插件,因此,我们只需要传入我们希望它去加载插件的目录的名称即可(注:名称不用再带_plugins了)如下:

这样,我们就可以将自己开发的插件放到myplugindir_plugins目录下,wkf框架在加载插件时,会同时到wkf_plugins和myplugindir_plugins目录下查找插件并加载。
往
期
推
荐

夜雨聆风