学习目标
了解QGIS C++加载矢量、栅格文件基本方式,并通过菜单调用这些方法。
参考内容
链接1:PyQGIS二次开发教程(一):准备工作
链接2:PyQGIS二次开发教程(二):加载影像与矢量
链接3:PyQGIS二次开发教程(三):学习制作菜单栏功能
作者:yoyi
码云镜像4:qgisapp.h、qgisapp.cpp
注意:没找到比较系统的C++二次开发内容,参考这个Python版的知乎专栏,写得比较详细清晰,非常有帮助,非常感谢该作者分享,下方中部分图片也直接引用专题中的图片,图片中水印均未删除,如涉及侵权,可联系我删除。
而且Python接口和C++基本一致,可能部分细节存在差异,需要注意。所以在学习的时候结合QGIS官方代码,能更准确一些。官方代码参考的是码云(Gitee),至于为什么不看Github,速度实在太慢了,还经常刷不出来,当然也可以下载下来看。当然有精力的话还是可以把qgisapp
完整代码阅读一遍的,可以更详细地了解QGIS功能模块。
主要内容
1.初始UI设计
使用QtDesigner设计图中界面,具体为
- 在MainWindow中间添加frame,并右击窗口将布局调整为垂直布局;
- 添加Dock Widget,吸附到窗口左侧。
- 然后添加一些初始化代码,具体参考链接1。
当然与Python不一样,需要为窗口类(App类)创建h文件和cpp文件,按照官方代码,该类直接继承自QMainWindow和对应的Ui类,(Qt默认创建的是将Ui类作为窗口类的一个成员变量的,我猜继承的好处是可以直接访问Ui的成员变量)
class QtGis : public QMainWindow, private Ui::QtGisClass
QtGis::QtGis(QWidget *parent) : QMainWindow(parent)
{
setupUi(this);
// 1. set title
setWindowTitle("QGIS Interface");
// 2. initial layer tree
QVBoxLayout* vl = new QVBoxLayout(dockWidgetContents);
mLayerTreeView = new QgsLayerTreeView(this);
vl->addWidget(mLayerTreeView);
// 3. initial map canvas
mMapCanvas = new QgsMapCanvas(this);
QHBoxLayout* hl = new QHBoxLayout(frame);
hl->setContentsMargins(0, 0, 0, 0);
hl->addWidget(mMapCanvas);
// 4. set layer tree view style
mModel = new QgsLayerTreeModel(QgsProject::instance()->layerTreeRoot(), this);
mModel->setFlag(QgsLayerTreeModel::AllowNodeRename);
mModel->setFlag(QgsLayerTreeModel::AllowNodeReorder);
mModel->setFlag(QgsLayerTreeModel::AllowNodeChangeVisibility);
mModel->setFlag(QgsLayerTreeModel::ShowLegendAsTree);
mModel->setAutoCollapseLegendNodes(10);
mLayerTreeView->setModel(mModel);
// 5. create bridge between layer tree and map canvas
mLayerTreeBridge = new QgsLayerTreeMapCanvasBridge(QgsProject::instance()->layerTreeRoot(), mMapCanvas, this);
}
基本上就是专栏中的python代码直接转的C++代码,忽略不一样的变量名。
2.创建图层管理类QgsLayerHandling
类名参考的是QGIS源代码,链接2中用的是qgisLayerUtils
,主要用于将不同数据源读取为图层Layer
,并将其添加到图层树中,算是一个工具类,全是静态函数。简单的情况下也可以不单独创建类,直接将函数写到App类中,因为即使使用了工具类,在App类中也写的封装函数。
工具类目前只有3个静态成员函数和1个静态变量。
QList<QgsMapLayer*> mLayers;
void QgsLayerHandling::addMapLayer(QgsMapLayer* layer, QgsMapCanvas* mapCanvas, bool firstAddLayer)
{
if (layer->isValid())
{
if (firstAddLayer)
{
mapCanvas->setDestinationCrs(layer->crs());
mapCanvas->setExtent(layer->extent());
}
while (!QgsProject::instance()->mapLayersByName(layer->name()).isEmpty())
{
layer->setName(layer->name() + "_1");
}
QgsProject::instance()->addMapLayer(layer);
mLayers.append(layer);
mapCanvas->setLayers(mLayers);
mapCanvas->refresh();
}
}
QgsRasterLayer* QgsLayerHandling::readRasterFile(QString path, QString name)
{
QgsRasterLayer* layer = new QgsRasterLayer(path, name);
return layer;
}
QgsVectorLayer* QgsLayerHandling::readVectorFile(QString path, QString name)
{
QgsVectorLayer* layer = new QgsVectorLayer(path, name, "ogr");
return layer;
}
眼泪:最开始的时候,把readVectorFile
函数是"ogr"
写成了"org"
,编译是通过了,当时没发现,但加载矢量文件时就是读不出来,查找了好久问题,换了好几个shp文件,最后才发现是拼写错了。其实OGR就是GDAL库中处理矢量的部分,具体可以学习下GDAL。
3.App类中调用
在App类中调用上面工具类,将添加图层函数封装调用,如下:
void QtGis::addRasterLayer(QString fileName)
{
QStringList temp = fileName.split('/');
QString basename = temp.at(temp.size() - 1);
QgsRasterLayer* layer = QgsLayerHandling::readRasterFile(fileName, basename);
if (mFirstAdd)
{
QgsLayerHandling::addMapLayer(layer, mMapCanvas, true);
mFirstAdd = false;
}
else
{
QgsLayerHandling::addMapLayer(layer, mMapCanvas);
}
}
代码只是示例,是根据链接2直接转译成C++的,只是加载栅格图层的,加载矢量图层类似。
如此这般一调用,就可以在窗口中加载GeoTiff和Shp文件了,但和链接2中不一致的是,我添加GeoTiff后,图层树中并没有图例图标,也不能展开单独显示RGB波段,不知道是哪的问题。Shp文件可以显示图例图标。
3.添加菜单并与函数进行链接
在QtDesigner中添加两个功能的菜单项,并将菜单的QAction也添加到工具栏中。
这样(这张图片也是从专栏中拷贝的,但由于图片太小,并没有水印)
然后再这样
然后再这样
然后链接
void QtGis::connectFunc()
{
connect(actionOpenRaster, &QAction::triggered, this, &QtGis::actionOpenRasterTriggered);
connect(actionOpenShp, &QAction::triggered, this, &QtGis::actionOpenShpTriggered);
}
void QtGis::actionOpenRasterTriggered()
{
QString fileName = QFileDialog::getOpenFileName(this, tr("Open tif file"), "", "GeoTiff(*.tif;*.tiff)");
addRasterLayer(fileName);
}
void QtGis::actionOpenShpTriggered()
{
QString fileName = QFileDialog::getOpenFileName(this, tr("Open shape file"), "", "*.shp");
addVectorLayer(fileName);
}
注意:在connect
的时候,第四个参数一定要加类名,像&QtGis::actionOpenRasterTriggered
这样,而不是&actionOpenRasterTriggered
这样,否则链接失败,虽然在同一个类里面。
最开始我就偷懒没写类名,结果发现不行,就尝试了另一种信号/槽的connect
方法,像这样
connect(actionOpenRaster, SIGNAL(triggered(bool)), this, SLOG(actionOpenRasterTriggered()));
,但也没成功,就只好再研究第一种方法了。
下步打算
- 尝试打开更多各类数据格式
- 尝试打开WMTS数据源
- 丰富界面功能,至少要显示鼠标点坐标
- 尝试测量和编辑
- 整饰和出图功能
问题总结
在尝试过程中也遇到了许多愚蠢的问题,在前面也基本提到了,这里再总结一下。
- 别写错字,就像上面的
ogr
,这么简单的低级错误可是笑死个人,关键还不容易发现,编译也不报错,运行也没提示,图层树里都有新图层了,但画布里就是没有东西。记得高中第一次接触VB编程的时候,第一个例子就是写错了个单词,结果让老师帮找好久才发现错误。 connect
第四个参数要写类名,也就是成员函数完整的名字。- 工具类中,静态成员变量初始化问题,最开始
mLayers
定义成了静态成员变量,然后在cpp
文件中又初始化了一遍,结果链接的时候直接报错,找不到的外部对象,当时还以为缺少哪个lib
库了,结果最后是静态成员变量问题。 - 在代码中设置中文标题的时候显示乱码,而在QtDesigner中设置就没有问题。经过一番百度和尝试,终于发现,原来是我的cpp文件默认编码是GB2312的,而QString默认是UTF8的,所有需要转换一下,使用函数
QString::fromLocal8Bit("中文字符串")
方式即可正常显示中文。
当然还有一个问题待解决,虽然不一定去解决:
- 程序Debug版本运行无显示,还不知道是什么原因,可能是缺少库吧。