Make QGraphicsItem only selectable in one graphicsview - python

I have a setup where two QGraphicViews display a single QGraphicsScene. One of these views is an overview the other the detail. Imagine something like:
The rectangle marking the current boundaries of the detailed view is part of the scene. It is the white rectangle in the upper view, which I will call in the text below as "bounding-box".
What I want is to be able to click in the overview- QGraphicsView and drag the bounding-box around to trigger a scrolling of the detail- QGraphicsView. Obviously, the bounding-box has to be only clickable in the overview- QGraphicsView, otherwise I would never be able to do manipulations in the detail- QGraphicsView, because the bounding-box covers the entire detail view.
So how can I make a QGraphicsItem be selectable only from a single QGraphicsView or, alternatively, how do I "insert" a QGraphicsItem only into a single QGraphicsView? Can I perhaps nest QGraphicsScenes so that one is the copy of another plus some extra items?

Extending my other answer which only concentrates on the movable QGraphicsItem I made an example specifically for your task.
from PySide import QtGui, QtCore
# special GraphicsRectItem that is aware of its position and does something if the position is changed
class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):
def __init__(self, callback=None):
super().__init__()
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.PointingHandCursor)
self.callback = callback
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionChange and self.callback:
self.callback(value)
return super().itemChange(change, value)
app = QtGui.QApplication([])
# the scene with some rectangles
scene = QtGui.QGraphicsScene()
scene.addRect(30, 30, 100, 50, pen=QtGui.QPen(QtCore.Qt.darkGreen))
scene.addRect(150, 0, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkYellow))
scene.addRect(80, 80, 100, 20, pen=QtGui.QPen(QtCore.Qt.darkMagenta))
scene.addRect(200, 10, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkRed))
window = QtGui.QWidget()
# put two graphicsviews into the window with different scaling for each
layout = QtGui.QVBoxLayout(window)
v1 = QtGui.QGraphicsView(scene)
v1.setFixedSize(500, 100)
v1.scale(0.5, 0.5)
v1.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
v1.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
layout.addWidget(v1)
v2 = QtGui.QGraphicsView(scene)
v2.setFixedSize(500, 500)
v2.scale(5, 5)
v2.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
v2.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
layout.addWidget(v2)
# the tracker rectangle
tracker = MovableGraphicsRectItem(lambda pos: v2.setSceneRect(pos.x(), pos.y(), 100, 100))
tracker.setRect(0, 0, 100, 100)
v2.setSceneRect(0, 0, 100, 100)
tracker.setPen(QtGui.QPen(QtCore.Qt.darkCyan))
scene.addItem(tracker)
window.show()
app.exec_()
You don't need to have Items that are only visible in one view or the other, you simply restrict the scene rectangle of one view to inside the draggable rectangle in the scene that is visible and draggable in the other view. See the image.

I really like this idea and am trying to generalise it to create a widget which you pass the 'main view' to and it creates an overview which you can use to pan and zoom in. Unfortunately I haven't got it working yet and don't have time to work on it right now but thought I would share the progress so far.
Here is the widget code:
"""
Overview widget
"""
from PyQt4 import QtGui, QtCore
class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):
'''special GraphicsRectItem that is aware of its position and does
something if the position is changed'''
def __init__(self, callback=None):
super(MovableGraphicsRectItem, self).__init__()
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.PointingHandCursor)
self.callback = callback
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionChange and self.callback:
self.callback(value)
return super(MovableGraphicsRectItem, self).itemChange(change, value)
def activate(self):
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.PointingHandCursor)
def deactivate(self):
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
self.setFlag(QtGui.QGraphicsItem.ItemSendsScenePositionChanges, False)
self.setCursor(QtCore.Qt.ArrowCursor)
class MouseInsideFilterObj(QtCore.QObject):
def __init__(self, enterCallback, leaveCallback):
QtCore.QObject.__init__(self)
self.enterCallback = enterCallback
self.leaveCallback = leaveCallback
def eventFilter(self, obj, event):
if event.type() == 10: # QtCore.QEvent.Type.Enter:
self.enterCallback(obj)
print('Enter event')
if event.type() == 11: # QtCore.QEvent.Type.Leave:
self.leaveCallback(obj)
print('Leave event')
return False
class Overview(QtGui.QGraphicsView):
'''provides a view that shows the entire scene and shows the area that
the main view is zoomed to. Alows user to move the view area around and
change the zoom level'''
def __init__(self, mainView):
QtGui.QGraphicsView.__init__(self)
self.setWindowTitle('Overview')
self.resize(QtCore.QSize(400, 300))
self._mainView = mainView
self.setScene(mainView.scene())
mouseFilter = MouseInsideFilterObj(self.enterGV, self.leaveGV)
self.viewport().installEventFilter(mouseFilter)
self._tracker = MovableGraphicsRectItem(
lambda pos: self._mainView.setSceneRect(
QtCore.QRectF(self._mainView.viewport().geometry())))
self._tracker.setRect(self._getMainViewArea_())
self._tracker.setPen(QtGui.QPen(QtCore.Qt.darkCyan))
self.scene().addItem(self._tracker)
def _getMainViewArea_(self):
mainView = self._mainView
visibleSceneRect = mainView.mapToScene(
mainView.viewport().geometry()).boundingRect()
return visibleSceneRect
def resizeEvent(self, event):
self.fitInView(self.sceneRect(), QtCore.Qt.KeepAspectRatio)
def leaveGV(self, gv):
if gv is self.overview:
print('exited overview')
self.tracker.deactivate()
def enterGV(self, gv):
if gv is self.overview:
print('using overview')
self.tracker.activate()
and here is the test script code:
import sys
from PyQt4 import QtGui, QtCore
import overviewWidget as ov
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
# the scene with some rectangles
scene = QtGui.QGraphicsScene()
scene.addRect(30, 30, 100, 50, pen=QtGui.QPen(QtCore.Qt.darkGreen))
scene.addRect(150, 0, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkYellow))
scene.addRect(80, 80, 100, 20, pen=QtGui.QPen(QtCore.Qt.darkMagenta))
scene.addRect(200, 10, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkRed))
# the main view
mainView = QtGui.QGraphicsView(scene)
mainView.resize(600, 400)
mainView.update()
mainView.show()
# the overview
overview = ov.Overview(mainView)
overview.update()
overview.show()
sys.exit(app.exec_())

QGraphicsItems have by default some of their abilities disabled to maximize performance. By enabling these abilities you can make them movable and you can make them aware of their position. Ideally one would then use the Signal/Slot mechanism to notify someone else of changes but again for performance reason QGraphicsItems are not inheriting from QObject. However sending events or manually calling callbacks are always possible.
You have to:
Enable flags QGraphicsItem.ItemIsMovable and QGraphicsItem.ItemSendsScenePositionChanges of your QGraphicsItem
Provide a custom implementation of method itemChange(change, value) and therein listen to QGraphicsItem.ItemPositionChange changes.
Act accordingly to these changes (in your case change the detailed view).
A small example:
from PySide import QtGui, QtCore
class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):
"""
A QGraphicsRectItem that can be moved and is aware of its position.
"""
def __init__(self):
super().__init__()
# enable moving and position tracking
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
# sets a non-default cursor
self.setCursor(QtCore.Qt.PointingHandCursor)
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionChange:
print(value)
return super().itemChange(change, value)
app = QtGui.QApplication([])
# create our movable rectangle
rectangle = MovableGraphicsRectItem()
rectangle.setRect(0, 0, 100, 100)
# create a scene and add our rectangle
scene = QtGui.QGraphicsScene()
scene.addItem(rectangle)
# create view, set fixed scene rectangle and show
view = QtGui.QGraphicsView(scene)
view.setSceneRect(0, 0, 600, 400)
view.show()
app.exec_()
In this example (Python 3.X) you can drag the rectangle around and the changing positions are printed to the console.
Some more comments:
You have two views and two associated scenes.
Their display is partly overlapping but this is not a problem because the top view will always consume all mouse events.
In order to change something in the other view you just have to send an event from the overriden itemChange method or call a callback.
You could also add Signal/Slot ability by inheriting from both, QGraphicsRectItem and QObject and then define a signal and emit it.
If by chance you also wanted a movable and position aware ellipse or other item you need to create your custom classes for each xxxItem class. I stumbled upon this problem several times and think it might be a disadvantage of the design.

Extending the answer of Trilarion, I was able to solve the problem, by installing a Eventfilter on the overview QgraphcisView. On the Enter event, the dragging is enabled, on the Leave event the dragging is disabled.
from PySide import QtGui, QtCore
# special GraphicsRectItem that is aware of its position and does something if the position is changed
class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):
def __init__(self, callback=None):
super(MovableGraphicsRectItem, self).__init__()
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.PointingHandCursor)
self.callback = callback
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionChange and self.callback:
self.callback(value)
return super(MovableGraphicsRectItem, self).itemChange(change, value)
def activate(self):
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.PointingHandCursor)
def deactivate(self):
self.setFlags(not QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.ArrowCursor)
class MouseInsideFilterObj(QtCore.QObject):#And this one
def __init__(self, enterCallback, leaveCallback):
QtCore.QObject.__init__(self)
self.enterCallback = enterCallback
self.leaveCallback = leaveCallback
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.Type.Enter:
self.enterCallback(obj)
if event.type() == QtCore.QEvent.Type.Leave:
self.leaveCallback(obj)
return True
class TestClass:
def __init__(self):
self.app = QtGui.QApplication([])
# the scene with some rectangles
self.scene = QtGui.QGraphicsScene()
self.scene.addRect(30, 30, 100, 50, pen=QtGui.QPen(QtCore.Qt.darkGreen))
self.scene.addRect(150, 0, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkYellow))
self.scene.addRect(80, 80, 100, 20, pen=QtGui.QPen(QtCore.Qt.darkMagenta))
self.scene.addRect(200, 10, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkRed))
self.window = QtGui.QWidget()
# put two graphicsviews into the window with different scaling for each
self.layout = QtGui.QVBoxLayout(self.window)
self.v1 = QtGui.QGraphicsView(self.scene)
self.v1.setFixedSize(500, 100)
self.v1.scale(0.5, 0.5)
self.v1.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.v1.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.layout.addWidget(self.v1)
self.v2 = QtGui.QGraphicsView(self.scene)
self.v2.setFixedSize(500, 500)
self.v2.scale(5, 5)
self.v2.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.v2.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.layout.addWidget(self.v2)
mouseFilter = MouseInsideFilterObj(self.enterGV, self.leaveGV)
self.v1.installEventFilter(mouseFilter)
# the tracker rectangle
self.tracker = MovableGraphicsRectItem(lambda pos: self.v2.setSceneRect(pos.x(), pos.y(), 100, 100))
self.tracker.setRect(0, 0, 100, 100)
self.v2.setSceneRect(0, 0, 100, 100)
self.tracker.setPen(QtGui.QPen(QtCore.Qt.darkCyan))
self.scene.addItem(self.tracker)
self.window.show()
self.app.exec_()
def leaveGV(self, gv):
if gv is self.v1:
self.tracker.deactivate()
def enterGV(self, gv):
if gv is self.v1:
self.tracker.activate()
TestClass()

Related

Ignore mouse drag over a QGraphicsObject

I have a simple QRectF that I've put in a class that inherits from QGraphicsObject. I want the area of this rect to pass through mouse drag events. i.e. right now, I have click and drag moving the rect. However, I need that event sent to the background where I need to select multiple items (which is possible by default). Setting the attribute WA_TransparentForMouseEvents seems perfect for this, but as I understand, this only for QWidget.
class GraphicsItem(QtWidgets.QGraphicsObject):
def __init__(self):
self._box = QtCore.QRectF(0, 0, 100, 100)
def mouseMoveEvent(self, event):
if (self._box.contains(event.pos()):
# set event transparency here
Is this possible?
Thanks.
You can define QGraphicsItem.shape() for your item. I don't know what your GraphicsItem looks like but here is a general example. The other item can be selected from inside the self._box region.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class GraphicsItem(QGraphicsObject):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._box = QRectF(0, 0, 100, 100)
self.setFlags(self.ItemIsSelectable | self.ItemIsMovable)
def boundingRect(self):
return QRectF(-100, -50, 300, 200)
def shape(self):
area = QPainterPath()
area.addRect(self.boundingRect())
box = QPainterPath()
box.addRect(self._box)
return area - box
def paint(self, painter, *args):
painter.setBrush(QColor(0, 255, 0, 180))
painter.drawPath(self.shape())
if __name__ == '__main__':
app = QApplication(sys.argv)
scene = QGraphicsScene(-200, -150, 500, 400)
rect = scene.addRect(30, 30, 30, 30, Qt.black, Qt.red)
rect.setFlags(rect.ItemIsSelectable | rect.ItemIsMovable)
scene.addItem(GraphicsItem())
view = QGraphicsView(scene)
view.show()
sys.exit(app.exec_())
Or if you specifically wanted to reimplement the mouse event handlers:
def mousePressEvent(self, event):
event.ignore() if event.pos() in self._box else super().mousePressEvent(event)

Why my program is closing when I am using Slider in pyqt5?

Why my program is closing when I am using Slider in pyqt5? It starts normal but after using Slider it close. I have to change size of circle in first window by using Slider from second window. This is my full code, beacuse I don't know what could be wrong.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QPainter, QBrush, QPen
from PyQt5.QtCore import Qt
import sys
class ThirdWindow(QWidget):
def __init__(self):
super().__init__()
hbox = QHBoxLayout()
sld = QSlider(Qt.Horizontal, self)
sld.setRange(0, 100)
sld.setFocusPolicy(Qt.NoFocus)
sld.setPageStep(5)
sld.valueChanged.connect(self.updateLabel)
self.label = QLabel('0', self)
self.label.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
self.label.setMinimumWidth(80)
hbox.addWidget(sld)
hbox.addSpacing(15)
hbox.addWidget(self.label)
self.setLayout(hbox)
self.setGeometry(600, 60, 500, 500)
self.setWindowTitle('QSlider')
self.show()
def updateLabel(self, value):
self.label.setText(str(value))
self.parentWidget().radius = value
self.parentWidget().repaint()
class Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setWindowTitle("Dialogi")
self.w = ThirdWindow()
actionFile = self.menuBar().addMenu("Dialog")
action = actionFile.addAction("Zmień tło")
action1 = actionFile.addAction("Zmień grubość koła")
action1.triggered.connect(self.w.show)
self.setGeometry(100, 60, 300, 300)
self.setStyleSheet("background-color: Green")
self.radius = 100 # add a variable radius to keep track of the circle radius
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(QPen(Qt.gray, 8, Qt.SolidLine))
# change function to include radius
painter.drawEllipse(100, 100, self.radius, self.radius)
app = QApplication(sys.argv)
screen = Window()
screen.show()
sys.exit(app.exec_())
parentWidget allows access to a widget declared as parent for Qt.
Creating an object using self.w = ... doesn't make that object a child of the parent (self), you are only creating a reference (w) to it.
If you run your program in a terminal/prompt you'll clearly see the following traceback:
Exception "unhandled AttributeError"
'NoneType' object has no attribute 'radius'
The NoneType refers to the result of parentWidget(), and since no parent has been actually set, it returns None.
While you could use parentWidget if you correctly set the parent for that widget (by adding the parent to the widget constructor, or using setParent()), considering the structure of your program it wouldn't be a good choice, and it's usually better to avoid changes to a "parent" object directly from the "child": the signal/slot mechanism of Qt is exactly intended for the modularity of Object Oriented Programming, as a child should not really "care" about its parent.
The solution is to connect to the slider from the "parent", and update it from there.
class ThirdWindow(QWidget):
def __init__(self):
# ...
# make the slider an *instance member*, so that we can easily access it
# from outside
self.slider = QSlider(Qt.Horizontal, self)
# ...
class Window(QMainWindow):
def __init__(self):
# ...
self.w.slider.valueChanged.connect(self.updateLabel)
def updateLabel(self, value):
self.radius = value
self.update()
You obviously need to remove the valueChanged connection in the ThirdWindow class, as you don't need it anymore.
Also note that you rarely need repaint(), and update() should be preferred instead.

how to make an overriden QGraphicsTextItem editable & movable?

I am using PyQt and I'm trying to re-implement a QGraphicsTextItem, but it seems I'm missing something.
I would like to make the NodeTag item's text editable. I have tried setting flags such as Qt.TextEditorInteraction and QGraphicsItem.ItemIsMovable , but those seem to be ignored...
Here is a Minimal Reproducible Example :
import sys
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication, QGraphicsItem, QGraphicsTextItem
from PyQt5.QtCore import *
from PyQt5.QtGui import QPen
class NodeTag(QGraphicsTextItem):
def __init__(self,text):
QGraphicsTextItem.__init__(self,text)
self.text = text
self.setPos(0,0)
self.setTextInteractionFlags(Qt.TextEditorInteraction)
# self.setFlag(QGraphicsItem.ItemIsFocusable, True) # All these flags are ignored...
# self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
def boundingRect(self):
return QRectF(0,0,80,25)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
painter.drawText(self.boundingRect(),self.text)
def mousePressEvent(self, event):
print("CLICK!")
# self.setTextInteractionFlags(Qt.TextEditorInteraction) # make text editable on click
# self.setFocus()
class GView(QGraphicsView):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.setGeometry(100, 100, 700, 450)
self.show()
class Scene(QGraphicsScene):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
tagItem = NodeTag("myText") # create a NodeTag item
self.addItem(tagItem)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__() # create default constructor for QWidget
self.setGeometry(900, 70, 1000, 800)
self.createGraphicView()
self.show()
def createGraphicView(self):
self.scene = Scene(self)
gView = GView(self)
scene = Scene(gView)
gView.setScene(scene)
# Set the main window's central widget
self.setCentralWidget(gView)
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
As you can see I have tried overriding the mousePressEvent and setting flags there too, but no luck so far.
Any help appreciated!
All QGraphicsItem subclasses have a paint method, and all items that paint some contents have that method overridden so that they can actually paint themselves.
The mechanism is the same as standard QWidgets, for which there is a paintEvent (the difference is that paint of QGraphicsItem receives an already instanciated QPainter), so if you want to do further painting other than what the class already provides, the base implementation must be called.
Consider that painting always happen from bottom to top, so everything that needs to be drawn behind the base painting has to be done before calling super().paint(), and everything that is going to be drawn in front of the default painting has to be placed after.
Depending on the situation, overriding might require that the default base implementation is called anyway, and that's important in your case for boundingRect too. QGraphicsTextItem automatically resizes itself when its contents change, so you should not always return a fixed QRect. If you need to have a minimum size, the solution is to merge a minimum rectangle with those provided by the default boundingRect() function.
Then, editing on a QGraphicsTextItem happens when the item gets focused, but since you also want to be able to move the item, things get trickier as both actions are based on mouse clicks. If you want to be able to edit the text with a single click, the solution is to make the item editable only when the mouse button has been released and has not been moved by some amount of pixels (the startDragDistance() property), otherwise the item is moved with the mouse. This obviously makes the ItemIsMovable flag useless, as we're going to take care of the movement internally.
Finally, since a minimum size is provided, we also need to override the shape() method in order to ensure that collision and clicks are correctly mapped, and return a QPainterPath that includes the whole bounding rect (for normal QGraphicsItem that should be the default behavior, but that doesn't happen with QGraphicsRectItem).
Here's a full implementation of what described above:
class NodeTag(QGraphicsTextItem):
def __init__(self, text):
QGraphicsTextItem.__init__(self, text)
self.startPos = None
self.isMoving = False
# the following is useless, not only because we are leaving the text
# painting to the base implementation, but also because the text is
# already accessible using toPlainText() or toHtml()
#self.text = text
# this is unnecessary too as all new items always have a (0, 0) position
#self.setPos(0, 0)
def boundingRect(self):
return super().boundingRect() | QRectF(0, 0, 80, 25)
def paint(self, painter, option, widget):
# draw the border *before* (as in "behind") the text
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
super().paint(painter, option, widget)
def shape(self):
shape = QPainterPath()
shape.addRect(self.boundingRect())
return shape
def focusOutEvent(self, event):
# this is required in order to allow movement using the mouse
self.setTextInteractionFlags(Qt.NoTextInteraction)
def mousePressEvent(self, event):
if (event.button() == Qt.LeftButton and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.startPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.startPos:
delta = event.pos() - self.startPos
if (self.isMoving or
delta.manhattanLength() >= QApplication.startDragDistance()):
self.setPos(self.pos() + delta)
self.isMoving = True
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if (not self.isMoving and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.setTextInteractionFlags(Qt.TextEditorInteraction)
self.setFocus()
# the following lines are used to correctly place the text
# cursor at the mouse cursor position
cursorPos = self.document().documentLayout().hitTest(
event.pos(), Qt.FuzzyHit)
textCursor = self.textCursor()
textCursor.setPosition(cursorPos)
self.setTextCursor(textCursor)
super().mouseReleaseEvent(event)
self.startPos = None
self.isMoving = False
As a side note, remember that QGraphicsTextItem supports rich text formatting, so even if you want more control on the text painting process you should not use QPainter.drawText(), because you'd only draw the plain text. In fact, QGraphicsTextItem draws its contents using the drawContents() function of the underlying text document.
Try it:
...
class NodeTag(QGraphicsTextItem):
def __init__(self, text, parent=None):
super(NodeTag, self).__init__(parent)
self.text = text
self.setPlainText(text)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemIsSelectable)
def focusOutEvent(self, event):
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
super(NodeTag, self).focusOutEvent(event)
def mouseDoubleClickEvent(self, event):
if self.textInteractionFlags() == QtCore.Qt.NoTextInteraction:
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
super(NodeTag, self).mouseDoubleClickEvent(event)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
# painter.drawText(self.boundingRect(),self.text)
super().paint(painter, option, widget)
...

Initially moved scroll bar inside QGraphics

I am trying to set the vertical and horizontal scroll bars initially moved inside a QGraphicsScene widget. The following code should move the bars and set them in the middle, but they are not moved:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(1000, 1000)
self.setFixedSize(1000, 1000)
self.scene = QtWidgets.QGraphicsScene(self)
self.view = QtWidgets.QGraphicsView(self.scene)
# This two lines should move the scroll bar
self.view.verticalScrollBar().setValue(500)
self.view.horizontalScrollBar().setValue(500)
self.diedrico = Diedrico()
self.diedrico.setFixedSize(2000, 2000)
self.scene.addWidget(self.diedrico)
self.setCentralWidget(self.view)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_R:
self.view.setTransform(QtGui.QTransform())
elif event.key() == QtCore.Qt.Key_Plus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
tr = self.view.transform() * scale_tr
self.view.setTransform(tr)
elif event.key() == QtCore.Qt.Key_Minus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
scale_inverted, invertible = scale_tr.inverted()
if invertible:
tr = self.view.transform() * scale_inverted
self.view.setTransform(tr)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())
I could move the bars when I used a scroll area such as in this question
The answer given by #S.Nick works fine, but I'd like to add some insight about why you are facing this issue and what's happening "under the hood".
First of all, in your code you try to set the values of the scroll bars before adding any object to the scene.
At that point, you just created the view and the scene. The view widget has not been shown (so it doesn't "know" its actual size yet), and the scene is empty, meaning that the sceneRect is null, as in 0 width and 0 height: in this scenario, the scroll bars have a maximum value of 0, and setting any value won't give any result.
NOTE: There is a very important aspect to keep in mind: unless
explicitly declared or requested, the sceneRect of a
QGraphicsScene is always null until a view shows it. And by
"requested" I mean that even just calling scene.sceneRect() is
enough to ensure that the scene actually and finally "knows" its
extent.
After trying to set the scroll bars (with no results), you added the widget to the scene. The problem is that a view (which is a QAbstractScrollArea descendant) only updates its scrollbars as soon as it's actually mapped on the screen.
This is a complex "path" that starts from showing the main parent window (if any), which, according to its contents resizes itself and, again, resizes its contents if they require it, eventually based on their [nested widget] size policies. Only then, the view "decides" if scrollbars are needed, and eventually sets their maximum. And, only then you can actuall set a value for those scroll bars, and that's because only then the view "asks" the scene about its sceneRect.
This also (partially) explains why the view behaves in different way than a standard scroll area: widgets have a sizeHint that is used by the QWidget that contains them inside the scroll area, and, theoretically, their size is mapped as soon as they're created. But. this depends on their size hints and policies, so you cannot guarantee the actual scroll area contents size until it's finally mapped/shown; long story short: it "works", but not perfectly - at least not until everything has finally been shown.
A test example
There are different ways to solve your problem, according to your needs and implementation.
Set the sceneRect independently, even before adding any object to the scene (but if those objects boundaries go outside the scene, you'll face some inconsistency)
Call scene.sceneRect() as explained above, after adding all objects
Set the scoll bars only after the view has been shown and resized
I've prepared an example that shows the three situations explained above. It will create a new view and update its scrollbars at different points according to the checkboxes, to show how differently they behave. Note that when setting the sceneRect I used a rectangle smaller than the widget size to better display its behavior: you can see that the visual result of "Set scene rect" and "Check scene rect" is similar, but the scroll bar positions are different.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class TestView(QtWidgets.QGraphicsView):
def __init__(self, setRect=False, checkScene=False, showEventCheck=False):
super(TestView, self).__init__()
self.setFixedSize(800, 800)
scene = QtWidgets.QGraphicsScene()
self.setScene(scene)
self.diedrico = Diedrico()
self.diedrico.setFixedSize(2000, 2000)
scene.addWidget(self.diedrico)
if setRect:
scene.setSceneRect(0, 0, 1500, 1500)
elif checkScene:
scene.sceneRect()
self.showEventCheck = showEventCheck
if not showEventCheck:
self.scroll()
def scroll(self):
self.verticalScrollBar().setValue(500)
self.horizontalScrollBar().setValue(500)
def showEvent(self, event):
super(TestView, self).showEvent(event)
if not event.spontaneous() and self.showEventCheck:
self.scroll()
class ViewTester(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
self.setRectCheck = QtWidgets.QCheckBox('Set scene rect')
layout.addWidget(self.setRectCheck)
self.checkSceneCheck = QtWidgets.QCheckBox('Check scene rect')
layout.addWidget(self.checkSceneCheck)
self.showEventCheck = QtWidgets.QCheckBox('Scroll when shown')
layout.addWidget(self.showEventCheck)
showViewButton = QtWidgets.QPushButton('Show view')
layout.addWidget(showViewButton)
showViewButton.clicked.connect(self.showView)
self.view = None
def showView(self):
if self.view:
self.view.close()
self.view.deleteLater()
self.view = TestView(
setRect = self.setRectCheck.isChecked(),
checkScene = self.checkSceneCheck.isChecked(),
showEventCheck = self.showEventCheck.isChecked()
)
self.view.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
viewTester = ViewTester()
viewTester.show()
sys.exit(app.exec_())
Finally, remember that using absolute values for scrollbars is not a good idea. If you want to "center" the view, consider using centerOn (and its item based overload), or set values based on scrollBar.maximum()/2.
You want to set the value when the widget is not yet formed, make it a moment.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(1000, 1000)
self.setFixedSize(1000, 1000)
self.scene = QtWidgets.QGraphicsScene(self)
self.view = QtWidgets.QGraphicsView(self.scene)
# This two lines should move the scroll bar
QtCore.QTimer.singleShot(0, self.set_Value) # +++
self.diedrico = Diedrico()
self.diedrico.setFixedSize(2000, 2000)
self.scene.addWidget(self.diedrico)
self.setCentralWidget(self.view)
def set_Value(self): # +++
self.view.verticalScrollBar().setValue(500)
self.view.horizontalScrollBar().setValue(500)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_R:
self.view.setTransform(QtGui.QTransform())
elif event.key() == QtCore.Qt.Key_Plus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
tr = self.view.transform() * scale_tr
self.view.setTransform(tr)
elif event.key() == QtCore.Qt.Key_Minus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
scale_inverted, invertible = scale_tr.inverted()
if invertible:
tr = self.view.transform() * scale_inverted
self.view.setTransform(tr)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())

Have 2 pyqt buttons move synchronized when mouse moves

I am currently making a program where a user selects an image qpushbutton. I already have superseded mouseMoveEvent, mousePressEvent, and mouseReleaseEvent in the button class to get a movable button. The buttons are currently moving independently, but I would like the buttons to move so that the horizontal distance between them stays the same.
So currently in pseudo code I have:
import stuff
import mvbutton as mv
class MasterClass(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
#more setup stuff, layout, etc
self.addbutton(image,name,size)
def addbutton(#args):
self.button=mv.dragbutton(#args)
#some more setup
#now rename so that each button has its own name
if name== "name1":
self.name1=self.button
else:
self.name2=self.button
self.button=""
#more code to set up
I supersede the mouse motion/press/release functions in the dragbutton class. I cannot, therefore reference the new self.name# there. So the self.move(pos) in my dragbutton class cannot get the self.name# because it is a different self. Any ideas on how I could get this to work? Thanks.
Done something very rough after trying to understand your requirement.
Hope this helps.
EDIT
tried to add more accuracy in moving. Won't do real time moving cause it has problems with lag and update. I guess the moving won't be jittery any more.
from PyQt4 import QtGui
import sys
class MultiButton(QtGui.QWidget):
def __init__(self, *args, **kwargs):
QtGui.QWidget.__init__(self, *args, **kwargs)
self._b1 = QtGui.QPushButton("B1")
self._b2 = QtGui.QPushButton("B2")
self._arrangeWidgets()
self.setStyleSheet("background-color: rgb(0, 0, 0);\n"+\
"color: rgb(255, 255, 255);\n"+\
"border:1px solid #7F462C ;\n")
self._moveStart = False
self._startX = 0
self._startY = 0
def _arrangeWidgets(self):
layout = QtGui.QHBoxLayout()
layout.addWidget(self._b1)
#horizontal spacing remains constant now
layout.addSpacing(90)
layout.addWidget(self._b2)
self.setLayout(layout)
def mousePressEvent(self,event):
self._moveStart = True
self._startX = event.pos().x() - self.pos().x()
self._startY = event.pos().y() - self.pos().y()
return QtGui.QWidget.mousePressEvent(self, event)
def mouseReleaseEvent(self, event):
if self._moveStart:
self.setGeometry(event.pos().x() - self._startX,event.pos().y() - self._startY,self.width(),self.height())
self._moveStart = False
self._startX = 0
self._startY = 0
return QtGui.QWidget.mouseReleaseEvent(self, event)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
wd = QtGui.QMainWindow()
wd.resize(500,500)
mb = MultiButton()
mb.setFixedSize(200,50)
wd.setCentralWidget(mb)
wd.show()
sys.exit(app.exec_())
here the MultiButton widget moves the two buttons keeping the horizontal space between the two always constant.

Categories