实验一至实验四,全部代码打包如下,复制链接即可下载
通过网盘分享的文件:pythonProject
链接: https://pan.baidu.com/s/1Z1BRMo0UkaLVVyrR6-HC0w?pwd=7uqm 提取码: 7uqm
一、实验目的
本次实验续接第一次实验课内容,在已经创建得到运行界面的基础上,完成地理空间数据的加载和简单图层控制,主要目的在于进一步提升学生二次开发的操作能力、对地理空间数据的理解以及代码编写的基本原理思路的掌握程度,具体目的如下:
1.实现数据加载功能:通过右键或打开菜单等交互方式,让用户能够选择并加载 QGIS 项目文件(.qgz)、栅格数据(.tif)和矢量数据(.shp),使用户能够在创建的地图界面中浏览和分析不同类型的地理空间数据;
2.完成图层控制功能:实现基本的图层控制功能,如移动图层、删除图层等,以提高用户对地图图层的管理效率,这部分功能可以通过建立简单的右键菜单,并通过调用QgsLayerTreeViewDefaultActions 类的方法来快速实现。
二、实验内容
- 加载数据—界面代码部分;
- 加载数据—逻辑代码部分;
- 图层操作—创建空白右键菜单;
- 图层操作—创建图层右键菜单;
- 图层控制。
三、实验过程
3.1 加载数据—界面代码部分
3.1.1 初始化界面
首先我们使用super()函数来调用MainWindow类的父类的构造函数,并调用了 setupUi()方法,将UI文件中定义的界面元素加载到当前的窗口中:
super(MainWindow, self).__init__()
self.setupUi(self)
self.setWindowTitle("20213032安迁岚")
3.1.2 初始化图层树与地图画布
在初始化过程中我们利用了垂直布局管理器v1与水平布局管理器 hl,分别与dockWidgetContents_2、frame组件关联,以此创建名为 layerTreeView 的图层树视图与地图画布 mapCanvas,该操作可以使我们不用在Qt Designer中添加布局以减少操作。
vl = QVBoxLayout(self.dockWidgetContents_2)
self.layerTreeView = QgsLayerTreeView(self)
vl.addWidget(self.layerTreeView)
self.mapCanvas = QgsMapCanvas(self)
hl = QHBoxLayout(self.frame)
hl.setContentsMargins(0, 0, 0, 0) # 设置周围间距
hl.addWidget(self.mapCanvas)
3.1.3 设置图层树风格
根据自身需要,设置图层树如下所示,包括允许图层节点重命名、允许图层拖拽排序、允许改变图层节点可视性、展示图例与当节点数大于等于10时自动折叠:
self.model = QgsLayerTreeModel(PROJECT.layerTreeRoot(), self)
self.model.setFlag(QgsLayerTreeModel.AllowNodeRename) # 允许图层节点重命名
self.model.setFlag(QgsLayerTreeModel.AllowNodeReorder) # 允许图层拖拽排序
self.model.setFlag(QgsLayerTreeModel.AllowNodeChangeVisibility) # 允许改变图层节点可视性
self.model.setFlag(QgsLayerTreeModel.ShowLegendAsTree) # 展示图例
self.model.setAutoCollapseLegendNodes(10) # 当节点数大于等于10时自动折叠
self.layerTreeView.setModel(self.model)
3.1.4 建立图层树与地图画布的桥接
最后使用QgsLayerTreeMapCanvasBridge类将图层树与地图画布连接起来:
# 4 建立图层树与地图画布的桥接
self.layerTreeBridge = QgsLayerTreeMapCanvasBridge(PROJECT.layerTreeRoot(), self.mapCanvas, self)
完成以上内容后再次运行main文件即可在得到窗口中看到我们所创建的图层树与地图画布,但此时我们仍不能向内添加数据,因为我们还未设置对应的槽函数并将二者绑定。
3.1.5 创建响应槽函数
在创建响应槽函数之前,我们首先需要在Qt Designer的界面中添加“actionOpen_Raster”与“actionOpen_Vector”两个QAction,具体步骤同实验一中的创建QAction类“actionOpen_Map”,在此不过多赘述,输入以下代码,创建打开栅格数据与矢量数据的槽函数:
以上槽函数的共同任务为当信号传递到槽函数时,打开一个文件选择读取的页面,但需要注意的是该代码无法实现文件读取功能,同时其中用到的DataManager类会在后续的data_control包中创建。
3.1.6 绑定槽函数
如下代码含义为:当用户执行动作时(比如点击相应的菜单项或工具栏按钮),就会触发槽函数中定义的方法执行,从而执行与用户动作相关联的操作。
3.2 加载数据—逻辑代码部分
结合上述实验过程与第一次实验课的内容,我们已经在窗体类的.py文件中编写了许多代码,考虑到后续实验过程需要反复使用到加载数据的方法,同时为了避免窗体类代码过于冗余,我们建立一个data_control的包专门用于数据的加载和管理等,用以隔离逻辑代码与界面相关代码:
3.2.1 创建管理类前准备
在创建DataManager类之前,我们首先需要获取当前正在运行的QGIS 项目的实例,并赋值给CUR_PROJECT变量,在DataManager类中,我们将会使用CUR_PROJECT变量来访问当前项目的实例,以便在类的方法中执行一些与项目相关的操作。
CUR_PROJECT = QgsProject.instance()
3.2.2 添加地图图层方法
如下所示为DataManager类中的一个方法,用于将一个有效的地图图层(QgsMapLayer)添加到当前的QGIS项目中,并显示在地图画布(QgsMapCanvas)上,同时仿照ArcGIS软件:如果这是项目中添加的第一个图层,则设置地图画布的坐标参考系统和视图范围与该图层相同:
3.2.3 添加栅格图层方法
该类方法主要功能是读取栅格数据文件并封装成一个对象:接受一个文件路径作为参数,并尝试从给定的路径读取一个栅格数据文件,如果文件存在,它将创建一个QgsRasterLayer 对象,若不存在则返回 None。
3.2.4 添加矢量图层方法
该类方法主要功能是读取矢量数据文件并封装成一个对象:接受一个文件路径作为参数,并尝试从给定的路径读取一个矢量数据文件,如果文件存在,它将创建一个QgsVectorLayer 对象,若不存在则返回 None。
3.2.5 整合并展示结果
将如上所示的三个类方法整合包装在DataManager类中,并存放于一个重新创建的dataControl.py文件中,用以在之后实验过程中的反复调用,在继承窗体类的文件中输入代码如下所示,选择main函数运行,我们即可在我们创建的界面中导入并展示图层了:
from data_control.dataControl import DataManager
3.3 图层操作—创建空白右键菜单
通过如上所示的实验操作,我们已经可以将本地的栅格、矢量与地图文件加载到界面中显示了,但是虽然加载进来了,我们却还不能对其执行任何的操作,甚至无法删掉已经添加的文件,在以下内容中我们将详细讲解如何新建图层右键菜单,并在里面添加一些最基础的功能,其基本思路与我们上述实验中添加数据的思路相同。
3.3.1 创建响应槽函数
该方法首先创建了一个QMenu类的实例,也就是右键菜单对象contextMenu,并使用contextMenu.addAction() 方法向该右键菜单对象中添加了打开地图、打开矢量文件、打开栅格文件三个QAction类。
除此之外,还另外创建了一个名为action_quit的QAction,并将其添加到右键菜单中,退出动作与QApplication.quit槽函数连接,用以退出菜单,最后,通过调用 contextMenu.exec(QCursor.pos())在鼠标右键点击的位置显示右键菜单。
通过如上所示代码,我们即编写了一个在右键图层时将会展示“打开地图”、“打开栅格”、“打开矢量”与“退出”四项内容的菜单。
3.3.2 绑定槽函数
该代码首先将图层树视图的customContextMenuRequested信号(即右键单击信号)连接到showContextMenu方法上,然后设置了图层树视图的上下文菜单策略为自定义上下文菜单,这意味着当用户在图层树视图上右键点击时,系统不会显示默认的上下文菜单,而是等待程序员自己来显示自定义的上下文菜单,以实现我们希望的功能。
3.3.3 整合并展示结果
将如上代码整合至继承窗体类的.py文件中,运行main文件即可得到如下结果:
3.4 图层操作—创建图层右键菜单
为图层创建右键菜单的基本步骤与创建空白右键菜单类似,需要特别注意的是,在GIS软件中图层具有多种类别,针对不同的图层我们也会存在不同的菜单,因此在创建右键菜单时,我们需要额外考虑选择不同的图层类型。
3.4.1 创建QAction动作
如下所示代码虽然看起来复杂,但其实本质就是创建Qt Designer中的QAction动作,详细来说,即为不同类型的图层右键菜单内容创建不同的动作并命名,但需要注意的是:不同菜单的内容并不是在此处定义的:
3.4.2 创建响应槽函数
如下所示代码首先定义了一个私有方法用于初始化菜单对象,在这个方法中,调用了三个私有方法,即我们的矢量数据菜单、栅格数据菜单和其他类型菜单,在不同的菜单中,我们分别添加了一系列与对应数据相关的动作。
这三个方法返回了相应的菜单对象,然后在__init_menu()方法中将它们分别赋值给了self.vector_menu、self.raster_menu和self.otherMenu,通过这样的设计,我们就可以在界面中便捷的创建并管理不同类型的菜单,以便提供给用户进行操作和选择。
3.4.3 判断图层类型
利用如下所示代码我们可以对右键选择的Layer进行判断,如果选中为None,则显示为打开数据菜单,同时该方法使用了Python中的类型判断,只有选中图层类型为QgsVectorLayer和QgsRasterLayer的时候才进行后续处理:
3.4.4 整合并展示结果
为了避免继承窗体类的.py文件中的代码过于冗余,我们可以将上述的代码整合在qGisMenucontrol.py的LayerTreeViewMenu类中,这个类将专门负责管理图层树的右键菜单及其相关功能,在继承窗体类的文件中输入代码如下所示,选择main函数运行,我们所创建的运行界面即可以弹出不同的菜单了:
from qGisMenucontrol import LayerTreeViewMenu
self.control = LayerTreeViewMenu(self.layerTreeView, self.mapCanvas)
3.5 图层控制
在上述实验过程的3.4部分我们已经可以通过右键图层打开对应菜单了,但通过与3.3的操作对比我们也可以看出,对于我们创建图层右键菜单,我们还未进行槽函数绑定这一操作,因此我们的右键菜单目前只是一具空壳,无法执行任何具体操作,接下来我们需要通过将对应功能与槽函数绑定,以真正实现图层控制。
3.5.1 更改Action函数
但在此步骤中我们槽函数绑定的功能来源,可以直接选择使用QGIS 中的一个类——QgsLayerTreeViewDefaultActions,它用于管理图层树视图中的默认操作,通过使用该类,我们可以很方便地将这些默认操作与图层树视图对象相关联,以实现基本的图层管理功能。
借助该类,我们实现图层控制的步骤就会简便许多,不需要重复进行创建响应槽函数与绑定槽函数,直接在我们之前步骤中初始化action的函数(3.4.1)中提取默认的QAction即可,如下图代码所示:
3.5.2 整合并展示结果
在上述代码整合至qGisMenucontrol.py的LayerTreeViewMenu类中,并在类的构造函数补充如下所示代码,此时选择main函数运行,我们所创建的运行界面布局可以弹出不同的菜单,同时可以执行相关功能了,测试‘Zoom to Layer’功能结果如下图所示,迅速地从世界地图中定位到了湖南省:
3.6 补充功能
在实验指导书中老师鼓励我们多尝试构造一些其它的图层控制功能,因此,为加深自己在本次实验中所学习知识的理解,我结合知乎上的文章:PyQGIS二次开发教程(六):状态栏与属性表实现 - yoyi的文章 - 知乎https://zhuanlan.zhihu.com/p/640545931,在已有实验内容的基础上补充了一个打开属性表功能与缩放至选择要素功能,其基本步骤与思路与之前实验内容类似,因此许多过程不再过多赘述。
3.6.1 打开属性表功能
我们首先新建一个attributeDialog.py文件,并构造AttributeDialog类如下图所示,该类的主要目的为创建一个能够显示和编辑QGIS中矢量图层属性表的对话框,并提供了一些基本的布局和功能,方法center将将对话框居中显示在屏幕上,方法openAttributeDialog用于加载图层的属性数据,并将其显示在属性表中:
如下所示代码在我们菜单类的action函数中定义了一个打开属性表的QAction,为与我们本身的实验内容相区别,我将名称改为中文:
如下所示代码在菜单类中定义了一个打开属性表的槽函数,其中包含一个实现具体功能的AttributeDialog类:
接下来通过如下所示代码将QAction(即信号)与响应槽函数相绑定:
最后,将如上代码整合运行main文件并验证打开属性表功能成功执行如下图所示:
3.6.2 缩放至所选要素功能
相比与打开属性表功能,该功能我们可以直接利用QgsLayerTreeViewDefaultActions类中的方法,快速而便捷的实现,首先通过QGIS Python API官网搜索得到该功能的方法名称为actionZoomToSelection:
随后修改代码如下所示,创建QAction动作并更改菜单类的Action函数:
最后运行main文件,验证缩放至所选要素功能成功执行如下图所示:
四、常见错误总结
4.1 退出代码 -1073740791 (0xC0000409)
问题:
在一般情况下我们创建响应槽函数并绑定槽函数后运行界面并执行相关操作即可得到结果,但许多同学可以打开运行界面却在读取数据后会闪退,并出现了如下所示的报错:
该报错表示:程序因为异常而被强制退出,官方描述可能的原因包括内存访问冲突、堆栈溢出、依赖项问题和硬件问题,该报错描述的过于抽象与笼统,导致许多同学无法正确发现错误并改正。但在我们实验过程中,出现上述错误的根本原因在于新建的qGisMenucontrol.py文件/继承窗体类文件代码编写不规范或有错误,但该文件中的错误不会直接显示在控制台。
解决方法:
右键代码弹出如下所示选项,选择“Modify Run Configuration”(修改运行配置),在弹出参数框中选择Add Run Options中的“Emulate terminal in output console”((在输出控制台中模拟终端)。
当启用了这个选项后,IDE的输出控制台会尽可能地模拟终端的功能和行为,当我们在开发命令行应用程序或需要与终端交互的场景下,启用该功能会使我们输出控制台更具交互性并显示我们具体代码的具体错误,更便于我们修改代码上的错误。
4.2 实例化菜单控制类LayerTreeViewMenu
问题:
在解决上述的闪退问题后,我们就需要修改我们代码中的种种错误,其中最常见的代码错误就是没有在继承窗体类中实例化我们的菜单控制类LayerTreeViewMenu。
通俗的理解就是:我们虽然已经创建了菜单控制类LayerTreeViewMenu,但并没有在继承窗体中“调用”该功能,具体的表现就是,我们可以打开运行界面,但在执行相关功能(例如:打开地图、右键菜单)时没有任何反应却也不会闪退。
解决方法:
在继承主窗体类的构造函数中输入以下代码,该代码的目的是创建一个新的LayerTreeViewMenu类的实例,并将该实例赋值给当前类的一个属性control,通过这样的方式,当前类的实例就可以在后续的操作中使用self.control来访问和操作这个新创建的实例了,出现该错误的根本原因还是在于对代码编写的相关逻辑不够清晰明确。
self.control = LayerTreeViewMenu(self.layerTreeView, self.mapCanvas)
五、实验心得与总结
5.1 槽函数等概念的关系总结
通过此次实验的深度学习,对于在Qt框架中,槽函数、信号、绑定槽函数和具体功能的实现等概念之间的关系有了更为深入具体的认识,该认识也极大地帮助了我后续实验的完成,因此总结如下图所示:
我认为其详细逻辑是:Qt将我们用户对象的相关操作定义为QAction,而QAction发射相关信号,我们将该信号与槽函数绑定,当action执行时,则绑定触发槽函数,而作为由接收者对象定义槽的函数中,又将绑定需要实现的具体功能,最终可以实现动作—>功能的转变。
在该思路中,左边部分多为在Qt界面中实现,而右边部分多为在Python的逻辑代码中实现。虽然此次实验许多的QAction我们都是在代码中直接创建的,但依然符合我们界面代码与逻辑代码相隔离的思想,也进一步印证了我们在第一实验课总结的二次开发的基本思路。
5.2 初始化界面的流程总结
回顾整个实验,初始化界面是我们在实验过程中遇到的第一个难点,对于部分同学来说直接复制粘贴代码或许没有很大难度,但关键在于理解与运用,创建一个初始化界面主要分为四步:
- 初始化图层树组件:利用QgsLayerTreeView类实例化一个图层树对象;
- 初始化地图画布:利用QgsMapCanvas类实例化一个地图画布对象;
- 设置图层树视图的样式和模型:根据需要设置图层树内容;
- 建立图层树与地图画布的桥接:利用QgsLayerTreeMapCanvasBridge类,确保图层树中的图层与地图画布中的图层保持同步。
5.3 加载数据的流程总结
虽然该部分总结为“加载数据的流程总结”,但加载数据作为Qt设计中的一个功能,之后所有的功能实现的流程都可以参考该总结,在理清弄懂二次开发的基本思路后我们或许就会发现其实代码的编写并没有我们想象中的那么困难:
- 点击按钮或者菜单(触发定义的QAction):用户在界面上点击了一个按钮或者菜单项,触发了相应的事件,即我们在界面中定义的QAciton;
- 打开一个选择文件的对话框(QAction绑定的响应槽函数):在槽函数中打开一个文件选择对话框,让用户选择要加载的文件,通过调用QFileDialog类来实现;
- 选中一个图层文件:该步骤取决于用户的自身需要;
- 加载数据到窗体中(槽函数中具体功能的实现):当用户选择了要加载的图层文件后,将这些数据加载到窗体中,通过定义DataManager类以实现。