QGraphicsScene and QGraphicsItem handlers - python

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.

Related

Qt insert QGraphicsItem in scene on click position

I am designing an app to draw electrical circuit models and I need to insert elements on scene by clicking on a button and after on the scene.
The way it should work is the following: I press the button to insert a node, then it shows a message saying "Press where you want to insert the element", you press, and the element appear on screen.
I think the problem is that I have to stop the code to get the position and then continue or something like that.
Below I show a part of the code (the original contains more classes and a second tab for calculations, that it is not needed for this trouble and it is not connected in any way):
from PyQt5 import sip
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import QRectF, Qt, QSize, QPointF, QPoint, QLineF, showbase
from PIL import Image
import calculation
class Main(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Nodal Equations")
self.setWindowIcon(QIcon("images/flash.ico"))
self.setGeometry(150,50,1650,950)
self.setFixedSize(self.size())
self.UI()
self.show()
def UI(self):
self.toolBar()
self.tabwidgets()
self.widgets()
self.layouts()
def toolBar(self):
self.tb = QToolBar("Toolbar")
self.addToolBar(Qt.RightToolBarArea, self.tb)
self.tb.setIconSize(QSize(50,50))
# Toolbar buttons
self.addNode = QAction(QIcon("images/node.png"),"Node",self)
self.tb.addAction(self.addNode)
self.addNode.triggered.connect(self.funcAddNode)
self.tb.addSeparator()
def tabwidgets(self):
self.tabs = QTabWidget()
self.tabs.blockSignals(True) # To update tabs. After defining every layout we have to set it to False
self.setCentralWidget(self.tabs) # If this is not used, we cannot see the widgets (Needed if we use QMainWindow)
self.modelViewTab = QWidget()
self.tabs.addTab(self.modelViewTab, "Model View")
def widgets(self):
# Model View widgets
self.view = ModelView()
self.instructionsLabel = QLabel("Welcome to Node Equations Solver. To start select an order on the toolbar menu", self)
def layouts(self):
# Model View Tab layouts
self.mainModelLayout = QVBoxLayout()
self.sceneLayout = QHBoxLayout()
self.instructionsLayout = QHBoxLayout()
self.mainModelLayout.setAlignment(Qt.AlignCenter)
##### Adding widgets
self.sceneLayout.addWidget(self.view)
self.instructionsLayout.addWidget(self.instructionsLabel)
##### Adding layouts
self.mainModelLayout.addLayout(self.sceneLayout)
self.mainModelLayout.addLayout(self.instructionsLayout)
self.modelViewTab.setLayout(self.mainModelLayout)
def funcAddNode(self):
self.node = Node(0,500,500)
self.view.scene.addItem(self.node)
class Node(QGraphicsEllipseItem):
def __init__(self,number, x, y):
super(Node, self).__init__(0, 0, 10, 10)
self.number = number
self.setPos(x, y)
self.setBrush(Qt.yellow)
self.setAcceptHoverEvents(True)
# Mouse hover events
def hoverEnterEvent(self, event):
app.instance().setOverrideCursor(Qt.OpenHandCursor)
def hoverLeaveEvent(self, event):
app.instance().restoreOverrideCursor()
# Mouse click events
def mousePressEvent(self, event):
app.instance().setOverrideCursor(Qt.ClosedHandCursor)
def mouseReleaseEvent(self, event):
app.instance().restoreOverrideCursor()
def mouseMoveEvent(self, event):
orig_cursor_position = event.lastScenePos()
updated_cursor_position = event.scenePos()
orig_position = self.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()
if updated_cursor_x < 0:
self.setPos(QPointF(0, updated_cursor_y))
elif updated_cursor_y < 0:
self.setPos(QPointF(updated_cursor_x, 0))
elif updated_cursor_x + self.boundingRect().right() > 1550:
self.setPos(QPointF(1550 - self.boundingRect().width(), updated_cursor_y))
elif updated_cursor_y + self.boundingRect().bottom() > 840:
self.setPos(QPointF(updated_cursor_x, 840 - self.boundingRect().height()))
else:
self.setPos(QPointF(updated_cursor_x, updated_cursor_y))
class ModelView(QGraphicsView):
def __init__(self):
super().__init__()
self.setRenderHints(QPainter.Antialiasing)
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.setSceneRect(0, 0, 1550, 840)
##### This is a way I tried to pick the cursor position and store it, but it didn't work
# def mousePressEvent(self, event):
# x = event.scenePos().x()
# y = event.scenePos().y()
# return (x,y)
def main():
global app
app = QApplication(sys.argv)
window = Main()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
You need to change the return (x,y) to something useful, i.e. emitting a signal or actually adding an element.
mousePressEvent is a method that does not return anything (void in C++).

How to restrain mouse cursor from leaving a QWidget area in PySide2

I have a widget containing two buttons that can be (drag and drop) swapped using the mouse middle button. I am trying to restrain the mouse cursor from leaving the QWidget area when dragging and dropping a Qpushbutton... I am using dragMoveEvent() which offsets the cursor every time it crosses the border of the widget. It works when you move the mouse slowly but fast movements will make the cursor leave the area. What is the best way to make this happen? Thanks.
PS: Go to the Drag and Drop area for reference
import os
import random
import sys
import time
from PySide2 import QtOpenGL
from PySide2 import QtWidgets
from PySide2.QtCore import QEvent, QMimeData, QPoint, QRect
from PySide2.QtGui import QCursor, QDrag, QWindow
# import nuke
# import nukescripts
from collapse import Collapse
try:
from PySide import QtGui, QtCore
except ImportError:
from PySide2 import QtCore
from PySide2 import QtWidgets as QtGui
from PySide2 import QtGui as QtG
class CreateNodeBoard(QtGui.QWidget):
def __init__(self, parent = None):
QtGui.QWidget.__init__(self, parent)
self.nukePathSeparator = "/"
#self.toolPath = self.getFullPathWithExt()
self.currentDir = os.path.dirname(os.path.realpath(__file__))
################################################################################
# GUI
################################################################################
self.setMinimumWidth(350)
self.mainLayout = QtGui.QVBoxLayout()
self.mainLayout.setSpacing(0)
self.mainLayout.setAlignment(QtCore.Qt.AlignTop)
self.setLayout(self.mainLayout)
self.target = None
self.setAcceptDrops(True)
self.nodeBoardWidget = QtGui.QWidget()
self.nodeBoardWidget.setAcceptDrops(True)
nodeBoardVLayout = QtWidgets.QVBoxLayout()
self.nodeBoardWidget.setLayout(nodeBoardVLayout)
self.userButtonLayout = QtGui.QGridLayout()
nodeBoardVLayout.addLayout(self.userButtonLayout)
button1 = QtWidgets.QPushButton("a")
button2 = QtWidgets.QPushButton("b")
self.userButtonLayout.addWidget(button1)
self.userButtonLayout.addWidget(button2)
self.userButtonLayout.setAlignment(QtCore.Qt.AlignLeft)
self.mainLayout.addWidget(self.nodeBoardWidget)
def get_index(self, pos):
for i in range(self.userButtonLayout.count()):
buttonGlob = self.userButtonLayout.itemAt(i).widget().mapToGlobal(QPoint(0,0))
if QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).contains(pos) and i != self.target:
return i
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
self.target = self.get_index(QCursor.pos())
else:
self.target = None
def mouseMoveEvent(self, event):
if event.buttons() & QtCore.Qt.MiddleButton and self.target is not None:
print("moving")
drag = QDrag(self.userButtonLayout.itemAt(self.target).widget())
pix = self.userButtonLayout.itemAt(self.target).widget().grab()
mimedata = QMimeData()
mimedata.setImageData(pix)
drag.setMimeData(mimedata)
drag.setPixmap(pix)
drag.setHotSpot(QPoint(40,10))
drag.exec_()
def dragMoveEvent(self, event):
cursorPos = QCursor.pos()
widgetPos = self.nodeBoardWidget.mapToGlobal(QPoint(0,0))
if cursorPos.x() < widgetPos.x() or cursorPos.y() < widgetPos.y():
QCursor.setPos(QCursor.pos().x() + 1 , QCursor.pos().y() + 1 )
event.accept()
def dragEnterEvent(self, event):
print("drag enter event")
if event.mimeData().hasImage():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
print("drop")
buttonGlob = self.userButtonLayout.itemAt(self.target).widget().mapToGlobal(self.pos())
if not QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).contains(QCursor.pos()):
source = self.get_index(QCursor.pos())
if source is None:
return
i, j = max(self.target, source), min(self.target, source)
p1, p2 = self.userButtonLayout.getItemPosition(i), self.userButtonLayout.getItemPosition(j)
self.userButtonLayout.addItem(self.userButtonLayout.takeAt(i), *p2)
self.userButtonLayout.addItem(self.userButtonLayout.takeAt(j), *p1)
self.target = None
app = QtWidgets.QApplication(sys.argv)
# Create a Qt widget, which will be our window.
window = CreateNodeBoard()
window.show() # IMPORTANT!!!!! Windows are hidden by default.
# Start the event loop.
app.exec_()
EDIT
So after further investigation and testing the code on both LINUX/WINDOWS I have come to the conclusion that both behaviours are caused by the programme exceeding the maximum recursion limit. Any time the mouse cursor during the drag event leaves the assigned widget something causes the events to call each other and this causes my app to crash. Having this as a standalone app does not cause any problem and I do not know why? Also, I have no ideas how this programme goes into recursion.
The previous solution of mine where I tried to create a "safe zone" for the mouse did not solve the issue as far as there are certain mouse movements causing the same bug.
Here is a better version of a working code. As I have already mentioned it works as a standalone GUI but causes the programme to crash within another software environment.
from __future__ import print_function
import sys
try:
from PySide import QtWidgets, QtCore
except ImportError:
from PySide2 import QtCore
from PySide2 import QtWidgets
from PySide2 import QtGui
from PySide2 import QtOpenGL
class CreateNodeBoard(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
################################################################################
# GUI
################################################################################
self.setMinimumWidth(350)
self.mainLayout = QtWidgets.QVBoxLayout()
self.mainLayout.setSpacing(0)
self.mainLayout.setAlignment(QtCore.Qt.AlignTop)
self.setLayout(self.mainLayout)
self.target = None
self.targetWidget = None
self.setAcceptDrops(True)
################################################################################
# GUI - NODE BOARD
################################################################################
# Create a Layout to hold all widgets
self.nodeBoardWidget = QtWidgets.QWidget()
self.nodeBoardWidget.setAcceptDrops(True)
nodeBoardVLayout = QtWidgets.QVBoxLayout()
self.nodeBoardWidget.setLayout(nodeBoardVLayout)
# create a grid layout inside nodeBoaardVLayout and load buttons from JSON
self.userButtonLayout = QtWidgets.QGridLayout()
nodeBoardVLayout.addLayout(self.userButtonLayout)
button1 = QtWidgets.QPushButton('button1')
self.userButtonLayout.addWidget(button1)
button2 = QtWidgets.QPushButton('button2')
self.userButtonLayout.addWidget(button2)
button3 = QtWidgets.QPushButton('test button')
button3.clicked.connect(self._test)
self.userButtonLayout.addWidget(button3)
self.userButtonLayout.setAlignment(QtCore.Qt.AlignLeft)
self.mainLayout.addWidget(self.nodeBoardWidget)
nodeBoardVLayout.addStretch(1)
############################################################################
# test
############################################################################
def _test(self):
print(self.topLevelWidget())
def dragLeaveEvent(self, event):
print("dragLeaveEvent :", event)
# XXX: does not work on macOS
# self.drag.cancel()
# parent = self.parent().mapToGlobal(self.drag.hotSpot())
# QtGui.QCursor.setPos(parent.x() + 50, parent.y() + 50)
# XXX: could still causes a crash
# q = QMessageBox()
# q.setText('no can do')
# q.exec_()
def leaveEvent(self, event):
pass
def enterEvent(self, event):
pass
################################################################################
# DRAG AND DROP
################################################################################
def get_index(self, pos):
for i in range(self.userButtonLayout.count()):
buttonGlob = self.userButtonLayout.itemAt(
i).widget().mapToGlobal(QtCore.QPoint(0, 0))
if QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).contains(pos) and i != self.target:
return i
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
self.target = self.get_index(QtGui.QCursor.pos())
else:
self.target = None
def mouseMoveEvent(self, event):
if event.buttons() and QtCore.Qt.MiddleButton and self.target is not None:
print("mouseClickEvent :", event)
self.drag = QtGui.QDrag(
self.userButtonLayout.itemAt(self.target).widget())
pix = self.userButtonLayout.itemAt(self.target).widget().grab()
mimedata = QtCore.QMimeData()
mimedata.setImageData(pix)
self.drag.setMimeData(mimedata)
self.drag.setPixmap(pix)
self.drag.setHotSpot(QtCore.QPoint(40, 10))
self.drag.exec_()
def dragMoveEvent(self, event):
# print("dragMoveEvent :", event)
cursorPos = QtGui.QCursor.pos()
widgetPos = self.nodeBoardWidget.mapToGlobal(QtCore.QPoint(0, 0))
if cursorPos.x() <= widgetPos.x() or cursorPos.y() <= widgetPos.y():
QtGui.QCursor.setPos(QtGui.QCursor.pos().x() +
10, QtGui.QCursor.pos().y() + 10)
def dragEnterEvent(self, event):
print("dragEnterEvent :", event)
# XXX: if ignored, will not crash but will not propagate events
event.accept()
def dropEvent(self, event):
# print("dropEvent :", event)
buttonGlob = self.userButtonLayout.itemAt(
self.target).widget().mapToGlobal(self.pos())
if not QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).contains(QtGui.QCursor.pos()):
source = self.get_index(QtGui.QCursor.pos())
if source is None:
return
i, j = max(self.target, source), min(self.target, source)
p1, p2 = self.userButtonLayout.getItemPosition(
i), self.userButtonLayout.getItemPosition(j)
self.userButtonLayout.addItem(self.userButtonLayout.takeAt(i), *p2)
self.userButtonLayout.addItem(self.userButtonLayout.takeAt(j), *p1)
self.target = None
class TestWidget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.test_widget = QtWidgets.QWidget()
self.set_test()
_layout = QtWidgets.QHBoxLayout()
_layout.addWidget(CreateNodeBoard())
_layout.addWidget(self.test_widget)
self.setLayout(_layout)
def set_test(self):
"""Adjacent test widget"""
self.test_widget.setAutoFillBackground(True)
self.test_widget.setPalette(QtGui.QColor(255, 0, 0))
_test_layout = QtWidgets.QVBoxLayout()
_test_layout.addWidget(QtWidgets.QLabel('TEST WIDGET'))
self.test_widget.setLayout(_test_layout)
try:
import nukescripts
except ImportError as error:
APP = QtWidgets.QApplication(sys.argv)
WINDOW = TestWidget()
WINDOW.show()
APP.exec_()
else:
nukescripts.panels.registerWidgetAsPanel(
'TestWidget', 'DragDrop',
'DragDrop.MainWindow')
Premise
This answer is very limited to the specific question (prevent the user to move the mouse outside the boundaries of the given widget). Unfortunately, it's not a complete solution due to many conceptual problems in the given code:
both drag and drop events should always be managed by the widget that will actually handle them (in this case, nodeBoardWidget), not their parent;
getting the layout index of an item should always consider the item geometry (using a fixed size is discouraged, since widget sizes depend on lots of aspects) and the fact that an item could not be a widget (nested layouts are still layout items, so layout.itemAt().widget() could return None);
"swapping" items based on item indexes doesn't always keep the item index, as the resulting indexes could be unreliable (especially for grid layouts);
Partial solution
The important aspect to keep in mind is that trying to move the mouse by a small and fixed amount to "fix" its position is wrong, because mouse events are not continuous: if the mouse is moved very fast from x=0 to x=100 you don't get all values between 0 and 100, but only a small fraction of the intermediate positions.
For the same reason, trying to "fix" the position just by a fixed amount of pixels is wrong, since the offset can be variable depending on the mouse speed.
The above results in dragMoveEvent not being called if the mouse moves too fast outside the parent boundaries. While in your specific case it "works", it's only because you implemented the function in the parent (which, as said, is not the suggested approach, and this is a clear example of that reason). If the mouse position has to be "contained", the dragLeaveEvent must be implemented instead.
class CreateNodeBoard(QtWidgets.QWidget):
def __init__(self, parent = None):
# ...
self.targetWidget = None
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
widget = QtWidgets.QApplication.widgetAt(event.globalPos())
if (widget != self.nodeBoardWidget and
self.nodeBoardWidget.isAncestorOf(widget)):
self.targetWidget = widget
def mouseMoveEvent(self, event):
if self.targetWidget:
drag = QDrag(self.targetWidget)
pix = self.targetWidget.grab()
mimedata = QMimeData()
mimedata.setImageData(pix)
drag.setMimeData(mimedata)
drag.setPixmap(pix)
drag.setHotSpot(QPoint(40,10))
drag.exec_()
def dragEnterEvent(self, event):
if self.nodeBoardWidget.isAncestorOf(event.source()):
event.accept()
def dragLeaveEvent(self, event):
geo = self.nodeBoardWidget.rect().translated(
self.nodeBoardWidget.mapToGlobal(QtCore.QPoint()))
pos = QtGui.QCursor.pos()
if pos not in geo:
if pos.x() < geo.x():
pos.setX(geo.x())
elif pos.x() > geo.right():
pos.setX(geo.right())
if pos.y() < geo.y():
pos.setY(geo.y())
elif pos.y() > geo.bottom():
pos.setY(geo.bottom())
QtGui.QCursor.setPos(pos)
I strongly suggest you to study the above example and considerations, as your code has lots of conceptual issues that my answer could not fix if not by creating a completely new example done from scratch. Also, since it's pretty clear that you're getting your code from various sources found on the web, I also suggest you to do that with awareness. Imitation is a good way of learning, but not without understanding what is being done. Do your research on all functions and classes used, and study all the related documentation, starting from layout managers, drag and drop and not forgetting about official code styling practices.
FOUND A FIX:
the dragEnterEvent caused the whole thing to go into recursion which causes the app to crash. (Linux terminal kept showing maximum recursion limit exceeded every time I moved the dragEvent outside the widgets area)
So to fix this I have created a condition inside the dragEnterEvent that if the mouse cursor moves outside the widgets it should ignore the event.
################################################################################
# DRAG AND DROP
################################################################################
def get_index(self, pos):
for i in range(self.userButtonLayout.count()):
buttonGlob = self.userButtonLayout.itemAt(i).widget().mapToGlobal(QtCore.QPoint(0,0))
if QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).contains(pos) and i != self.target:
return i
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
self.target = self.get_index(QtGui.QCursor.pos())
if event.buttons() & QtCore.Qt.MiddleButton and self.target is not None:
drag = QtGui.QDrag(self.userButtonLayout.itemAt(self.target).widget())
pix = self.userButtonLayout.itemAt(self.target).widget().grab()
mimedata = QtCore.QMimeData()
mimedata.setImageData(pix)
drag.setMimeData(mimedata)
drag.setPixmap(pix)
drag.setHotSpot(QtCore.QPoint(40,10))
drag.exec_()
else:
self.target = None
def dragLeaveEvent(self, event):
if self.cursorInWidget():
drag = QtGui.QDrag(self.userButtonLayout.itemAt(self.target).widget())
drag.cancel()
def cursorInWidget(self):
cursorPos = QtGui.QCursor.pos()
widgetWidth = self.nodeBoardWidget.geometry().width()
widgetHeight = self.nodeBoardWidget.geometry().height()
widgetPos = self.nodeBoardWidget.mapToGlobal(QtCore.QPoint(0,0))
if cursorPos.x() <= widgetPos.x() or cursorPos.y() <= widgetPos.y() or cursorPos.x() >= (widgetPos.x() + widgetWidth) or cursorPos.y() >= (widgetPos.y() + widgetHeight):
return False
else:
return True
def dragEnterEvent(self, event):
if self.cursorInWidget():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
buttonGlob = self.userButtonLayout.itemAt(self.target).widget().mapToGlobal(self.pos())
if not QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).contains(QtGui.QCursor.pos()):
source = self.get_index(QtGui.QCursor.pos())
if source is None:
return
i, j = max(self.target, source), min(self.target, source)
p1, p2 = self.userButtonLayout.getItemPosition(i), self.userButtonLayout.getItemPosition(j)
self.userButtonLayout.addItem(self.userButtonLayout.takeAt(i), *p2)
self.userButtonLayout.addItem(self.userButtonLayout.takeAt(j), *p1)
self.target = None

How to draw both object on the same window?

Prologue: Both of the object represented below are on the same window.
I am having problem with the not updating
I am having a problem related to self.setGeometry(x,y,w,h) function. So what I want to achieve is multiple rect, rendered in parallel, which each has a line protruding out of their rectangle when clicked. (Creating a connector though that is not the point of this topic).
In this case, I have rect A and rect B rendering together. Rect A has a line protruding out to the position of the mouse.
(obj A) (obj B)
____ ____
| | | |
| \ | | |
---\ ----
\
(Mouse)
An example code of what am I trying to achieve.
# File: connectors.py
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget
class Connector(QWidget):
def __init__(self, rect: List[int]):
super().__init__()
self.setGeometry(0, 0, 1920, 1080)
self.rect = rect
self.clicked = False
self.begin = QPoint(rect[0], rect[1])
self.end = QPoint(0,0)
def paintEvent(self, event):
qp = QPainter()
qp.begin(self)
qp.setPen(Qt.red)
qp.drawRect(*self.rect)
if self.clicked:
qp = QPainter()
qp.begin(self)
qp.setPen(Qt.red)
qp.drawLine(self.begin, self.end)
self.update()
def mousePressEvent(self, event):
if event.button() == 1:
self.clicked = True
self.end = event.pos()
def mouseMoveEvent(self, event):
if self.clicked:
self.end = event.pos()
def mouseReleaseEvent(self, event):
if event.button() == 1:
self.clicked = False
self.end = event.pos()
# File: main.py
scene = QGraphicsScene()
scene.addWidget( Connector((400, 400, 100, 100)) )
scene.addWidget( Connector((400, 600, 100, 100)) )
But what I am ending up is PyQt showing the top-most object onto the screen thus only showing one object, BUT I also tried to minimize the geometry leading the protruding line, when clicked, cutting off on the border.
Explanation:
In your case it has 2 widgets where you draw the rectangles where one is on top of another so you will only see one of them: the one above.
Solution:
Qt GraphicsView Framework (QGraphicsView, QGraphicsScene, QGraphicsXItem, etc.) works differently and painting is not used directly since they offer basic items that implement all the functionalities so in this case you must use QGraphicsRectItem with QGraphicsLineItem and modify it with the information of the QGraphicsView.
Considering the above, the solution is:
import sys
from PyQt5.QtCore import QRectF, Qt
from PyQt5.QtWidgets import (
QApplication,
QGraphicsLineItem,
QGraphicsRectItem,
QGraphicsScene,
QGraphicsView,
)
class RectItem(QGraphicsRectItem):
def __init__(self, rect, parent=None):
super().__init__(parent)
self.setRect(QRectF(*rect))
self.setPen(Qt.red)
self._line_item = QGraphicsLineItem(self)
self.line_item.setPen(Qt.red)
l = self.line_item.line()
l.setP1(self.rect().topLeft())
l.setP2(self.rect().topLeft())
self.line_item.setLine(l)
self.line_item.hide()
#property
def line_item(self):
return self._line_item
def move_line_to(self, sp):
lp = self.mapFromScene(sp)
l = self.line_item.line()
l.setP2(lp)
self.line_item.setLine(l)
class GraphicsView(QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.setScene(QGraphicsScene())
self.scene().addItem(RectItem((400, 400, 100, 100)))
self.scene().addItem(RectItem((400, 600, 100, 100)))
def mousePressEvent(self, event):
vp = event.pos()
sp = self.mapToScene(vp)
self.move_lines(sp)
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
vp = event.pos()
sp = self.mapToScene(vp)
self.move_lines(sp)
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
for item in self.items():
if isinstance(item, RectItem):
item.line_item.hide()
super().mouseReleaseEvent(event)
def move_lines(self, sp):
for item in self.items():
if isinstance(item, RectItem):
item.line_item.show()
item.move_line_to(sp)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = GraphicsView()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

PyQt5: Check if mouse is held down in enter-event

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

How to update a QPixmap in a QGraphicsView with PyQt

I am trying to paint on a QPixmap inside a QGraphicsView. The painting works fine, but the QGraphicsView doesn't update it.
Here is some working code:
#!/usr/bin/env python
from PyQt4 import QtCore
from PyQt4 import QtGui
class Canvas(QtGui.QPixmap):
""" Canvas for drawing"""
def __init__(self, parent=None):
QtGui.QPixmap.__init__(self, 64, 64)
self.parent = parent
self.imH = 64
self.imW = 64
self.fill(QtGui.QColor(0, 255, 255))
self.color = QtGui.QColor(0, 0, 0)
def paintEvent(self, point=False):
if point:
p = QtGui.QPainter(self)
p.setPen(QtGui.QPen(self.color, 1, QtCore.Qt.SolidLine))
p.drawPoints(point)
def clic(self, mouseX, mouseY):
self.paintEvent(QtCore.QPoint(mouseX, mouseY))
class GraphWidget(QtGui.QGraphicsView):
""" Display, zoom, pan..."""
def __init__(self):
QtGui.QGraphicsView.__init__(self)
self.im = Canvas(self)
self.imH = self.im.height()
self.imW = self.im.width()
self.zoomN = 1
self.scene = QtGui.QGraphicsScene(self)
self.scene.setItemIndexMethod(QtGui.QGraphicsScene.NoIndex)
self.scene.setSceneRect(0, 0, self.imW, self.imH)
self.scene.addPixmap(self.im)
self.setScene(self.scene)
self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter)
self.setMinimumSize(400, 400)
self.setWindowTitle("pix")
def mousePressEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
pos = self.mapToScene(event.pos())
self.im.clic(pos.x(), pos.y())
#~ self.scene.update(0,0,64,64)
#~ self.updateScene([QtCore.QRectF(0,0,64,64)])
self.scene.addPixmap(self.im)
print('items')
print(self.scene.items())
else:
return QtGui.QGraphicsView.mousePressEvent(self, event)
def wheelEvent(self, event):
if event.delta() > 0:
self.scaleView(2)
elif event.delta() < 0:
self.scaleView(0.5)
def scaleView(self, factor):
n = self.zoomN * factor
if n < 1 or n > 16:
return
self.zoomN = n
self.scale(factor, factor)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
widget = GraphWidget()
widget.show()
sys.exit(app.exec_())
The mousePressEvent does some painting on the QPixmap. But the only solution I have found to update the display is to make a new instance (which is not a good solution).
How do I just update it?
The pixmap can't be linked to your scene, the item uses an internal copy of it, so you have to update the QGraphicsPixmapItem with the new pixmap:
def __init__(self):
...
# save the item as a member
self.imItem = self.scene.addPixmap(self.im)
...
def mousePressEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
pos = self.mapToScene(event.pos())
self.im.clic(pos.x(), pos.y())
self.imItem.setPïxmap(self.im)
...
But it would make more sense to make your class Canvas inherit from QGraphicsPixmapItem instead of QPixmap, you would still have to get the pixmap with pixmap(), paint on it, and call setPixmap to update it. As a bonus, that code would be in the item own mousePressEvent method.

Categories

Resources