起因
最近在开发符号配置工具,最终的应用成果就是具备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里重载那个函数就行。以上是个人愚见,望大佬赐教!