PyQt: QGraphicsScene events are not propagated down to GraphicItems - python

I have a GraphicScene inside a QGraphicView populated with many rectangles (items). I would like each rectangle to respond to the mouse click but I'm not able to find the hook to attach the event handler to the correct object and to get the event propagated to it.
I attached an event handler to the scene:
scene.event = myfunction
and it worked (it was firing every event) but I was unable to attach the same function to one of its children. Can you give me an insight on where to search for such an entry point?

So - I'm not really sure what you're doing there, but I can't think of anything in PyQt where you should be mapping a custom function directly to a scene's event method.
Do you have an actual example?
If you're doing:
scene.mousePressEvent = my_mouse_function
Then that is not how you want to do that.
You can look into using an event filters (http://doc.qt.nokia.com/4.7-snapshot/eventsandfilters.html#event-filters).
Best way to get what you want is to subclass the QGraphicsItem (whichever one you are using - QGraphicsRectItem, QGraphicsPathItem, etc.) and overload the mousePressEvent method on it.
http://doc.qt.nokia.com/4.7-snapshot/qgraphicsitem.html#mousePressEvent
For instance:
from PyQt4.QtGui import QGraphicsRectItem
class MyItem(QGraphicsRectItem):
def mousePressEvent(self, event):
super(MyItem, self).mousePressEvent(event)
print 'overloaded'
scene.addItem(MyItem())

Either subclass the view, scene, item etc and reimplement mousePressEvent and/or mouseReleaseEvent; or install an event filter on those items.
For an example that uses an event filter on a scene, see this answer.
Here's a demo which reimplements mouseReleaseEvent on the view:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.view = View(self)
self.label = QtGui.QLabel(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(self.label)
class View(QtGui.QGraphicsView):
def __init__(self, parent):
QtGui.QGraphicsView.__init__(self, parent)
self.setScene(QtGui.QGraphicsScene(self))
for index, name in enumerate('One Two Three Four Five'.split()):
item = QtGui.QGraphicsRectItem(
index * 60, index * 60, 50, 50)
item.setData(0, name)
self.scene().addItem(item)
def mouseReleaseEvent(self, event):
pos = event.pos()
item = self.itemAt(pos)
if item is not None:
text = 'Rectangle <b>%s</b>' % item.data(0).toString()
else:
text = 'No Rectangle (%d, %d)' % (pos.x(), pos.y())
self.parent().label.setText(text)
QtGui.QGraphicsView.mouseReleaseEvent(self, event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.resize(400, 400)
window.show()
sys.exit(app.exec_())

Related

Pyside2 how to get mouse position?

I want to get mouse position in my pyside2 application.(not desktop mouse position that QCursor gives) and I tried two way. Bellow is my code.
import sys
from PySide2 import QtGui, QtWidgets, QtCore
class Palette(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super().__init__(parent)
def mousePressEvent(self, event):
print(event.pos()) # always return (0,0)
print(QtWidgets.QWidget.mapToGlobal(QtCore.QPoint(0, 0))) #makes parameter type error
print(QtWidgets.QWidget.mapToGlobal(QtWidgets.QWidget)) # makes parameter type error
print(QtWidgets.QWidget.mapToGlobal(QtWidgets.QWidget.pos())) # makes parameter type error
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
palette = Palette(self)
view = QtWidgets.QGraphicsView(palette, self)
view.resize(500, 500)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.resize(500, 500)
main_window.show()
app.exec_()
I very wonder how i can get my mouse pos...
From what I understand you want to get the position on the window if you click anywhere in a widget.
To solve the problem, the logic is as follows:
Get the mouse position with respect to the widget
Convert that position to a global position, that is, with respect to the screen.
Convert that global position to a position relative to the window.
For the first step if mousePressEvent() is used, event.pos() returns the position relative to the widget.
For the second step you must convert that position relative to the widget to global with mapToGlobal().
And for the third step mapFromGlobal() of window is used.
def mousePressEvent(self, event):
p = event.pos() # relative to widget
gp = self.mapToGlobal(p) # relative to screen
rw = self.window().mapFromGlobal(gp) # relative to window
print("position relative to window: ", rw)
super(Widget, self).mousePressEvent(event)
Update:
The QGraphicsScene is not a widget, it is not a visual element, although it is part of the representation of a visual element: the QGraphicsView. For you to understand I will explain you with an analogy, let's say that there is a cameraman recording a scene, in that example the QGraphicsScene is the scene and the QGraphicsView is what the camera records, that is, it shows a piece of the QGraphicsScene, so there could be another cameraman recording the scene from another point, so it would show the same scene from another perspective, so the position of the scene depends on the camera, so if your current question would be equivalent to saying which is the position of the point P respect to the camera i-th, and that from the scene is impossible, you should get it from the camera.
So in conclusion you should not use QGraphicsScene but QGraphicsView, the following solutions implement the same logic using 2 different methods:
1. Creating a custom class of QGraphicsView:
import sys
from PySide2 import QtGui, QtWidgets, QtCore
class GraphicsView(QtWidgets.QGraphicsView):
def mousePressEvent(self, event):
p = event.pos() # relative to widget
gp = self.mapToGlobal(p) # relative to screen
rw = self.window().mapFromGlobal(gp) # relative to window
print("position relative to window: ", rw)
super(GraphicsView, self).mousePressEvent(event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
scene = QtWidgets.QGraphicsScene(self)
view = GraphicsView(scene, self)
self.setCentralWidget(view)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.resize(500, 500)
main_window.show()
sys.exit(app.exec_())
2. Using eventfilter:
import sys
from PySide2 import QtGui, QtWidgets, QtCore
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
scene = QtWidgets.QGraphicsScene(self)
self._view = QtWidgets.QGraphicsView(scene, self)
self.setCentralWidget(self._view)
self._view.installEventFilter(self)
def eventFilter(self, obj, event):
if obj is self._view and event.type() == QtCore.QEvent.MouseButtonPress:
p = event.pos() # relative to widget
gp = self.mapToGlobal(p) # relative to screen
rw = self.window().mapFromGlobal(gp) # relative to window
print("position relative to window: ", rw)
return super(MainWindow, self).eventFilter(obj, event)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.resize(500, 500)
main_window.show()
sys.exit(app.exec_())
On the other hand mapToGlobal() is a method that must be called by an instance, when you use QtWidgets.QWidget.mapToGlobal() there is no instance, my question is, what widget do you have the position? the position you have with respect to self, so you must use self.mapToGlobal() , that works for the objects belonging to a class that inherit from QWidget as QGraphicsView, but not in QGraphicsScene since it does not inherit from QWidget, it is not a widget as indicated in the lines above.
I have recently found a more universal way of getting the cursor position, if you don't want to go through sub classing and events.
# get cursor position
cursor_position = QtGui.QCursor.pos()
print cursor_position
### Returns PySide2.QtCore.QPoint(3289, 296)

Center non-editable QComboBox Text with PyQt

Is it possible to change the text alignment of a QComboBox that is not not editable? This answer achieves this by making the QComboBox editable
How to center text in QComboBox?
However, I don't want that because with that change, the clickable part of the ComboBox to trigger the list is now only the arrow on the right, whereas before the entire area was clickable and triggers the drop down menu.
The answer is partly already given in the linked question How to center text in QComboBox?
What remains is to make the read-only object clickable. This can be done as suggested here.
...giving you the following code:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
layout = QtGui.QVBoxLayout(self)
self.combo = QtGui.QComboBox()
self.combo.setEditable(True)
self.ledit = self.combo.lineEdit()
self.ledit.setAlignment(QtCore.Qt.AlignCenter)
# as suggested in the comment to
# https://stackoverflow.com/questions/23770287/how-to-center-text-in-qcombobox
self.ledit.setReadOnly(True) #
self.combo.addItems('One Two Three Four Five'.split())
layout.addWidget(self.combo)
self.clickable(self.combo).connect(self.combo.showPopup)
self.clickable(self.ledit).connect(self.combo.showPopup)
def clickable(self,widget):
""" class taken from
https://wiki.python.org/moin/PyQt/Making%20non-clickable%20widgets%20clickable """
class Filter(QtCore.QObject):
clicked = QtCore.pyqtSignal()
def eventFilter(self, obj, event):
if obj == widget:
if event.type() == QtCore.QEvent.MouseButtonRelease:
if obj.rect().contains(event.pos()):
self.clicked.emit()
# The developer can opt for .emit(obj) to get the object within the slot.
return True
return False
filter = Filter(widget)
widget.installEventFilter(filter)
return filter.clicked
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

How to add a second pixmap to an abstract button when pushed in PySide/PyQt

I'm very new to PySide/PyQt environment. I'm trying to make a menu of buttons on top and assign a task to each so that when they are clicked a function draws a painting on the central window. But I also want to make the button change when they are clicked.
I think this might be an straighforward problem to solve if I use QPushButton, but my buttons are images and I'm using the method suggested HERE and use QAbstractButton to create them.
It is mentioned there that
You can add second pixmap and draw it only when the mouse pointer is
hover over button.
And I'm trying to do exactly that. My question is this:
what are possible ways to achieve this? Are the same methods in QPushButtons applicable here? If so, are there any examples of it somewhere?
Here is a snippet of my code:
import sys
from PySide import QtGui, QtCore
BACKGROUND_COLOR = '#808080'
ICON_PATH_ACTIVE = 'icons/activ'
ICON_PATH_PASSIVE = 'icons/pasiv'
class MainWindow(QtGui.QMainWindow):
def __init__(self, app=None):
super(MainWindow, self).__init__()
self.initUI()
def initUI(self):
dockwidget = QtGui.QWidget()
self.setGeometry(200, 200, 400, 300)
hbox = QtGui.QHBoxLayout()
1_button = PicButton(QtGui.QPixmap("icons/pasiv/1.png"))
2_button = PicButton(QtGui.QPixmap("icons/pasiv/2.png"))
3_button = PicButton(QtGui.QPixmap("icons/pasiv/3.png"))
hbox.addWidget(1_button)
hbox.addWidget(2_button)
hbox.addWidget(3_button)
vbox = QtGui.QVBoxLayout()
vbox.addLayout(hbox)
vbox.setAlignment(hbox, QtCore.Qt.AlignTop)
dockwidget.setLayout(vbox)
self.setCentralWidget(dockwidget)
class PicButton(QtGui.QAbstractButton):
def __init__(self, pixmap, parent=None):
super(PicButton, self).__init__(parent)
self.pixmap = pixmap
self.setFixedSize(100, 100)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.drawPixmap(event.rect(), self.pixmap)
def main():
app = QtGui.QApplication(sys.argv)
central = MainWindow()
central.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Thank you.
Use a regular QPushButton with an icon.
iplay = QtGui.QIcon("path/play_icon.png")
ipause = QtGui.QIcon("path/pause_icon.png")
btn = QtGui.QPushButton(ipause, "", None)
def toggle_play():
if btn.icon() == ipause:
btn.setIcon(iplay)
# Do Pause Action
else:
btn.setIcon(ipause)
# Do Play Action
btn.clicked.connect(toggle_play)
btn.show()
If you want hover functionality then you will have to subclass the QPushButton
class MyButton(QtGui.QPushButton):
custom_click_signal = QtCore.Signal()
def enterEvent(self, event):
super().enterEvent(event)
# Change icon hove image here
def leaveEvent(self, event):
super().leaveEvent(event)
# Change icon back to original image here.
def mousePressEvent(self, event):
super().mousePressEvent(event)
self.custom_click_signal.emit()
# connect to signal btn.custom_click_signal.connect(method)
Icons are probably the easiest way instead of manually managing the paint event. There are also mousePressEvent and mouseReleaseEvents if you want the icon to change for someone holding the button down.

Adding tools to Scribble app with PYQT4

I am trying to write an app which works as MS Paint or other simple graphics editor. My goal is to provide such funcionality as: different tools( drawing lines, figures etc.), posibility to change colors, width, size and undo/redo function. I thought that the easiest way to do it was to create separete classes for drawing and viewing objects. Unfortunetly, it is not working properly. Here`s my code:
import sys
from PyQt4 import QtGui, QtCore
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
self.update()
self.view = View(self)
self.action = Action(self.view.scene)
self.UI()
def UI(self):
self.setGeometry(300,300,300,300)
self.setWindowTitle('Pen styles')
self.undo = QtGui.QPushButton("undo", self)
self.undo.clicked.connect(self.action.undoStack.undo)
self.redo = QtGui.QPushButton("redo", self)
self.redo.clicked.connect(self.action.undoStack.redo)
self.redo.move(0,50)
self.tool = QtGui.QPushButton("tool", self)
self.tool.setCheckable(True)
self.tool.clicked[bool].connect(self.new)
self.tool.move(0,100)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.view)
self.setLayout(layout)
self.show()
def new(self):
pass
class Action(QtGui.QGraphicsScene):
def __init__(self, scene):
QtGui.QGraphicsScene.__init__(self)
self.undoStack = QtGui.QUndoStack(self)
self.scene = scene
self.scene.addLine(0,0,150,150)
self.undoStack = QtGui.QUndoStack(self)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.start = event.pos()
self.scene.addLine(0,0,150,250)
def mouseReleaseEvent(self, event):
start = QtCore.QPointF(self.mapToScene(self.start))
end = QtCore.QPointF(self.mapToScene(event.pos()))
self.drawing(start, end)
def drawing(self, start, end):
self.line = QtGui.QGraphicsLineItem(QtCore.QLineF(start, end))
self.line.setPen(QtGui.QPen(QtCore.Qt.blue,3,
QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
self.scene.addLine(0,0,150,300)
command = Draw(self.line, self.scene)
self.undoStack.push(command)
class Draw(QtGui.QUndoCommand):
def __init__(self, line, scene):
QtGui.QUndoCommand.__init__(self)
self.line = line
self.scene = scene
def redo(self):
self.scene.addItem(self.line)
def undo(self):
self.scene.removeItem(self.line)
class View(QtGui.QGraphicsView):
def __init__(self, parent):
QtGui.QGraphicsView.__init__(self, parent)
self.scene = QtGui.QGraphicsScene()
self.action = Action(self.scene)
self.setScene(self.scene)
self.setSceneRect(QtCore.QRectF(self.viewport().rect()))
def main():
app = QtGui.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
My problem is that i cannot draw anything using my mouse. If class Action and View are combined everything is working fine, but when I put them away nothing happens(no error as well). The reason I did this separation is that I simple want to add another classes with other functionality (drawing elipses, rects...) and swap them with Action class. I added line to the scene (in Action init) and it`s being painted corectly, but the MouseEvents dont work at all.The button "tool" was made for changing the drawing tool.
This way, in my opinion, is the best way to achive ability to scribble with different tools on the same canvas(scene). My way aint good so I am asking for help. What can I do fix this code, so it works as I want? Where are the mistakes? or maybe the whole approach is wrong and it should be done another way? I use PYQT 4 and Python 3.4 .
Your current code has two instances of a QGraphicsScene created inside View.__init__. One is a standard QGraphicsScene and the other is your subclass Action. But your view can only be attached to one scene, so one of these is redundant and not functioning correctly. You have attached it to the QGraphicsScene, so the Action object is the one not working.
Instead, you should kill off the plain boring QGraphicsScene, and only instantiate the Action class. Use the instantiated Action object in the call to view.setScene(). Similarly, in the Action class, there is no need to pass in another scene. Just use itself (so replace all instances of self.scene with self in the Action class)

Moving a QGraphicsItem around a central point in PyQt4

I'm using Python 2.7 and PyQt4. I am trying to have a half-circle object that is a QGraphicsItem. I want to be able to move it using the mouse, by clicking and dragging. I can create the object and move it around with the mouse by setting the flag ItemIsMovable. Now the half-circle moves around freely but I want it to move just around the fixed central point. It is difficult to describe, but it should be something similar to a dial. How can I accomplish this?
you can use QGraphicsItem::mouseMoveEvent event to track item's movements within the scene and correct its position once it's moved off the restricted area. Pls, check if an example below would work for you:
import sys
from PyQt4 import QtGui, QtCore
class TestEclipseItem(QtGui.QGraphicsEllipseItem):
def __init__(self, parent=None):
QtGui.QGraphicsPixmapItem.__init__(self, parent)
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
# set move restriction rect for the item
self.move_restrict_rect = QtCore.QRectF(20, 20, 200, 200)
# set item's rectangle
self.setRect(QtCore.QRectF(50, 50, 50, 50))
def mouseMoveEvent(self, event):
# check of mouse moved within the restricted area for the item
if self.move_restrict_rect.contains(event.scenePos()):
QtGui.QGraphicsEllipseItem.mouseMoveEvent(self, event)
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
scene = QtGui.QGraphicsScene(-50, -50, 600, 600)
ellipseItem = TestEclipseItem()
scene.addItem(ellipseItem)
view = QtGui.QGraphicsView()
view.setScene(scene)
view.setGeometry(QtCore.QRect(0, 0, 400, 200))
self.setCentralWidget(view)
def main():
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
hope this helps, regards

Categories

Resources