跳转至

本节代码片段需要导入以下模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from qgis.PyQt.QtGui import (
    QColor,
)

from qgis.PyQt.QtCore import Qt, QRectF

from qgis.PyQt.QtWidgets import QMenu

from qgis.core import (
    QgsVectorLayer,
    QgsPoint,
    QgsPointXY,
    QgsProject,
    QgsGeometry,
    QgsMapRendererJob,
    QgsWkbTypes,
)

from qgis.gui import (
    QgsMapCanvas,
    QgsVertexMarker,
    QgsMapCanvasItem,
    QgsMapMouseEvent,
    QgsRubberBand,
)

9 使用地图画布⚓︎

地图画布控件可能是QGIS中最重要的控件,因为它显示了由重叠地图图层组成的地图,并允许与地图和图层进行交互。画布始终显示由当前画布范围定义地图的一部分。通过使用 地图工具 完成交互:有平移,缩放,识别图层,测量,矢量编辑等工具。与其他图形程序类似,总有一个工具处于活动状态,用户可以在可用工具之间切换。

地图画布由qgis.gui模块中的QgsMapCanvas类实现。该类基于Qt Graphics View框架。该框架通常提供场景和视图,其中放置自定义图形项,并且用户可以与它们交互。我们假设你对Qt足够熟悉,了解图形场景,视图和项的概念。如果没有,请阅读框架概述

无论何时平移,放大/缩小(或触发刷新的其他操作)地图,地图都会在当前范围内再次渲染。图层将渲染为图像(使用QgsMapRendererJob类),并显示在画布上。QgsMapCanvas类还控制渲染图的刷新。除了作为背景的项,可能还有更多的 地图画布项

典型的地图画布项是橡皮条(用于测量,矢量编辑等)或顶点标记。画布项通常用于给地图工具提供视觉反馈,例如,在创建新多边形时,地图工具会创建一个橡皮条画布项,显示多边形的当前形状。所有地图画布项都是QgsMapCanvasItem 的子类,它为基类QGraphicsItem对象添加了更多功能。

总而言之,地图画布架构包含三个概念:

  • 地图画布——用于浏览地图
  • 地图画布项——可以在地图画布上显示的其他项
  • 地图工具——用于与地图画布交互

9.1 嵌入地图画布⚓︎

地图画布是一个控件,就像任何其他Qt控件一样,因此使用它就像创建和显示它一样简单

1
2
canvas = QgsMapCanvas()
canvas.show()

这将生成一个带有地图画布的独立窗口。它也可以嵌入到现有的控件或窗口中。使用.ui文件和Qt设计师时,在表单上放置一个QWidget并将其提升为新类:设置QgsMapCanvas为类名并设置qgis.gui为头文件。pyuic5工具将搞定它(译者注:编译为py脚本文件)。这是嵌入画布的一种非常方便的方法。另一种可能性是手动编写代码构造地图画布和其他控件(作为主窗口或对话框的子窗口)并创建布局。

默认情况下,地图画布具有黑色背景,不使用消除锯齿。设置白色背景并启用抗锯齿来实现平滑渲染

1
2
canvas.setCanvasColor(Qt.white)
canvas.enableAntiAliasing(True)

(如果你想知道,Qt来自PyQt.QtCore模块,并且 Qt.white是预定义的QColor实例之一。)

现在是时候添加一些地图图层了。我们首先打开一个图层并将其添加到当前项目中。然后我们将设置画布范围并设置画布的图层列表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
vlayer = QgsVectorLayer("testdata/airports.shp", "Airports layer", "ogr")
if not vlayer.isValid():
    print("图层加载失败!")

# 将图层添加到注册表
QgsProject.instance().addMapLayer(vlayer)

# 缩放到图层
canvas.setExtent(vlayer.extent())

# 设置地图画布的图层集
canvas.setLayers([vlayer])

执行这些命令后,画布将显示已加载的图层。

9.2 橡皮条和顶点标记⚓︎

在画布上的地图顶部显示一些其他数据,使用地图画布项。可以创建自定义画布项类(如下所述),但为方便起见,有两个有用的画布项类:QgsRubberBand用于绘制折线或多边形,QgsVertexMarker绘制点。它们都使用地图坐标,因此在平移或缩放画布时会自动移动/缩放形状。

显示折线

1
2
3
r = QgsRubberBand(canvas, QgsWkbTypes.LineGeometry)  # 线
points = [QgsPointXY(-100, 45), QgsPointXY(10, 60), QgsPointXY(120, 45)]
r.setToGeometry(QgsGeometry.fromPolyline(points), None)

显示多边形

1
2
3
r = QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry)  # 多边形
points = [[QgsPointXY(-100, 35), QgsPointXY(10, 50), QgsPointXY(120, 35)]]
r.setToGeometry(QgsGeometry.fromPolygonXY(points), None)

请注意,多边形的点不是普通列表:实际上,它是包含多边形线环的环列表:第一个环是外边框,第二个(可选)环对应于多边形中的孔。

橡皮条允许一些定制,即改变它们的颜色和线宽

1
2
r.setColor(QColor(0, 0, 255))
r.setWidth(3)

画布项绑定到画布场景。要暂时隐藏它们(并再次显示它们),请使用hide()show()组合。完全删除该项,你必须将其从画布的场景中删除

1
canvas.scene().removeItem(r)

(在C ++中,可以只删除该项,但是在Pythondel r中 只删除引用,并且该对象仍然存在,因为它由画布拥有)

橡皮条也可用于绘制点,但 QgsVertexMarker类更适合于此(QgsRubberBand仅在所需点周围绘制一个矩形)。

你可以像这样使用顶点标记:

1
2
m = QgsVertexMarker(canvas)
m.setCenter(QgsPointXY(10,40))

这将在位置 [10,45] 上绘制一个红十字。可以自定义图标类型,大小,颜色和宽度

1
2
3
4
m.setColor(QColor(0, 255, 0))
m.setIconSize(5)
m.setIconType(QgsVertexMarker.ICON_BOX) # or ICON_CROSS, ICON_X
m.setPenWidth(3)

临时隐藏顶点标记并从画布中删除它们,使用与橡皮条相同的方法。

9.3 在画布中使用地图工具⚓︎

以下示例构造一个窗口,其中包含用于地图平移和缩放的地图画布和基本地图工具。激活每个工具:平移工具QgsMapToolPan,放大缩小工具QgsMapToolZoom。设置为可被选中,允许自动处理选中/未选中的操作状态——当激活地图工具时,一个工具被选中时,取消选中上一个工具。使用setMapTool()方法激活地图工具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from qgis.gui import *
from qgis.PyQt.QtWidgets import QAction, QMainWindow
from qgis.PyQt.QtCore import Qt


class MyWnd(QMainWindow):
    def __init__(self, layer):
        QMainWindow.__init__(self)

        self.canvas = QgsMapCanvas()
        self.canvas.setCanvasColor(Qt.white)

        self.canvas.setExtent(layer.extent())
        self.canvas.setLayers([layer])

        self.setCentralWidget(self.canvas)

        self.actionZoomIn = QAction("Zoom in", self)
        self.actionZoomOut = QAction("Zoom out", self)
        self.actionPan = QAction("Pan", self)

        self.actionZoomIn.setCheckable(True)
        self.actionZoomOut.setCheckable(True)
        self.actionPan.setCheckable(True)

        self.actionZoomIn.triggered.connect(self.zoomIn)
        self.actionZoomOut.triggered.connect(self.zoomOut)
        self.actionPan.triggered.connect(self.pan)

        self.toolbar = self.addToolBar("Canvas actions")
        self.toolbar.addAction(self.actionZoomIn)
        self.toolbar.addAction(self.actionZoomOut)
        self.toolbar.addAction(self.actionPan)

        # 创建地图工具
        self.toolPan = QgsMapToolPan(self.canvas)
        self.toolPan.setAction(self.actionPan)
        self.toolZoomIn = QgsMapToolZoom(self.canvas, False)  # false = in
        self.toolZoomIn.setAction(self.actionZoomIn)
        self.toolZoomOut = QgsMapToolZoom(self.canvas, True)  # true = out
        self.toolZoomOut.setAction(self.actionZoomOut)

        self.pan()

    def zoomIn(self):
        self.canvas.setMapTool(self.toolZoomIn)

    def zoomOut(self):
        self.canvas.setMapTool(self.toolZoomOut)

    def pan(self):
        self.canvas.setMapTool(self.toolPan)

你可以在Python控制台编辑器中尝试上述代码。调用画布窗口,添加以下代码以实例化MyWnd类。它们将在新创建的画布上渲染当前选定的图层

1
2
w = MyWnd(iface.activeLayer())
w.show()

9.3.1 使用QgsMapToolIdentifyFeature选择要素⚓︎

你可以使用地图工具QgsMapToolIdentifyFeature为用户选择一个要素,这个要素将被传递给回调函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def callback(feature):
    """当用户选择要素后被调用"""
    print("You clicked on feature {}".format(feature.id()))


canvas = iface.mapCanvas()
feature_identifier = QgsMapToolIdentifyFeature(canvas)

# 表示被选择的图层
feature_identifier.setLayer(vlayer)

# 当用户识别要素时触发槽函数,使用回调函数
feature_identifier.featureIdentified.connect(callback)

# 激活这个地图工具
canvas.setMapTool(feature_identifier)

9.3.2 将项目添加到地图画布上下文菜单⚓︎

使用地图画布也可以通过使用contextMenuAboutToShow信号添加到其上下文菜单中的条目来完成。

当你右键单击地图画布时,以下代码会在默认条目旁边添加 My MenuMy Action 操作。

1
2
3
4
5
6
7
8
9
# 用于填充上下文菜单的插槽
def populateContextMenu(menu: QMenu, event: QgsMapMouseEvent):
    subMenu = menu.addMenu('My Menu')
    action = subMenu.addAction('My Action')
    action.triggered.connect(lambda *args:
                             print(f'Action triggered at {event.x()},{event.y()}'))

canvas.contextMenuAboutToShow.connect(populateContextMenu)
canvas.show()

9.4 编写自定义地图工具⚓︎

你可以编写自定义工具,来实现用户在画布上执行自定义行为的操作。

地图工具应继承自QgsMapTool类或任何派生类,并使用setMapTool() 在画布中选择为激活工具。

下面是一个地图工具示例,它允许通过在画布上单击并拖动来定义矩形范围。定义矩形后,它会在控制台中打印其边界坐标。它使用前面描述的橡皮条元素来显示所定义的矩形。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class RectangleMapTool(QgsMapToolEmitPoint):
    def __init__(self, canvas):
        self.canvas = canvas
        QgsMapToolEmitPoint.__init__(self, self.canvas)
        self.rubberBand = QgsRubberBand(self.canvas, QgsWkbTypes.PolygonGeometry)
        self.rubberBand.setColor(Qt.red)
        self.rubberBand.setWidth(1)
        self.reset()

    def reset(self):
        self.startPoint = self.endPoint = None
        self.isEmittingPoint = False
        self.rubberBand.reset(QgsWkbTypes.PolygonGeometry)

    def canvasPressEvent(self, e):
        self.startPoint = self.toMapCoordinates(e.pos())
        self.endPoint = self.startPoint
        self.isEmittingPoint = True
        self.showRect(self.startPoint, self.endPoint)

    def canvasReleaseEvent(self, e):
        self.isEmittingPoint = False
        r = self.rectangle()
        if r is not None:
            print("Rectangle:", r.xMinimum(),
                  r.yMinimum(), r.xMaximum(), r.yMaximum()
                  )

    def canvasMoveEvent(self, e):
        if not self.isEmittingPoint:
            return

        self.endPoint = self.toMapCoordinates(e.pos())
        self.showRect(self.startPoint, self.endPoint)

    def showRect(self, startPoint, endPoint):
        self.rubberBand.reset(QgsWkbTypes.PolygonGeometry)
        if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
            return

        point1 = QgsPointXY(startPoint.x(), startPoint.y())
        point2 = QgsPointXY(startPoint.x(), endPoint.y())
        point3 = QgsPointXY(endPoint.x(), endPoint.y())
        point4 = QgsPointXY(endPoint.x(), startPoint.y())

        self.rubberBand.addPoint(point1, False)
        self.rubberBand.addPoint(point2, False)
        self.rubberBand.addPoint(point3, False)
        self.rubberBand.addPoint(point4, True)  # true to update canvas
        self.rubberBand.show()

    def rectangle(self):
        if self.startPoint is None or self.endPoint is None:
            return None
        elif (self.startPoint.x() == self.endPoint.x() or \
              self.startPoint.y() == self.endPoint.y()):
            return None

        return QgsRectangle(self.startPoint, self.endPoint)

    def deactivate(self):
        QgsMapTool.deactivate(self)
        self.deactivated.emit()

9.5 编写自定义地图画布项⚓︎

这是一个自定义画布项的示例,该画布项绘制了一个圆:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class CircleCanvasItem(QgsMapCanvasItem):
    def __init__(self, canvas):
        super().__init__(canvas)
        self.center = QgsPoint(0, 0)
        self.size = 100

    def setCenter(self, center):
        self.center = center

    def center(self):
        return self.center

    def setSize(self, size):
        self.size = size

    def size(self):
        return self.size

    def boundingRect(self):
        return QRectF(self.center.x() - self.size / 2,
                      self.center.y() - self.size / 2,
                      self.center.x() + self.size / 2,
                      self.center.y() + self.size / 2)

    def paint(self, painter, option, widget):
        path = QPainterPath()
        path.moveTo(self.center.x(), self.center.y());
        path.arcTo(self.boundingRect(), 0.0, 360.0)
        painter.fillPath(path, QColor("red"))


# 使用自定义项:
item = CircleCanvasItem(iface.mapCanvas())
item.setCenter(QgsPointXY(200, 200))
item.setSize(80)