Create a QGraphicsItem at the mouse double-click position - python

I'm writing a program that should create a QGraphicsItem when a double-click happens (inside the scene area), and also that item must be created at double-click position. I've already written some code, but it doesn't work properly. When I double-click on the scene, an item gets created, but at a completely different place. So right now that is the problem. I need it to be created at mouse's position.
Here is my code:
ui_path = "C:/Users/User/Desktop/programming/Creator/Creator.ui"
class Creator(QtWidgets.QWidget):
count = 0
def __init__(self):
super(Creator, self).__init__()
loader = QtUiTools.QUiLoader()
self.ui = loader.load(ui_path, self)
self.variables()
self.ui.canvas_area.viewport().installEventFilter(self) #canvas_area is the QGraphicsView
self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.CustomizeWindowHint | Qt.WindowStaysOnTopHint)
def variables(self):
self.scene = QtWidgets.QGraphicsScene()
self.ui.canvas_area.setScene(self.scene)
def eventFilter(self, obj, event):
if obj is self.ui.canvas_area.viewport():
if event.type() == QtCore.QEvent.MouseButtonDblClick:
self.createItems(event)
return super(Creator, self).eventFilter(obj, event)
def createItems(self, event):
pos = event.pos()
self._x = pos.x()
self._y = pos.y()
rect = self.scene.addRect(self._x, self._y, 40, 40, QPen(Qt.red), QBrush(Qt.gray))
rect.setFlag(QGraphicsItem.ItemIsMovable)
rect.setFlag(QGraphicsItem.ItemIsFocusable)
rect.setFlag(QGraphicsItem.ItemIsSelectable)
if __name__ == '__main__':
creator_window = Creator()
creator_window.ui.show()
I've read something about mapFromScene and mapToScene, and that these can solve the problem - but I don't really understand how to use them. Also there are some examples of this, but all that I've found were in C++, which I know nothing about. So if someone could help me figure out how to solve this problem I would really appreciate that.

You first must set the scene-rect for the scene:
self.scene.setSceneRect(0, 0, 1000, 1000)
then you must convert the event position to scene co-ordinates, like this:
pos = self.canvas_area.mapToScene(event.pos())
and that should be all that's needed to fix your example.
A different approach that may be worth considering would be to install an event-filter on the scene instead:
self.scene.installEventFilter(self)
and then filter on the more specialised graphics events:
def eventFilter(self, obj, event):
if obj is self.scene:
if event.type() == QtCore.QEvent.GraphicsSceneMouseDoubleClick:
self.createItems(event)
return super(Creator, self).eventFilter(obj, event)
This creates a QGraphicsSceneMouseEvent, which has several useful features that a standard QMouseEvent doesn't have. This includes scenePos(), which saves having to map to scene co-ordinates all the time, allowing you to then simply do:
pos = event.scenePos()

Related

PyQt5 QTabBar paintEvent with tabs that can move

I would like to have a QTabBar with customised painting in the paintEvent(self,event) method, whilst maintaining the moving tabs animations / mechanics. I posted a question the other day about something similar, but it wasn't worded too well so I have heavily simplified the question with the following code:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtTest import QTest
import sys
class MainWindow(QMainWindow):
def __init__(self,parent=None,*args,**kwargs):
QMainWindow.__init__(self,parent,*args,**kwargs)
self.tabs = QTabWidget(self)
self.tabs.setTabBar(TabBar(self.tabs))
self.tabs.setMovable(True)
for color in ["red","orange","yellow","lime","green","cyan","blue","purple","violet","magenta"]:
title = color
widget = QWidget(styleSheet="background-color:%s" % color)
pixmap = QPixmap(8,8)
pixmap.fill(QColor(color))
icon = QIcon(pixmap)
self.tabs.addTab(widget,icon,title)
self.setCentralWidget(self.tabs)
self.showMaximized()
class TabBar(QTabBar):
def __init__(self,parent,*args,**kwargs):
QTabBar.__init__(self,parent,*args,**kwargs)
def paintEvent(self,event):
painter = QStylePainter(self)
option = QStyleOptionTab()
for i in range(self.count()):
self.initStyleOption(option,i)
#Customise 'option' here
painter.drawControl(QStyle.CE_TabBarTab,option)
def tabSizeHint(self,index):
return QSize(112,48)
def exceptHook(e,v,t):
sys.__excepthook__(e,v,t)
if __name__ == "__main__":
sys.excepthook = exceptHook
application = QApplication(sys.argv)
mainwindow = MainWindow()
application.exec_()
there are some clear problems:
Dragging the tab to 'slide' it in the QTabBar is not smooth (it doens't glide) - it jumps to the next index.
The background tabs (non-selected tabs) don't glide into place once displaced - they jump into position.
When the tab is slid to the end of the tab bar (past the most right tab) and then let go of it doesn't glide back to the last index - it jumps there.
When sliding a tab, it stays in its original place and at the mouse cursor (in its dragging position) at the same time, and only when the mouse is released does the tab only show at the correct place (up until then it is also showing at the index it is originally from).
How can I modify the painting of a QTabBar with a QStyleOptionTab whilst maintaining all of the moving mechanics / animations of the tabs?
While it might seem a slightly simple widget, QTabBar is not, at least if you want to provide all of its features.
If you closely look at its source code, you'll find out that within the mouseMoveEvent() a private QMovableTabWidget is created whenever the drag distance is wide enough. That QWidget is a child of QTabBar that shows a QPixmap grab of the "moving" tab using the tab style option and following the mouse movements, while at the same moment that tab becomes invisible.
While your implementation might seem reasonable (note that I'm also referring to your original, now deleted, question), there are some important issues:
it doesn't account for the above "moving" child widget (in fact, with your code I can still see the original tab, even if that is that moving widget that's not actually moving since no call to the base implementation of mouseMoveEvent() is called);
it doesn't actually tabs;
it doesn't correctly process mouse events;
This is a complete implementation partially based on the C++ sources (I've tested it even with vertical tabs, and it seems to behave as it should):
class TabBar(QTabBar):
class MovingTab(QWidget):
'''
A private QWidget that paints the current moving tab
'''
def setPixmap(self, pixmap):
self.pixmap = pixmap
self.update()
def paintEvent(self, event):
qp = QPainter(self)
qp.drawPixmap(0, 0, self.pixmap)
def __init__(self,parent, *args, **kwargs):
QTabBar.__init__(self,parent, *args, **kwargs)
self.movingTab = None
self.isMoving = False
self.animations = {}
self.pressedIndex = -1
def isVertical(self):
return self.shape() in (
self.RoundedWest,
self.RoundedEast,
self.TriangularWest,
self.TriangularEast)
def createAnimation(self, start, stop):
animation = QVariantAnimation()
animation.setStartValue(start)
animation.setEndValue(stop)
animation.setEasingCurve(QEasingCurve.InOutQuad)
def removeAni():
for k, v in self.animations.items():
if v == animation:
self.animations.pop(k)
animation.deleteLater()
break
animation.finished.connect(removeAni)
animation.valueChanged.connect(self.update)
animation.start()
return animation
def layoutTab(self, overIndex):
oldIndex = self.pressedIndex
self.pressedIndex = overIndex
if overIndex in self.animations:
# if the animation exists, move its key to the swapped index value
self.animations[oldIndex] = self.animations.pop(overIndex)
else:
start = self.tabRect(overIndex).topLeft()
stop = self.tabRect(oldIndex).topLeft()
self.animations[oldIndex] = self.createAnimation(start, stop)
self.moveTab(oldIndex, overIndex)
def finishedMovingTab(self):
self.movingTab.deleteLater()
self.movingTab = None
self.pressedIndex = -1
self.update()
# reimplemented functions
def tabSizeHint(self, i):
return QSize(112, 48)
def mousePressEvent(self, event):
super().mousePressEvent(event)
if event.button() == Qt.LeftButton:
self.pressedIndex = self.tabAt(event.pos())
if self.pressedIndex < 0:
return
self.startPos = event.pos()
def mouseMoveEvent(self,event):
if not event.buttons() & Qt.LeftButton or self.pressedIndex < 0:
super().mouseMoveEvent(event)
else:
delta = event.pos() - self.startPos
if not self.isMoving and delta.manhattanLength() < QApplication.startDragDistance():
# ignore the movement as it's too small to be considered a drag
return
if not self.movingTab:
# create a private widget that appears as the current (moving) tab
tabRect = self.tabRect(self.pressedIndex)
overlap = self.style().pixelMetric(
QStyle.PM_TabBarTabOverlap, None, self)
tabRect.adjust(-overlap, 0, overlap, 0)
pm = QPixmap(tabRect.size())
pm.fill(Qt.transparent)
qp = QStylePainter(pm, self)
opt = QStyleOptionTab()
self.initStyleOption(opt, self.pressedIndex)
if self.isVertical():
opt.rect.moveTopLeft(QPoint(0, overlap))
else:
opt.rect.moveTopLeft(QPoint(overlap, 0))
opt.position = opt.OnlyOneTab
qp.drawControl(QStyle.CE_TabBarTab, opt)
qp.end()
self.movingTab = self.MovingTab(self)
self.movingTab.setPixmap(pm)
self.movingTab.setGeometry(tabRect)
self.movingTab.show()
self.isMoving = True
self.startPos = event.pos()
isVertical = self.isVertical()
startRect = self.tabRect(self.pressedIndex)
if isVertical:
delta = delta.y()
translate = QPoint(0, delta)
startRect.moveTop(startRect.y() + delta)
else:
delta = delta.x()
translate = QPoint(delta, 0)
startRect.moveLeft(startRect.x() + delta)
movingRect = self.movingTab.geometry()
movingRect.translate(translate)
self.movingTab.setGeometry(movingRect)
if delta < 0:
overIndex = self.tabAt(startRect.topLeft())
else:
if isVertical:
overIndex = self.tabAt(startRect.bottomLeft())
else:
overIndex = self.tabAt(startRect.topRight())
if overIndex < 0:
return
# if the target tab is valid, move the current whenever its position
# is over the half of its size
overRect = self.tabRect(overIndex)
if isVertical:
if ((overIndex < self.pressedIndex and movingRect.top() < overRect.center().y()) or
(overIndex > self.pressedIndex and movingRect.bottom() > overRect.center().y())):
self.layoutTab(overIndex)
elif ((overIndex < self.pressedIndex and movingRect.left() < overRect.center().x()) or
(overIndex > self.pressedIndex and movingRect.right() > overRect.center().x())):
self.layoutTab(overIndex)
def mouseReleaseEvent(self,event):
super().mouseReleaseEvent(event)
if self.movingTab:
if self.pressedIndex > 0:
animation = self.createAnimation(
self.movingTab.geometry().topLeft(),
self.tabRect(self.pressedIndex).topLeft()
)
# restore the position faster than the default 250ms
animation.setDuration(80)
animation.finished.connect(self.finishedMovingTab)
animation.valueChanged.connect(self.movingTab.move)
else:
self.finishedMovingTab()
else:
self.pressedIndex = -1
self.isMoving = False
self.update()
def paintEvent(self, event):
if self.pressedIndex < 0:
super().paintEvent(event)
return
painter = QStylePainter(self)
tabOption = QStyleOptionTab()
for i in range(self.count()):
if i == self.pressedIndex and self.isMoving:
continue
self.initStyleOption(tabOption, i)
if i in self.animations:
tabOption.rect.moveTopLeft(self.animations[i].currentValue())
painter.drawControl(QStyle.CE_TabBarTab, tabOption)
I strongly suggest you to carefully read and try to understand the above code (along with the source code), as I didn't comment everything I've done, and it's very important to understand what's happening if you really need to do further subclassing in the future.
Update
If you need to alter the appearance of the dragged tab while moving it, you need to update its pixmap. You can just store the QStyleOptionTab when you create it, and then update when necessary. In the following example the WindowText (note that QPalette.Foreground is obsolete) color is changed whenever the index of the tab is changed:
def mouseMoveEvent(self,event):
# ...
if not self.movingTab:
# ...
self.movingOption = opt
def layoutTab(self, overIndex):
# ...
self.moveTab(oldIndex, overIndex)
pm = QPixmap(self.movingTab.pixmap.size())
pm.fill(Qt.transparent)
qp = QStylePainter(pm, self)
self.movingOption.palette.setColor(QPalette.WindowText, <someColor>)
qp.drawControl(QStyle.CE_TabBarTab, self.movingOption)
qp.end()
self.movingTab.setPixmap(pm)
Another small suggestion: while you can obviously use the indentation style you like, when sharing your code on public spaces like StackOverflow it's always better to stick to common conventions, so I suggest you to always provide your code with 4-spaces indentations; also, remember that there should always be a space after each comma separated variable, as it dramatically improves readability.

PyQt5 tooltip with clickable hyperlink?

Can you suggest me a way to make hyperlink in PyQt5 tooltip clickable? Tried like this:
from PyQt5 import QtWidgets
app = QtWidgets.QApplication([])
w = QtWidgets.QMainWindow()
QtWidgets.QLabel(parent = w, text = 'Hover mouse here', toolTip = 'Unclickable link')
w.show()
app.exec_()
Link is visible, but not clickable, unfortunatelly.
This is not an easy task to achieve.
One of the most important aspects is that users are accustomed to the conventional behavior of tool tips: if the mouse cursor hovers them (or they are clicked), they would probably disappear; this is to avoid any possibility that some important part of the widget they refer to becomes hidden (imagine a table that shows a big tooltip for a cell and hides the values of other cells that are near the first one).
Qt follows the same concepts; so, not only you can't interactively click on a tooltip, but it's usually almost impossible to hover a tooltip at all.
The only solution is to create your own tooltip.
In the following (rather complex) example, I'm going to show how to achieve that.
Note that this implementation is not perfect: I could test it under Linux only, but, most importantly, it's not application-wide (it could theoretically be possible, though).
The basic concept is to install an event filter on all widgets that could potentially have a clickable url, intercept every QEvent that is of QEvent.ToolTip type, and create a widget that behaves like it afterwards.
I tried to implement it as much as similar to the standard QToolTip object (which is only accessible almost by static methods). The only difference here is that the static method returns the instance, which allows to connect to the linkActivated signal.
class ClickableTooltip(QtWidgets.QLabel):
__instance = None
refWidget = None
refPos = None
menuShowing = False
def __init__(self):
super().__init__(flags=QtCore.Qt.ToolTip)
margin = self.style().pixelMetric(
QtWidgets.QStyle.PM_ToolTipLabelFrameWidth, None, self)
self.setMargin(margin + 1)
self.setForegroundRole(QtGui.QPalette.ToolTipText)
self.setWordWrap(True)
self.mouseTimer = QtCore.QTimer(interval=250, timeout=self.checkCursor)
self.hideTimer = QtCore.QTimer(singleShot=True, timeout=self.hide)
def checkCursor(self):
# ignore if the link context menu is visible
for menu in self.findChildren(
QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly):
if menu.isVisible():
return
# an arbitrary check for mouse position; since we have to be able to move
# inside the tooltip margins (standard QToolTip hides itself on hover),
# let's add some margins just for safety
region = QtGui.QRegion(self.geometry().adjusted(-10, -10, 10, 10))
if self.refWidget:
rect = self.refWidget.rect()
rect.moveTopLeft(self.refWidget.mapToGlobal(QtCore.QPoint()))
region |= QtGui.QRegion(rect)
else:
# add a circular region for the mouse cursor possible range
rect = QtCore.QRect(0, 0, 16, 16)
rect.moveCenter(self.refPos)
region |= QtGui.QRegion(rect, QtGui.QRegion.Ellipse)
if QtGui.QCursor.pos() not in region:
self.hide()
def show(self):
super().show()
QtWidgets.QApplication.instance().installEventFilter(self)
def event(self, event):
# just for safety...
if event.type() == QtCore.QEvent.WindowDeactivate:
self.hide()
return super().event(event)
def eventFilter(self, source, event):
# if we detect a mouse button or key press that's not originated from the
# label, assume that the tooltip should be closed; note that widgets that
# have been just mapped ("shown") might return events for their QWindow
# instead of the actual QWidget
if source not in (self, self.windowHandle()) and event.type() in (
QtCore.QEvent.MouseButtonPress, QtCore.QEvent.KeyPress):
self.hide()
return super().eventFilter(source, event)
def move(self, pos):
# ensure that the style has "polished" the widget (font, palette, etc.)
self.ensurePolished()
# ensure that the tooltip is shown within the available screen area
geo = QtCore.QRect(pos, self.sizeHint())
try:
screen = QtWidgets.QApplication.screenAt(pos)
except:
# support for Qt < 5.10
for screen in QtWidgets.QApplication.screens():
if pos in screen.geometry():
break
else:
screen = None
if not screen:
screen = QtWidgets.QApplication.primaryScreen()
screenGeo = screen.availableGeometry()
# screen geometry correction should always consider the top-left corners
# *last* so that at least their beginning text is always visible (that's
# why I used pairs of "if" instead of "if/else"); also note that this
# doesn't take into account right-to-left languages, but that can be
# accounted for by checking QGuiApplication.layoutDirection()
if geo.bottom() > screenGeo.bottom():
geo.moveBottom(screenGeo.bottom())
if geo.top() < screenGeo.top():
geo.moveTop(screenGeo.top())
if geo.right() > screenGeo.right():
geo.moveRight(screenGeo.right())
if geo.left() < screenGeo.left():
geo.moveLeft(screenGeo.left())
super().move(geo.topLeft())
def contextMenuEvent(self, event):
# check the children QMenu objects before showing the menu (which could
# potentially hide the label)
knownChildMenus = set(self.findChildren(
QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly))
self.menuShowing = True
super().contextMenuEvent(event)
newMenus = set(self.findChildren(
QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly))
if knownChildMenus == newMenus:
# no new context menu? hide!
self.hide()
else:
# hide ourselves as soon as the (new) menus close
for m in knownChildMenus ^ newMenus:
m.aboutToHide.connect(self.hide)
m.aboutToHide.connect(lambda m=m: m.aboutToHide.disconnect())
self.menuShowing = False
def mouseReleaseEvent(self, event):
# click events on link are delivered on button release!
super().mouseReleaseEvent(event)
self.hide()
def hide(self):
if not self.menuShowing:
super().hide()
def hideEvent(self, event):
super().hideEvent(event)
QtWidgets.QApplication.instance().removeEventFilter(self)
self.refWidget.window().removeEventFilter(self)
self.refWidget = self.refPos = None
self.mouseTimer.stop()
self.hideTimer.stop()
def resizeEvent(self, event):
super().resizeEvent(event)
# on some systems the tooltip is not a rectangle, let's "mask" the label
# according to the system defaults
opt = QtWidgets.QStyleOption()
opt.initFrom(self)
mask = QtWidgets.QStyleHintReturnMask()
if self.style().styleHint(
QtWidgets.QStyle.SH_ToolTip_Mask, opt, self, mask):
self.setMask(mask.region)
def paintEvent(self, event):
# we cannot directly draw the label, since a tooltip could have an inner
# border, so let's draw the "background" before that
qp = QtGui.QPainter(self)
opt = QtWidgets.QStyleOption()
opt.initFrom(self)
style = self.style()
style.drawPrimitive(style.PE_PanelTipLabel, opt, qp, self)
# now we paint the label contents
super().paintEvent(event)
#staticmethod
def showText(pos, text:str, parent=None, rect=None, delay=0):
# this is a method similar to QToolTip.showText;
# it reuses an existent instance, but also returns the tooltip so that
# its linkActivated signal can be connected
if ClickableTooltip.__instance is None:
if not text:
return
ClickableTooltip.__instance = ClickableTooltip()
toolTip = ClickableTooltip.__instance
toolTip.mouseTimer.stop()
toolTip.hideTimer.stop()
# disconnect all previously connected signals, if any
try:
toolTip.linkActivated.disconnect()
except:
pass
if not text:
toolTip.hide()
return
toolTip.setText(text)
if parent:
toolTip.refRect = rect
else:
delay = 0
pos += QtCore.QPoint(16, 16)
# adjust the tooltip position if necessary (based on arbitrary margins)
if not toolTip.isVisible() or parent != toolTip.refWidget or (
not parent and toolTip.refPos and
(toolTip.refPos - pos).manhattanLength() > 10):
toolTip.move(pos)
# we assume that, if no parent argument is given, the current activeWindow
# is what we should use as a reference for mouse detection
toolTip.refWidget = parent or QtWidgets.QApplication.activeWindow()
toolTip.refPos = pos
toolTip.show()
toolTip.mouseTimer.start()
if delay:
toolTip.hideTimer.start(delay)
return toolTip
class ToolTipTest(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
count = 1
tip = 'This is link {c}'
for row in range(4):
for col in range(4):
button = QtWidgets.QPushButton('Hello {}'.format(count))
layout.addWidget(button, row, col)
button.setToolTip(tip.format(c=count))
button.installEventFilter(self)
count += 1
def toolTipLinkClicked(self, url):
print(url)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ToolTip and source.toolTip():
toolTip = ClickableTooltip.showText(
QtGui.QCursor.pos(), source.toolTip(), source)
toolTip.linkActivated.connect(self.toolTipLinkClicked)
return True
return super().eventFilter(source, event)

Change cursor shape on event key

I'm trying to change the cursor shape on key event:
When i press 'C', i want to display a LineCursor,
when i press 'S', i want to display a CrossCursor, and
when i press 'N', i want to display the standard ArrowCursor.
The cursor change only if it leave the canvas and return to it,
but not if the cursor stay in the canvas.
self.update() on the canvas don't work
Here the code to reproduce the problem :
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setObjectName("MainWindow")
self.resize(942, 935)
self.centralwidget = QWidget(self)
self.centralwidget.setObjectName("centralwidget")
self.horizontalLayout = QHBoxLayout(self.centralwidget)
self.horizontalLayout.setObjectName("horizontalLayout")
self.MainView = QGraphicsView(self.centralwidget)
self.MainView.setObjectName("MainView")
self.horizontalLayout.addWidget(self.MainView)
self.setCentralWidget(self.centralwidget)
self.setWindowTitle("MainWindow")
self.scene = QGraphicsScene( 0.,0., 1240., 1780. )
self.canvas = Canvas()
self.widget = QWidget()
box_layout = QVBoxLayout()
self.widget.setLayout(box_layout)
box_layout.addWidget(self.canvas)
self.scene.addWidget(self.widget)
self.MainView.setScene(self.scene)
self.MainView.setRenderHints(QPainter.Antialiasing)
self.MainView.fitInView(0, 0, 45, 55, Qt.KeepAspectRatio)
self.show()
empty = QPixmap(1240, 1748)
empty.fill(QColor(Qt.white))
self.canvas.newPixmap(empty)
def keyPressEvent(self, e):
key = e.key()
if key == Qt.Key_C:
self.canvas.setCutCursor()
elif key == Qt.Key_N:
self.canvas.setNormalCursor()
elif key == Qt.Key_S:
self.canvas.setSelectionCursor()
class Canvas(QLabel):
def __init__(self):
super().__init__()
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.setSizePolicy(sizePolicy)
self.setAlignment(Qt.AlignLeft)
self.setAlignment(Qt.AlignTop)
def newPixmap(self, pixmap):
self.setPixmap(pixmap)
def setCutCursor(self):
newCursor = QPixmap(500,3)
newCursor.fill(QColor("#000000"))
self.setCursor(QCursor(newCursor))
def setSelectionCursor(self):
self.setCursor(Qt.CrossCursor)
def setNormalCursor(self):
self.setCursor(QCursor(Qt.ArrowCursor))
if __name__ == '__main__':
app = QApplication(sys.argv)
mainWindow = MainWindow()
sys.exit(app.exec_())
It seems to be an old bug that was never resolved: setCursor on QGraphicsView don't work when add QWidget on the QGraphicsScene
There is a possible workaround, but it's far from perfect.
First of all, you have to consider that while dealing with a QGraphicsScene and its view[s] is not easy when dealing with mouse events and widget proxies, mostly because of the multiple nested levels of events and interaction between the actual view (and its parent, up to the top level window) and the proxy itself, which is an abstraction of the widget you added to the scene. While Qt devs did a huge amount of work to make it as transparent as possible, at some point you will probably face some unexpected or undesired behavior that is usually hard to fix or work around, and that's also because a graphics scene might be visualized in more than a single view.
Besides the aforementioned bug, you have to consider that a graphics view uses QWidget.setCursor internally whenever any of its items call setCursor on themselves, and since the view is a very complex widget, at some point it might even try to "restore" the cursor if it thinks it should (even if it shouldn't).
Finally, some events which also have something to do with focus might become in the way of all that.
The first workaround is to set the cursor to the view itself (or, better, the view's viewport, which is the actual widget that shows the scene contents). To ensure that, we obviously need to check if the cursor is inside the canvas.
Unfortunately, because of the event handling written above, this could become a bit messy, as some events are even delayed by at least a cycle within the main Qt event loop; the result is that while setting a cursor the first time might work, setting it again might not, and even if it would, it's possible that the cursor will not be applied until the mouse is moved at least by one pixel.
As a second workaround, we need an event filter to bypass all that and check the cursor whenever the mouse is moved within the viewport margins.
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
# ...
self.show()
empty = QPixmap(1240, 1748)
empty.fill(QColor(Qt.darkGray))
self.canvas.newPixmap(empty)
# install an event filter on the view's viewport;
# this is very, *VERY* important: on the *VIEWPORT*!
# if you install it on the view, it will *not* work
self.MainView.viewport().installEventFilter(self)
def insideCanvasRect(self, pos):
canvasRect = self.canvas.rect()
# translate the canvas rect to its top level window to get the actual
# geometry according to the scene; we can't use canvas.geometry(), as
# geometry() is based on the widget's parent coordinates, and that
# parent could also have any number of parents in turn;
canvasRect.translate(self.canvas.mapTo(self.canvas.window(), QPoint(0, 0)))
# map the geometry to the view's transformation, which probably uses
# some scaling, but also translation *and* shearing; the result is a
# polygon, as with shearing you could transform a rectangle to an
# irregular quadrilateral
polygon = self.MainView.mapFromScene(QRectF(canvasRect))
# tell if the point is within the resulting polygon
return polygon.containsPoint(pos, Qt.WindingFill)
def eventFilter(self, source, event):
if source == self.MainView.viewport() and (
(event.type() == QEvent.MouseMove and not event.buttons()) or
(event.type() == QEvent.MouseButtonRelease)
):
# process the event
super(MainWindow, self).eventFilter(source, event)
if self.insideCanvasRect(event.pos()):
source.setCursor(self.canvas.cursor())
else:
source.unsetCursor()
# usually a mouse move event within the view's viewport returns False,
# but in that case the event would be propagated to the parents, up
# to the top level window, which might reset the *previous* cursor
# at some point, no matter if we try to avoid that; to prevent that
# we return True to avoid propagation.
# Note that this will prevent any upper-level filtering and *could*
# also create some issues for the drag and drop framework
if event.type() == QEvent.MouseMove:
return True
return super(MainWindow, self).eventFilter(source, event)
def keyPressEvent(self, e):
# send the canvas a fake leave event
QApplication.sendEvent(self.canvas, QEvent(QEvent.Leave))
key = e.key()
if key == Qt.Key_C:
self.canvas.setCutCursor()
elif key == Qt.Key_N:
self.canvas.setNormalCursor()
elif key == Qt.Key_S:
self.canvas.setSelectionCursor()
pos = self.canvas.rect().center()
event = QEnterEvent(pos, self.canvas.mapTo(self.canvas.window(), pos), self.canvas.mapToGlobal(pos))
# send a fake enter event (mapped to the center of the widget, just to be sure)
QApplication.sendEvent(self.canvas, event)
# if we're inside the widget, set the view's cursor, otherwise it will not
# be set until the mouse is moved
if self.insideCanvasRect(self.MainView.viewport().mapFromGlobal(QCursor.pos())):
self.MainView.viewport().setCursor(self.canvas.cursor())

collidesWithItem() function not detecting collision (PyQt)

I'm writing an application that allows the user to add items to a scene. I dont want any new items being drawn over items that have already been drawn and to do that I decided to use the collidesWithItem() function to detect collision. With my code i still can draw over added items even though there is obviously collision and I debugged the program and the collidesWithItem() function keeps returning "False".
The items are added by clicking on the toolbar of the Form.
Down below is my code:
class graphicsScene(QtGui.QGraphicsScene, QtGui.QWidget):
def __init__(self, parent=None):
super(graphicsScene, self).__init__(parent)
self.i = 0
self.setSceneRect(-180, -90, 360, 180)
global overlapped
overlapped = 0
def mousePressEvent(self, event):
global host_cs
global overlapped
if host_cs == 1:
if len(hostItem_list) == 0:
self.host_item = host_Object()
hostItem_list.append(self.host_item.host_pixItem)
else:
self.host_item = host_Object()
for host in hostItem_list:
if self.host_item.host_pixItem.collidesWithItem(host):
print 'collision'
overlapped = 1
break
elif self.host_item.host_pixItem.collidesWithItem(host) == False:
overlapped = 0
if overlapped == 0:
hostItem_list.append(self.host_item.host_pixItem)
def mouseReleaseEvent(self, event):
global host_cs
if host_cs == 1:
if overlapped == 0:
self.addItem(self.host_item.host_pixItem)
self.host_item.host_pixItem.setPos(event.scenePos())
self.i += 1
host_list.append('h' + str(self.i))
class host_Object(QtGui.QGraphicsPixmapItem, QtGui.QWidget):
def __init__(self, parent=None):
super(host_Object, self).__init__(parent)
pixmap = QtGui.QPixmap("host.png")
self.host_pixItem = QtGui.QGraphicsPixmapItem(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio))
self.host_pixItem.setFlag(QtGui.QGraphicsPixmapItem.ItemIsSelectable)
self.host_pixItem.setFlag(QtGui.QGraphicsPixmapItem.ItemIsMovable)
class Form(QtGui.QMainWindow, QtGui.QWidget):
def __init__(self):
super(Form, self).__init__()
self.ui = uic.loadUi('form.ui')
self.ui.actionHost.triggered.connect(self.place_host)
self.scene = graphicsScene()
self.ui.view.setScene(self.scene)
def place_host(self):
host_cs = 1
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
form = Form()
form.ui.show()
app.exec_()
Multiple inheritance is source of all evil things in application design.
Also it is forbidden in Qt to double inherit QObject.
So all your classes with multiple inheritance has big flaws.
Even class host_Object(QtGui.QGraphicsPixmapItem, QtGui.QWidget) is wrong and problematic since item can't be a QWidget and QGraphicsItem at the same time.
I would recommend you to avoid double inheritance as a general rule not only for Qt framework but for any language.

Qgraphicsview items not being placed where they should be

I recently created a program that will create QgraphicsEllipseItems whenever the mouse is clicked. That part works! However, it's not in the exact place where my cursor is. It seems to be slightly higher than where my mouse cursor is. I do have a QGraphicsRectItem created so maybe the two items are clashing with each other and moving off of one another? How can I get these circles to be placed on top of the rectangle item? Here's the code
class MyView(QtGui.QGraphicsView):
def __init__(self):
QtGui.QGraphicsView.__init__(self)
self.scene = QtGui.QGraphicsScene(self)
self.item = QtGui.QGraphicsRectItem(400, 400, 400, 400)
self.scene.addItem(self.item)
self.setScene(self.scene)
def paintMarkers(self):
self.cursor = QtGui.QCursor()
self.x = self.cursor.pos().x()
self.y = self.cursor.pos().y()
self.circleItem = QtGui.QGraphicsEllipseItem(self.x,self.y,10,10)
self.scene.addItem(self.circleItem)
self.circleItem.setPen(QtGui.QPen(QtCore.Qt.red, 1.5))
self.setScene(self.scene)
class Window(QtGui.QMainWindow):
def __init__(self):
#This initializes the main window or form
super(Window,self).__init__()
self.setGeometry(50,50,1000,1000)
self.setWindowTitle("Pre-Alignment system")
self.view = MyView()
self.setCentralWidget(self.view)
def mousePressEvent(self,QMouseEvent):
self.view.paintMarkers()
Much thanks!
There are two issues with the coordinates you are using to place the QGraphics...Items. The first is that the coordinates from QCursor are global screen coordinates, so you need to use self.mapFromGlobal() to convert them to coordinates relative to the QGraphicsView.
Secondly, you actually want the coordinates relative to the current QGraphicsScene, as this is where you are drawing the item. This is because the scene can be offset from the view (for example panning around a scene that is bigger than a view). To do this, you use self.mapToScene() on the coordinates relative to the QGraphicsView.
I would point out that typically you would draw something on the QGraphicsScene in response to some sort of mouse event in the QGraphicsView, which requires reimplementing things like QGraphicsView.mouseMoveEvent or QGraphicsView.mousePressEvent. These event handlers are passed a QEvent which contains the mouse coordinates relative to the view, and so you don't need to do the global coordinates transformation I mentioned in the first paragraph in these cases.
Update
I've just seen your other question, and now understand some of the issue a bit better. You shouldn't be overriding the mouse event in the main window. Instead override it in the view. For example:
class MyView(QtGui.QGraphicsView):
def __init__(self):
QtGui.QGraphicsView.__init__(self)
self.scene = QtGui.QGraphicsScene(self)
self.item = QtGui.QGraphicsRectItem(400, 400, 400, 400)
self.scene.addItem(self.item)
self.setScene(self.scene)
def paintMarkers(self, event):
# event position is in coordinates relative to the view
# so convert them to scene coordinates
p = self.mapToScene(event.x(), event.y())
self.circleItem = QtGui.QGraphicsEllipseItem(0,0,10,10)
self.circleItem.setPos(p.x()-self.circleItem.boundingRect().width()/2.0,
p.y()-self.circleItem.boundingRect().height()/2.0)
self.scene.addItem(self.circleItem)
self.circleItem.setPen(QtGui.QPen(QtCore.Qt.red, 1.5))
# self.setScene(self.scene) # <-- this line should not be needed here
# Note, I've renamed the second argument `event`. Otherwise you locally override the QMouseEvent class
def mousePressEvent(self, event):
self.paintMarkers(event)
# you may want to preserve the default mouse press behaviour,
# in which case call the following
return QGraphicsView.mousePressEvent(self, event)
Here we have not needed to use QWidget.mapFromGlobal() (what I covered in the first paragraph) because we use a mouse event from the QGraphicsView which returns coordinates relative to that widget only.
Update 2
Note: I've updated how the item is created/placed in the above code based on this answer.

Categories

Resources