9. Using the Map Canvas

힌트

여러분이 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)

The Map canvas widget is probably the most important widget within QGIS because it shows the map composed from overlaid map layers and allows interaction with the map and layers. The canvas always shows a part of the map defined by the current canvas extent. The interaction is done through the use of map tools: there are tools for panning, zooming, identifying layers, measuring, vector editing and others. Similar to other graphics programs, there is always one tool active and the user can switch between the available tools.

The map canvas is implemented with the QgsMapCanvas class in the qgis.gui module. The implementation is based on the Qt Graphics View framework. This framework generally provides a surface and a view where custom graphics items are placed and user can interact with them. We will assume that you are familiar enough with Qt to understand the concepts of the graphics scene, view and items. If not, please read the overview of the framework.

Whenever the map has been panned, zoomed in/out (or some other action that triggers a refresh), the map is rendered again within the current extent. The layers are rendered to an image (using the QgsMapRendererJob class) and that image is displayed on the canvas. The QgsMapCanvas class also controls refreshing of the rendered map. Besides this item which acts as a background, there may be more map canvas items.

Typical map canvas items are rubber bands (used for measuring, vector editing etc.) or vertex markers. The canvas items are usually used to give visual feedback for map tools, for example, when creating a new polygon, the map tool creates a rubber band canvas item that shows the current shape of the polygon. All map canvas items are subclasses of QgsMapCanvasItem which adds some more functionality to the basic QGraphicsItem objects.

요약하면 맵 캔버스 아키텍처는 다음 3가지 개념으로 이루어집니다.

  • 맵 캔버스 — 맵을 보여주는 데 쓰입니다.

  • map canvas items — additional items that can be displayed on the map canvas

  • map tools — for interaction with the map canvas

9.1. 맵 캔버스 내장시키기

Map canvas is a widget like any other Qt widget, so using it is as simple as creating and showing it.

canvas = QgsMapCanvas()
canvas.show()

This produces a standalone window with map canvas. It can be also embedded into an existing widget or window. When using .ui files and Qt Designer, place a QWidget on the form and promote it to a new class: set QgsMapCanvas as class name and set qgis.gui as header file. The pyuic5 utility will take care of it. This is a very convenient way of embedding the canvas. The other possibility is to manually write the code to construct map canvas and other widgets (as children of a main window or dialog) and create a layout.

맵 캔버스의 초기값은 배경은 검은색, 안티알리아싱은 사용하지 않는 것입니다. 배경을 하얀색으로 설정하고 부드러운 렌더링을 위해 안티알리아싱을 활성화하려면 다음과 같이 하십시오.

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

(In case you are wondering, Qt comes from PyQt.QtCore module and Qt.white is one of the predefined QColor instances.)

Now it is time to add some map layers. We will first open a layer and add it to the current project. Then we will set the canvas extent and set the list of layers for the canvas.

 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. 고무줄과 버텍스 마커

To show some additional data on top of the map in canvas, use map canvas items. It is possible to create custom canvas item classes (covered below), however there are two useful canvas item classes for convenience: QgsRubberBand for drawing polylines or polygons, and QgsVertexMarker for drawing points. They both work with map coordinates, so the shape is moved/scaled automatically when the canvas is being panned or zoomed.

To show a polyline:

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)

폴리곤의 포인트들이 1차원 리스트가 아니라는 점에 주의하십시오. 실제로, 폴리곤의 포인트들은 폴리곤의 선형 폐곡선을 담고 있는 폐곡선 리스트입니다. 첫 번째 폐곡선은 외곽 경계선이고, 그 다음의 (있을 수도 있고 없을 수도 있는) 폐곡선은 폴리곤 내부의 구멍에 해당합니다.

고무줄을 사용자 지정 할 수 있습니다. 즉 색상 및 선 두께를 변경할 수도 있습니다.

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

The canvas items are bound to the canvas scene. To temporarily hide them (and show them again), use the hide() and show() combo. To completely remove the item, you have to remove it from the scene of the canvas

canvas.scene().removeItem(r)

(C++의 경우 아이템을 그냥 삭제하는 것도 가능하지만, 파이썬에서 del r 명령어는 참조만 삭제할 뿐 실제 오브젝트는 캔버스가 소유하고 있으므로 계속 남아 있을 것입니다.)

Rubber band can be also used for drawing points, but the QgsVertexMarker class is better suited for this (QgsRubberBand would only draw a rectangle around the desired point).

You can use the vertex marker like this:

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

This will draw a red cross on position [10,45]. It is possible to customize the icon type, size, color and pen width

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

For temporary hiding of vertex markers and removing them from canvas, use the same methods as for rubber bands.

9.3. 캔버스에서 맵 도구 사용하기

The following example constructs a window that contains a map canvas and basic map tools for map panning and zooming. Actions are created for activation of each tool: panning is done with QgsMapToolPan, zooming in/out with a pair of QgsMapToolZoom instances. The actions are set as checkable and later assigned to the tools to allow automatic handling of checked/unchecked state of the actions – when a map tool gets activated, its action is marked as selected and the action of the previous map tool is deselected. The map tools are activated using setMapTool() method.

 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)

You can try the above code in the Python console editor. To invoke the canvas window, add the following lines to instantiate the MyWnd class. They will render the currently selected layer on the newly created canvas

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

9.3.1. Select a feature using QgsMapToolIdentifyFeature

You can use the map tool QgsMapToolIdentifyFeature for asking to the user to select a feature that will be sent to a callback function.

 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. Add items to map canvas contextual menu

Interaction with map canvas can also be done through entries you may add to its contextual menu using the contextMenuAboutToShow signal.

The following code adds My menu ► My Action action next to default entries when you right-click over the map canvas.

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. 사용자 지정 맵 도구 작성

You can write your custom tools, to implement a custom behavior to actions performed by users on the canvas.

Map tools should inherit from the QgsMapTool, class or any derived class, and selected as active tools in the canvas using the setMapTool() method as we have already seen.

다음 예시 코드는 캔버스 상에서 클릭과 드래그로 사각형 범위를 정의하도록 해주는 맵 도구입니다. 사각형이 정의되면, 콘솔에 그 범위 좌표를 출력합니다. 이전에 설명했던 고무줄 기능을 사용해서 확정되기 전의 사각형을 표시할 것입니다.

 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. 사용자 지정 맵 캔버스 아이템 작성

Here is an example of a custom canvas item that draws a circle:

 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)