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)

マップキャンバスウィジェットはQGISで最も重要なウィジェットでしょう。なぜなら、重ね合う地図レイヤから構成される地図を表示し、地図やレイヤの対話処理を可能にするからです。キャンバスは常に、現在のキャンバスの範囲によって決まるマップの一部を表示します。対話処理は マップツール の使用により行われます。マップツールには:パン、ズーム、レイヤの識別、測定、ベクタ編集などがあります。他のグラフィックプログラムと同様に、常に1つのツールがアクティブで、ユーザーは利用可能なツールの間で切り替えることができます。

マップキャンバスは qgis.gui モジュールの QgsMapCanvas クラスで実装されます。この実装は、Qt Graphics Viewフレームワークをベースにしています。このフレームワークは一般的に、カスタムグラフィックアイテムが配置され、ユーザーがそれらを操作することができるサーフェスとビューを提供します。 ここでは、グラフィックシーン、ビュー、アイテムの概念を理解できるほどQtに精通していることを前提とします。もしそうでなければ、フレームワークの概要 を読んでください。

マップがパン、ズームイン/アウト(またはリフレッシュをトリガーする他のアクション)されるたびに、マップは現在の範囲内で再レンダリングされます。レイヤは画像にレンダリングされ( QgsMapRendererJob クラスを使用)、その画像はキャンバス上に表示されます。また、 QgsMapCanvas クラスはレンダリングされたマップの更新を制御します。背景として機能するこのアイテムの他にも、マップキャンバスアイテム がある場合があります。

典型的なマップキャンバスアイテムは、ラバーバンド(測定、ベクター編集などに使用)または頂点マーカーです。キャンバスアイテムは通常、マップツールの視覚的なフィードバックを与えるために使用されます。例えば、新しいポリゴンを作成するとき、マップツールはポリゴンの現在の形状を示すラバーバンドのキャンバスアイテムを作成します。全てのマップキャンバスアイテムは QgsMapCanvasItem のサブクラスで、基本的な QGraphicsItem オブジェクトにさらにいくつかの機能を追加しています。

要約すると、マップキャンバスアーキテクチャは3つのコンセプトからなります:

  • map canvas --- 地図の可視化

  • マップキャンバスアイテム -- マップキャンバスに表示される追加の項目

  • マップツール -- マップキャンバスとの対話処理用

9.1. マップキャンバスを埋め込む

マップキャンバスは他のQtウィジェットと同様にウィジェットなので、作成し表示するだけで簡単に使用できます。

canvas = QgsMapCanvas()
canvas.show()

マップキャンバスを持つスタンドアロンウィンドウを作成します。また、既存のウィジェットやウィンドウに埋め込むこともできます。.ui ファイルと Qtデザイナーを使用する場合は、フォーム上に QWidget を配置し、それを新しいクラスに昇格させます: クラス名として QgsMapCanvas を、ヘッダーファイルとして qgis.gui をセットしてください。pyuic5 ユーティリティがこれを処理します。これは、キャンバスを埋め込むためのとても便利な方法です。もう一つの可能性は、マップキャンバスと他のウィジェット(メインウィンドウやダイアログの子ウィジェットとして)を構築し、レイアウトを作成するコードを手動で書くことです。

デフォルトでは、マップキャンバスの背景色は黒でありアンチエイリアスは使用されません。背景を白に設定し、投影をなめらかにするためのアンチエイリアスを有効にするには

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

(不思議に思うかもしれませんが、QtPyQt.QtCore モジュールに由来し、Qt.white は定義済みの QColor インスタンスの 1 つです。)

それでは、地図レイヤを追加していきましょう。まずレイヤを開いて、現在のプロジェクトに追加します。次に、キャンバスの範囲を設定し、キャンバスのレイヤのリストを設定します。

 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. ラバーバンドと頂点マーカー

キャンバスで地図の上に追加データを表示するには、マップキャンバスアイテムを使います。カスタムのキャンバスアイテムクラスを作る(以下で説明します)ことも可能ですが、便利なキャンバスアイテムクラスが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. QgsMapToolIdentifyFeatureを使って地物を選択します

マップツール 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)