通过委托优雅的使用qgis_app模块(QGIS解耦合,QGIS二次开发,QGIS源码研究,面向对象设计原则)

起因

最近在开发符号配置工具,最终的应用成果就是具备QGIS样式相关的所有功能,当然一些基本的功能(图层管理,坐标系等也要有)。然而,在开发过程中遇到了一个问题。
由于符号配置工具和QGIS的区别就在于它只包含QGIS样式配置方面的功能,所以这个工具就特别像QGIS的应用层,很多API都是直接从qgis_app(这是QGIS的主应用模块,打开QGIS那个主界面就与其密切相关)里获得,可当运行时就直接崩溃了。经过追踪,发现死在了这个函数 QgisApp::instance()->mapCanvas(),为什么,原因是我没有创建QgisApp实例,所以QgisApp::instance()返回的是null。这就尴尬了,我只是想独立的创建自己的应用(自己的窗口,布局,巴拉巴拉),结果还要创建QgisApp对象,这显然不合理。

探索

QGIS的core、gui模块提供了许多有用的API,但是这些API还是有一些偏底层。当我们去做一些和QGIS非常相似的功能,或者说QGIS主应用(特指app模块)已经有的很多功能时,我们经常会发现,app里有很多有用的API可以直接用,可真当你去用的时候,你会发现它们耦合在QgisApp类周围,这个玩意可是主窗口(继承自QMainWindow),程序要想运行就必须创建它,不创建就很可能出现我遇到的问题。这就尴尬了,由于这个对象超级庞大,包含非常多我们不需要的东西,更重要的是,创建之后主窗口和QGIS几乎无异,那我创建你有何用?其实这个问题之前有人也提过,当时我看到没啥感觉,现在一开发,瞬间不开心了。有一种方法是把需要的源码拿过来修改,可是一些小的类还行,大的类就会发现牵连好多文件,而且我不能把所有与获取相关的源码都拿来改一遍吧。看来是得动一动这个app模块了。

研究

通过追踪源码发现,几乎都死在获取画布的地方,这就很无语,画布完全可以由我自己创建自己管理,非得从你app获取着实不爽。就因为获取一个画布导致程序死机实在令人唏嘘。如果能让app内部获取我自己的画布就好了,那该怎么办呢?

方案1

最容易想到的就是引入中间类,当然要修改下源码。大致结构如下图
在这里插入图片描述
Adapter是一个中间类,构造时需要一个我的应用的引用(就叫它MyApp吧)。其内部包含返回MyApp画布的函数。
QgisApp增加了setAdapter函数(静态的)用于设置Adapter。
使用时,在MyApp里构造好Adapter,再把Adapter设置到QgisApp。这样当Qgis内部调用获取画布的函数时,先判断QgisApp实例是否存在,若存在就返回QGIS的画布,否则返回Adapter的画布,也就是返回了MyApp的画布。这样做可以与原有代码兼容,灵活选择。但是这样就一个问题,就是adapter依赖了具体的对象,当别人想构造自己的应用时,不一定类名和我的相同。这就使得这个方案不具有普遍性,于是舍弃。

方案2

经常搞面向对象的可能就迅速想到,依赖具体的不行,可以依赖抽象啊,这不正是面向对象的依赖倒置原则吗?的确是这样。当你真正遇到问题然后去想面向对象的这些原则时,你就会感觉,哇,好神奇,好厉害,好有趣。具体的做法大致如下图
在这里插入图片描述

从图中可以看出,原来Adapter依赖具体的MyApp现在改为了依赖抽象,而MyApp则实现该接口,这样一来,用户只需要实现该接口就可以使用了,确实不错。应该说,该方案用于解决获取画布问题已经可以了。但是实际使用中,我们可能获取的不止是画布,可能还有图层树等等,这个时候就需要在QgisApp的函数里都要进行判断QgisApp实例是否存在,然后决定是否使用Adapter,太麻烦了,有没有更好的办法呢?

最终方案

我们想,其实我们要做的就是替换掉QgisApp里与获取相关的功能,把他们变成我们自己的,所以QgisApp::instance()返回的不应该是QgisApp而应该是我们的应用接口,然而这样做所有调用QgisApp::instance()的文件都要改,至少得包含一个接口头文件,而且这样做还不能和源码兼容,当使用官方版主应用时,一定会出现问题。所以我们是不是可以让QgisApp和我的应用共同继承一个玩意?于是下面的方案就出现了
在这里插入图片描述
CVBF_DelegateQGISApp:是一个委托类抽象类,注意不是接口。能够被设置进QgisApp。
MyApp:继承CVBF_DelegateQGISApp。其内部的instance()获取的不再是QgisApp实例,而是CVBF_DelegateQGISApp。
这样我们只需要在instance这个函数里判断QgisApp对象是否存在就可以了,不需要在每个获取的地方都判断,判断完之后交给委托处理就行。
当你按照上述结构重构完代码编译时,会发现报许多错误,原因就是CVBF_DelegateQGISApp里面没有那些获取画布之类的函数。没关系,缺啥加啥就行,直到编译没有错误,这也是一劳永逸的方法。注意写这些函数的时候一定不要用纯虚函数,这也就是CVBF_DelegateQGISApp不是接口而是抽象类的原因。如果使用接口则MyApp一定要实现这些接口,但MyApp可能只需要获取一个画布就行,所以没必要实现那么多。尽管这违反了里式替换原则,但是至少目前没啥问题,可以先用着。

成果

最后看一下修改后的代码吧

#ifndef VBF_DELEDATEQGISAPP
#define VBF_DELEDATEQGISAPP

#include <QMainWindow>
#include <QgsCoordinateReferenceSystem.h>
#include <QgsMapCanvas.h>
#include <QgsClipboard.h>
#include <QgsRectangle.h>

class QgsMessageBar;
class QgsAdvancedDigitizingDockWidget;
class QgsLayerTreeView;
class QAction;
class QMenu;
class QgsLayerTreeMapCanvasBridge;
class QgsMapOverviewCanvas;
class QgsVectorLayerTools;
class QgsComposer;
class QgsRasterLayer;
class QgsPluginLayer;
class QgisAppStyleSheet;
class QgsVectorLayer;

//QGIS主应用委托,用户可继承该类以代理QgisApp类的部分功能
class CVBF_DelegateQGISApp : public QMainWindow
{

public:
	CVBF_DelegateQGISApp(QWidget * parent, Qt::WindowFlags fl)
		: QMainWindow( parent, fl )
	{

	}

	virtual void emitCustomSrsValidation(QgsCoordinateReferenceSystem &crs){}
	virtual void layerProperties(){}
	virtual void attributeTable(){}
	virtual QgsMapCanvas *mapCanvas(){ return new QgsMapCanvas(); }
	virtual QSet<QgsComposer*> printComposers() const{ QSet<QgsComposer*> composers; return composers;}
	virtual void openProject( const QString & fileName ){};
	virtual QgsClipboard *clipboard() { return NULL; }
	virtual QAction *actionToggleEditing() { return NULL; }
	virtual QAction *actionSaveActiveLayerEdits() { return NULL; }
	virtual QAction *actionAllEdits() { return NULL; }
	virtual QAction *actionSaveEdits() { return NULL; }
	virtual QAction *actionSaveAllEdits() { return NULL; }
	virtual QAction *actionRollbackEdits() { return NULL; }
	virtual QAction *actionRollbackAllEdits() { return NULL; }
	virtual QAction *actionCancelEdits() { return NULL; }
	virtual QAction *actionCancelAllEdits() { return NULL; }
	virtual QAction *actionLayerSaveAs() { return NULL; }
	virtual QAction *actionRemoveLayer() { return NULL; }
	virtual QAction *actionDuplicateLayer() { return NULL; }
	virtual QAction *actionDeleteSelected() { return NULL; }
	virtual void addUserInputWidget( QWidget* widget ) {}
	virtual QgsAdvancedDigitizingDockWidget *cadDockWidget() { return NULL; }
	virtual QgsMessageBar *messageBar() { return NULL; }
	virtual void updateProjectFromTemplates(){}
	virtual QgsLayerTreeView *layerTreeView(){ return NULL;}
	virtual void openProject( QAction *action ){}
	virtual int messageTimeout(){ return 0;}
	virtual QMenu *panelMenu(){return NULL;}
	virtual void markDirty(){}
	virtual QAction *actionHideAllLayers() { return NULL; }
	virtual QAction *actionShowAllLayers() { return NULL; }
	virtual QAction *actionHideSelectedLayers() { return NULL; }
	virtual QAction *actionShowSelectedLayers() { return NULL; }
	virtual QgsLayerTreeMapCanvasBridge *layerTreeCanvasBridge() { return NULL; }
	virtual QgsMapOverviewCanvas* mapOverviewCanvas() { return NULL; }
	virtual QgsVectorLayerTools *vectorLayerTools() { return NULL; }
	virtual void saveEdits( QgsMapLayer *layer, bool leaveEditable = true, bool triggerRepaint = true ){}
	virtual void deleteSelected( QgsMapLayer *layer = nullptr, QWidget *parent = nullptr, bool promptConfirmation = false ){}
	virtual void editCopy( QgsMapLayer *layerContainingSelection = nullptr ){}
	virtual void editPaste( QgsMapLayer *destinationLayer = nullptr ){}
	virtual void openFile( const QString & fileName ){}
	virtual bool uniqueComposerTitle( QWidget *parent, QString& composerTitle, bool acceptEmpty, const QString& currentTitle = QString() ){ return true; }
	virtual QgsComposer* createNewComposer( QString title = QString() ){ return NULL;};
	virtual void deleteComposer( QgsComposer *c ){}
	virtual void toggleEditing(){}
	virtual QString crsAndFormatAdjustedLayerUri( const QString& uri, const QStringList& supportedCrs, const QStringList& supportedFormats ) const{ return QString();}
	virtual void addVectorLayer(){}
	virtual bool addRasterLayer( QgsRasterLayer * theRasterLayer ){ return true; }
	virtual QgsPluginLayer* addPluginLayer( const QString& uri, const QString& baseName, const QString& providerKey ) { return NULL; }
	virtual QgisAppStyleSheet *styleSheetBuilder(){ return NULL; }
	virtual void showLayerProperties( QgsMapLayer *ml ){}
	virtual QAction *actionShowPinnedLabels() { return NULL; }
	virtual void showOptionsDialog( QWidget *parent = nullptr, const QString& currentPage = QString() ){}
	virtual void setIconSizes( int size ){}
	virtual void namUpdate(){}
	virtual bool toggleEditing( QgsMapLayer *layer, bool allowCancel = true ){ return true; }
	virtual QgsRasterLayer *addRasterLayer( QString const & uri, QString const & baseName, QString const & providerKey ){ return NULL;}
	virtual QgsComposer *duplicateComposer( QgsComposer *currentComposer, QString title = QString() ){ return NULL; }
	virtual void setExtent( const QgsRectangle& theRect ){};
	virtual QgsVectorLayer *addVectorLayer( const QString& vectorLayerPath, const QString& baseName, const QString& providerKey ){return NULL;};
};

#endif
//对于QgisApp的修改

//1.
class APP_EXPORT QgisApp : public CVBF_DelegateQGISApp, private Ui::MainWindow

//2.
//添加 设置委托 w_s_y
  static void SetDelegate(CVBF_DelegateQGISApp *delegate);
//-----------------20210115

//3.
//修改 w_s_y 返回委托 如果QGIS主应用实例已经存在,则返回主应用,反之返回用户委托
//static QgisApp *instance() { return smInstance; }
//---------------------------------------------------------
static CVBF_DelegateQGISApp *instance() { return smInstance ? smInstance : m_instanceDelegate; }
//---------------------------------------------------------

//4.
//添加 QGIS主应用代理 w_s_y
static CVBF_DelegateQGISApp *m_instanceDelegate;
//--------------------20200114
//如何使用,例如在CVBF_WndToolSymbolConfig中

//1.继承
class  CVBF_WndToolSymbolConfig : public CVBF_DelegateQGISApp

//2重载
QgsMapCanvas *mapCanvas();

//3.在构造函数里设置
QgisApp::SetDelegate(this);

好了,接下来你就可以为所欲为的使用app模块了,再遇到崩溃,先看哪里崩,然后在你的APP里重载那个函数就行。以上是个人愚见,望大佬赐教!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值