基于QGIS的二次开发(三):图层属性与状态栏设计

一、实验目的

本次实验续接第二次实验课的实验内容,在已经创建得到运行界面且可以加载地图数据和简单图层控制的基础上,完成图层基本属性信息的查看、状态栏的设置以及矢量数据属性表的查看的功能,主要目的在于进一步提升学生二次开发的操作以及对MVC软件设计模式的理解,具体目的如下:

  1. 实现图层属性查看功能:通过实验,学习如何设计和实现图层属性查看功能,包括获取图层的基本属性信息(名称、来源、CRS等),并将其显示在用户界面上,重点学习如何通过创建类以管理QAction相关对象;
  2. 设计地图画布状态栏:通过实验,学习如何在界面上设计和布局地图画布的状态栏,包括显示地图的坐标、比例尺等基本属性信息,使用户用户可以在不和MapCanvas交互的情况下,快速了解地图的基本属性;
  3. 实现矢量属性表查看功能:通过实验,学习如何设计和实现矢量属性表查看功能,进一步巩固设计实现Action类的思路与步骤,并尝试在掌握思路的条件下独立创建类以管理QAction相关对象。

二、实验内容

  • 图层属性-界面代码;
  • 图层属性-逻辑代码-实现界面类型;
  • 图层属性-逻辑代码-实现Action类;
  • 状态栏设计;
  • 显示属性表格。

三、实验过程

3.1 图层属性-界面代码

在展示图层的属性信息之前,我们首先需要借助一些方法获得图层的属性信息,在QGIS中针对QgsRasterLayer和QgsVectorLayer两类不同的数据提供了不同的属性页面,那么首先我们需要弄清楚:不同的图层有哪些不同的信息,又该如何获取这些信息。在获得这些信息后,基于此我们才能开展我们的界面设计工作,因此我把获取图层属性信息与界面设计归纳为图层属性中的界面代码部分。

3.1.1 构建代码基本框架

 基于上述要求,通过查阅资料与参考已有的GIS软件我们可以了解到栅格与矢量图层相同与不同的属性信息,因此我们首先构建代码结构如下所示,利用父类ILayerInfo获取栅格与矢量图层共同具有的图层属性信息,再由子类RasterLayerProperty与VectorLayerProperty获取特有的属性信息,以减少代码冗余量:

3.1.2 获取共同属性信息

在如上所示代码中,我们将获取图层属性信息这一功能单独封装在类ILayerInfo中,并分别利用QgsMapLayer类、QgsCoordinateReferenceSystem类与QgsDataProvider类中的方法用以获取相关图层属性信息:

3.1.3 获取特有属性信息

通过如上所示方法我们获得了矢量与栅格图层共同拥有的基本图层属性信息,接下里我们构建两个ILayerInfo类的子类:RasterLayerProperty与VectorLayerProperty,这两个子类不仅通过调用父类的构造函数获取得到了图层的共有信息,同时获取了图层的特有信息(数据提供者类型/存储类型):

最后将上述代码内容与代码结构整合至data_control文件夹的layer_info.py文件中。

3.1.4 新建无按钮对话框

通过上述的实验内容,我们已经分别获得了栅格图层与矢量图层的属性信息,接下来就可以进入我们非常熟悉的界面—>界面代码—>逻辑代码的流程,因为图层属性是在点击图层菜单的选项后弹出对话框显示的,因此我们需要在Qt Designer中分别设计矢量与栅格的属性显示界面。

打开Qt设计师后,在新建窗体中选择新建Dialog with Buttons,界面的设计结构我们可以参考常用的GIS软件,基本结构如下右图所示,主要包含三个分别代表Apply、Cancel与OK的按钮两个选项卡窗口部件:

3.1.5 选项卡中布局详解

如下左图所示为第一个选项卡窗口部件中的内容——图层信息,主要包含三个分组框,用于分别包含常规、CRS与提供者的信息,每个分组框内再包括六个TextLabel;如下右图所示为第二个选项卡窗口部件中的内容——图层渲染,该窗口部件中不需要我们额外添加部件,但需要特别注意的是一定要添加scrollArea,将该窗口设置为可滚动区域

3.1.6 .ui文件转.py文件

最后将矢量与栅格的属性对话框分别保存为.ui文件,并在PyCharm中利用pyUIC工具转化为.py文件,具体步骤与实验一中的内容相似,在此不再过多赘述:

3.2 图层属性-逻辑代码-实现界面类型

与在实验一中构建继承窗体类同理,由.ui文件转成的.py文件是无法修改的,为了将逻辑与界面分离,我们需要继承这个代码里的对象,在继承对象的基础上进行我们的逻辑代码编写,以实现我们的功能。

3.2.1 创建继承对话框类

基于上述要求,首先我们需要构建一个继承对话框类如下代码所示,该类继承自 QDialog类和 Ui_Dialog类,这意味着RasterLayerPropertyWindow类拥有对话框窗口的功能,并且可以使用Ui_Dialog 类中定义的用户界面:

3.2.2 设置图层与信息对象

在我们继承对话框的类中,set_layer方法用于设置图层并创建相应的图层信息对象,并利用__syn_info_view方法同步更新图层信息视图,而get_layer方法则用于获取当前设置的图层

3.2.3 设置并获取渲染部件

set_render_widget和get_render方法用于设置和获取渲染部件,该方法首先接受一个名为renderWidget的参数,该参数表示渲染部件,并利用addWidget 方法将渲染部件添加到对话框窗口中,以便在界面中显示:

3.2.4 同步图层信息与视图

上述提到的__syn_info_view方法主要用于将我们获取得到的图层属性设置到相应的标签控件中,该方法确保了图层信息视图与当前设置的图层的信息保持同步,并在界面上进行更新:

3.2.5 按钮控件与槽函数绑定

最后定义如下所示的四个方法用于管理对话框窗口中的按钮点击事件,可以通俗的理解为在__connect_trigger方法中将界面中的按钮控件与三个槽函数进行绑定,确保在用户点击按钮时执行相应的操作,并在需要时应用和关闭对话框窗口:

最后将上述代码内容整合至qgisUtils文件夹的layer_property.py文件中。

3.3 图层属性-逻辑代码-实现Action类

通过如上所示的步骤与代码编写,我们已经实现了获取图层的属性信息、设计属性展示界面并通过逻辑代码将二者联系起来,可以说我们所有的“菜”已经在后厨里被做好了,那么我们现在需要做的,就是把“菜”端到我们的运行界面上来,因此接下来就进入了我们在实验二中重点学习的实现Action类

3.3.1 创建QAction对象

与在实验二中创建QAction对象的方法不同,在实验二中我们的QAction都是直接在__init_action方法中创建的,但是打开属性信息的QAciton我们是创建了一个ActionRasterLayerProperty类对其进行封装如下代码所示:

3.3.2 创建响应槽函数

如下所示代码创建了一个用于显示栅格图层属性窗口的 ActionRasterLayerProperty 类,它通过创建了一个 RasterLayerPropertyWindow 类的实例 dlg_properties,封装了与打开栅格图层属性窗口相关的操作,并在最后定义了一个名为 __show_property 的私有方法,通过调用属性窗口的 exec() 方法,显示栅格图层的属性窗口:

3.3.3 绑定响应槽函数

QAction对象与响应槽函数设置完毕后,我们直接在创建响应槽函数的类的构造函数中,利用triggered方法将动作与响应槽函数进行绑定即可,如下代码所示:

3.3.4 绑定QAction对象

在上述内容全部完成后,我们已经创建了对应的QAction类型,并且为这个动作绑定了相应的响应槽函数,接下来我们只差“临门一脚”,将该QAction对象绑定在我们之前实验中创建的矢量/栅格菜单中即可,如下代码所示:

需要特别注意的是:绑定之前一定要先创建一个ActionRasterLayerProperty类的实例并将其初始化,许多同学都是漏做此步骤直接添加了__init_action中的action导致无法运行得到结果。

3.3.5 整合并展示结果

将上述代码整合在qGisMenucontrol.py文件中,点击运行main文件,输入数据后右键打开菜单,选择“Property”选项即可得到结果如下图所示:

3.4 状态栏设计

状态栏是GIS软件中不可或缺的一部分,它提供了用户与地图交互的基本信息反馈,通过状态栏我们可以对MapCanvas进行基本的特性读取,比如显示 CRS 信息,基本比例尺等等,实现了用户在不和MapCanvas交互的情况下快速了解地图的基本属性,从而更有效地进行地图浏览和分析。

3.4.1 初始化显示控件

首先,我们在qGisUtils文件夹的status_bar.py文件中构建了一个StatusBarHelper类,用于管理主窗口的状态栏,该类为管理主窗口的状态栏的辅助类,其构造函数如下所示,在该构造函数中我们初始化了相应的控件,其主要作用有:创建控件实例、设置控件属性、组织控件布局,这一部分属于界面代码

3.4.2 创建响应槽函数

界面代码编写结束后,进入我们逻辑代码的编写部分,首先是创建响应槽函数,具体包括在状态栏中显示鼠标当前位置的坐标(show_xy函数)、显示地图的当前比例尺(show_scale函数),并允许用户手动更改比例尺(change_scale函数)、显示当前地图的坐标参考系统信息(show_crs),并实时更新:

3.4.3 绑定响应槽函数

响应槽函数设置完毕后,我们使用connect方法,将地图画布中的信号与我们类中的槽函数绑定起来,可以看到我们在状态栏设计中并没有使用QAction对象与槽函数绑定,这是因为状态栏的设计更多地涉及到与地图画布相关的交互和信息展示,而不是菜单栏或工具栏上的动作,通过这个例子我们可以深入理解信号与QAction之间的关系与区别:

3.4.4 整合并展示结果

最后,我们将上述代码整合至qGisUtils文件的status_bar.py文件中,并在继承主窗体的类中输入代码如下所示,初始化我们在上述步骤中创建的StatusBarHelper类,并将我们地图画布中的参数传递给它:

此时选择main函数文件运行,在打开的界面中即会有状态栏显示地图状态如下图所示:

3.5 显示属性表格

在之前的实验内容中我们针对不同的图层类型设计了不同的右键创建菜单,接下来我们将设计点击矢量菜单内容打开属性表,在此处我们还是通过将信号设置为 QAction 来实现该菜单的点击。

3.5.1 初始化界面类

我们首先在qGisUtils文件夹的attributeDialog.py文件中构建AttributeDialog类,该类的主要目的为创建一个能够显示和编辑QGIS中矢量图层属性表的对话框,其构造函数如下代码所示,在该构造函数中我们完成了对话框的初始化设置界面组件的创建

3.5.2 创建响应槽函数

界面代码编写结束后,进入我们逻辑代码的编写部分,首先是在AttributeDialog类中用于具体实现相应功能的两个方法函数,方法center用于将对话框居中显示在屏幕上,方法openAttributeDialog用于加载图层的属性数据,并将其显示在属性表中:

接下来,在我们qGisMenucontrol.py文件中的LayerTreeViewMenu类里构建函数如下代码所示,该函数在我们的菜单类中实例化了AttributeDialog类,并利用show() 方法显示我们的属性表对话框:

3.5.3 创建QAction对象

如下所示代码在我们菜单类的action函数中定义了一个打开属性表的QAction,为与我们上一实验课中的实验内容相区别,我将名称改为中文:

除此之外,不要忘记将该QAction对象加入到我们矢量图层的菜单中: 

3.5.4 绑定响应槽函数

接下来,通过如下所示代码将我们创建的QAction对象(actionOpenAttributeDialog)利用triggered方法与响应槽函数(openAttributeDialog)相绑定:

3.5.5 整合并展示结果

最后,整合上述代码,选择运行main文件并验证是否能打开属性表,若功能可成功执行则可得到结果如下图所示:

四、常见错误总结

4.1 Bug修正总结

Bug:打开地图时若未选择地图会闪退

问题描述:

在此次实验过程中由于我们需要不断打开运行界面以验证功能是否实现,我发现在运行界面中若我们选择打开地图功能,但没有选择相应的文件路径,运行界面就会闪退退出,需要我们重新运行main文件以打开运行界面。

解决方案:

问题代码出在data_control文件夹的dataControl.py文件的addMapLayer方法中,原代码如下所示,它表示图层对象有效时,则执行后续操作:

 但在实际情况下,我们选择的图层对象可能是None(比如:打开了错误的矢量文件读取框因此想要关闭重新选取栅格文件读取框),而None对象没有isValid的属性,因此会出现闪退Bug,为解决这个问题,我们只需要在if条件语句下修改代码如下所示:

修改过后,在读取地图过程中如果仍出现None对象,则会跳过addMapLayer方法不会出现运行界面闪退现象了。

Bug:打开地图属性信息无法缩小对话框

问题描述:

部分同学在可以成功运行main函数文件并打开图层属性信息对话框后会出现一个奇怪的Bug,虽然所有其它的功能都是正常的,但是得到的图层属性信息对话框却无法正常缩放大小,一旦调整大小就会闪退,而且只有栅格图层会出现这样的情况。

解决方案:

在进行多次代码调试与深入回溯思考后,发现出现该Bug的原因在于:在Qt Designer设计Layer Render时没有添加scrollArea(即没有添加滚动条),对话框为了显示所有内容就会把对话框“撑大”,不允许缩小对话框,矢量图层不会出现这类情况是因为它的显示内容没有栅格图层多,因此在Qt Designer中请务必确保对象排布如下图所示:

4.2 循环引用错误

问题描述:

循环引用是指两个或多个对象之间相互引用的情况,体现在我们此次实验中即:在一个包中,A的完全导入依赖于B的导入,但是B的完全导入又需要导入A,这样的错误会直接触发ImportError,出现这种错误的根本原因在于模块之间的相互依赖关系设计不当,导致导入关系形成了闭环。

为了深入了解这一过程背后的机理,我们首先需要明白import的执行过程:当A执行到import B,则停止执行A模块后面的代码,转而开始执行B模块的代码,当B模块从头执行到import A的地方时,Python此时并不会回过头去接着执行A剩余的代码,而是将A模块在中断前已经初始化的属性全加载到B模块中,B中获取不到A中想要的属性,即会报错。

解决方案:

但如果问题已经出现,我们不大可能重新设计思考代码编写思路与依赖关系,但在了解到import的执行过程背后的机理后,我们可以从其它角度出发考虑该问题并提出解决方案:

1.将 from xxx import yyy 改成 import xxx; 使用xxx.yyy形式来访问:

我们可以在初始化文件中导入包的模块,在另外一个模块需要使用时直接导入包,而在需要使用模块中的某个方法时,使用xxx.yyy的形式来访问即可,需要注意的是import...只能import到模块,不能import模块里面的成员变量,因此该方案的缺点就是访问模块里面的成员变量十分繁琐;

2.把导入语句放在语句块中(延迟访问):

除上述方法外,从import执行的深层机理考虑,我们可以将导入语句下放到语句块中,即我们哪里需要外部模块,就在哪里导入外部模块,这样的代码自然不会引起循环导入错误,但缺陷是增加了代码的复杂性和可读性,具体例子如下图所示:

 总的来说,这两种方法都可以解决循环引用问题,但需要根据具体情况选择合适的方法,并权衡其优缺点,但归根结底避免循环引用错误的最好办法还是设计良好的代码结构和模块依赖关系

五、实验总结与心得

5.1 MVC软件设计模式理解

在前两次的实验报告中,我均结合自己的实验过程总结了二次开发过程中的许多内容,随着学习的不断深入,也让我对MVC软件设计模式这一概念的理解越来越深刻,因此基于目前的学习程度我将自我的理解总结如下:

首先,MVC软件设计模式包含三个元素,表示数据的模型(Model)、表示用户界面的视图(View)和定义了用户在界面上操作的控制器(Controller),这样描述或许还有点抽象,我们来结合Qt中的具体运用分别进行详细的讲解:

  1. Model(data_control包):负责与数据源通信,与数据相关的操作都在模型中进行,比如获取图层属性信息、读取地图文件都属于模型部分;
  2. View(界面代码):负责显示数据给用户,并且接收用户的输入,比如由.ui文件转的.py文件以及继承类的.py文件都属于视图部分;
  3. Controller(逻辑代码):负责接收用户的输入,并作出相应的反应,比如由我们所创建的各个槽函数,就属于非常典型的控制器部分。

三者之间的关系为:用户对界面进行操作,视图将发射信号给控制器,控制器接收到视图发出的信号后,根据信号的类型和参数调用相应的方法对模型进行更新,模型更新数据后,最后通知视图更新显示内容给用户。

5.2 实现Action类思路总结

此次实验中我们首次接触同时也是我们认为需要重点学习的,就是如何通过创建类以管理QAction相关对象(3.3实现Action类),通俗来说就是如何通过我们自己的代码编写一个类似于实验二中提到的DefaultAction:

  1. 获取数据:从我们需要创建的Action类希望实现什么功能出发,考虑这个功能需要从哪些数据源获取数据、并进行哪些处理数据等操作;
  2. 设计界面:在Qt Designer中根据用户与数据展示需求,构建并设计美观易读的用户交互界面,使用户能够直观地理解和操作数据;
  3. 实现界面类型:回到逻辑的代码编写中,包括继承ui.py文件中不能修改的窗体类以及添加渲染器等等;
  4. 实现Action类:最后我们需要在逻辑代码中创建QAction对象(连接界面发出的信号),创建响应槽函数(连接获取数据的方法),并将二者绑定起来,最终实现我们的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值