YoloKokura

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.libQt5Widgets.libqgis_core.libqgis_app.libqgis_gui.lib。至此,开发环境的配置就大功告成啦!🎉🎉🎉

2. 技术路线

2.1 基类说明

​查看源代码可知,QGIS对数据格式的支持是以拓展插件的方式实现的,打开软件之后,QGIS会通过QgsProviderRegistry类扫描Plugin Path下的所有文件(Plugin Path一般位于QGIS目录下的plugins文件夹),找到其中的数据源插件,即各种dll文件中的QgsVectorDataProvider的继承类。因此我们的思路就是自己继承QgsVectorDataProvider,用于链接矢量图层和VCT文件数据源。

cpp
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();
}

​由于没找到相关资料,所以我在类的结构上参考了源代码中的DelimitedTextMemory的结构,代码分别位于QGIS源代码根目录下的“providers\delimitedtext”和“src\core\providers\memory”目录下。我们可以看到,这些插件主要继承了QgsVectorDataProviderQgsProviderMetadataQgsAbstractDataSourceWidgetQgsProviderGuiMetadataQgsSourceSelectProviderQgsAbstractFeatureSource、*QgsAbstractFeatureIteratorFromSource< T >*等。

QgsVectorDataProvider是我们的重点之一,主要负责将矢量文件数据源文件和矢量图层联系起来,文件的读取和要素的改动都通过QgsVectorDataProvider实现。因此我们需要将VCT文件的读写写在这里,后面也可以用代码支持要素的编辑保存。

QgsProviderMetadata负责保存QgsVectorDataProvider的key和description,这在QgisApp的初始化中是必要的,QGIS会通过QgsProviderMetadata的createProvider方法来创建我们定义的dataprovider。另外QgsProviderMetadata要求实现的encodeUri和decodeUri方法在对dataprovider的必要参数uri的处理上也很重要,但是在这个例子中我们的uri只是vct文件的储存路径,因此这里不是重点。

QgsAbstractDataSourceWidget是QGIS中文件选择框的基类,要实现选择数据源文件,添加相应图层的功能,就必须继承这个接口,继承类主要负责文件选择窗口的用户操作逻辑。QgsProviderGuiMetadataQgsSourceSelectProvider之间的关系,和前面提到的QgsProviderMetadataQgsVectorDataProvider的关系相似:QgsProviderGuiMetadataQgsDataSourceManager初始化时会提供QgsSourceSelectProvider的List,我们自己继承的QgsSourceSelectProvider也是这样创建实例的。而QgsSourceSelectProvider则存放了一些必要的前端说明信息,比如图标,说明文字等,这里也定义了我们自己的窗口在QgsDataSourceManager中同其他窗口的相对顺序。

QgsAbstractFeatureSource用于存放我们从文件中读取的要素,QgsAbstractFeatureIteratorFromSource< T >则可以从相应的QgsAbstractFeatureSource实现类中对要素进行遍历等操作。

总的来看,用户打开文件选择窗口,选择vct文件,到QGIS添加图层可以分为用户操作和后台响应两块。QgsAbstractDataSourceWidgetQgsProviderGuiMetadataQgsSourceSelectProvider负责从创建文件选择窗口,并接收用户输入信息。QgsProviderMetadataQgsVectorDataProviderQgsAbstractFeatureSource和*QgsAbstractFeatureIteratorFromSource< T >*负责按照将读取到的数据转换成新建矢量图层所必须的信息,同时还可以进一步拓展,支持要素的编辑、空间索引的创建等功能。

2.2 继承类的部分实现

从2.1 基类说明这里,我们可以发现,我们要做的就是分别继承上述几个抽象类,在这几个类里面写自己的逻辑即可。下面是我自己的继承类,以及一些比较重要的,需要实现的方法。

2.2.1 QgsVctProvider

QgsVctProvider

QgsVctProvider可以说是这里的主类,首先需要实现QgsVectorDataProvider里面的纯虚函数,包括featureSourcestorageTypegetFeatureswkbTypefeatureCountfieldscapabilitiescreateSpatialIndexhasSpatialIndexnamedescriptionextentisValidcrssetSubsetStringsupportsSubsetStringsubsetString等。这些函数一般都是返回对应图层的某些信息,如空间参考系,要素总数等,根据自己数据格式文件确定即可,然后在capabilities函数中我们需要返回这个dataProvider支持的功能,详见QGIS的官方文档。如果不支持某些功能,在对应的函数里可以直接返回false,比如不支持空间索引创建的话,createSpatialIndex可以直接返回false。由于我们当前的目标是支持文件的读写,所以我们的参数如下面所示:

cpp
QgsVectorDataProvider::Capabilities QgsVctProvider::capabilities() const {
    return AddFeatures | DeleteFeatures | ChangeGeometries |
        ChangeAttributeValues | AddAttributes | DeleteAttributes | RenameAttributes;
}

getFeatures函数将根据特定的请求返回一个QgsFeatureIterator,这个iterator所对应的要素是符合请求条件的全部要素。

cpp
QgsFeatureIterator QgsVctProvider::getFeatures(const QgsFeatureRequest &request) const {
    return QgsFeatureIterator(new QgsVctFeatureIterator(new QgsVctFeatureSource(this), true, request));
}

最后我们在构造函数中,还需要先利用setNativeTypes方法设置这个dataProvider支持的字段类型,然后再实现文件的读取,因为当用户选择了一个文件之后,QGIS会直接产生一个对应的dataProvider实例。具体的文件读取代码这里不提,逻辑是QgsVctProvider在构造函数中得到了uri,也就是文件路径(从文件选择窗口得到),然后调用预先写好的writeData方法读取并存放数据。

cpp
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

QgsVCTFeatureSource中要实现的方法只有构造函数和getFeatures。在构造函数中,我们需要取得QgsVCTProvider中的一些必要数据,比如所有要素,所有字段等。getFeatures返回对应的Iterator类。

cpp
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

QgsVctFeatureIterator中的关键函数为rewindclosefetchFeature

rewind函数将Iterator的指向重定向为第一个要素。

cpp
bool QgsVctFeatureIterator::rewind() {
    if (mClosed)
        return false;
    mSelectIterator = mSource->mFeatures.constBegin();

    eturn true;
}

close函数将关闭这个Iterator,当要素的操作出现异常时应该调用这个函数。

cpp
bool QgsVctFeatureIterator::close() {
    if (mClosed)
        return false;

    iteratorClosed();

    return true;
}

fetchFeature函数是为了查看要素集中是否存在要找的要素,QGIS在每一次绘制时似乎都会调用这个方法,我对它的作用还不是很明确。😩

cpp
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

QgsVctProviderMetadata中需要对uri进行处理,而由于我在读取vct文件时uri直接是文件路径,因此这里就比较简单,如果是连接数据库等,这里就需要提供请求参数等。

cpp
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

QgsVctSourceSelectProvider中需要实现providerKeytextorderingiconcreateDataSourceWidget函数。providerKeytext是简单的返回对应的key和说明文字。ordering将定义我们的dataProvider在QgsDataSourceManager窗口中的位置,查看源代码可知QGIS自有顺序的规定。因此,我们相关代码中将顺序定位于OrderLocalProvider之后。这里由于没有设计VCT文件的图标,因此icon这里没返回有意义的值。

cpp
    //! 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)
    };
cpp
    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

QgsVctProviderGuiMetadata中需要注意sourceSelectProviders函数。

cpp
    QList<QgsSourceSelectProvider *> sourceSelectProviders() override {
        QList<QgsSourceSelectProvider *> providers;
        providers << new QgsVctSourceSelectProvider;
        return providers;
    }

2.2.7 QgsVctSourceSelect

QgsVctSourceSelect

QgsVctSourceSelect是我们内嵌在QgsDataSourceManager窗口中的文件选择窗口,我们在这里需要利用Qt Designer设计一个.ui文件,然后利用其命令行工具将ui文件转换为.h文件,然后在我们的类中实现空间的连接和信号处理。.ui文件中一定要有一个QDialogButtonBox控件,以便QGIS在其中放置自己的确认按钮。

cpp
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中其他矢量数据文件格式的读取,其他的功能,比如创建空间索引、修改要素等功能则需要改变QgsVectorDataProviderCapability中的返回值,然后按照要求实现指定函数即可。Again,项目的代码见Github。🎉🎉🎉

Tags: