QGIS的数据格式拓展插件开发
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。🎉🎉🎉