I encountered the problem indicated in the title. The only solutions I found were quite old and perhaps something has changed since then and now it is possible to make the DragMode.ScrollHandDrag action for the middle mouse key?
class InfiniteCanvas(QGraphicsView):
def __init__(self, parent=None):
super(InfiniteCanvas, self).__init__(parent)
self.setAcceptDrops(True)
self.setScene(QGraphicsScene(self))
self.setRenderHint(QPainter.Antialiasing)
self.setRenderHint(QPainter.SmoothPixmapTransform)
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.left_mouse_active = False
screen = QApplication.primaryScreen()
screen_size = screen.size()
width = screen_size.width() * 0.7
height = screen_size.height() * 0.7
self.setGeometry(self.x(), self.y(), width, height)
self.setStyleSheet(f"background-color: #2a2a2a;")
def mousePressEvent(self, event):
if event.button() == Qt.MidButton:
self.viewport().setCursor(Qt.ClosedHandCursor)
self.original_event = event
handmade_event = QMouseEvent(QEvent.MouseButtonPress,QPointF(event.pos()),Qt.LeftButton,event.buttons(),Qt.KeyboardModifiers())
self.mousePressEvent(handmade_event)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dragMoveEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
if __name__ == "__main__":
app = QApplication(sys.argv)
view = InfiniteCanvas()
view.show()
sys.exit(app.exec())
This is the sample code I use in the mousePressEvent method however it completely rules out the possibility of using the left mouse button for anything else.
You are trying to call the default mousePressEvent
with the synthesized left click event, but since you overrode that function without ever calling the base implementation, nothing will happen.
When you do the following:
def mousePressEvent(self, event):
if event.button() == Qt.MidButton:
self.viewport().setCursor(Qt.ClosedHandCursor)
self.original_event = event
handmade_event = QMouseEvent(...)
self.mousePressEvent(handmade_event)
then the same mousePressEvent()
will be called again from itself, but since now the button is the left one, the block within if event.button() == Qt.MidButton
is skipped and there's nothing else to do after that: the event is completely ignored, because you are overriding its expected behavior.
Your attempt to fix that with dragEnterEvent()
and dragMoveEvent()
is pointless and wrong, because those handlers are related to actual drag and drop, which is a different type of event management, used to drag objects that may eventually be dropped onto other components, even between different applications.
The correct procedure would be to add an else
clause and call super()
with the default event handler:
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.MiddleButton:
handmade_event = QMouseEvent(
QEvent.Type.MouseButtonPress,
event.position(), Qt.MouseButton.LeftButton,
event.buttons(), event.modifiers())
super().mousePressEvent(handmade_event)
else:
super().mousePressEvent(event)
Note the changes done above:
mousePressEvent()
with the left button will do that on its own when the ScrollHandDrag
mode is set;event.pos()
has been deprecated, and you should use event.position()
instead (which already is a QPointF);MidButton
has been virtually considered obsolete for years, and it's deprecated since 5.15; the correct name is MiddleButton
;Then, QGraphicsView assumes the scrolling state set from the left button event, and it ignores the actual buttons()
in mouseMoveEvent()
, allowing scrolling movements even if the left button is not actually pressed while moving. In reality, though, we should still theoretically override that too, similarly to what done before. Be aware of that, in case it will stop working for you in a future Qt version.
Finally, we must do the same in the mouseReleaseEvent()
in order to tell the internal state of graphics view that the mouse button has been released (even if virtually), which will also restore the cursor.
def mouseReleaseEvent(self, event):
if event.button() == Qt.MouseButton.MiddleButton:
handmade_event = QMouseEvent(
QEvent.Type.MouseButtonRelease,
event.position(), Qt.MouseButton.LeftButton,
event.buttons(), event.modifiers())
super().mouseReleaseEvent(handmade_event)
else:
super().mouseReleaseEvent(event)
Be aware that all the above will never work as soon as the scene rect shown on the the view can fit its geometry, because the ScrollHandDrag
only works within the scroll bar range (the fact that you made them invisible is irrelevant).
If you actually want to be able to scroll no matter the scene rect size (which is what you probably need, given the name of your class), you cannot do it with the above.
Instead, you should set a scene rect on the view that is large enough to allow (virtually) infinite scrolling, ensure that you properly set the transformationAnchor
to NoAnchor
, and then call translate()
in mouseMoveEvent()
with the delta of the current mouse movement.
from random import randrange
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
class InfiniteCanvas(QGraphicsView):
_isScrolling = _isSpacePressed = False
def __init__(self, parent=None):
super(InfiniteCanvas, self).__init__(parent)
scene = QGraphicsScene(self)
for _ in range(10):
item = scene.addRect(randrange(1000), randrange(1000), 50, 50)
item.setFlag(item.GraphicsItemFlag.ItemIsMovable)
self.setScene(scene)
self.setRenderHints(
QPainter.RenderHint.Antialiasing
| QPainter.RenderHint.SmoothPixmapTransform
)
# Important! Without this, self.translate() will not work!
self.setTransformationAnchor(self.ViewportAnchor.NoAnchor)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
screen = QApplication.screenAt(QCursor.pos())
geo = screen.availableGeometry()
geo.setSize(geo.size() * .7)
geo.moveCenter(screen.availableGeometry().center())
self.setGeometry(geo)
self.setSceneRect(-32000, -32000, 64000, 64000)
self.fitInView(scene.itemsBoundingRect())
def mousePressEvent(self, event):
if (
event.button() == Qt.MouseButton.MiddleButton
or self._isSpacePressed
and event.button() == Qt.MouseButton.LeftButton
):
self._isScrolling = True
self.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
self.scrollPos = event.position()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._isScrolling:
newPos = event.position()
delta = newPos - self.scrollPos
t = self.transform()
self.translate(delta.x() / t.m11(), delta.y() / t.m22())
self.scrollPos = newPos
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self._isScrolling:
self._isScrolling = False
if self._isSpacePressed:
self.viewport().setCursor(Qt.CursorShape.OpenHandCursor)
else:
self.viewport().unsetCursor()
super().mouseReleaseEvent(event)
def keyPressEvent(self, event):
if event.key() == Qt.Key.Key_Space and not event.isAutoRepeat():
self._isSpacePressed = True
self.viewport().setCursor(Qt.CursorShape.OpenHandCursor)
else:
super().keyPressEvent(event)
def keyReleaseEvent(self, event):
if event.key() == Qt.Key.Key_Space and not event.isAutoRepeat():
self._isSpacePressed = False
if not self._isScrolling:
self.viewport().unsetCursor()
else:
super().keyReleaseEvent(event)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
view = InfiniteCanvas()
view.show()
sys.exit(app.exec())
As an unrelated note, you shall never, ever set generic QSS properties on complex widgets like scroll areas (which is the case of QGraphicsView) or combo boxes, as you did with this line:
self.setStyleSheet(f"background-color: #2a2a2a;")
Doing so will propagate that property to any child widget (including scroll bars or context menus), which is very problematic for complex widgets like scroll areas or comboboxes.
You should always use proper selector types instead, unless you are completely sure that you're applying the QSS to a simple standalone widget (like a button or a label).
Also see these related posts: