给QGIS开发一个卷帘工具

本文详细介绍了一款基于QGIS的Python插件开发过程,包括开发环境配置、关键类和方法实现,如MapSwipePlugin和MapSwipeTool。该插件通过自定义工具实现地图卷帘效果,展示了如何在QGIS中利用Python进行地图操作和用户交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

版权声明:未经作者允许不得转载,此插件不得用于商业用途。

目录

开发环境

插件开发

__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开发插件的方法,自行查看官方文档

__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)

实现结果

项目地址:MapSwipeTool

<think>我们首先需要理解用户意图:用户想了解在高德地图中使用卷帘功能的方法。卷帘功能通常用于地图对比,比如同时显示两个不同时期的地图,通过拖动滑条来查看变化。然而,高德地图官方并没有直接称为“卷帘功能”的工具。因此,我们需要搜索高德地图是否支持类似功能,或者是否有其他方法实现卷帘效果。经过搜索,发现高德地图并没有内置的卷帘功能。但是,用户可以通过其他方式实现类似效果,比如使用高德地图的JSAPI开发自定义功能,或者结合第三方工具。由于用户可能不是开发者,我们考虑普通用户如何使用。实际上,在高德地图手机App或网页版中,没有直接的卷帘功能按钮。但是,我们可以提供一种变通方法:使用分屏对比。另一种可能是,用户可能指的是高德地图中的“图层切换”功能,比如切换卫星图和标准地图,但这并不是卷帘。因此,我们需要提供两种方法:1.针对普通用户,使用高德地图内置的分屏功能(如果存在)进行对比。2.针对开发者,使用高德地图JSAPI实现卷帘效果。但是,经过搜索,高德地图App目前没有分屏对比功能。网页版也没有内置卷帘工具。所以,对于普通用户,可能无法直接使用卷帘功能。不过,高德地图在专业版(企业版)中可能有更多功能,但普通用户无法使用。因此,我们可能需要引导用户使用其他地图工具(如ArcGIS、QGIS等)来实现卷帘,或者使用高德地图API进行开发。但是,用户的问题明确是在高德地图中使用,所以我们主要提供开发实现的指导。考虑到用户可能是普通用户,也可能是开发者,我们分两种情况回答:一、普通用户:目前高德地图官方应用(手机App和网页版)未提供卷帘功能。可以尝试以下替代方法:-使用两个设备或两个浏览器窗口分别打开高德地图的不同图层或不同时期的地图,进行人工对比。-使用其他支持卷帘功能的地图应用,如GoogleEarth、ArcGISOnline等。二、开发者:如果需要在自己的网站中实现卷帘效果,可以使用高德地图JSAPI结合自定义代码实现。下面提供使用高德地图JSAPI实现卷帘效果的步骤:步骤1:引入高德地图JSAPI在HTML文件中引入高德地图JS API,注意使用最新版本。步骤2:创建两个地图容器在页面中创建两个div作为地图容器,并设置样式使其并排显示,或者一个覆盖另一个(通过调整透明度或裁剪)。步骤3:初始化两个地图实例分别初始化两个地图实例,可以设置不同的图层(如一个卫星图,一个标准地图)或不同时间的地图。步骤4:实现卷帘效果可以通过鼠标事件来控制两个地图的显示区域。常见做法是在两个地图容器上层添加一个可拖动的分割条,拖动时改变两个容器的显示范围。示例代码:注意:以下代码仅为示例,实际使用时需要根据需求调整。```html<!DOCTYPEhtml><html><head><meta charset="utf-8"><title>高德地图卷帘效果示例</title><style>#container{position:relative;width:100%;height:600px;}#map1,#map2 {position: absolute;top:0;width:50%;height:100%;}#map1 {left:0;}#map2{left:50%;}#slider {position: absolute;top:0;left:50%;width:4px;height:100%;background-color: #fff;cursor:ew-resize;z-index:1000;}</style></head> <body> <divid="container"><div id="map1"></div><div id="map2"></div><div id="slider"></div></div> <scriptsrc="https://webapi.amap.com/maps?v=2.0&key=你的高德地图key"></script> <script>//初始化第一个地图(例如标准地图)varmap1= newAMap.Map('map1', {viewMode:'2D',//默认使用2D模式layers:[newAMap.TileLayer()], //使用标准图层center:[116.397428,39.90923],//中心点坐标zoom:11//缩放级别});//初始化第二个地图(例如卫星地图)var map2 =new AMap.Map('map2',{viewMode: '2D',layers: [new AMap.TileLayer.Satellite()], //使用卫星图层center:[116.397428,39.90923],zoom:11});//获取滑块元素varslider =document.getElementById('slider');varcontainer= document.getElementById('container');varmap1Div =document.getElementById('map1');varmap2Div =document.getElementById('map2');//滑块拖动事件slider.addEventListener('mousedown', function(e){e.preventDefault();document.addEventListener('mousemove', mouseMoveHandler);document.addEventListener('mouseup',function() {document.removeEventListener('mousemove',mouseMoveHandler);});});functionmouseMoveHandler(e) {//计算滑块位置var containerRect =container.getBoundingClientRect();var x= e.clientX- containerRect.left;//限制滑块在容器内if(x<0)x =0;if (x >containerRect.width)x =containerRect.width;//设置滑块位置slider.style.left =x +'px';//调整两个地图容器的宽度map1Div.style.width =x +'px';map2Div.style.left =x +'px';map2Div.style.width =(containerRect.width- x) +'px';//重新调整地图大小map1.setSize();map2.setSize();}</script> </body></html> ```注意:需要将代码中的“你的高德地图key”替换为实际的高德地图API key。以上代码实现了两个地图并排显示,中间有一个可拖动的滑块,拖动滑块可以改变两个地图的显示区域,达到卷帘效果。由于普通用户在高德地图官方应用中无法使用卷帘功能,因此主要提供给开发者参考。最后,按照要求生成相关问题。§§相关问题§§: 1.高德地图卷帘功能在手机App上如何操作?2.高德地图JS API实现卷帘效果有哪些注意事项? 3.除了卷帘功能,高德地图还有哪些地图对比功能?</think>### 高德地图卷帘功能使用方法 高德地图官方未提供内置的卷帘功能,但可通过以下方法实现类似效果: --- #### **方法一:分屏手动对比(普通用户适用)** 打开高德地图App,进入目标区域。 调出侧边栏,选择「图层」切换至「卫星图」或其他需对比的图层。 使用手机分屏功能(如iOS画中画、安卓多窗口),打开另一个高德地图实例显示不同时期/图层的地图。 手动滑动屏幕,对比两侧地图差异。 --- #### **方法二:JS API开发实现(开发者适用)** 通过高德地图JavaScript API自定义卷帘效果,需编码实现: 1. **初始化双地图容器** 创建两个`div`容器,分别加载不同图层的地图: ```html <div id="mapLeft" style="width:50%; height:100%; float:left"></div> <div id="mapRight" style="width:50%; height:100%; float:left"></div> ``` 2. **绑定同步操作** 使用`syncMap()`函数同步地图中心点与缩放级别: ```javascript const map1 = new AMap.Map('mapLeft', { layers: [new AMap.TileLayer()] }); const map2 = new AMap.Map('mapRight', { layers: [new AMap.TileLayer.Satellite()] }); function syncMap() { map2.setCenter(map1.getCenter()); map2.setZoom(map1.getZoom()); } map1.on('moveend', syncMap); ``` 3. **添加卷帘控件** 引入第三方库(如`leaflet-side-by-side`)或自定义滑块: ```javascript const slider = document.createElement('input'); slider.type = 'range'; slider.style.position = 'absolute'; zIndex: 1000; document.body.appendChild(slider); slider.addEventListener('input', (e) => { document.getElementById('mapLeft').style.width = e.target.value + '%'; }); ``` --- #### **方法三:专业工具替代方案** - **QGIS软件**:导入高德地图WMTS服务,使用QGIS Temporal Controller」模块实现时空卷帘。 - **ArcGIS Online**:通过「Swipe Widget」工具对比高德地图切片服务。 --- > **注意事项**: > - 普通用户需依赖系统分屏功能,部分安卓设备需开启「强制分屏」选项。 > - 开发者方案需申请高德地图API Key(每日调用限额300万次)。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

llc的足迹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值