So I have a frameless QDialog that I want to be able to move around simply by clicking and dragging it. Given the code below, dragging the dialog always snaps the very top-left (0,0) of the dialog to the mouse. How might I circumvent this, or rather, what might the math be for it?
Standard QDialog with the following basic subclass:
class Main(QtGui.QDialog):
def __init__(self, args):
QtGui.QDialog.__init__(self)
def mouseMoveEvent(self, event):
super(Main, self).mouseMoveEvent(event)
if self.leftClick == True: self.moveWindow(event.globalPos())
def mousePressEvent(self, event):
super(Main, self).mousePressEvent(event)
if event.button() == QtCore.Qt.LeftButton:
self.leftClick = True
def mouseReleaseEvent(self, event):
super(Main, self).mouseReleaseEvent(event)
self.leftClick = False
Instead of event.pos(), try calling event.globalPos(). From the QMouseEvent reference, "If you move the widget as a result of the mouse event, use the global position returned by globalPos() to avoid a shaking motion."
Proposed solution moves Window, but mouse cursor jumps to 0,0 of Window. I wanted mouse cursor to stay on x,y of the Window all the time.
Here is upgraded version of the code [in QT5]:
X=0
X2=8 #!!!!
Y=0
Y2=30 #!!!!
class Main(QtWidgets.QMainWindow):
leftClick = False #! IMPORTANT
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
def mouseMoveEvent(self, event):
super(Main, self).mouseMoveEvent(event)
if self.leftClick == True:
self.move(event.globalPos().x()-X-X2,event.globalPos().y()-Y-Y2)
def mousePressEvent(self, event):
super(Main, self).mousePressEvent(event)
if event.button() == QtCore.Qt.LeftButton:
self.leftClick = True
global X,Y
X=event.pos().x()
Y=event.pos().y()
def mouseReleaseEvent(self, event):
super(Main, self).mouseReleaseEvent(event)
self.leftClick = False
For frameless window (made with window.setMask()) I need some constants like X2 and Y2, because "masked" frameless window is a bit smaller than a real framed window. Don't know how to calculate this difference yet.
UP. After long time I've found one critical bug. If you press Left Mouse Button on any pushbutton (just after the application start) and drag the mouse cursor away from that pushbutton, your application will crash, because we refer to nonexistent variable LeftClick. That's why in class Main we need to create LeftClick.
Related
I'm new to Qt, and specially to PyQt5. I'm trying to develop a GUI using QGraphicsView, QGraphicsScene and QGraphicsPixmapItem. My objective is to add items to the scene when the user clicks on the scene (achieved using mousePressedEvent() in a QGraphicsScene subclass) and, using mouseMoveEvent(), I was able to move the element.
Then, I discovered that, with my implementation, the items could be moved like "pushing" them from outside the bounding rect. So, in order to fix it, after some searching, I decided to implement a subclass of QGraphicsPixmapItem to implement its own event functions.
Nevertheless, I found out that my item does not recognize mousePressed nor mouseMove events, but the ones from QGraphicsScene. My questions are:
What is the most efficient way to move elements without having the first problem I encountered?
Is it possible to combine both scene and item event handlers? I have not understood event propagation completely.
To make it more clear, I leave my code down below for the moving problem:
#!/usr/bin/env python3
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
class GraphicsScene(QGraphicsScene):
def __init__(self):
super(GraphicsScene, self).__init__()
self.image = 'car.png' # Image of your own
self.inserted = False
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton and not self.inserted:
img = QPixmap(self.image).scaled(50, 50, Qt.KeepAspectRatio)
pixmap = QGraphicsPixmapItem(img)
offset = pixmap.boundingRect().topLeft() - pixmap.boundingRect().center()
pixmap.setOffset(offset.x(), offset.y())
pixmap.setShapeMode(QGraphicsPixmapItem.BoundingRectShape)
pixmap.setFlag(QGraphicsItem.ItemIsSelectable, True)
pixmap.setPos(event.scenePos())
super().mousePressEvent(event)
self.addItem(pixmap)
self.inserted = True
else:
pass
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
item = self.itemAt(event.scenePos(), QTransform())
if item is None:
return
orig_cursor_position = event.lastScenePos()
updated_cursor_position = event.scenePos()
orig_position = item.scenePos()
updated_cursor_x = updated_cursor_position.x() - orig_cursor_position.x() + orig_position.x()
updated_cursor_y = updated_cursor_position.y() - orig_cursor_position.y() + orig_position.y()
item.setPos(QPointF(updated_cursor_x, updated_cursor_y))
class MainWindow(QMainWindow):
def __init__(self):
super(QMainWindow, self).__init__()
self.resize(600, 600)
self.canvas = QGraphicsView()
self.scene = GraphicsScene()
self.setCentralWidget(self.canvas)
self.canvas.setScene(self.scene)
def showEvent(self, event):
self.canvas.setSceneRect(QRectF(self.canvas.viewport().rect()))
def resizeEvent(self, event):
self.canvas.setSceneRect(QRectF(self.canvas.viewport().rect()))
app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec_())
I think that the OP is unnecessarily complicated since the QGraphicsItems (like QGraphicsPixmapItem) already implement this functionality and it only remains to activate the QGraphicsItem::ItemIsMovable flag:
class GraphicsScene(QGraphicsScene):
def __init__(self):
super(GraphicsScene, self).__init__()
self.image = "car.png" # Image of your own
self.inserted = False
def mousePressEvent(self, event):
super().mousePressEvent(event)
if event.button() == Qt.LeftButton and not self.inserted:
img = QPixmap(self.image).scaled(50, 50, Qt.KeepAspectRatio)
pixmap = QGraphicsPixmapItem(img)
pixmap.setOffset(-pixmap.boundingRect().center())
pixmap.setShapeMode(QGraphicsPixmapItem.BoundingRectShape)
pixmap.setFlag(QGraphicsItem.ItemIsSelectable, True)
pixmap.setFlag(QGraphicsItem.ItemIsMovable, True)
pixmap.setPos(event.scenePos())
self.addItem(pixmap)
self.inserted = True
Override mouseMoveEvent is unnecessary.
I have a row of buttons, each of which can accept drops. However, when I leave a button with my cursor with another button being dragged, the 'dragLeaveEvent' is not being called.
class Button(QtGui.QPushButton):
def __init__(self):
super(Button, self).__init__()
self.setAcceptDrops(True)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
drag = QtGui.QDrag(self)
mime = QtCore.QMimeData()
mime.setText("f")
drag.setMimeData(mime)
drag.exec_()
def dragEnterEvent(self, event):
print "enter"
def dragLeaveEvent(self, event):
print "leave"
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.mainLayout = QtGui.QVBoxLayout()
self.setLayout(self.mainLayout)
for i in range(10):
btn = Button()
self.mainLayout.addWidget(btn)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec_())
As the documentation about dragEnterEvent() reports (it's from Qt5, but the same was valid for Qt4 also):
If the event is ignored, the widget won't receive any drag move events.
Note: any drag move events.
This also means drop events.
By default, all drag events are ignored for most widgets if the drag enter event is not accepted, so if you want to receive all events (including the leave event) that first event must be accepted.
class Button(QtGui.QPushButton):
# ...
def dragEnterEvent(self, event):
event.accept()
print "enter"
According to the drag and drop documentation, I was able to implement the drag functionality on my QToolButton, but that is overriding standard button click behaviour and I am unable to check if the button was pressed or there was an intent to start a drag by dragging mouse.
Here is my QToolBar..
class toolbarButton(QToolButton):
def __init__(self, parent = None, item = None):
super(toolbarButton, self).__init__(parent)
self.setIcon(...)
self.setIconSize(QSize(40, 40))
self.dragStartPosition = 0
...
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragStartPosition = event.pos()
def mouseMoveEvent(self, event):
if not (event.buttons() and Qt.LeftButton):
return
if (event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance():
return
drag = QDrag(self)
mimeData = QMimeData()
...
drag.setMimeData(mimeData)
drag.exec(Qt.CopyAction)
def sizeHint(self):
return self.minimumSizeHint()
def minimumSizeHint(self):
return QSize(30, 30)
My initial thought, was to add the emit when the distance is less than startdragdistance but that would be incorrect as it would fire everytime I moved my mouse. Is there a way of achieving this in PyQt5? That I get standard QToolButton behaviour on button press and my custom behaviour on button drag?
When you override a method you are removing the default behavior, so that does not happen then you must call the parent method through super():
def mousePressEvent(self, event):
super().mousePressEvent(event)
if event.button() == Qt.LeftButton:
self.dragStartPosition = event.pos()
My actual application is much more complicated than this, but the example below sums up the majority of my problem. I have multiple QLabels that I've subclassed to make them clickable. The labels display 16x16 images which requires a process of loading the images via Pillow, converting them to ImageQt objects, and then setting the pixmap of the label. In the example, I have 3 clickable QLabels that run the print_something function each time I click on them. My goal is to be able to hold the mouse down, and for each label I hover over, the function gets called. Any pointers would be great.
from PyQt5 import QtCore, QtWidgets, QtGui
from PIL import Image
from PIL.ImageQt import ImageQt
import sys
class ClickableLabel(QtWidgets.QLabel):
def __init__(self):
super().__init__()
clicked = QtCore.pyqtSignal()
def mousePressEvent(self, ev):
if app.mouseButtons() & QtCore.Qt.LeftButton:
self.clicked.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central_widget = QtWidgets.QWidget()
self.setFixedSize(300, 300)
image = Image.open("16x16image.png")
image_imageqt = ImageQt(image)
hbox = QtWidgets.QHBoxLayout()
hbox.setSpacing(0)
hbox.addStretch()
label01 = ClickableLabel()
label01.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label01.clicked.connect(self.print_something)
hbox.addWidget(label01)
label02 = ClickableLabel()
label02.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label02.clicked.connect(self.print_something)
hbox.addWidget(label02)
label03 = ClickableLabel()
label03.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label03.clicked.connect(self.print_something)
hbox.addWidget(label03)
hbox.addStretch()
central_widget.setLayout(hbox)
self.setCentralWidget(central_widget)
def print_something(self):
print("Printing something..")
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
The cause of the problem is stated in the docs for QMouseEvent:
Qt automatically grabs the mouse when a mouse button is pressed inside
a widget; the widget will continue to receive mouse events until the
last mouse button is released.
It does not look like there is a simple way around this, so something hackish will be required. One idea is to initiate a fake drag and then use dragEnterEvent instead of enterEvent. Something like this should probably work:
class ClickableLabel(QtWidgets.QLabel):
clicked = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.dragstart = None
def mousePressEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
self.dragstart = event.pos()
self.clicked.emit()
def mouseReleaseEvent(self, event):
self.dragstart = None
def mouseMoveEvent(self, event):
if (self.dragstart is not None and
event.buttons() & QtCore.Qt.LeftButton and
(event.pos() - self.dragstart).manhattanLength() >
QtWidgets.qApp.startDragDistance()):
self.dragstart = None
drag = QtGui.QDrag(self)
drag.setMimeData(QtCore.QMimeData())
drag.exec_(QtCore.Qt.LinkAction)
def dragEnterEvent(self, event):
event.acceptProposedAction()
if event.source() is not self:
self.clicked.emit()
Here is my simplified widget. I want to:
hide the mouse
lock it to the center of the window
get the relative mouse movement
I want similar mouse control to first person shooters.
class QGLControllerWidget(QtOpenGL.QGLWidget):
def __init__(self, app):
fmt = QtOpenGL.QGLFormat()
fmt.setVersion(3, 3)
fmt.setProfile(QtOpenGL.QGLFormat.CoreProfile)
fmt.setSampleBuffers(True)
fmt.setDepthBufferSize(24)
super(QGLControllerWidget, self).__init__(fmt, None)
self.setMouseTracking(True)
def mouseMoveEvent(self, event):
print(event.x(), event.y())