I made a custom map tool that can both drag and drop objects and select them.
It works well, the only problem is that the selection by rectangle gets features that are hidden by the layer style.
Selecting feature by rect:
But two features are selected:
How to select only those features that are visible on the canvas?
The full code of the tool that can be run in the Qgis python editor:
from qgis.gui import QgsMapTool, QgsMapMouseEvent
from qgis.core import QgsFeature, QgsGeometry, QgsPointXY
from qgis.PyQt import QtCore, QtWidgets
class DragAndDropFeatureTool(QgsMapTool):
def __init__(self, canvas):
super().__init__(canvas)
self.canvas = canvas
self.start_point = None
self.end_point = None
self.feature = None
self.layer = None
# Geometry projection transformation objects
map_crs = iface.mapCanvas().mapSettings().destinationCrs() # project coordinate system
lyr_crs = QgsCoordinateReferenceSystem(4326) # layer coordinate system (EPSG:4326)
# geometry to map projection
self.to_map_crs = QgsCoordinateTransform(lyr_crs, map_crs, QgsProject.instance())
# geometry to layer projection
self.to_lyr_crs = QgsCoordinateTransform(map_crs, lyr_crs, QgsProject.instance())
# flag to check if the left mouse button was pressed
self.is_left_button_pressed = False
# create a rubber line object
# to display the geometry of the dragged object on the canvas
self.rubber_band = QgsRubberBand(self.canvas, QgsWkbTypes.PolygonGeometry)
self.rubber_band.setColor(QColor(255, 0, 0, 50))
self.rubber_band.setWidth(2)
# object geometry moved
self.new_geometry = None
# Mouse button pressed.
def canvasPressEvent(self, event: QgsMapMouseEvent):
self.layer = iface.activeLayer()
if (self.layer.isEditable() and
self.is_layer_visible(self.layer) and
event.button() == Qt.LeftButton):
point = self.to_lyr_crs.transform(event.mapPoint())
self.feature = None
result = QgsMapToolIdentifyFeature(self.canvas).identify(event.x(),
event.y(), [self.layer], QgsMapToolIdentify.TopDownStopAtFirst)
# if the mouse button is pressed over the object
if result:
feature = result[0].mFeature
self.feature = feature
self.start_point = point
self.is_left_button_pressed = True
# the geometry of the object on which the left mouse button is pressed
self.new_geometry = feature.geometry()
self.rubber_band.setToGeometry(feature.geometry(), self.layer)
# draw object geometry on canvas
self.rubber_band.show()
# if the mouse button is pressed over an empty space
else:
self.start_point = self.toMapCoordinates(event.pos())
self.end_point = self.start_point
self.is_left_button_pressed = True
# The cursor moves across the screen.
def canvasMoveEvent(self, event: QgsMapMouseEvent):
# if the left mouse button is pressed
# and the user drags the object (self.feature)
if self.is_left_button_pressed and self.feature:
point = self.to_lyr_crs.transform(event.mapPoint())
# update the dragged object on the canvas
delta_x = point.x() - self.start_point.x()
delta_y = point.y() - self.start_point.y()
geometry = self.feature.geometry()
geometry.translate(delta_x, delta_y)
self.rubber_band.setToGeometry(geometry, self.layer)
self.new_geometry = geometry
# if the left mouse button is pressed
elif self.is_left_button_pressed:
self.end_point = self.toMapCoordinates(event.pos())
# draw a rectangle on the canvas
self.showRect(self.start_point, self.end_point)
# The mouse button is released.
def canvasReleaseEvent(self, event: QgsMapMouseEvent):
# if the left mouse button was released
if event.button() == Qt.LeftButton:
# if the user dragged the object (self.feature)
# (or just clicked on the object)
if self.feature:
# drag endpoint
end_point = self.to_lyr_crs.transform(event.mapPoint())
# set the geometry (self.new_geometry),
# created by the canvasMoveEvent function
self.layer.changeGeometry(self.feature.id(), self.new_geometry)
# select the moved object
self.layer.selectByIds([self.feature.id()])
# if the user didn't drag the feature (self.feature = None)
else:
# get the rectangle created from the click point and
# left mouse button release points
mouse_rect = self.rectangle()
# if the rectangle turned out (the user pressed and released
# mouse button at different points on the screen)
if mouse_rect:
modifiers = QtWidgets.QApplication.keyboardModifiers()
# if the Shift button was pressed when releasing
if modifiers == QtCore.Qt.ShiftModifier:
# add to the selection objects intersecting with the rectangle
self.layer.selectByRect(mouse_rect, QgsVectorLayer.AddToSelection)
# if the Control button was pressed when releasing
elif modifiers == QtCore.Qt.ControlModifier:
# subtract from the selection the objects intersecting with the rectangle
self.layer.selectByRect(mouse_rect, QgsVectorLayer.RemoveFromSelection)
else:
# select objects that intersect with the rectangle
self.layer.selectByRect(mouse_rect)
# if the rectangle is not received
# (user just clicked on the map in an empty space)
else:
# reset the old selection
self.layer.removeSelection()
# toggle the flag, the left mouse button is no longer held down
self.is_left_button_pressed = False
self.rubber_band.reset() # remove rubber band from canvas
def showRect(self, startPoint, endPoint):
self.rubber_band.reset(QgsWkbTypes.PolygonGeometry)
if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
return
point1 = QgsPointXY(startPoint.x(), startPoint.y())
point2 = QgsPointXY(startPoint.x(), endPoint.y())
point3 = QgsPointXY(endPoint.x(), endPoint.y())
point4 = QgsPointXY(endPoint.x(), startPoint.y())
self.rubber_band.addPoint(point1, False)
self.rubber_band.addPoint(point2, False)
self.rubber_band.addPoint(point3, False)
self.rubber_band.addPoint(point4, True) # true to update canvas
self.rubber_band.show()
def rectangle(self):
if self.start_point is None or self.end_point is None:
return None
elif (self.start_point.x() == self.end_point.x() or \
self.start_point.y() == self.end_point.y()):
return None
# in layer coordinates
start_point = self.to_lyr_crs.transform(self.start_point)
end_point = self.to_lyr_crs.transform(self.end_point)
return QgsRectangle(start_point, end_point)
def is_layer_visible(self, layer: QgsMapLayer):
layer_tree_root = QgsProject.instance().layerTreeRoot()
layer_tree_layer = layer_tree_root.findLayer(layer)
return layer_tree_layer.isVisible()
def deactivate(self):
QgsMapTool.deactivate(self)
self.deactivated.emit()
canvas = iface.mapCanvas()
move_tool = DragAndDropFeatureTool(canvas)
iface.mapCanvas().setMapTool(move_tool)
Features selection happens in def canvasReleaseEvent (the layer must be editable).