1. 程式人生 > >讓我們開發一個非常好用的捲簾工具

讓我們開發一個非常好用的捲簾工具

版權宣告:未經作者允許不得轉載,此外掛不得用於商業用途。

目錄

開發環境

外掛開發

__init__.py

map_swipe_plugin.py

map_swipe_tool.py

active

deactivate

canvasPressEvent

canvasReleaseEvent

canvasMoveEvent

swipe_map.py

實現結果


開發環境

  • QGIS 3.2
  • python(QGIS自帶)

外掛開發

關於QGIS中使用python開發外掛的方法,自行檢視官方文件(https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins.html

__init__.py

from .map_swipe_plugin import MapSwipePlugin


def classFactory(iface):
    return MapSwipePlugin(iface)

map_swipe_plugin.py

import os

from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QComboBox, QPushButton
from qgis.core import QgsProject
from qgis.gui import QgsMapToolPan

from .map_swipe_tool import MapSwipeTool

plugin_path = os.path.dirname(__file__)


class MapSwipePlugin:

    def __init__(self, iface):
        self.iface = iface
        self.canvas = iface.mapCanvas()
        # 圖層變化訊號
        QgsProject.instance().layerTreeRoot().layerOrderChanged.connect(self.combobox_add_items)

    def initGui(self):
        self.menu = self.title = "捲簾工具"
        self._create_widget()
        self.tool = MapSwipeTool(plugin_path, self.combobox, self.iface)
        self.tool.deactivated.connect(self.tool_deactivated)
        self.widget_action = self.iface.addToolBarWidget(self.widget)

    def unload(self):
        self.canvas.setMapTool(QgsMapToolPan(self.iface.mapCanvas()))
        self.iface.removeToolBarIcon(self.widget_action)
        del self.widget_action

    def run(self):
        if self.canvas.mapTool() != self.tool:
            self.prevTool = self.canvas.mapTool()
            self.canvas.setMapTool(self.tool)
        else:
            self.canvas.setMapTool(self.prevTool)

        if self.pushbutton.isChecked() and self.combobox.isHidden():
            self.combobox.show()
            self.combobox_add_items()
        else:
            self.combobox.hide()

    def _create_widget(self):
        icon = QIcon(os.path.join(plugin_path, 'icon.png'))
        # 新建widget
        self.widget = QWidget(self.iface.mainWindow())
        self.hlayout = QHBoxLayout(self.widget)
        self.hlayout.setContentsMargins(0, 0, 0, 0)
        self.pushbutton = QPushButton(icon, '', self.widget)
        self.pushbutton.setCheckable(True)
        self.pushbutton.setFlat(True)
        self.combobox = QComboBox(self.widget)
        self.hlayout.addWidget(self.pushbutton)
        self.hlayout.addWidget(self.combobox)

        self.combobox.hide()
        self.combobox_add_items()
        self.pushbutton.clicked.connect(self.run)

    def combobox_add_items(self):
        self.combobox.clear()
        layers = QgsProject.instance().layerTreeRoot().layerOrder()
        self.combobox.addItems([layer.name() for layer in layers])

    def tool_deactivated(self):
        '''tool非啟用狀態'''
        self.pushbutton.setChecked(False)
        self.combobox.hide()

map_swipe_tool.py

import os
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QCursor, QPixmap
from qgis.gui import QgsMapTool
from qgis.core import QgsProject, QgsMapSettings, QgsMapRendererParallelJob
from .swipe_map import SwipeMap


class MapSwipeTool(QgsMapTool):
    def __init__(self, plugin_path, combobox, iface):
        super(MapSwipeTool, self).__init__(iface.mapCanvas())
        self.combobox = combobox
        self.map_canvas = iface.mapCanvas()
        self.view = iface.layerTreeView()
        self.swipe = SwipeMap(self.map_canvas)
        self.hasSwipe = None
        self.start_point = QPoint()

        self.cursorSV = QCursor(QPixmap(os.path.join(plugin_path, 'images/split_v.png')))
        self.cursorSH = QCursor(QPixmap(os.path.join(plugin_path, 'images/split_h.png')))
        self.cursorUP = QCursor(QPixmap(os.path.join(plugin_path, 'images/up.png')))
        self.cursorDOWN = QCursor(QPixmap(os.path.join(plugin_path, 'images/down.png')))
        self.cursorLEFT = QCursor(QPixmap(os.path.join(plugin_path, 'images/left.png')))
        self.cursorRIGHT = QCursor(QPixmap(os.path.join(plugin_path, 'images/right.png')))

    def activate(self):
        self.map_canvas.setCursor(QCursor(Qt.CrossCursor))
        self._connect()
        self.hasSwipe = False
        self.setLayersSwipe()

    def canvasPressEvent(self, e):
        self.hasSwipe = True
        direction = None
        w, h = self.map_canvas.width(), self.map_canvas.height()
        if 0.25 * w < e.x() < 0.75 * w and e.y() < 0.5 * h:
            direction = 0  # '⬇'
            self.swipe.isVertical = False
        if 0.25 * w < e.x() < 0.75 * w and e.y() > 0.5 * h:
            direction = 1  # '⬆'
            self.swipe.isVertical = False
        if e.x() < 0.25 * w:
            direction = 2  # '➡'
            self.swipe.isVertical = True
        if e.x() > 0.75 * w:
            direction = 3  # '⬅'
            self.swipe.isVertical = True

        self.swipe.set_direction(direction)
        self.map_canvas.setCursor(self.cursorSH if self.swipe.isVertical else self.cursorSV)
        self.swipe.set_img_extent(e.x(), e.y())

    def canvasReleaseEvent(self, e):
        self.hasSwipe = False
        self.canvasMoveEvent(e)
        # 滑鼠釋放後,移除繪製的線
        self.swipe.set_img_extent(-9999, -9999)

    def canvasMoveEvent(self, e):
        if self.hasSwipe:
            self.swipe.set_img_extent(e.x(), e.y())
        else:
            # 設定當前cursor
            w, h = self.map_canvas.width(), self.map_canvas.height()
            if e.x() < 0.25 * w:
                self.canvas().setCursor(self.cursorRIGHT)
            if e.x() > 0.75 * w:
                self.canvas().setCursor(self.cursorLEFT)
            if 0.25 * w < e.x() < 0.75 * w and e.y() < 0.5 * h:
                self.canvas().setCursor(self.cursorDOWN)
            if 0.25 * w < e.x() < 0.75 * w and e.y() > 0.5 * h:
                self.canvas().setCursor(self.cursorUP)

    def _connect(self, isConnect=True):
        signal_slot = (
            {'signal': self.map_canvas.mapCanvasRefreshed, 'slot': self.setMap},
            {'signal': self.combobox.currentIndexChanged, 'slot': self.setLayersSwipe},
            {'signal': QgsProject.instance().removeAll, 'slot': self.disable}
        )
        if isConnect:
            for item in signal_slot:
                item['signal'].connect(item['slot'])
        else:
            for item in signal_slot:
                item['signal'].disconnect(item['slot'])

    def setLayersSwipe(self, ):
        current_layer = QgsProject.instance().mapLayersByName(self.combobox.currentText())
        if len(current_layer) == 0:
            return
        layers = QgsProject.instance().layerTreeRoot().layerOrder()
        layer_list = []
        for layer in layers:
            if layer.id() == current_layer[0].id():
                continue
            layer_list.append(layer)
        self.swipe.clear()
        self.swipe.setLayersId(layer_list)
        self.setMap()

    def disable(self):
        self.swipe.clear()
        self.hasSwipe = False

    def deactivate(self):
        self.deactivated.emit()
        self.swipe.clear()
        self._connect(False)

    def setMap(self):
        def finished():
            self.swipe.setContent(job.renderedImage(), self.map_canvas.extent())

        if len(self.swipe.layers) == 0:
            return

        settings = QgsMapSettings(self.map_canvas.mapSettings())
        settings.setLayers(self.swipe.layers)

        job = QgsMapRendererParallelJob(settings)
        job.start()
        job.finished.connect(finished)
        job.waitForFinished()

MapSwipeTool繼承了QgsMapTool類,重新實現了activate、deactivate、canvasPressEvent、canvasReleaseEvent、canvasMoveEvent方法,還用到了QgsMapRendererParallelJob類,QgsMapRendererParallelJob是QGIS提供的並行繪圖類,不做詳解,下面對重新實現的方法詳細說明:

active

工具啟用後執行此函式

deactivate

工具非啟用狀態執行此函式

canvasPressEvent

    def canvasPressEvent(self, e):
        self.hasSwipe = True
        direction = None
        w, h = self.map_canvas.width(), self.map_canvas.height()
        if 0.25 * w < e.x() < 0.75 * w and e.y() < 0.5 * h:
            direction = 0  # '⬇'
            self.swipe.isVertical = False
        if 0.25 * w < e.x() < 0.75 * w and e.y() > 0.5 * h:
            direction = 1  # '⬆'
            self.swipe.isVertical = False
        if e.x() < 0.25 * w:
            direction = 2  # '➡'
            self.swipe.isVertical = True
        if e.x() > 0.75 * w:
            direction = 3  # '⬅'
            self.swipe.isVertical = True

        self.swipe.set_direction(direction)
        self.map_canvas.setCursor(self.cursorSH if self.swipe.isVertical else self.cursorSV)
        self.swipe.set_img_extent(e.x(), e.y())

畫布的滑鼠按壓事件,將畫布分為四個部分,獲得捲簾的方向,然後設定滑鼠游標,具體實現方式自行腦補,原理如下圖:

canvasReleaseEvent

    def canvasReleaseEvent(self, e):
        self.hasSwipe = False
        self.canvasMoveEvent(e)
        # 滑鼠釋放後,移除繪製的線
        self.swipe.set_img_extent(-9999, -9999)

畫布的滑鼠釋放事件,滑鼠釋放後要把繪製的線移出畫布

self.swipe.set_img_extent(-9999, -9999)

canvasMoveEvent

    def canvasMoveEvent(self, e):
        if self.hasSwipe:
            self.swipe.set_img_extent(e.x(), e.y())
        else:
            # 設定當前cursor
            w, h = self.map_canvas.width(), self.map_canvas.height()
            if e.x() < 0.25 * w:
                self.canvas().setCursor(self.cursorRIGHT)
            if e.x() > 0.75 * w:
                self.canvas().setCursor(self.cursorLEFT)
            if 0.25 * w < e.x() < 0.75 * w and e.y() < 0.5 * h:
                self.canvas().setCursor(self.cursorDOWN)
            if 0.25 * w < e.x() < 0.75 * w and e.y() > 0.5 * h:
                self.canvas().setCursor(self.cursorUP)

畫布的滑鼠移動事件,當使用捲簾時,獲得滑鼠的位置(e.x(),e.y()),當不使用捲簾時,根據滑鼠的位置設定滑鼠的游標形狀,原理與canvasPressEvent相同

swipe_map.py

from PyQt5.QtCore import QRectF, QPointF, Qt
from PyQt5.QtGui import QPen, QColor
from qgis.gui import QgsMapCanvasItem


class SwipeMap(QgsMapCanvasItem):
    def __init__(self, canvas):
        super(SwipeMap, self).__init__(canvas)
        self.length = 0
        self.isVertical = True
        self.layers = []
        self.is_paint = False

    def setContent(self, image, rect):
        self.copyimage = image
        self.setRect(rect)

    def clear(self):
        del self.layers[:]
        self.is_paint = False

    def setLayersId(self, layers):
        del self.layers[:]
        for item in layers:
            self.layers.append(item)

    def set_direction(self, direction):
        # 0:'⬇', 1:'⬆', 2:'➡', 3:'⬅'
        if direction == 0:
            self.direction = 0
        elif direction == 1:
            self.direction = 1
        elif direction == 2:
            self.direction = 2
        else:
            self.direction = 3
        self.startx, self.starty, self.endx, self.endy = 0, 0, self.boundingRect().width(), self.boundingRect().height()

    def set_img_extent(self, x, y):
        self.x = x
        self.y = y
        if self.direction == 0:  # 0:'⬇'
            self.endy = y
        elif self.direction == 1:  # 1:'⬆'
            self.starty = y
        elif self.direction == 2:  # 2:'➡'
            self.endx = x
        else:  # 3:'⬅'
            self.startx = x
        self.is_paint = True
        self.update()

    def paint(self, painter, *args):
        if len(self.layers) == 0 or self.is_paint == False:
            return

        w = self.boundingRect().width()
        h = self.boundingRect().height()

        pen = QPen(Qt.DashDotDotLine)
        pen.setColor(QColor(18, 150, 219))
        pen.setWidth(4)

        if self.isVertical:
            painter.setPen(pen)
            painter.drawLine(QPointF(self.x, 0), QPointF(self.x, h))
        else:
            painter.setPen(pen)
            painter.drawLine(QPointF(0, self.y), QPointF(w, self.y))

        image = self.copyimage.copy(self.startx, self.starty, self.endx, self.endy)
        painter.drawImage(QRectF(self.startx, self.starty, self.endx, self.endy), image)

此模組為繪製地圖模組,首先獲得繪製地圖的範圍

self.startx, self.starty, self.endx, self.endy = 0, 0, self.boundingRect().width(), self.boundingRect().height()

然後重寫繪製函式paint

    def paint(self, painter, *args):
        if len(self.layers) == 0 or self.is_paint == False:
            return

        w = self.boundingRect().width()
        h = self.boundingRect().height()

        pen = QPen(Qt.DashDotDotLine)
        pen.setColor(QColor(18, 150, 219))
        pen.setWidth(4)

        if self.isVertical:
            painter.setPen(pen)
            painter.drawLine(QPointF(self.x, 0), QPointF(self.x, h))
        else:
            painter.setPen(pen)
            painter.drawLine(QPointF(0, self.y), QPointF(w, self.y))

        image = self.copyimage.copy(self.startx, self.starty, self.endx, self.endy)
        painter.drawImage(QRectF(self.startx, self.starty, self.endx, self.endy), image)

實現結果

參考連結:https://plugins.qgis.org/plugins/mapswipetool_plugin/