QGIS的数据格式拓展插件开发
VCT格式是我国在《地理空间数据交换格式》这一国家标准文件中制定的矢量数据交换格式。国产的GIS软件一般有对VCT格式的支持,但是在QGIS等国际GIS上支持较少😅😅。我尝试着在QGIS中通过编写C++插件的方式,增加对VCT格式的支持,🤔🤔🤔途中发现QGIS拓展数据格式的资料还比较少,所以记下一篇博客,如果有理解不到位的地方,还望及时指出。该小项目的代码见Github。
这篇博文很原始了,之前是发在CSDN里的,但是图片没有搬运到图床,anyway也不是很重要的类图。我的兴趣已经不在CPP的客户端上了,从某种角度上,GIS的发展似乎也不应该局限于传统的桌面端软件,因此我比较怀疑这篇文章到底还有多大价值。
环境配置
开发环境:VS 2015
Qt版本:5.11.2
QGIS版本:3.12(下载一份源代码和发行版软件,讲道理可以只下载源代码然后自己编译出debug版本的,写插件时输出应该也更方便,但是我太菜了不懂cmake,编译不出来😢,就拿源代码去看内部原理,然后把插件写好了放进软件里再测试了😂就更加麻烦。)
首先,我们在VS 2015的菜单栏点击工具
->扩展和更新
,在拓展和更新
窗口的左侧选择菜单中选择联机
,然后搜索qt,选择搜索结果中的第一项“Qt Visual Studio Tools”,安装完成后重启VS 2015,可以看到菜单栏已经出现了Qt VS Tools
,点击Qt VS Tools
->Qt Options
,在Qt Versions
标签下设置Qt编译器路径,例如,我的就是“C:\Qt\Qt5.11.2\5.11.2\msvc2015”。
设置完成之后,我们新建一个项目,模板是Qt Class Library,因为我们要开发的是一个插件,最后生成的是一个dll文件,而非一个带图形界面的应用。🤔我们点击菜单栏中的项目
->属性
,在属性页中选择C/C++
->常规
,选择右侧页面中的附加包含目录
,添加QGIS和Qt的库路径,以我为例,路径分别为:
C:\Users\lenovo\Desktop\osgeow\apps\Qt5\include\QtGui
C:\Users\lenovo\Desktop\osgeow\apps\Qt5\include\QtWidgets
C:\Users\lenovo\Desktop\osgeow\apps\Qt5\include\QtXml
C:\Users\lenovo\Desktop\osgeow\apps\Qt5\include\QtCore
C:\Users\lenovo\Desktop\osgeow\apps\qgis\include
C:\Users\lenovo\Desktop\osgeow\include
然后点击属性页中的链接器
->常规
->附加库目录
,添加QGIS的lib文件路径,例如我的:
C:\Users\lenovo\Desktop\osgeow\apps\qgis\lib
点击输入
->附加依赖项
,添加Qt5Xml.lib、Qt5Widgets.lib、qgis_core.lib、qgis_app.lib、qgis_gui.lib。至此,开发环境的配置就大功告成啦!🎉🎉🎉
2. 技术路线
2.1 基类说明
查看源代码可知,QGIS对数据格式的支持是以拓展插件的方式实现的,打开软件之后,QGIS会通过QgsProviderRegistry类扫描Plugin Path下的所有文件(Plugin Path一般位于QGIS目录下的plugins文件夹),找到其中的数据源插件,即各种dll文件中的QgsVectorDataProvider的继承类。因此我们的思路就是自己继承QgsVectorDataProvider,用于链接矢量图层和VCT文件数据源。
QgsProviderRegistry::QgsProviderRegistry( const QString &pluginPath )
{
// At startup, examine the libs in the qgis/lib dir and store those that
// are a provider shared lib
// check all libs in the current plugin directory and get name and descriptions
//TODO figure out how to register and identify data source plugin for a specific
//TODO layer type
#if 0
char **argv = qApp->argv();
QString appDir = argv[0];
int bin = appDir.findRev( "/bin", -1, false );
QString baseDir = appDir.left( bin );
QString mLibraryDirectory = baseDir + "/lib";
#endif
mLibraryDirectory.setPath( pluginPath );
init();
}
由于没找到相关资料,所以我在类的结构上参考了源代码中的DelimitedText和Memory的结构,代码分别位于QGIS源代码根目录下的“providers\delimitedtext”和“src\core\providers\memory”目录下。我们可以看到,这些插件主要继承了QgsVectorDataProvider、QgsProviderMetadata、QgsAbstractDataSourceWidget、QgsProviderGuiMetadata、QgsSourceSelectProvider、QgsAbstractFeatureSource、*QgsAbstractFeatureIteratorFromSource< T >*等。
QgsVectorDataProvider是我们的重点之一,主要负责将矢量文件数据源文件和矢量图层联系起来,文件的读取和要素的改动都通过QgsVectorDataProvider实现。因此我们需要将VCT文件的读写写在这里,后面也可以用代码支持要素的编辑保存。
QgsProviderMetadata负责保存QgsVectorDataProvider的key和description,这在QgisApp的初始化中是必要的,QGIS会通过QgsProviderMetadata的createProvider方法来创建我们定义的dataprovider。另外QgsProviderMetadata要求实现的encodeUri和decodeUri方法在对dataprovider的必要参数uri的处理上也很重要,但是在这个例子中我们的uri只是vct文件的储存路径,因此这里不是重点。
QgsAbstractDataSourceWidget是QGIS中文件选择框的基类,要实现选择数据源文件,添加相应图层的功能,就必须继承这个接口,继承类主要负责文件选择窗口的用户操作逻辑。QgsProviderGuiMetadata和QgsSourceSelectProvider之间的关系,和前面提到的QgsProviderMetadata与QgsVectorDataProvider的关系相似:QgsProviderGuiMetadata在QgsDataSourceManager初始化时会提供QgsSourceSelectProvider的List,我们自己继承的QgsSourceSelectProvider也是这样创建实例的。而QgsSourceSelectProvider则存放了一些必要的前端说明信息,比如图标,说明文字等,这里也定义了我们自己的窗口在QgsDataSourceManager中同其他窗口的相对顺序。
QgsAbstractFeatureSource用于存放我们从文件中读取的要素,QgsAbstractFeatureIteratorFromSource< T >则可以从相应的QgsAbstractFeatureSource实现类中对要素进行遍历等操作。
总的来看,用户打开文件选择窗口,选择vct文件,到QGIS添加图层可以分为用户操作和后台响应两块。QgsAbstractDataSourceWidget、QgsProviderGuiMetadata和QgsSourceSelectProvider负责从创建文件选择窗口,并接收用户输入信息。QgsProviderMetadata、QgsVectorDataProvider、QgsAbstractFeatureSource和*QgsAbstractFeatureIteratorFromSource< T >*负责按照将读取到的数据转换成新建矢量图层所必须的信息,同时还可以进一步拓展,支持要素的编辑、空间索引的创建等功能。
2.2 继承类的部分实现
从2.1 基类说明这里,我们可以发现,我们要做的就是分别继承上述几个抽象类,在这几个类里面写自己的逻辑即可。下面是我自己的继承类,以及一些比较重要的,需要实现的方法。
2.2.1 QgsVctProvider
QgsVctProvider可以说是这里的主类,首先需要实现QgsVectorDataProvider里面的纯虚函数,包括featureSource、storageType、getFeatures、wkbType、featureCount、fields、capabilities、createSpatialIndex、hasSpatialIndex、name、description、extent、isValid、crs、setSubsetString、supportsSubsetString、subsetString等。这些函数一般都是返回对应图层的某些信息,如空间参考系,要素总数等,根据自己数据格式文件确定即可,然后在capabilities函数中我们需要返回这个dataProvider支持的功能,详见QGIS的官方文档。如果不支持某些功能,在对应的函数里可以直接返回false,比如不支持空间索引创建的话,createSpatialIndex可以直接返回false。由于我们当前的目标是支持文件的读写,所以我们的参数如下面所示:
QgsVectorDataProvider::Capabilities QgsVctProvider::capabilities() const {
return AddFeatures | DeleteFeatures | ChangeGeometries |
ChangeAttributeValues | AddAttributes | DeleteAttributes | RenameAttributes;
}
getFeatures函数将根据特定的请求返回一个QgsFeatureIterator,这个iterator所对应的要素是符合请求条件的全部要素。
QgsFeatureIterator QgsVctProvider::getFeatures(const QgsFeatureRequest &request) const {
return QgsFeatureIterator(new QgsVctFeatureIterator(new QgsVctFeatureSource(this), true, request));
}
最后我们在构造函数中,还需要先利用setNativeTypes方法设置这个dataProvider支持的字段类型,然后再实现文件的读取,因为当用户选择了一个文件之后,QGIS会直接产生一个对应的dataProvider实例。具体的文件读取代码这里不提,逻辑是QgsVctProvider在构造函数中得到了uri,也就是文件路径(从文件选择窗口得到),然后调用预先写好的writeData方法读取并存放数据。
QgsVctProvider::QgsVctProvider(const QString &uri, const ProviderOptions &options)
: QgsVectorDataProvider(uri, options) {
// Add supported types to enable creating expression fields in field calculator
setNativeTypes(QList<NativeType>()
//string type
<< QgsVectorDataProvider::NativeType(tr("Char"), QStringLiteral("Char"), QVariant::Char, 0, 10)
<< QgsVectorDataProvider::NativeType(tr("Varchar"), QStringLiteral("Varchar"), QVariant::String, -1, -1)
//interger types
<< QgsVectorDataProvider::NativeType(tr("Int1"), QStringLiteral("Int1"), QVariant::Int, -1, -1, 0, 0)
<< QgsVectorDataProvider::NativeType(tr("Int2"), QStringLiteral("Int2"), QVariant::Int, -1, -1, 0, 0)
<< QgsVectorDataProvider::NativeType(tr("Int4"), QStringLiteral("Int4"), QVariant::Int, -1, -1, 0, 0)
<< QgsVectorDataProvider::NativeType(tr("Int8"), QStringLiteral("Int8"), QVariant::Int, -1, -1, 0, 0)
//floating point
<< QgsVectorDataProvider::NativeType(tr("Float"), QStringLiteral("Float"), QVariant::Double, -1, -1, -1, -1)
<< QgsVectorDataProvider::NativeType(tr("Double"), QStringLiteral("Double"), QVariant::Double, -1, -1, -1, -1)
//date types
<< QgsVectorDataProvider::NativeType(tr("Date"), QStringLiteral("Date"), QVariant::Date, -1, -1, -1, -1)
<< QgsVectorDataProvider::NativeType(tr("Time"), QStringLiteral("Time"), QVariant::Time, -1, -1, -1, -1)
<< QgsVectorDataProvider::NativeType(tr("Datetime"), QStringLiteral("Datetime"), QVariant::DateTime, -1, -1, -1, -1)
//binary type: store file path in a string
<< QgsVectorDataProvider::NativeType(tr("Varbin"), QStringLiteral("Varbin"), QVariant::String, -1, -1)
);
mUri = uri;
readData(mUri);
mNextFeatureId = featureCount() + 1;
}
2.2.2 QgsVctFeatureSource
QgsVCTFeatureSource中要实现的方法只有构造函数和getFeatures。在构造函数中,我们需要取得QgsVCTProvider中的一些必要数据,比如所有要素,所有字段等。getFeatures返回对应的Iterator类。
QgsVctFeatureSource::QgsVctFeatureSource(const QgsVctProvider *p)
: mExtent(p->mExtent)
, mGeometryType(p->mGeometryType)
, mCrs(p->mCrs)
, mFeatures(p -> mFeatures)
, mFields(p->mFields)
{
}
QgsFeatureIterator QgsVctFeatureSource::getFeatures(const QgsFeatureRequest &request) {
return QgsFeatureIterator(new QgsVctFeatureIterator(this, false, request));
}
2.2.3 QgsVctFeatureIterator
QgsVctFeatureIterator中的关键函数为rewind、close和fetchFeature。
rewind函数将Iterator的指向重定向为第一个要素。
bool QgsVctFeatureIterator::rewind() {
if (mClosed)
return false;
mSelectIterator = mSource->mFeatures.constBegin();
eturn true;
}
close函数将关闭这个Iterator,当要素的操作出现异常时应该调用这个函数。
bool QgsVctFeatureIterator::close() {
if (mClosed)
return false;
iteratorClosed();
return true;
}
fetchFeature函数是为了查看要素集中是否存在要找的要素,QGIS在每一次绘制时似乎都会调用这个方法,我对它的作用还不是很明确。😩
bool QgsVctFeatureIterator::fetchFeature(QgsFeature &feature) {
feature.setValid(false);
if (mClosed)
return false;
if (mSelectIterator != mSource->mFeatures.constEnd()) {
feature = mSelectIterator.value();
feature.setValid(true);
feature.setFields(mSource->mFields);
geometryToDestinationCrs(feature, mTransform);
++mSelectIterator;
return true;
}
else {
close();
return false;
}
}
2.2.4 QgsVctProviderMetadata
QgsVctProviderMetadata中需要对uri进行处理,而由于我在读取vct文件时uri直接是文件路径,因此这里就比较简单,如果是连接数据库等,这里就需要提供请求参数等。
QVariantMap QgsVctProviderMetadata::decodeUri(const QString &uri ) {
QVariantMap components;
components.insert(QStringLiteral("path"), QUrl(uri).toLocalFile());
return components;
}
QString QgsVctProviderMetadata::encodeUri(const QVariantMap &parts) {
return QStringLiteral("file://%1").arg(parts.value(QStringLiteral("path")).toString());
}
2.2.5 QgsVctSourceSelectProvider
在QgsVctSourceSelectProvider中需要实现providerKey、text、ordering、icon和createDataSourceWidget函数。providerKey和text是简单的返回对应的key和说明文字。ordering将定义我们的dataProvider在QgsDataSourceManager窗口中的位置,查看源代码可知QGIS自有顺序的规定。因此,我们相关代码中将顺序定位于OrderLocalProvider之后。这里由于没有设计VCT文件的图标,因此icon这里没返回有意义的值。
//! Provider ordering groups
enum Ordering
{
OrderLocalProvider = 0, //!< Starting point for local file providers (e.g. OGR)
OrderDatabaseProvider = 1000, //!< Starting point for database providers (e.g. Postgres)
OrderRemoteProvider = 2000, //!< Starting point for remote (online) providers (e.g. WMS)
OrderGeoCmsProvider = 3000, //!< Starting point for GeoCMS type providers (e.g. GeoNode)
OrderOtherProvider = 4000, //!< Starting point for other providers (e.g. plugin based providers)
};
int ordering() const override { return QgsSourceSelectProvider::OrderLocalProvider + 20; }
QIcon icon() const override { return QgsApplication::getThemeIcon(QStringLiteral("")); }
QgsAbstractDataSourceWidget *createDataSourceWidget(QWidget *parent = nullptr, Qt::WindowFlags fl = Qt::Widget, QgsProviderRegistry::WidgetMode widgetMode = QgsProviderRegistry::WidgetMode::Embedded) const override {
return new QgsVctSourceSelect(parent, fl,widgetMode);
}
2.2.6 QgsVctProviderGuiMetadata
QgsVctProviderGuiMetadata中需要注意sourceSelectProviders函数。
QList<QgsSourceSelectProvider *> sourceSelectProviders() override {
QList<QgsSourceSelectProvider *> providers;
providers << new QgsVctSourceSelectProvider;
return providers;
}
2.2.7 QgsVctSourceSelect
QgsVctSourceSelect是我们内嵌在QgsDataSourceManager窗口中的文件选择窗口,我们在这里需要利用Qt Designer设计一个.ui文件,然后利用其命令行工具将ui文件转换为.h文件,然后在我们的类中实现空间的连接和信号处理。.ui文件中一定要有一个QDialogButtonBox控件,以便QGIS在其中放置自己的确认按钮。
QgsVctSourceSelect::QgsVctSourceSelect(QWidget *parent, Qt::WindowFlags fl, QgsProviderRegistry::WidgetMode theWidgetMode)
: QgsAbstractDataSourceWidget( parent, fl, theWidgetMode ) {
setupUi(this);
QgsGui::instance()->enableAutoGeometryRestore(this);
setupButtons(buttonBox);
connect(toolButtonFilePath, &QAbstractButton::clicked, this, &QgsVctSourceSelect::openFileDialog);
connect(lineEditFilePath, &QLineEdit::textChanged, this, &QgsVctSourceSelect::onFileChanged);
}
3. 总结
以上内容简单的实现了QGIS中其他矢量数据文件格式的读取,其他的功能,比如创建空间索引、修改要素等功能则需要改变QgsVectorDataProvider的Capability中的返回值,然后按照要求实现指定函数即可。Again,项目的代码见Github。🎉🎉🎉