9. 使用地图画布

提示

如果您在pyqgis控制台之外,则此页面上的代码片段需要以下导入:

 1from qgis.PyQt.QtGui import (
 2    QColor,
 3)
 4
 5from qgis.PyQt.QtCore import Qt, QRectF
 6
 7from qgis.PyQt.QtWidgets import QMenu
 8
 9from qgis.core import (
10    QgsVectorLayer,
11    QgsPoint,
12    QgsPointXY,
13    QgsProject,
14    QgsGeometry,
15    QgsMapRendererJob,
16    QgsWkbTypes,
17)
18
19from qgis.gui import (
20    QgsMapCanvas,
21    QgsVertexMarker,
22    QgsMapCanvasItem,
23    QgsMapMouseEvent,
24    QgsRubberBand,
25)

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

地图画布是用 QgsMapCanvas 中的类 qgis.gui 模块。该实现基于Qt Graphics View框架。该框架通常提供放置定制图形项并且用户可以与其交互的表面和视图。我们将假设您对Qt足够熟悉,能够理解图形场景、视图和项目的概念。如果没有,请阅读 overview of the framework

每当地图被平移、放大/缩小(或触发刷新的某些其他操作)时,地图将在当前范围内再次渲染。将这些层呈现为图像(使用 QgsMapRendererJob 类),并且该图像显示在画布上。这个 QgsMapCanvas 类还控制渲染地图的刷新。除了这个作为背景的项目外,可能还有更多 map canvas items

典型的地图画布项目是橡皮筋(用于测量、矢量编辑等)。或顶点标记。画布项通常用于为地图工具提供视觉反馈,例如,当创建新的多边形时,地图工具会创建一个显示多边形当前形状的橡皮筋画布项。所有地图画布项目都是的子类 QgsMapCanvasItem ,它为基本的 QGraphicsItem 物体。

总而言之,地图画布体系结构由三个概念组成:

  • 地图画布-用于查看地图

  • 地图画布项目-可在地图画布上显示的其他项目

  • 地图工具-用于与地图画布交互

9.1. 嵌入地图画布

Map Canvas是一个小部件,就像其他任何Qt小部件一样,所以使用它就像创建和显示它一样简单。

canvas = QgsMapCanvas()
canvas.show()

这将产生一个带有地图画布的独立窗口。它还可以嵌入到现有的小部件或窗口中。使用时 .ui 文件和Qt设计器,将一个 QWidget 并将其提升到一个新的类:Set QgsMapCanvas 作为类名和集合 qgis.gui 作为头文件。这个 pyuic5 公用事业公司会处理它的。这是一种非常方便的嵌入画布的方式。另一种可能是手动编写代码来构建地图画布和其他小部件(作为主窗口或对话框的子窗口)并创建布局。

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

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

(如果您想知道, Qt 来自 PyQt.QtCore 模块和 Qt.white 是预定义的 QColor 实例。)

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

 1vlayer = QgsVectorLayer('testdata/airports.shp', "Airports layer", "ogr")
 2if not vlayer.isValid():
 3    print("Layer failed to load!")
 4
 5# add layer to the registry
 6QgsProject.instance().addMapLayer(vlayer)
 7
 8# set extent to the extent of our layer
 9canvas.setExtent(vlayer.extent())
10
11# set the map canvas layer set
12canvas.setLayers([vlayer])

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

9.2. 橡皮筋和顶点标记

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

显示多段线的步骤:

r = QgsRubberBand(canvas, QgsWkbTypes.LineGeometry)  # line
points = [QgsPoint(-100, 45), QgsPoint(10, 60), QgsPoint(120, 45)]
r.setToGeometry(QgsGeometry.fromPolyline(points), None)

显示多边形的步骤

r = QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry)  # polygon
points = [[QgsPointXY(-100, 35), QgsPointXY(10, 50), QgsPointXY(120, 35)]]
r.setToGeometry(QgsGeometry.fromPolygonXY(points), None)

请注意,多边形点不是一个简单的列表:实际上,它是包含多边形线性环的环的列表:第一个环是外边界,其他(可选)环对应于多边形中的洞。

橡皮筋允许进行一些定制,即更改其颜色和线条宽度

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

将画布项目绑定到画布场景。若要临时隐藏它们(并再次显示它们),请使用 hide()show() 套餐。要完全移除该项目,必须将其从画布场景中移除

canvas.scene().removeItem(r)

(在C++中可以只删除该项,但在Python中则可以 del r 只需删除引用,该对象仍将存在,因为它属于画布)

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

可以按如下方式使用顶点标记:

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

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

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() 方法。

 1from qgis.gui import *
 2from qgis.PyQt.QtWidgets import QAction, QMainWindow
 3from qgis.PyQt.QtCore import Qt
 4
 5class MyWnd(QMainWindow):
 6    def __init__(self, layer):
 7        QMainWindow.__init__(self)
 8
 9        self.canvas = QgsMapCanvas()
10        self.canvas.setCanvasColor(Qt.white)
11
12        self.canvas.setExtent(layer.extent())
13        self.canvas.setLayers([layer])
14
15        self.setCentralWidget(self.canvas)
16
17        self.actionZoomIn = QAction("Zoom in", self)
18        self.actionZoomOut = QAction("Zoom out", self)
19        self.actionPan = QAction("Pan", self)
20
21        self.actionZoomIn.setCheckable(True)
22        self.actionZoomOut.setCheckable(True)
23        self.actionPan.setCheckable(True)
24
25        self.actionZoomIn.triggered.connect(self.zoomIn)
26        self.actionZoomOut.triggered.connect(self.zoomOut)
27        self.actionPan.triggered.connect(self.pan)
28
29        self.toolbar = self.addToolBar("Canvas actions")
30        self.toolbar.addAction(self.actionZoomIn)
31        self.toolbar.addAction(self.actionZoomOut)
32        self.toolbar.addAction(self.actionPan)
33
34        # create the map tools
35        self.toolPan = QgsMapToolPan(self.canvas)
36        self.toolPan.setAction(self.actionPan)
37        self.toolZoomIn = QgsMapToolZoom(self.canvas, False) # false = in
38        self.toolZoomIn.setAction(self.actionZoomIn)
39        self.toolZoomOut = QgsMapToolZoom(self.canvas, True) # true = out
40        self.toolZoomOut.setAction(self.actionZoomOut)
41
42        self.pan()
43
44    def zoomIn(self):
45        self.canvas.setMapTool(self.toolZoomIn)
46
47    def zoomOut(self):
48        self.canvas.setMapTool(self.toolZoomOut)
49
50    def pan(self):
51        self.canvas.setMapTool(self.toolPan)

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

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

9.3.1. 使用QgsMapTool标识要素选择要素

您可以使用地图工具 QgsMapToolIdentifyFeature 用于要求用户选择将被发送到回调函数的特征。

 1def callback(feature):
 2  """Code called when the feature is selected by the user"""
 3  print("You clicked on feature {}".format(feature.id()))
 4
 5canvas = iface.mapCanvas()
 6feature_identifier = QgsMapToolIdentifyFeature(canvas)
 7
 8# indicates the layer on which the selection will be done
 9feature_identifier.setLayer(vlayer)
10
11# use the callback as a slot triggered when the user identifies a feature
12feature_identifier.featureIdentified.connect(callback)
13
14# activation of the map tool
15canvas.setMapTool(feature_identifier)

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

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

下面的代码添加了 My menu ► My Action 在地图画布上单击鼠标右键时,请执行默认条目旁边的操作。

1# a slot to populate the context menu
2def populateContextMenu(menu: QMenu, event: QgsMapMouseEvent):
3    subMenu = menu.addMenu('My Menu')
4    action = subMenu.addAction('My Action')
5    action.triggered.connect(lambda *args:
6                             print(f'Action triggered at {event.x()},{event.y()}'))
7
8canvas.contextMenuAboutToShow.connect(populateContextMenu)
9canvas.show()

9.4. 编写自定义地图工具

您可以编写定制工具,以实现用户在画布上执行的操作的定制行为。

地图工具应继承自 QgsMapTool 类或任何派生类,并在画布中使用 setMapTool() 方法,正如我们已经看到的。

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

 1class RectangleMapTool(QgsMapToolEmitPoint):
 2  def __init__(self, canvas):
 3    self.canvas = canvas
 4    QgsMapToolEmitPoint.__init__(self, self.canvas)
 5    self.rubberBand = QgsRubberBand(self.canvas, QgsWkbTypes.PolygonGeometry)
 6    self.rubberBand.setColor(Qt.red)
 7    self.rubberBand.setWidth(1)
 8    self.reset()
 9
10  def reset(self):
11    self.startPoint = self.endPoint = None
12    self.isEmittingPoint = False
13    self.rubberBand.reset(QgsWkbTypes.PolygonGeometry)
14
15  def canvasPressEvent(self, e):
16    self.startPoint = self.toMapCoordinates(e.pos())
17    self.endPoint = self.startPoint
18    self.isEmittingPoint = True
19    self.showRect(self.startPoint, self.endPoint)
20
21  def canvasReleaseEvent(self, e):
22    self.isEmittingPoint = False
23    r = self.rectangle()
24    if r is not None:
25      print("Rectangle:", r.xMinimum(),
26            r.yMinimum(), r.xMaximum(), r.yMaximum()
27           )
28
29  def canvasMoveEvent(self, e):
30    if not self.isEmittingPoint:
31      return
32
33    self.endPoint = self.toMapCoordinates(e.pos())
34    self.showRect(self.startPoint, self.endPoint)
35
36  def showRect(self, startPoint, endPoint):
37    self.rubberBand.reset(QgsWkbTypes.PolygonGeometry)
38    if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
39      return
40
41    point1 = QgsPointXY(startPoint.x(), startPoint.y())
42    point2 = QgsPointXY(startPoint.x(), endPoint.y())
43    point3 = QgsPointXY(endPoint.x(), endPoint.y())
44    point4 = QgsPointXY(endPoint.x(), startPoint.y())
45
46    self.rubberBand.addPoint(point1, False)
47    self.rubberBand.addPoint(point2, False)
48    self.rubberBand.addPoint(point3, False)
49    self.rubberBand.addPoint(point4, True)    # true to update canvas
50    self.rubberBand.show()
51
52  def rectangle(self):
53    if self.startPoint is None or self.endPoint is None:
54      return None
55    elif (self.startPoint.x() == self.endPoint.x() or \
56          self.startPoint.y() == self.endPoint.y()):
57      return None
58
59      return QgsRectangle(self.startPoint, self.endPoint)
60
61  def deactivate(self):
62    QgsMapTool.deactivate(self)
63    self.deactivated.emit()

9.5. 编写自定义地图画布项目

下面是一个绘制圆形的自定义画布项目的示例:

 1class CircleCanvasItem(QgsMapCanvasItem):
 2  def __init__(self, canvas):
 3    super().__init__(canvas)
 4    self.center = QgsPoint(0, 0)
 5    self.size   = 100
 6
 7  def setCenter(self, center):
 8    self.center = center
 9
10  def center(self):
11    return self.center
12
13  def setSize(self, size):
14    self.size = size
15
16  def size(self):
17    return self.size
18
19  def boundingRect(self):
20    return QRectF(self.center.x() - self.size/2,
21      self.center.y() - self.size/2,
22      self.center.x() + self.size/2,
23      self.center.y() + self.size/2)
24
25  def paint(self, painter, option, widget):
26    path = QPainterPath()
27    path.moveTo(self.center.x(), self.center.y());
28    path.arcTo(self.boundingRect(), 0.0, 360.0)
29    painter.fillPath(path, QColor("red"))
30
31
32# Using the custom item:
33item = CircleCanvasItem(iface.mapCanvas())
34item.setCenter(QgsPointXY(200,200))
35item.setSize(80)