Issue with fitInView of QGraphicsView when ItemIgnoresTransformations is on - pyqt4

While trying to implement a scene where item sizes do not change but distances between items get magnified I encountered this problem with the following code which draws a rectangle and the text "A". Now if I set the flag ItemIgnoresTransformations on the rectangle item, zooming in causes the rectangle to vanish (click and drag mouse button around the rectangle). But that does not happen in case of the text. Also, the new viewport area set by fitInView is very different for I asked for:
import sys
from PyQt4 import QtCore, QtGui
class GV(QtGui.QGraphicsView):
def __init__(self, *args, **kwargs):
QtGui.QGraphicsView.__init__(self, *args, **kwargs)
def mousePressEvent(self, event):
pos = QtCore.QPoint(event.pos())
self.startPos = pos
def mouseReleaseEvent(self, event):
pos = QtCore.QPoint(event.pos())
self.endPos = pos
rect = QtCore.QRect(self.startPos, self.endPos)
sceneRect = self.mapToScene(rect).boundingRect()
print 'Selected area: viewport coordinate:', rect, \
', scene coordinate:', sceneRect
self.fitInView(sceneRect)
print 'new viewport in scene coordinates:', \
self.mapToScene(self.viewport().geometry()).boundingRect()
class Scene(QtGui.QGraphicsScene):
def __init__(self, *args, **kwargs):
QtGui.QGraphicsScene.__init__(self, *args, **kwargs)
self.itemA = QtGui.QGraphicsSimpleTextItem('A')
self.itemA.setPos(20, 20)
self.itemA.setFlag(QtGui.QGraphicsItem.ItemIgnoresTransformations, True)
self.addItem(self.itemA)
self.itemB = QtGui.QGraphicsRectItem(30, 50, 20, 20)
self.addItem(self.itemB)
self.itemB.setFlag(QtGui.QGraphicsItem.ItemIgnoresTransformations, True)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
widget = QtGui.QMainWindow()
scene = Scene()
view = GV(scene, widget)
widget.setCentralWidget(view)
widget.show()
app.exec_()
Any explanations will be appreciated!

In fact, the rectangle does not vanish. But it moves around "strangely".
self.itemB = QtGui.QGraphicsRectItem(30, 50, 20, 20)
This line may not be what you want. This creates an item and puts a rectangle/square starting from (30, 50) in local coordinates. Then you add this to the scene. This gives you an item anchored at (0, 0), spans up to (50, 70) but draws a rectangle only in the bottom right 20x20.
When you set ItemIgnoresTransformations, item can't do its regular transformations in case of a zoom. Scene zooms in, for item to ignore this transformation, it kind of "shrinks" itself. But it's still anchored at (0, 0) and the rectangle is at the bottom-right, so the drawn rectangle moves toward the upper-left.
Solution is rather simple. Don't create your rectangle in local coordinates, i.e. your rectangle should start from (0, 0) and you should position it explicitly. Which translates to this:
self.itemB = QtGui.QGraphicsRectItem(0, 0, 20, 20)
self.itemB.setPos(30, 50)

Related

How to resize a window from the edges after adding the property QtCore.Qt.FramelessWindowHint

Good night.
I have seen some programs with new borderless designs and still you can make use of resizing.
At the moment I know that to remove the borders of a pyqt program we use:
QtCore.Qt.FramelessWindowHint
And that to change the size of a window use QSizeGrip.
But how can we resize a window without borders?
This is the code that I use to remove the border of a window but after that I have not found information on how to do it in pyqt5.
I hope you can help me with an example of how to solve this problem
from PyQt5.QtWidgets import QMainWindow,QApplication
from PyQt5 import QtCore
class Main(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
app = QApplication([])
m = Main()
m.show()
m.resize(800,600)
app.exec_()
If you use a QMainWindow you can add a QStatusBar (which automatically adds a QSizeGrip) just by calling statusBar():
This function creates and returns an empty status bar if the status bar does not exist.
Otherwise, you can manually add grips, and their interaction is done automatically based on their position. In the following example I'm adding 4 grips, one for each corner, and then I move them each time the window is resized.
class Main(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.gripSize = 16
self.grips = []
for i in range(4):
grip = QSizeGrip(self)
grip.resize(self.gripSize, self.gripSize)
self.grips.append(grip)
def resizeEvent(self, event):
QMainWindow.resizeEvent(self, event)
rect = self.rect()
# top left grip doesn't need to be moved...
# top right
self.grips[1].move(rect.right() - self.gripSize, 0)
# bottom right
self.grips[2].move(
rect.right() - self.gripSize, rect.bottom() - self.gripSize)
# bottom left
self.grips[3].move(0, rect.bottom() - self.gripSize)
UPDATE
Based on comments, also side-resizing is required. To do so a good solution is to create a custom widget that behaves similarly to QSizeGrip, but for vertical/horizontal resizing only.
For better implementation I changed the code above, used a gripSize to construct an "inner" rectangle and, based on it, change the geometry of all widgets, for both corners and sides.
Here you can see the "outer" rectangle and the "inner" rectangle used for geometry computations:
Then you can create all geometries, for QSizeGrip widgets (in light blue):
And for custom side widgets:
from PyQt5 import QtCore, QtGui, QtWidgets
class SideGrip(QtWidgets.QWidget):
def __init__(self, parent, edge):
QtWidgets.QWidget.__init__(self, parent)
if edge == QtCore.Qt.LeftEdge:
self.setCursor(QtCore.Qt.SizeHorCursor)
self.resizeFunc = self.resizeLeft
elif edge == QtCore.Qt.TopEdge:
self.setCursor(QtCore.Qt.SizeVerCursor)
self.resizeFunc = self.resizeTop
elif edge == QtCore.Qt.RightEdge:
self.setCursor(QtCore.Qt.SizeHorCursor)
self.resizeFunc = self.resizeRight
else:
self.setCursor(QtCore.Qt.SizeVerCursor)
self.resizeFunc = self.resizeBottom
self.mousePos = None
def resizeLeft(self, delta):
window = self.window()
width = max(window.minimumWidth(), window.width() - delta.x())
geo = window.geometry()
geo.setLeft(geo.right() - width)
window.setGeometry(geo)
def resizeTop(self, delta):
window = self.window()
height = max(window.minimumHeight(), window.height() - delta.y())
geo = window.geometry()
geo.setTop(geo.bottom() - height)
window.setGeometry(geo)
def resizeRight(self, delta):
window = self.window()
width = max(window.minimumWidth(), window.width() + delta.x())
window.resize(width, window.height())
def resizeBottom(self, delta):
window = self.window()
height = max(window.minimumHeight(), window.height() + delta.y())
window.resize(window.width(), height)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.mousePos = event.pos()
def mouseMoveEvent(self, event):
if self.mousePos is not None:
delta = event.pos() - self.mousePos
self.resizeFunc(delta)
def mouseReleaseEvent(self, event):
self.mousePos = None
class Main(QtWidgets.QMainWindow):
_gripSize = 8
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.sideGrips = [
SideGrip(self, QtCore.Qt.LeftEdge),
SideGrip(self, QtCore.Qt.TopEdge),
SideGrip(self, QtCore.Qt.RightEdge),
SideGrip(self, QtCore.Qt.BottomEdge),
]
# corner grips should be "on top" of everything, otherwise the side grips
# will take precedence on mouse events, so we are adding them *after*;
# alternatively, widget.raise_() can be used
self.cornerGrips = [QtWidgets.QSizeGrip(self) for i in range(4)]
#property
def gripSize(self):
return self._gripSize
def setGripSize(self, size):
if size == self._gripSize:
return
self._gripSize = max(2, size)
self.updateGrips()
def updateGrips(self):
self.setContentsMargins(*[self.gripSize] * 4)
outRect = self.rect()
# an "inner" rect used for reference to set the geometries of size grips
inRect = outRect.adjusted(self.gripSize, self.gripSize,
-self.gripSize, -self.gripSize)
# top left
self.cornerGrips[0].setGeometry(
QtCore.QRect(outRect.topLeft(), inRect.topLeft()))
# top right
self.cornerGrips[1].setGeometry(
QtCore.QRect(outRect.topRight(), inRect.topRight()).normalized())
# bottom right
self.cornerGrips[2].setGeometry(
QtCore.QRect(inRect.bottomRight(), outRect.bottomRight()))
# bottom left
self.cornerGrips[3].setGeometry(
QtCore.QRect(outRect.bottomLeft(), inRect.bottomLeft()).normalized())
# left edge
self.sideGrips[0].setGeometry(
0, inRect.top(), self.gripSize, inRect.height())
# top edge
self.sideGrips[1].setGeometry(
inRect.left(), 0, inRect.width(), self.gripSize)
# right edge
self.sideGrips[2].setGeometry(
inRect.left() + inRect.width(),
inRect.top(), self.gripSize, inRect.height())
# bottom edge
self.sideGrips[3].setGeometry(
self.gripSize, inRect.top() + inRect.height(),
inRect.width(), self.gripSize)
def resizeEvent(self, event):
QtWidgets.QMainWindow.resizeEvent(self, event)
self.updateGrips()
app = QtWidgets.QApplication([])
m = Main()
m.show()
m.resize(240, 160)
app.exec_()
to hide the QSizeGrip on the corners where they shouldn't be showing, you can just change the background color of the QSizeGrip to camouflage them to the background. add this to each of the corners of musicamante's answer:
self.cornerGrips[0].setStyleSheet("""
background-color: transparent;
""")

Fixed size QGraphicsView and QGraphicsScene

I have a QMdiSubWindow, that contains a QTabWidget which contains QGraphicsView inside a widget with a QHBoxLayout. The sub window is in an mdi area in my main window. My objective is to have a fixed size for QGraphicScene, depending upon the paper size i am using.
The view should then either conform to the same size as the scene, showing no scroll bars if this area can fit the total available mdi area, resizing the subwindow as well, or should resize to fill the available area resizing the sub window too, and should show scroll bars to navigate to the remaining space.
My current approach revolves around resizing the mdi subwindow first, then making a call to resize the current active widget in the window's QTabWidget.
This is all done as per musicamante's implementation.
The current widget chooses the min value between the parent(QStackedWidget)'s parent(QTabWidget)'s parent (QMdiSubWindow)'s rect (as it is resized first) and the dimensions of the QGraphicsScene which is updated when I set the dimensions.
-> Set QGraphicsScene dimensions from user input
-> Resize sub window
-> Resize QGraphicsView
I use the same logic, on my program, which fails to show scroll bars at all and the view expands to fit the QGraphicsScene despite being told to be the size of the sub window instead (sub window is of correct size), and the following example, where they do show as necessary but are clipped off partially.
from PyQt5 import QtWidgets, QtCore, QtGui
paperSizes = {
"A0": {
"72": [2384, 3370],
"96": [3179, 4494],
"150": [4967, 7022],
"300": [9933, 14043]
},
"A1": {
"72": [1684, 2384],
"96": [2245, 3179],
"150": [3508, 4967],
"300": [7016, 9933]
},
"A2": {
"72": [1191, 1684],
"96": [1587, 2245],
"150": [2480, 3508],
"300": [4960, 7016]
},
"A3": {
"72": [842, 1191],
"96": [1123, 1587],
"150": [1754, 2480],
"300": [3508, 4960]
},
"A4": {
"72": [595, 842],
"96": [794, 1123],
"150": [1240, 1754],
"300": [2480, 3508]
}
}
class canvas(QtWidgets.QWidget):
def __init__(self, parent=None, size= 'A4', ppi= '72'):
super(canvas, self).__init__(parent)
self._ppi = ppi
self._canvasSize = size
self.painter = QtWidgets.QGraphicsScene()
self.painter.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.white))
self.view = QtWidgets.QGraphicsView(self.painter)
# self.view.setMinimumSize(595, 842)
self.layout = QtWidgets.QHBoxLayout(self)
self.layout.addWidget(self.view, stretch = 1, alignment= QtCore.Qt.AlignCenter)
self.setLayout(self.layout)
self.painter.setSceneRect(0, 0, *paperSizes[self.canvasSize][self.ppi])
def resizeView(self, w, h):
self.painter.setSceneRect(0, 0, w, h)
self.adjustView()
def adjustView(self):
self.view.setSceneRect(0, 0, self.painter.sceneRect().width() - self.view.frameWidth() * 2,
self.painter.sceneRect().height())
# give the view some time to adjust itself
QtWidgets.QApplication.processEvents()
width = self.painter.sceneRect().width() + self.view.frameWidth()*2
if self.view.verticalScrollBar().isVisible():
width += self.style().pixelMetric(QtWidgets.QStyle.PM_ScrollBarExtent)
height = self.painter.sceneRect().height() + self.view.frameWidth()*2
if self.view.verticalScrollBar().isVisible():
height += self.style().pixelMetric(QtWidgets.QStyle.PM_ScrollBarExtent)
self.view.setFixedWidth(min(self.parent().rect().width() - self.view.frameWidth()*2, width))
self.view.setFixedHeight(min(self.parent().rect().height()- self.view.frameWidth()*2, height))
def resizeEvent(self, event):
self.adjustView()
def setCanvasSize(self, size):
self.canvasSize = size
def setCanvasPPI(self, ppi):
self.ppi = ppi
#property
def canvasSize(self):
return self._canvasSize
#property
def ppi(self):
return self._ppi
#canvasSize.setter
def canvasSize(self, size):
self._canvasSize = size
if self.painter:
self.resizeView(*paperSizes[self.canvasSize][self.ppi])
#ppi.setter
def ppi(self, ppi):
self._ppi = ppi
if self.painter:
self.resizeView(*paperSizes[self.canvasSize][self.ppi])
#property
def dimensions(self):
#returns the dimension of the current scene
return self.painter.sceneRect().width(), self.painter.sceneRect().height()
class AppDemo(QtWidgets.QMainWindow):
def __init__(self):
super(AppDemo, self).__init__()
self.centralwidget = canvas(self)
width, height = paperSizes['A4']['72']
wsetter = min(1800, width - self.centralwidget.view.frameWidth()*2)
hsetter = min(900, height - self.centralwidget.view.frameWidth()*2)
self.setFixedSize(wsetter, hsetter)
self.centralwidget.resizeView(width, height)
self.setCentralWidget(self.centralwidget)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.resizer)
self.show()
def resizer(self, point):
width, height = paperSizes['A0']['300']
wsetter = min(1800, width - self.centralwidget.view.frameWidth()*2)
hsetter = min(900, height - self.centralwidget.view.frameWidth()*2)
self.setFixedSize(wsetter, hsetter)
self.centralWidget().resizeView(width, height)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = AppDemo()
w.setWindowTitle('AppDemo')
w.centralwidget.resizeView(*paperSizes['A0']['72'])
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
If you are going to always keep the default transformation (no scaling), it could make sense to set the view sceneRect using the actual scene rect as a reference.
Note that, since the window could be resized, it's usually better to use a common function to do so, and call it also within the resizeEvent(). This also ensures that the view is resized if the vertical scroll bar becomes visible.
class canvas(QtWidgets.QWidget):
def __init__(self, parent=None, size= 'A4', ppi= '72'):
# ...
w, h = paperSizes[self.canvasSize][self.ppi]
self.painter.setSceneRect(0, 0, w, h)
# ensure that the scene is always aligned on the left, instead of being
# centered (the default)
self.view.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
# no need for this here
# self.view.setSceneRect(0, 0, w, h)
def resizeView(self, w, h):
self.painter.setSceneRect(0, 0, w, h)
self.adjustView()
def adjustView(self):
self.view.setSceneRect(0, 0, self.painter.sceneRect().width() - self.view.frameWidth() * 2,
self.painter.sceneRect().height())
# give the view some time to adjust itself
QtWidgets.QApplication.processEvents()
width = self.painter.sceneRect().width() + self.view.frameWidth() * 2
if self.view.verticalScrollBar().isVisible():
width += self.style().pixelMetric(QtWidgets.QStyle.PM_ScrollBarExtent)
self.view.setFixedWidth(width)
def resizeEvent(self, event):
self.adjustView()
Obviously, if you're sure that the view will always show the full width of the scene, you could also completely disable the horizontal scrollbar by setting its horizontalScrollBarPolicy to Qt.ScrollBarAlwaysOff.

Is there an equivalent of Toastr for PyQt?

I am working on my first PyQt project and I would like to come up with a way to provide the user with success or error messages when they complete tasks. With Javascript in the past, I used Toastr and I was curious if there is anything like it for Python applications. I considered using the QDialog class in PyQt, but I would rather not have separate windows as popups if possible since even modeless dialog windows would be distracting for the user.
UPDATE: I've updated the code, making it possible to show desktop-wise notifications (see below).
Implementing a desktop-aware toaster like widget is not impossible, but presents some issues that are platform dependent. On the other hand, a client-side one is easier.
I've created a small class that is able to show a notification based on the top level window of the current widget, with the possibility to set the message text, the icon, and if the notification is user-closable. I also added a nice opacity animation, that is common in such systems.
Its main use is based on a static method, similarly to what QMessageBox does, but it can also be implemented in a similar fashion by adding other features.
UPDATE
I realized that making a desktop-wise notification is not that hard (but some care is required for cross-platform development, I'll leave that up to the programmer).
The following is the updated code that allows using None as a parent for the class, making the notification a desktop widget instead of a child widget of an existing Qt one. If you're reading this and you're not interested in such a feature, just check the editing history for the original (and slightly simpler) code.
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class QToaster(QtWidgets.QFrame):
closed = QtCore.pyqtSignal()
def __init__(self, *args, **kwargs):
super(QToaster, self).__init__(*args, **kwargs)
QtWidgets.QHBoxLayout(self)
self.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
QtWidgets.QSizePolicy.Maximum)
self.setStyleSheet('''
QToaster {
border: 1px solid black;
border-radius: 4px;
background: palette(window);
}
''')
# alternatively:
# self.setAutoFillBackground(True)
# self.setFrameShape(self.Box)
self.timer = QtCore.QTimer(singleShot=True, timeout=self.hide)
if self.parent():
self.opacityEffect = QtWidgets.QGraphicsOpacityEffect(opacity=0)
self.setGraphicsEffect(self.opacityEffect)
self.opacityAni = QtCore.QPropertyAnimation(self.opacityEffect, b'opacity')
# we have a parent, install an eventFilter so that when it's resized
# the notification will be correctly moved to the right corner
self.parent().installEventFilter(self)
else:
# there's no parent, use the window opacity property, assuming that
# the window manager supports it; if it doesn't, this won'd do
# anything (besides making the hiding a bit longer by half a second)
self.opacityAni = QtCore.QPropertyAnimation(self, b'windowOpacity')
self.opacityAni.setStartValue(0.)
self.opacityAni.setEndValue(1.)
self.opacityAni.setDuration(100)
self.opacityAni.finished.connect(self.checkClosed)
self.corner = QtCore.Qt.TopLeftCorner
self.margin = 10
def checkClosed(self):
# if we have been fading out, we're closing the notification
if self.opacityAni.direction() == self.opacityAni.Backward:
self.close()
def restore(self):
# this is a "helper function", that can be called from mouseEnterEvent
# and when the parent widget is resized. We will not close the
# notification if the mouse is in or the parent is resized
self.timer.stop()
# also, stop the animation if it's fading out...
self.opacityAni.stop()
# ...and restore the opacity
if self.parent():
self.opacityEffect.setOpacity(1)
else:
self.setWindowOpacity(1)
def hide(self):
# start hiding
self.opacityAni.setDirection(self.opacityAni.Backward)
self.opacityAni.setDuration(500)
self.opacityAni.start()
def eventFilter(self, source, event):
if source == self.parent() and event.type() == QtCore.QEvent.Resize:
self.opacityAni.stop()
parentRect = self.parent().rect()
geo = self.geometry()
if self.corner == QtCore.Qt.TopLeftCorner:
geo.moveTopLeft(
parentRect.topLeft() + QtCore.QPoint(self.margin, self.margin))
elif self.corner == QtCore.Qt.TopRightCorner:
geo.moveTopRight(
parentRect.topRight() + QtCore.QPoint(-self.margin, self.margin))
elif self.corner == QtCore.Qt.BottomRightCorner:
geo.moveBottomRight(
parentRect.bottomRight() + QtCore.QPoint(-self.margin, -self.margin))
else:
geo.moveBottomLeft(
parentRect.bottomLeft() + QtCore.QPoint(self.margin, -self.margin))
self.setGeometry(geo)
self.restore()
self.timer.start()
return super(QToaster, self).eventFilter(source, event)
def enterEvent(self, event):
self.restore()
def leaveEvent(self, event):
self.timer.start()
def closeEvent(self, event):
# we don't need the notification anymore, delete it!
self.deleteLater()
def resizeEvent(self, event):
super(QToaster, self).resizeEvent(event)
# if you don't set a stylesheet, you don't need any of the following!
if not self.parent():
# there's no parent, so we need to update the mask
path = QtGui.QPainterPath()
path.addRoundedRect(QtCore.QRectF(self.rect()).translated(-.5, -.5), 4, 4)
self.setMask(QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon()))
else:
self.clearMask()
#staticmethod
def showMessage(parent, message,
icon=QtWidgets.QStyle.SP_MessageBoxInformation,
corner=QtCore.Qt.TopLeftCorner, margin=10, closable=True,
timeout=5000, desktop=False, parentWindow=True):
if parent and parentWindow:
parent = parent.window()
if not parent or desktop:
self = QToaster(None)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint |
QtCore.Qt.BypassWindowManagerHint)
# This is a dirty hack!
# parentless objects are garbage collected, so the widget will be
# deleted as soon as the function that calls it returns, but if an
# object is referenced to *any* other object it will not, at least
# for PyQt (I didn't test it to a deeper level)
self.__self = self
currentScreen = QtWidgets.QApplication.primaryScreen()
if parent and parent.window().geometry().size().isValid():
# the notification is to be shown on the desktop, but there is a
# parent that is (theoretically) visible and mapped, we'll try to
# use its geometry as a reference to guess which desktop shows
# most of its area; if the parent is not a top level window, use
# that as a reference
reference = parent.window().geometry()
else:
# the parent has not been mapped yet, let's use the cursor as a
# reference for the screen
reference = QtCore.QRect(
QtGui.QCursor.pos() - QtCore.QPoint(1, 1),
QtCore.QSize(3, 3))
maxArea = 0
for screen in QtWidgets.QApplication.screens():
intersected = screen.geometry().intersected(reference)
area = intersected.width() * intersected.height()
if area > maxArea:
maxArea = area
currentScreen = screen
parentRect = currentScreen.availableGeometry()
else:
self = QToaster(parent)
parentRect = parent.rect()
self.timer.setInterval(timeout)
# use Qt standard icon pixmaps; see:
# https://doc.qt.io/qt-5/qstyle.html#StandardPixmap-enum
if isinstance(icon, QtWidgets.QStyle.StandardPixmap):
labelIcon = QtWidgets.QLabel()
self.layout().addWidget(labelIcon)
icon = self.style().standardIcon(icon)
size = self.style().pixelMetric(QtWidgets.QStyle.PM_SmallIconSize)
labelIcon.setPixmap(icon.pixmap(size))
self.label = QtWidgets.QLabel(message)
self.layout().addWidget(self.label)
if closable:
self.closeButton = QtWidgets.QToolButton()
self.layout().addWidget(self.closeButton)
closeIcon = self.style().standardIcon(
QtWidgets.QStyle.SP_TitleBarCloseButton)
self.closeButton.setIcon(closeIcon)
self.closeButton.setAutoRaise(True)
self.closeButton.clicked.connect(self.close)
self.timer.start()
# raise the widget and adjust its size to the minimum
self.raise_()
self.adjustSize()
self.corner = corner
self.margin = margin
geo = self.geometry()
# now the widget should have the correct size hints, let's move it to the
# right place
if corner == QtCore.Qt.TopLeftCorner:
geo.moveTopLeft(
parentRect.topLeft() + QtCore.QPoint(margin, margin))
elif corner == QtCore.Qt.TopRightCorner:
geo.moveTopRight(
parentRect.topRight() + QtCore.QPoint(-margin, margin))
elif corner == QtCore.Qt.BottomRightCorner:
geo.moveBottomRight(
parentRect.bottomRight() + QtCore.QPoint(-margin, -margin))
else:
geo.moveBottomLeft(
parentRect.bottomLeft() + QtCore.QPoint(margin, -margin))
self.setGeometry(geo)
self.show()
self.opacityAni.start()
class W(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QVBoxLayout(self)
toasterLayout = QtWidgets.QHBoxLayout()
layout.addLayout(toasterLayout)
self.textEdit = QtWidgets.QLineEdit('Ciao!')
toasterLayout.addWidget(self.textEdit)
self.cornerCombo = QtWidgets.QComboBox()
toasterLayout.addWidget(self.cornerCombo)
for pos in ('TopLeft', 'TopRight', 'BottomRight', 'BottomLeft'):
corner = getattr(QtCore.Qt, '{}Corner'.format(pos))
self.cornerCombo.addItem(pos, corner)
self.windowBtn = QtWidgets.QPushButton('Show window toaster')
toasterLayout.addWidget(self.windowBtn)
self.windowBtn.clicked.connect(self.showToaster)
self.screenBtn = QtWidgets.QPushButton('Show desktop toaster')
toasterLayout.addWidget(self.screenBtn)
self.screenBtn.clicked.connect(self.showToaster)
# a random widget for the window
layout.addWidget(QtWidgets.QTableView())
def showToaster(self):
if self.sender() == self.windowBtn:
parent = self
desktop = False
else:
parent = None
desktop = True
corner = QtCore.Qt.Corner(self.cornerCombo.currentData())
QToaster.showMessage(
parent, self.textEdit.text(), corner=corner, desktop=desktop)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = W()
w.show()
sys.exit(app.exec_())
Try it:
import sys
from PyQt5.QtCore import (QRectF, Qt, QPropertyAnimation, pyqtProperty,
QPoint, QParallelAnimationGroup, QEasingCurve)
from PyQt5.QtGui import QPainter, QPainterPath, QColor, QPen
from PyQt5.QtWidgets import (QLabel, QWidget, QVBoxLayout, QApplication,
QLineEdit, QPushButton)
class BubbleLabel(QWidget):
BackgroundColor = QColor(195, 195, 195)
BorderColor = QColor(150, 150, 150)
def __init__(self, *args, **kwargs):
text = kwargs.pop("text", "")
super(BubbleLabel, self).__init__(*args, **kwargs)
self.setWindowFlags(
Qt.Window | Qt.Tool | Qt.FramelessWindowHint |
Qt.WindowStaysOnTopHint | Qt.X11BypassWindowManagerHint)
# Set minimum width and height
self.setMinimumWidth(200)
self.setMinimumHeight(58)
self.setAttribute(Qt.WA_TranslucentBackground, True)
layout = QVBoxLayout(self)
# Top left and bottom right margins (16 below because triangles are included)
layout.setContentsMargins(8, 8, 8, 16)
self.label = QLabel(self)
layout.addWidget(self.label)
self.setText(text)
# Get screen height and width
self._desktop = QApplication.instance().desktop()
def setText(self, text):
self.label.setText(text)
def text(self):
return self.label.text()
def stop(self):
self.hide()
self.animationGroup.stop()
self.close()
def show(self):
super(BubbleLabel, self).show()
# Window start position
startPos = QPoint(
self._desktop.screenGeometry().width() - self.width() - 100,
self._desktop.availableGeometry().height() - self.height())
endPos = QPoint(
self._desktop.screenGeometry().width() - self.width() - 100,
self._desktop.availableGeometry().height() - self.height() * 3 - 5)
self.move(startPos)
# Initialization animation
self.initAnimation(startPos, endPos)
def initAnimation(self, startPos, endPos):
# Transparency animation
opacityAnimation = QPropertyAnimation(self, b"opacity")
opacityAnimation.setStartValue(1.0)
opacityAnimation.setEndValue(0.0)
# Set the animation curve
opacityAnimation.setEasingCurve(QEasingCurve.InQuad)
opacityAnimation.setDuration(4000)
# Moving up animation
moveAnimation = QPropertyAnimation(self, b"pos")
moveAnimation.setStartValue(startPos)
moveAnimation.setEndValue(endPos)
moveAnimation.setEasingCurve(QEasingCurve.InQuad)
moveAnimation.setDuration(5000)
# Parallel animation group (the purpose is to make the two animations above simultaneously)
self.animationGroup = QParallelAnimationGroup(self)
self.animationGroup.addAnimation(opacityAnimation)
self.animationGroup.addAnimation(moveAnimation)
# Close window at the end of the animation
self.animationGroup.finished.connect(self.close)
self.animationGroup.start()
def paintEvent(self, event):
super(BubbleLabel, self).paintEvent(event)
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) # Antialiasing
rectPath = QPainterPath() # Rounded Rectangle
triPath = QPainterPath() # Bottom triangle
height = self.height() - 8 # Offset up 8
rectPath.addRoundedRect(QRectF(0, 0, self.width(), height), 5, 5)
x = self.width() / 5 * 4
triPath.moveTo(x, height) # Move to the bottom horizontal line 4/5
# Draw triangle
triPath.lineTo(x + 6, height + 8)
triPath.lineTo(x + 12, height)
rectPath.addPath(triPath) # Add a triangle to the previous rectangle
# Border brush
painter.setPen(QPen(self.BorderColor, 1, Qt.SolidLine,
Qt.RoundCap, Qt.RoundJoin))
# Background brush
painter.setBrush(self.BackgroundColor)
# Draw shape
painter.drawPath(rectPath)
# Draw a line on the bottom of the triangle to ensure the same color as the background
painter.setPen(QPen(self.BackgroundColor, 1,
Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.drawLine(x, height, x + 12, height)
def windowOpacity(self):
return super(BubbleLabel, self).windowOpacity()
def setWindowOpacity(self, opacity):
super(BubbleLabel, self).setWindowOpacity(opacity)
# Since the opacity property is not in QWidget, you need to redefine one
opacity = pyqtProperty(float, windowOpacity, setWindowOpacity)
class TestWidget(QWidget):
def __init__(self, *args, **kwargs):
super(TestWidget, self).__init__(*args, **kwargs)
layout = QVBoxLayout(self)
self.msgEdit = QLineEdit(self, returnPressed=self.onMsgShow)
self.msgButton = QPushButton("Display content", self, clicked=self.onMsgShow)
layout.addWidget(self.msgEdit)
layout.addWidget(self.msgButton)
def onMsgShow(self):
msg = self.msgEdit.text().strip()
if not msg:
return
if hasattr(self, "_blabel"):
self._blabel.stop()
self._blabel.deleteLater()
del self._blabel
self._blabel = BubbleLabel()
self._blabel.setText(msg)
self._blabel.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = TestWidget()
w.show()
sys.exit(app.exec_())
There is nothing like that even in Qt 6.
Anyways, you said "but I would rather not have separate windows as popups if possible since even modeless dialog windows would be distracting for the user.".
Yes, there are two things necessary for the toast, and there is a solution.
Should not be a separated window - Qt.SubWindow
self.setWindowFlags(Qt.SubWindow)
Should ignore the mouse event, be unable to focus - Qt.WA_TransparentForMouseEvents
self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
Based on those rules, i made the toast that user can set the text, font, color(text or background) of it.
Here is my repo if you want to check the detail: https://github.com/yjg30737/pyqt-toast

Pyqt4: How to correct QGraphicsItem position?

I'm trying to draw a path on a QGraphicsView. However, the position seems not right. The first point(red) is (0,0), which is supposed to be at the top-left corner. How do I move the drawing to the right position?
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import QPointF as qpf
import sys
from PyQt4.QtGui import QPainterPath
data= [qpf(0,0),qpf(40,30),qpf(30,60),qpf(70,90),qpf(20,120),qpf(60,150)]
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.view = View(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.view)
class View(QtGui.QGraphicsView):
def __init__(self, parent):
QtGui.QGraphicsView.__init__(self, parent)
self.setScene(QtGui.QGraphicsScene(self))
item = QtGui.QGraphicsRectItem(data[0].x()-2,data[0].y()-2,4,4)
item.setBrush(QtCore.Qt.red)
self.scene().addItem(item)
self.path = path = QPainterPath(data[0])
for d in data[1:]:
path.lineTo(d)
item = QtGui.QGraphicsPathItem(path)
self.scene().addItem(item)
def mouseReleaseEvent(self, event):
pos = event.pos()
rect = QtCore.QRectF(pos.x()-2, pos.y()-2,4,4)
item = QtGui.QGraphicsRectItem(rect)
self.scene().addItem(item)
if self.path.intersects(rect):
print 'on line'
else:
print 'Not belong to line (%d, %d)' % (pos.x(), pos.y())
QtGui.QGraphicsView.mouseReleaseEvent(self, event)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.resize(400, 400)
window.show()
sys.exit(app.exec_())
This behavior is because you draw using QGraphicsScene and QGraphicsView. This is because view and scene have some automatism, which are very convenient, normally. The scene coordinated can be completely different from the view (which is in pixels).
From the docs: "After you call show(), the view will by default scroll to the center of the scene and display any items that are visible at this point." show() is called implicitly in your case, I believe after you add an item to the scene.
I can think of two possibilities to get what you want:
1) change the view onto your scene, so that the scene-coordinate (0, 0) is in the upper left corner of your view.
2) Do NOT use QGraphicsScene and QGraphicsView, but just draw on a widget from its paint event as shown for example here. This means your dimensions are all in pixels, i.e. the coordinates of your points are pixels. And no automatism that might confuse you is done.
You would want to use self.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) in your view class constructor, that will make 0,0 of the scene coordinates the same as 0,0 in view coordinates.AlignmentFlag
I would also suggest pos = self.mapToScene(event.pos()) in the mouseReleaseEvent. that way if you zoom in or scroll the scene, the click will happen in the right place in the view. QGraphicsView.mapFromScene

PyQt: QGraphicsItem added at a wrong position

I have a subclass of QGraphicsItem and I want to add instances of it to the scene on 'Control+LMB click'. The trouble is that the item is added at the position with coordinates that are two times larger than they should be. At the same time adding ellipses with scene.addEllipse(...) works fine.
#!/usr/bin/env python
import sys
from PyQt4.QtCore import (QPointF, QRectF, Qt, )
from PyQt4.QtGui import (QApplication, QMainWindow, QGraphicsItem,
QGraphicsScene, QGraphicsView, QPen, QStyle)
MapSize = (512, 512)
class DraggableMark(QGraphicsItem):
def __init__(self, position, scene):
super(DraggableMark, self).__init__(None, scene)
self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
self.rect = QRectF(position.x(), position.y(), 15, 15)
self.setPos(position)
scene.clearSelection()
def boundingRect(self):
return self.rect
def paint(self, painter, option, widget):
pen = QPen(Qt.SolidLine)
pen.setColor(Qt.black)
pen.setWidth(1)
if option.state & QStyle.State_Selected:
pen.setColor(Qt.blue)
painter.setPen(pen)
painter.drawEllipse(self.rect)
class GraphicsScene(QGraphicsScene):
def __init__(self, parent=None):
super(GraphicsScene, self).__init__(parent)
self.setSceneRect(0, 0, *MapSize)
def mousePressEvent(self, event):
super(GraphicsScene, self).mousePressEvent(event)
if event.button() != Qt.LeftButton:
return
modifiers = QApplication.keyboardModifiers()
pos = event.scenePos()
if modifiers == Qt.ControlModifier:
print("Control + Click: (%d, %d)" % (pos.x(), pos.y()))
DraggableMark(pos, self)
self.addEllipse(QRectF(pos.x(), pos.y(), 10, 10))
else:
print("Click: (%d, %d)" % (pos.x(), pos.y()))
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.scene = GraphicsScene(self)
self.scene.addRect(QRectF(0, 0, *MapSize), Qt.red)
self.view = QGraphicsView()
self.view.setScene(self.scene)
self.view.resize(self.scene.width(), self.scene.height())
self.setCentralWidget(self.view)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
rect = QApplication.desktop().availableGeometry()
window.resize(int(rect.width()), int(rect.height()))
window.show()
app.exec_()
I see you have answered your own question. However I would like to explain why this works.
Every QGraphicsItem has its own local coordinate system. So when you do
self.rect = QRectF(position.x(), position.y(), 15, 15)
you basically start from the (0, 0) of the item's local coordinate system and go to the given x and y which you take from position. This basically means that your rectangle will be drawn at position.x() + position.x() and position.y() + position.y() with the first position.x()/position.y() being the position of the QGraphicsItem inside your scene and the second position.x()/position.y() being the position inside the local coordinate system of your item.
If you want to start from the origin of the QGraphicsItem, you have to use
self.rect = QRectF(0, 0, 15, 15)
This ensures that you start from the origin of the local coordinate system.
This issue is particularly tricky due to the fact that by default objects are added to the (0, 0) of a scene. So position.x() + position.x() and position.y() + position.y() in this case will actually not show the issue at hand since 0+0 is always equal to 0. It is the moment you change the default position to something else when the problem will occur.
Here is a 3D figure that visualizes what I'm describing above (I was unable to find a 2D example but the principle is the same :P):
The world here is the scene while the object is the QGraphicsItem residing in that scene.
Changing
self.rect = QRectF(position.x(), position.y(), 15, 15)
to
self.rect = QRectF(0, 0, 15, 15)
solved the problem

Resources