In a QGraphicsview, how can I keep the image moving when the cursor is on the edge, without having to move the mouse?
-
In my program, I have a QDialog that displays a picture that you can zoom and pan around, if you pan with the right mouse button clicked, you can draw a rectangle. When you are drawing a rectangle and reach the edge of the view, the picture should move so that you can keep drawing the rectangle. Right now it only works if you keep moving the cursor slightly to trigger the mouseMoveEvent.
If I try to programatically call the mouseMoveEvent when the mouse isnt moving, to keep the picture moving, it moves the picture, but for some reason the view doesnt update, so you only see that the picture has moved after moving the mouse again. So basically if you keep the mouse on the edge for some time without moving it, and then move it, the picture moves by a lot suddenly.
Interestingly, when I call the mouseMoveEvent programatically, for some reason the event.position() changes, even though the event is always the same one.
Here is a minimal reproducible example:
the adjust_view function is the original one where you have to move the cursor up and down to keep the picture moving
the adjust_view_loop function is my attempt to programatically make it work
Im using Python 3.10.7 and PySide6.3.2
from PySide6.QtCore import Signal, Qt, QRectF, QPoint, QRect from PySide6.QtGui import QPainter, QPixmap, QImage, QPen, QCursor, QMouseEvent from PySide6.QtWidgets import ( QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsRectItem, QApplication, QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, ) import warnings import time import sys class MyDialog(QDialog): MIN_WIDTH = 900 MIN_HEIGHT = int(MIN_WIDTH * 2 / 3) def __init__(self, path, parent=None): super().__init__(parent) self.path = path self.setFixedSize(self.MIN_WIDTH, self.MIN_HEIGHT) self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.rejected.connect(self.reject) self.buttonBox.accepted.connect(self.accept) self.layout = QVBoxLayout() self.image_viewer = PhotoViewer(self) image = QImage(path) self.pixmap = QPixmap(image) self.image_viewer.set_photo(self.pixmap) bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.buttonBox) self.layout.addWidget(self.image_viewer) self.layout.addLayout(bottom_layout) self.setLayout(self.layout) def exec(self): self.image_viewer.viewport().setMinimumSize( self.MIN_WIDTH - 100, self.MIN_HEIGHT - 100 ) self.image_viewer.fit_in_view() self.image_viewer.calc_delta() super().exec() class PhotoViewer(QGraphicsView): """ class for displaying the pictures """ DELTA = 10 mouse_event_signal = Signal() def __init__(self, parent): super(PhotoViewer, self).__init__(parent) warnings.filterwarnings("error") self.zoom = 0 self._empty = True self._photo = QGraphicsPixmapItem() self._photo.setTransformationMode(Qt.SmoothTransformation) self.set_scene() self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setRenderHints(QPainter.SmoothPixmapTransform | QPainter.Antialiasing) def set_scene(self): self.scene = SelectColorBarDialogScene(self) self.scene.addItem(self._photo) self.setScene(self.scene) def fit_in_view(self, scale=True): """ Resizes the photo to fit in the view """ rect = QRectF(self._photo.pixmap().rect()) if not rect.isNull(): self.setSceneRect(rect) unity = self.transform().mapRect(QRectF(0, 0, 1, 1)) self.scale(1 / unity.width(), 1 / unity.height()) viewrect = self.viewport().rect() scenerect = self.transform().mapRect(rect) factor = min( viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height(), ) self.scale(factor, factor) self.zoom = 0 def mousePressEvent(self, event): if event.buttons() & Qt.RightButton: self.update() super(PhotoViewer, self).mousePressEvent(event) def mouseMoveEvent(self, event, pass_adjust=False): super(PhotoViewer, self).mouseMoveEvent(event) if event.buttons() & Qt.RightButton: if not self.scene.begin.isNull() and not self.scene.destination.isNull(): if not pass_adjust: # self.adjust_view(event) self.adjust_view_loop(event) print(event.globalPosition()) print(event.position()) print("_______________") self.update() #! When programatically changed, the mouse pos changes even though cursor is not moved def adjust_view_loop(self, event): edges = self.check_if_point_on_edge(self.scene.destination) if ( edges and self.scene.destination.x() < self.photo_size.width() - self.DELTA and self.scene.destination.y() < self.photo_size.height() - self.DELTA and self.scene.destination.x() > self.DELTA and self.scene.destination.y() > self.DELTA ): i = 0 self.cursor_pos = QCursor.pos() while True: i += 1 old_view = self.mapToScene(self.viewport().rect()).boundingRect() new_view = [0, 0, 0, 0] for _ in range(edges.bit_count()): if edges & 1 == 1: new_view[1] = -self.DELTA new_view[3] = -self.DELTA elif edges & 4 == 4: new_view[1] = self.DELTA new_view[3] = self.DELTA elif edges & 2 == 2: new_view[0] = -self.DELTA new_view[2] = -self.DELTA elif edges & 8 == 8: new_view[0] = self.DELTA new_view[2] = self.DELTA new_view_rect = old_view.adjusted( new_view[0], new_view[1], new_view[2], new_view[3] ) self.fitInView(new_view_rect, Qt.KeepAspectRatio) x = self.scene.destination.x() y = self.scene.destination.y() self.set_rect() if i % 2 == 0: new_event = QMouseEvent( event.type(), QPoint(event.position().x(), event.position().y() + 1), QPoint( event.globalPosition().x(), event.globalPosition().y() + 1 ), event.button(), event.buttons(), event.modifiers(), ) else: new_event = QMouseEvent( event.type(), QPoint(event.position().x(), event.position().y() - 1), QPoint( event.globalPosition().x(), event.globalPosition().y() - 1 ), event.button(), event.buttons(), event.modifiers(), event.pointingDevice(), ) self.mouseMoveEvent(new_event, True) self.update() print("Looping...") time.sleep(0.5) if not self.on_loop_signal(): break self.set_rect() def adjust_view(self, event): edges = self.check_if_point_on_edge(self.scene.destination) if ( edges and self.scene.destination.x() < self.photo_size.width() - self.DELTA and self.scene.destination.y() < self.photo_size.height() - self.DELTA and self.scene.destination.x() > self.DELTA and self.scene.destination.y() > self.DELTA ): old_view = self.mapToScene(self.viewport().rect()).boundingRect() new_view = [0, 0, 0, 0] for _ in range(edges.bit_count()): if edges & 1 == 1: new_view[1] = -self.DELTA new_view[3] = -self.DELTA elif edges & 4 == 4: new_view[1] = self.DELTA new_view[3] = self.DELTA elif edges & 2 == 2: new_view[0] = -self.DELTA new_view[2] = -self.DELTA elif edges & 8 == 8: new_view[0] = self.DELTA new_view[2] = self.DELTA new_view_rect = old_view.adjusted( new_view[0], new_view[1], new_view[2], new_view[3] ) self.fitInView(new_view_rect, Qt.KeepAspectRatio) x = self.scene.destination.x() y = self.scene.destination.y() self.set_rect() self.set_rect() def set_rect(self): for item in self.scene.items(): if isinstance(item, QGraphicsRectItem): self.scene.removeItem(item) self.rect = QRect(self.scene.begin, self.scene.destination) self.scene.addRect(self.rect.normalized(), QPen(Qt.red)) def get_rect(self): return self.rect.normalized() def mouseReleaseEvent(self, event): if event.button() & Qt.RightButton: self.set_rect() self.update() super(PhotoViewer, self).mouseReleaseEvent(event) def check_if_point_on_edge(self, point): rect = self.mapToScene( self.viewport().rect() ).boundingRect() # left, top, width, height edges = 0 # top = 1, bottom = 2, left = 4, right = 8 internal_rect = rect.adjusted(self.DELTA, self.DELTA, -self.DELTA, -self.DELTA) if rect.contains(point) and not internal_rect.contains(point): if point.x() < internal_rect.left(): edges = edges | 2 elif point.x() > internal_rect.right(): edges = edges | 8 if point.y() < internal_rect.top(): edges = edges | 1 elif point.y() > internal_rect.bottom(): edges = edges | 4 return edges def wheelEvent(self, event): self.calc_delta() if event.angleDelta().y() > 0: factor = 1.25 self.zoom += 1 else: factor = 0.8 self.zoom -= 1 if self.zoom > 0: self.scale(factor, factor) elif self.zoom == 0: self.fit_in_view() else: self.zoom = 0 def calc_delta(self): rect_width = self.mapToScene(self.viewport().rect()).boundingRect().width() self.DELTA = int(rect_width / 25) def set_photo(self, pixmap=None): """ Sets the photo to be displayed, if pixmap is None, the photo is set to empty """ if pixmap and not pixmap.isNull(): self._empty = False self.setDragMode(QGraphicsView.ScrollHandDrag) self._photo.setPixmap(pixmap) else: self._empty = True self.setDragMode(QGraphicsView.NoDrag) self._photo.setPixmap(QPixmap()) self.photo_size = QRectF(self._photo.pixmap().rect()) def on_loop_signal(self): if self.cursor_pos != QCursor.pos(): self.mouse_event_signal.emit() return False return True class SelectColorBarDialogScene(QGraphicsScene): def __init__(self, parent): super(SelectColorBarDialogScene, self).__init__(parent) self.begin, self.destination = QPoint(), QPoint() def mousePressEvent(self, event): if event.buttons() & Qt.RightButton: self.begin = event.scenePos().toPoint() self.destination = self.begin self.update() super(SelectColorBarDialogScene, self).mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() & Qt.RightButton: self.destination = event.scenePos().toPoint() self.update() super(SelectColorBarDialogScene, self).mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() & Qt.RightButton: self.begin, self.destination = QPoint(), QPoint() self.update() super(SelectColorBarDialogScene, self).mouseReleaseEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) dialog = MyDialog(path=None) #! set picture path here dialog.exec() sys.exit(app.exec_())
-
In my program, I have a QDialog that displays a picture that you can zoom and pan around, if you pan with the right mouse button clicked, you can draw a rectangle. When you are drawing a rectangle and reach the edge of the view, the picture should move so that you can keep drawing the rectangle. Right now it only works if you keep moving the cursor slightly to trigger the mouseMoveEvent.
If I try to programatically call the mouseMoveEvent when the mouse isnt moving, to keep the picture moving, it moves the picture, but for some reason the view doesnt update, so you only see that the picture has moved after moving the mouse again. So basically if you keep the mouse on the edge for some time without moving it, and then move it, the picture moves by a lot suddenly.
Interestingly, when I call the mouseMoveEvent programatically, for some reason the event.position() changes, even though the event is always the same one.
Here is a minimal reproducible example:
the adjust_view function is the original one where you have to move the cursor up and down to keep the picture moving
the adjust_view_loop function is my attempt to programatically make it work
Im using Python 3.10.7 and PySide6.3.2
from PySide6.QtCore import Signal, Qt, QRectF, QPoint, QRect from PySide6.QtGui import QPainter, QPixmap, QImage, QPen, QCursor, QMouseEvent from PySide6.QtWidgets import ( QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsRectItem, QApplication, QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, ) import warnings import time import sys class MyDialog(QDialog): MIN_WIDTH = 900 MIN_HEIGHT = int(MIN_WIDTH * 2 / 3) def __init__(self, path, parent=None): super().__init__(parent) self.path = path self.setFixedSize(self.MIN_WIDTH, self.MIN_HEIGHT) self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.rejected.connect(self.reject) self.buttonBox.accepted.connect(self.accept) self.layout = QVBoxLayout() self.image_viewer = PhotoViewer(self) image = QImage(path) self.pixmap = QPixmap(image) self.image_viewer.set_photo(self.pixmap) bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.buttonBox) self.layout.addWidget(self.image_viewer) self.layout.addLayout(bottom_layout) self.setLayout(self.layout) def exec(self): self.image_viewer.viewport().setMinimumSize( self.MIN_WIDTH - 100, self.MIN_HEIGHT - 100 ) self.image_viewer.fit_in_view() self.image_viewer.calc_delta() super().exec() class PhotoViewer(QGraphicsView): """ class for displaying the pictures """ DELTA = 10 mouse_event_signal = Signal() def __init__(self, parent): super(PhotoViewer, self).__init__(parent) warnings.filterwarnings("error") self.zoom = 0 self._empty = True self._photo = QGraphicsPixmapItem() self._photo.setTransformationMode(Qt.SmoothTransformation) self.set_scene() self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setRenderHints(QPainter.SmoothPixmapTransform | QPainter.Antialiasing) def set_scene(self): self.scene = SelectColorBarDialogScene(self) self.scene.addItem(self._photo) self.setScene(self.scene) def fit_in_view(self, scale=True): """ Resizes the photo to fit in the view """ rect = QRectF(self._photo.pixmap().rect()) if not rect.isNull(): self.setSceneRect(rect) unity = self.transform().mapRect(QRectF(0, 0, 1, 1)) self.scale(1 / unity.width(), 1 / unity.height()) viewrect = self.viewport().rect() scenerect = self.transform().mapRect(rect) factor = min( viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height(), ) self.scale(factor, factor) self.zoom = 0 def mousePressEvent(self, event): if event.buttons() & Qt.RightButton: self.update() super(PhotoViewer, self).mousePressEvent(event) def mouseMoveEvent(self, event, pass_adjust=False): super(PhotoViewer, self).mouseMoveEvent(event) if event.buttons() & Qt.RightButton: if not self.scene.begin.isNull() and not self.scene.destination.isNull(): if not pass_adjust: # self.adjust_view(event) self.adjust_view_loop(event) print(event.globalPosition()) print(event.position()) print("_______________") self.update() #! When programatically changed, the mouse pos changes even though cursor is not moved def adjust_view_loop(self, event): edges = self.check_if_point_on_edge(self.scene.destination) if ( edges and self.scene.destination.x() < self.photo_size.width() - self.DELTA and self.scene.destination.y() < self.photo_size.height() - self.DELTA and self.scene.destination.x() > self.DELTA and self.scene.destination.y() > self.DELTA ): i = 0 self.cursor_pos = QCursor.pos() while True: i += 1 old_view = self.mapToScene(self.viewport().rect()).boundingRect() new_view = [0, 0, 0, 0] for _ in range(edges.bit_count()): if edges & 1 == 1: new_view[1] = -self.DELTA new_view[3] = -self.DELTA elif edges & 4 == 4: new_view[1] = self.DELTA new_view[3] = self.DELTA elif edges & 2 == 2: new_view[0] = -self.DELTA new_view[2] = -self.DELTA elif edges & 8 == 8: new_view[0] = self.DELTA new_view[2] = self.DELTA new_view_rect = old_view.adjusted( new_view[0], new_view[1], new_view[2], new_view[3] ) self.fitInView(new_view_rect, Qt.KeepAspectRatio) x = self.scene.destination.x() y = self.scene.destination.y() self.set_rect() if i % 2 == 0: new_event = QMouseEvent( event.type(), QPoint(event.position().x(), event.position().y() + 1), QPoint( event.globalPosition().x(), event.globalPosition().y() + 1 ), event.button(), event.buttons(), event.modifiers(), ) else: new_event = QMouseEvent( event.type(), QPoint(event.position().x(), event.position().y() - 1), QPoint( event.globalPosition().x(), event.globalPosition().y() - 1 ), event.button(), event.buttons(), event.modifiers(), event.pointingDevice(), ) self.mouseMoveEvent(new_event, True) self.update() print("Looping...") time.sleep(0.5) if not self.on_loop_signal(): break self.set_rect() def adjust_view(self, event): edges = self.check_if_point_on_edge(self.scene.destination) if ( edges and self.scene.destination.x() < self.photo_size.width() - self.DELTA and self.scene.destination.y() < self.photo_size.height() - self.DELTA and self.scene.destination.x() > self.DELTA and self.scene.destination.y() > self.DELTA ): old_view = self.mapToScene(self.viewport().rect()).boundingRect() new_view = [0, 0, 0, 0] for _ in range(edges.bit_count()): if edges & 1 == 1: new_view[1] = -self.DELTA new_view[3] = -self.DELTA elif edges & 4 == 4: new_view[1] = self.DELTA new_view[3] = self.DELTA elif edges & 2 == 2: new_view[0] = -self.DELTA new_view[2] = -self.DELTA elif edges & 8 == 8: new_view[0] = self.DELTA new_view[2] = self.DELTA new_view_rect = old_view.adjusted( new_view[0], new_view[1], new_view[2], new_view[3] ) self.fitInView(new_view_rect, Qt.KeepAspectRatio) x = self.scene.destination.x() y = self.scene.destination.y() self.set_rect() self.set_rect() def set_rect(self): for item in self.scene.items(): if isinstance(item, QGraphicsRectItem): self.scene.removeItem(item) self.rect = QRect(self.scene.begin, self.scene.destination) self.scene.addRect(self.rect.normalized(), QPen(Qt.red)) def get_rect(self): return self.rect.normalized() def mouseReleaseEvent(self, event): if event.button() & Qt.RightButton: self.set_rect() self.update() super(PhotoViewer, self).mouseReleaseEvent(event) def check_if_point_on_edge(self, point): rect = self.mapToScene( self.viewport().rect() ).boundingRect() # left, top, width, height edges = 0 # top = 1, bottom = 2, left = 4, right = 8 internal_rect = rect.adjusted(self.DELTA, self.DELTA, -self.DELTA, -self.DELTA) if rect.contains(point) and not internal_rect.contains(point): if point.x() < internal_rect.left(): edges = edges | 2 elif point.x() > internal_rect.right(): edges = edges | 8 if point.y() < internal_rect.top(): edges = edges | 1 elif point.y() > internal_rect.bottom(): edges = edges | 4 return edges def wheelEvent(self, event): self.calc_delta() if event.angleDelta().y() > 0: factor = 1.25 self.zoom += 1 else: factor = 0.8 self.zoom -= 1 if self.zoom > 0: self.scale(factor, factor) elif self.zoom == 0: self.fit_in_view() else: self.zoom = 0 def calc_delta(self): rect_width = self.mapToScene(self.viewport().rect()).boundingRect().width() self.DELTA = int(rect_width / 25) def set_photo(self, pixmap=None): """ Sets the photo to be displayed, if pixmap is None, the photo is set to empty """ if pixmap and not pixmap.isNull(): self._empty = False self.setDragMode(QGraphicsView.ScrollHandDrag) self._photo.setPixmap(pixmap) else: self._empty = True self.setDragMode(QGraphicsView.NoDrag) self._photo.setPixmap(QPixmap()) self.photo_size = QRectF(self._photo.pixmap().rect()) def on_loop_signal(self): if self.cursor_pos != QCursor.pos(): self.mouse_event_signal.emit() return False return True class SelectColorBarDialogScene(QGraphicsScene): def __init__(self, parent): super(SelectColorBarDialogScene, self).__init__(parent) self.begin, self.destination = QPoint(), QPoint() def mousePressEvent(self, event): if event.buttons() & Qt.RightButton: self.begin = event.scenePos().toPoint() self.destination = self.begin self.update() super(SelectColorBarDialogScene, self).mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() & Qt.RightButton: self.destination = event.scenePos().toPoint() self.update() super(SelectColorBarDialogScene, self).mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() & Qt.RightButton: self.begin, self.destination = QPoint(), QPoint() self.update() super(SelectColorBarDialogScene, self).mouseReleaseEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) dialog = MyDialog(path=None) #! set picture path here dialog.exec() sys.exit(app.exec_())
@elver
Try the following.I would not "synthesize" a mouse (move) event in code (i.e. create a
QMouseEvent
and callself.mouseMoveEvent()
). Factor the code out from yourmouseMoveEvent()
to do its work given suitable parameters. Call that from a genuinemouseMoveEvent()
and also call it explicitly from your code when you want to do edge scroll.Don't do any
time.sleep()
, bad idea. Anything time-based needed should useQTimer
to do the stuff after thesleep()
instead.Get those done, then see whether you still have the "view doesnt update" issue.
-
@elver
Try the following.I would not "synthesize" a mouse (move) event in code (i.e. create a
QMouseEvent
and callself.mouseMoveEvent()
). Factor the code out from yourmouseMoveEvent()
to do its work given suitable parameters. Call that from a genuinemouseMoveEvent()
and also call it explicitly from your code when you want to do edge scroll.Don't do any
time.sleep()
, bad idea. Anything time-based needed should useQTimer
to do the stuff after thesleep()
instead.Get those done, then see whether you still have the "view doesnt update" issue.
@JonB Thanks for your reply! I've tried that, and the same thing happens. I think the problem stems from that I am doing the edge scroll as a loop:
while self.cursor_pos == QCursor.pos(): adjust_view()Maybe that is preventing the QGraphicsview from updating, although i am calling self.update() every time inside adjust_view().
I can see that the viewport is changing, but somehow it just isn't updating, and as soon as i exit the loop by moving the mouse it updates.Should using QThread solve that issue? I tried it some time ago, but couldnt make it work properly
-
@JonB Thanks for your reply! I've tried that, and the same thing happens. I think the problem stems from that I am doing the edge scroll as a loop:
while self.cursor_pos == QCursor.pos(): adjust_view()Maybe that is preventing the QGraphicsview from updating, although i am calling self.update() every time inside adjust_view().
I can see that the viewport is changing, but somehow it just isn't updating, and as soon as i exit the loop by moving the mouse it updates.Should using QThread solve that issue? I tried it some time ago, but couldnt make it work properly
@elver
I did not study you code.while
loops are bad :) I don't know what your code does but get rid of it. Nowhile
s, no loops, nosleep()
s. Callingupdate()
does not do anything if inside a loop, only when your code exits and Qt gets to return to the top-level event loop does updating happen. Don't do anything "busy"!And BTW the very last thing you want to do here is introduce threads :)
-
@elver
I did not study you code.while
loops are bad :) I don't know what your code does but get rid of it. Nowhile
s, no loops, nosleep()
s. Callingupdate()
does not do anything if inside a loop, only when your code exits and Qt gets to return to the top-level event loop does updating happen. Don't do anything "busy"!And BTW the very last thing you want to do here is introduce threads :)
@JonB Hahaha alright thanks! what could I do other than a loop if I want to repeat the same action while the cursor is not moving? I cant imagine doing that without having a loop somewhere. Could I somehow exit the loop after every run to update everything and then go back in?
-
In my program, I have a QDialog that displays a picture that you can zoom and pan around, if you pan with the right mouse button clicked, you can draw a rectangle. When you are drawing a rectangle and reach the edge of the view, the picture should move so that you can keep drawing the rectangle. Right now it only works if you keep moving the cursor slightly to trigger the mouseMoveEvent.
If I try to programatically call the mouseMoveEvent when the mouse isnt moving, to keep the picture moving, it moves the picture, but for some reason the view doesnt update, so you only see that the picture has moved after moving the mouse again. So basically if you keep the mouse on the edge for some time without moving it, and then move it, the picture moves by a lot suddenly.
Interestingly, when I call the mouseMoveEvent programatically, for some reason the event.position() changes, even though the event is always the same one.
Here is a minimal reproducible example:
the adjust_view function is the original one where you have to move the cursor up and down to keep the picture moving
the adjust_view_loop function is my attempt to programatically make it work
Im using Python 3.10.7 and PySide6.3.2
from PySide6.QtCore import Signal, Qt, QRectF, QPoint, QRect from PySide6.QtGui import QPainter, QPixmap, QImage, QPen, QCursor, QMouseEvent from PySide6.QtWidgets import ( QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsRectItem, QApplication, QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, ) import warnings import time import sys class MyDialog(QDialog): MIN_WIDTH = 900 MIN_HEIGHT = int(MIN_WIDTH * 2 / 3) def __init__(self, path, parent=None): super().__init__(parent) self.path = path self.setFixedSize(self.MIN_WIDTH, self.MIN_HEIGHT) self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.rejected.connect(self.reject) self.buttonBox.accepted.connect(self.accept) self.layout = QVBoxLayout() self.image_viewer = PhotoViewer(self) image = QImage(path) self.pixmap = QPixmap(image) self.image_viewer.set_photo(self.pixmap) bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.buttonBox) self.layout.addWidget(self.image_viewer) self.layout.addLayout(bottom_layout) self.setLayout(self.layout) def exec(self): self.image_viewer.viewport().setMinimumSize( self.MIN_WIDTH - 100, self.MIN_HEIGHT - 100 ) self.image_viewer.fit_in_view() self.image_viewer.calc_delta() super().exec() class PhotoViewer(QGraphicsView): """ class for displaying the pictures """ DELTA = 10 mouse_event_signal = Signal() def __init__(self, parent): super(PhotoViewer, self).__init__(parent) warnings.filterwarnings("error") self.zoom = 0 self._empty = True self._photo = QGraphicsPixmapItem() self._photo.setTransformationMode(Qt.SmoothTransformation) self.set_scene() self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setRenderHints(QPainter.SmoothPixmapTransform | QPainter.Antialiasing) def set_scene(self): self.scene = SelectColorBarDialogScene(self) self.scene.addItem(self._photo) self.setScene(self.scene) def fit_in_view(self, scale=True): """ Resizes the photo to fit in the view """ rect = QRectF(self._photo.pixmap().rect()) if not rect.isNull(): self.setSceneRect(rect) unity = self.transform().mapRect(QRectF(0, 0, 1, 1)) self.scale(1 / unity.width(), 1 / unity.height()) viewrect = self.viewport().rect() scenerect = self.transform().mapRect(rect) factor = min( viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height(), ) self.scale(factor, factor) self.zoom = 0 def mousePressEvent(self, event): if event.buttons() & Qt.RightButton: self.update() super(PhotoViewer, self).mousePressEvent(event) def mouseMoveEvent(self, event, pass_adjust=False): super(PhotoViewer, self).mouseMoveEvent(event) if event.buttons() & Qt.RightButton: if not self.scene.begin.isNull() and not self.scene.destination.isNull(): if not pass_adjust: # self.adjust_view(event) self.adjust_view_loop(event) print(event.globalPosition()) print(event.position()) print("_______________") self.update() #! When programatically changed, the mouse pos changes even though cursor is not moved def adjust_view_loop(self, event): edges = self.check_if_point_on_edge(self.scene.destination) if ( edges and self.scene.destination.x() < self.photo_size.width() - self.DELTA and self.scene.destination.y() < self.photo_size.height() - self.DELTA and self.scene.destination.x() > self.DELTA and self.scene.destination.y() > self.DELTA ): i = 0 self.cursor_pos = QCursor.pos() while True: i += 1 old_view = self.mapToScene(self.viewport().rect()).boundingRect() new_view = [0, 0, 0, 0] for _ in range(edges.bit_count()): if edges & 1 == 1: new_view[1] = -self.DELTA new_view[3] = -self.DELTA elif edges & 4 == 4: new_view[1] = self.DELTA new_view[3] = self.DELTA elif edges & 2 == 2: new_view[0] = -self.DELTA new_view[2] = -self.DELTA elif edges & 8 == 8: new_view[0] = self.DELTA new_view[2] = self.DELTA new_view_rect = old_view.adjusted( new_view[0], new_view[1], new_view[2], new_view[3] ) self.fitInView(new_view_rect, Qt.KeepAspectRatio) x = self.scene.destination.x() y = self.scene.destination.y() self.set_rect() if i % 2 == 0: new_event = QMouseEvent( event.type(), QPoint(event.position().x(), event.position().y() + 1), QPoint( event.globalPosition().x(), event.globalPosition().y() + 1 ), event.button(), event.buttons(), event.modifiers(), ) else: new_event = QMouseEvent( event.type(), QPoint(event.position().x(), event.position().y() - 1), QPoint( event.globalPosition().x(), event.globalPosition().y() - 1 ), event.button(), event.buttons(), event.modifiers(), event.pointingDevice(), ) self.mouseMoveEvent(new_event, True) self.update() print("Looping...") time.sleep(0.5) if not self.on_loop_signal(): break self.set_rect() def adjust_view(self, event): edges = self.check_if_point_on_edge(self.scene.destination) if ( edges and self.scene.destination.x() < self.photo_size.width() - self.DELTA and self.scene.destination.y() < self.photo_size.height() - self.DELTA and self.scene.destination.x() > self.DELTA and self.scene.destination.y() > self.DELTA ): old_view = self.mapToScene(self.viewport().rect()).boundingRect() new_view = [0, 0, 0, 0] for _ in range(edges.bit_count()): if edges & 1 == 1: new_view[1] = -self.DELTA new_view[3] = -self.DELTA elif edges & 4 == 4: new_view[1] = self.DELTA new_view[3] = self.DELTA elif edges & 2 == 2: new_view[0] = -self.DELTA new_view[2] = -self.DELTA elif edges & 8 == 8: new_view[0] = self.DELTA new_view[2] = self.DELTA new_view_rect = old_view.adjusted( new_view[0], new_view[1], new_view[2], new_view[3] ) self.fitInView(new_view_rect, Qt.KeepAspectRatio) x = self.scene.destination.x() y = self.scene.destination.y() self.set_rect() self.set_rect() def set_rect(self): for item in self.scene.items(): if isinstance(item, QGraphicsRectItem): self.scene.removeItem(item) self.rect = QRect(self.scene.begin, self.scene.destination) self.scene.addRect(self.rect.normalized(), QPen(Qt.red)) def get_rect(self): return self.rect.normalized() def mouseReleaseEvent(self, event): if event.button() & Qt.RightButton: self.set_rect() self.update() super(PhotoViewer, self).mouseReleaseEvent(event) def check_if_point_on_edge(self, point): rect = self.mapToScene( self.viewport().rect() ).boundingRect() # left, top, width, height edges = 0 # top = 1, bottom = 2, left = 4, right = 8 internal_rect = rect.adjusted(self.DELTA, self.DELTA, -self.DELTA, -self.DELTA) if rect.contains(point) and not internal_rect.contains(point): if point.x() < internal_rect.left(): edges = edges | 2 elif point.x() > internal_rect.right(): edges = edges | 8 if point.y() < internal_rect.top(): edges = edges | 1 elif point.y() > internal_rect.bottom(): edges = edges | 4 return edges def wheelEvent(self, event): self.calc_delta() if event.angleDelta().y() > 0: factor = 1.25 self.zoom += 1 else: factor = 0.8 self.zoom -= 1 if self.zoom > 0: self.scale(factor, factor) elif self.zoom == 0: self.fit_in_view() else: self.zoom = 0 def calc_delta(self): rect_width = self.mapToScene(self.viewport().rect()).boundingRect().width() self.DELTA = int(rect_width / 25) def set_photo(self, pixmap=None): """ Sets the photo to be displayed, if pixmap is None, the photo is set to empty """ if pixmap and not pixmap.isNull(): self._empty = False self.setDragMode(QGraphicsView.ScrollHandDrag) self._photo.setPixmap(pixmap) else: self._empty = True self.setDragMode(QGraphicsView.NoDrag) self._photo.setPixmap(QPixmap()) self.photo_size = QRectF(self._photo.pixmap().rect()) def on_loop_signal(self): if self.cursor_pos != QCursor.pos(): self.mouse_event_signal.emit() return False return True class SelectColorBarDialogScene(QGraphicsScene): def __init__(self, parent): super(SelectColorBarDialogScene, self).__init__(parent) self.begin, self.destination = QPoint(), QPoint() def mousePressEvent(self, event): if event.buttons() & Qt.RightButton: self.begin = event.scenePos().toPoint() self.destination = self.begin self.update() super(SelectColorBarDialogScene, self).mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() & Qt.RightButton: self.destination = event.scenePos().toPoint() self.update() super(SelectColorBarDialogScene, self).mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() & Qt.RightButton: self.begin, self.destination = QPoint(), QPoint() self.update() super(SelectColorBarDialogScene, self).mouseReleaseEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) dialog = MyDialog(path=None) #! set picture path here dialog.exec() sys.exit(app.exec_())
@elver said in In a QGraphicsview, how can I keep the image moving when the cursor is on the edge, without having to move the mouse?:
from PySide6.QtCore import Signal, Qt, QRectF, QPoint, QRect
[...]
This is too large for a minimal working example. Cut out all of the stuff unrelated to the problem.
Here is an example of detecting a mouse dragged outside of a window, with an operation repeated until the button is released or the cursor moves back over it:
from PyQt5 import QtWidgets, QtCore, QtGui class View(QtWidgets.QGraphicsView): def __init__(self, parent=None): super().__init__(parent) self.scene = QtWidgets.QGraphicsScene() self.workTimer = QtCore.QTimer() self.workTimer.setInterval(33) self.workTimer.timeout.connect(self.autoscroll) def mouseMoveEvent(self, event): self.position = event.pos() if self.autoscroll(): self.workTimer.start() else: self.workTimer.stop() super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): self.workTimer.stop() super().mouseReleaseEvent(event) def autoscroll(self): geometry = self.geometry() if not geometry.contains(self.position): if self.position.x() < 0: print("-x") elif self.position.x() >= geometry.width(): print("+x") elif self.position.y() < 0: print("-y") elif self.position.y() >= geometry.height(): print("+y") return True else: return False app = QtWidgets.QApplication([]) view = View() view.show() app.exec()
-
@JonB Hahaha alright thanks! what could I do other than a loop if I want to repeat the same action while the cursor is not moving? I cant imagine doing that without having a loop somewhere. Could I somehow exit the loop after every run to update everything and then go back in?
@elver said in In a QGraphicsview, how can I keep the image moving when the cursor is on the edge, without having to move the mouse?:
what could I do other than a loop if I want to repeat the same action while the cursor is not moving?
Use QTimer
-