Is there any way to display a 360-degree image in PyQt5 Python? - python-3.x

I want to be able to choose a 360-degree image and show it in a PyQt5 window making it interact-able and moveable, is there any way to do this? I am done with the part of choosing the file but can't seem to display it in a Qlabel.
I am done with the part of choosing the file but can't seem to display it in a Qlabel.
I expect the picture to be interactable like the 360-images shown on Facebook

You cannot set the pixmap for the label, as it would show the full image entirely.
Instead, you will need to create your own QWidget, and provide by yourself "panning" implementation by overriding the paintEvent (which actually draws the image) and mouse events for interaction.
I made a very basic example, which does not support zooming (which would require much more computation). The source image I used is taken from here
.
It creates a local pixmap which is repeated 3 times horizontally to ensure that you can pan infinitely, uses mouseMoveEvent to compute the position via a QTimer and limits the vertical position to the image height, while resetting the horizontal position whenever the x coordinate is beyond the half of the image width.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Panoramic(QtWidgets.QWidget):
def __init__(self, imagePath):
QtWidgets.QWidget.__init__(self)
self.setCursor(QtCore.Qt.CrossCursor)
# keep a reference of the original image
self.source = QtGui.QPixmap(imagePath)
self.pano = QtGui.QPixmap(self.source.width() * 3, self.source.height())
self.center = self.pano.rect().center()
# use a QPointF for precision
self.delta = QtCore.QPointF()
self.deltaTimer = QtCore.QTimer(interval=25, timeout=self.moveCenter)
self.sourceRect = QtCore.QRect()
# create a pixmap with three copies of the source;
# this could be avoided by smart repainting and translation of the source
# but since paintEvent automatically clips the painting, it should be
# faster then computing the new rectangle each paint cycle, at the cost
# of a few megabytes of memory.
self.setMaximumSize(self.source.size())
qp = QtGui.QPainter(self.pano)
qp.drawPixmap(0, 0, self.source)
qp.drawPixmap(self.source.width(), 0, self.source)
qp.drawPixmap(self.source.width() * 2, 0, self.source)
qp.end()
def moveCenter(self):
if not self.delta:
return
self.center += self.delta
# limit the vertical position
if self.center.y() < self.sourceRect.height() * .5:
self.center.setY(self.sourceRect.height() * .5)
elif self.center.y() > self.source.height() - self.height() * .5:
self.center.setY(self.source.height() - self.height() * .5)
# reset the horizontal position if beyond the center of the virtual image
if self.center.x() < self.source.width() * .5:
self.center.setX(self.source.width() * 1.5)
elif self.center.x() > self.source.width() * 2.5:
self.center.setX(self.source.width() * 1.5)
self.sourceRect.moveCenter(self.center.toPoint())
self.update()
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.mousePos = event.pos()
def mouseMoveEvent(self, event):
if event.buttons() != QtCore.Qt.LeftButton:
return
delta = event.pos() - self.mousePos
# use a fraction to get small movements, and ensure we're not too fast
self.delta.setX(max(-25, min(25, delta.x() * .125)))
self.delta.setY(max(-25, min(25, delta.y() * .125)))
if not self.deltaTimer.isActive():
self.deltaTimer.start()
def mouseReleaseEvent(self, event):
self.deltaTimer.stop()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.drawPixmap(self.rect(), self.pano, self.sourceRect)
# resize and reposition the coordinates whenever the window is resized
def resizeEvent(self, event):
self.sourceRect.setSize(self.size())
self.sourceRect.moveCenter(self.center)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Panoramic('pano.jpg')
w.show()
sys.exit(app.exec_())

Related

How to fit a QGraphicsView to the width of the parent view in PyQt5?

The below class is a test to display a pdf file and a text box ontop inside a QGraphicsView. I would like to show only the top part of the pdf. And it is supposed to fill the width of the view. So it is correct here that the view does not have the same aspect ratio as the pdf.
The GraphicsView class will later be instantiated in the MainView class. The MainView is instantiated in the Application class.
I am using the resizeEvent method to scale the QGraphicsView to the viewport width when the window size changes. But for some reason I cannot get the pdf file to fill the width when the window first gets opened and loads the pdf. So when running the code, the PDF is way smaller then the window width and only when I resize the window, it pops into the correct width.
This is what it looks like, when I run the code:
For debugging purposes I set the background color of the scene to red. So I guess the pdf does not fit the scene width?
I tried calling the _update_view function inside the load_pdf function, I also tried to scale the view inside the load_pdf function, but this does not work. Does anybody have an idea how to fix this?
import fitz
import sys
from PyQt5 import QtWidgets, QtGui, QtCore
class GraphicsView(QtWidgets.QWidget):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self._init_view()
self._init_scene()
self._init_pixmap_item()
self._init_text_box()
self._init_layout()
def _init_view(self):
self.scale_factor = 6
self.view = QtWidgets.QGraphicsView()
self._turn_off_scrollbars()
self._enable_anti_aliasing()
self._set_transform_anchor()
self._install_event_filter()
def _turn_off_scrollbars(self):
self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
def _enable_anti_aliasing(self):
self.view.setRenderHint(QtGui.QPainter.Antialiasing)
self.view.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
def _set_transform_anchor(self):
self.view.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
def _install_event_filter(self):
self.view.viewport().installEventFilter(self)
def _init_scene(self):
self.scene = QtWidgets.QGraphicsScene(self.view)
self.view.setScene(self.scene)
#red background for debugging
self.scene.setBackgroundBrush(QtGui.QBrush(QtGui.QColor("red")))
def _init_pixmap_item(self):
self.pixmap_item = QtWidgets.QGraphicsPixmapItem()
self.scene.addItem(self.pixmap_item)
def _init_text_box(self):
self.font_size = 12
self.font_name = QtGui.QFont("Helvetica", self.font_size)
self.font_color = QtGui.QColor("blue")
self.v_pos = 10
self.h_pos = 10
self.text_box = QtWidgets.QGraphicsTextItem("Hello World")
self.text_box.setDefaultTextColor(self.font_color)
self.text_box.setFont(self.font_name)
self.text_box.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
self.text_box.setTransform(QtGui.QTransform.fromScale(2.25, 2.25), True)
self.scene.addItem(self.text_box)
def _init_layout(self):
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.view)
self.setLayout(layout)
self.setGeometry(100, 100, 1400, 400)
self.setMinimumSize(800, 400)
def load_pdf(self, path):
pdf_doc = fitz.open(path)
page = pdf_doc[0]
self._transform_pdf_page(page)
self._position_text_box()
self._update_view()
def _transform_pdf_page(self, page):
matrix = fitz.Matrix(self.scale_factor, self.scale_factor)
pixmap = page.get_pixmap(matrix=matrix, alpha=False)
image = QtGui.QImage(
pixmap.samples, pixmap.width, pixmap.height, QtGui.QImage.Format_RGB888)
self._fit_image(image)
def _fit_image(self, image):
self.pixmap_item.setPixmap(QtGui.QPixmap.fromImage(image))
self.scene.setSceneRect(self.pixmap_item.boundingRect())
self.view.fitInView(self.scene.sceneRect(), QtCore.Qt.KeepAspectRatio)
def _position_text_box(self):
# To Do: Test on multiple pdfs
# Maybe add an offset, so that the scale factor is the same everywhere
self.text_box.setPos(
self.view.sceneRect().width() - self.text_box.boundingRect().width() * 2.3 - self.h_pos * 2.3,
self.text_box.boundingRect().height() * 1.7 - self.h_pos * 2.3)
def resizeEvent(self, event):
self._update_view()
return super().resizeEvent(event)
def _update_view(self):
self.view.resetTransform()
aspect_ratio = self.scene.sceneRect().height() / self.scene.sceneRect().width()
view_width = self.view.viewport().width()
view_height = aspect_ratio * view_width
self.view.setTransform(
QtGui.QTransform().scale(
view_width / self.view.sceneRect().width(),
view_height / self.view.sceneRect().height()))
self.view.ensureVisible(0, 0, 0, 0)
self.view.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = GraphicsView()
window.load_pdf(
"C:/test.pdf"
)
window.show()
sys.exit(app.exec_())
To solve my problem, I had to make a change to the _update_view method. The method worked as expected when the user manually resized the window. However, it did not produce the desired outcome of scaling the QGraphicsView to the width of the window, right after opening the window and running the load_pdf method.
The line view_width = self.view.viewport().width() in the _update_view method returned a viewport width of 100 instead of (approximately) 1400, which is the initial width of the window.
Prepending the _update_view method with self.view.viewport().resize(self.width(), self.height()) instead of self.view.resetTransform() solves my problem, because it will always first resize the view to the width and height of the GraphicsView class widget. I also had to multiply the viewport width by 0.97, because the PDF was still being displayed a bit larger than the view.
def _update_view(self):
self.view.viewport().resize(self.width(), self.height())
aspect_ratio = self.scene.sceneRect().height() / self.scene.sceneRect().width()
view_width = self.view.viewport().width() * 0.97
view_height = aspect_ratio * view_width
self.view.setTransform(
QtGui.QTransform().scale(
view_width / self.view.sceneRect().width(),
view_height / self.view.sceneRect().height()))
self.view.ensureVisible(0, 0, 0, 0)
self.view.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)

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;
""")

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

Resizing a Gtk.DrawingArea in PyGtk3 [duplicate]

This question already has an answer here:
Cannot reduce size of gtk window programatically
(1 answer)
Closed 3 years ago.
Please run the code below to get something like this:
Basically, I've set up a Gtk.DrawingArea, which is inside a Gtk.Viewport, which is also in a Gtk.Scrolledwindow. In said DrawingArea I draw an image, and with the 2 buttons you can scale said image.
# -*- encoding: utf-8 -*-
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
class MyWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="DrawingTool")
self.set_default_size(800, 600)
# The ratio
self.ratio = 1.
# Image
filename = "image.jpg"
self.original_image = GdkPixbuf.Pixbuf.new_from_file(filename)
self.displayed_image = GdkPixbuf.Pixbuf.new_from_file(filename)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
# Zoom buttons
self.button_zoom_in = Gtk.Button(label="Zoom-In")
self.button_zoom_out = Gtk.Button(label="Zoom-Out")
# |ScrolledWindow
# |-> Viewport
# |--> DrawingArea
scrolledwindow = Gtk.ScrolledWindow()
viewport = Gtk.Viewport()
self.drawing_area = Gtk.DrawingArea()
self.drawing_area.set_size_request(
self.displayed_image.get_width(), self.displayed_image.get_height())
self.drawing_area.set_events(Gdk.EventMask.ALL_EVENTS_MASK)
# Pack
viewport.add(self.drawing_area)
scrolledwindow.add(viewport)
box.pack_start(self.button_zoom_in, False, True, 0)
box.pack_start(self.button_zoom_out, False, True, 0)
box.pack_start(scrolledwindow, True, True, 0)
self.add(box)
# Connect
self.connect("destroy", Gtk.main_quit)
self.button_zoom_in.connect("clicked", self.on_button_zoom_in_clicked)
self.button_zoom_out.connect("clicked", self.on_button_zoom_out_clicked)
self.drawing_area.connect("enter-notify-event", self.on_drawing_area_mouse_enter)
self.drawing_area.connect("leave-notify-event", self.on_drawing_area_mouse_leave)
self.drawing_area.connect("motion-notify-event", self.on_drawing_area_mouse_motion)
self.drawing_area.connect("draw", self.on_drawing_area_draw)
self.show_all()
def on_button_zoom_in_clicked(self, widget):
self.ratio += 0.1
self.scale_image()
self.drawing_area.queue_draw()
def on_button_zoom_out_clicked(self, widget):
self.ratio -= 0.1
self.scale_image()
self.drawing_area.queue_draw()
def scale_image(self):
self.displayed_image = self.original_image.scale_simple(self.original_image.get_width() * self.ratio,
self.original_image.get_height() * self.ratio, 2)
def on_drawing_area_draw(self, drawable, cairo_context):
pixbuf = self.displayed_image
self.drawing_area.set_size_request(pixbuf.get_width(), pixbuf.get_height())
Gdk.cairo_set_source_pixbuf(cairo_context, pixbuf, 0, 0)
cairo_context.paint()
def on_drawing_area_mouse_enter(self, widget, event):
print("In - DrawingArea")
def on_drawing_area_mouse_leave(self, widget, event):
print("Out - DrawingArea")
def on_drawing_area_mouse_motion(self, widget, event):
(x, y) = int(event.x), int(event.y)
offset = ( (y*self.displayed_image.get_rowstride()) +
(x*self.displayed_image.get_n_channels()) )
pixel_intensity = self.displayed_image.get_pixels()[offset]
print("(" + str(x) + ", " + str(y) + ") = " + str(pixel_intensity))
MyWindow()
Gtk.main()
Run the application. I've also set up some callbacks so when you hover the mouse pointer over the image, you get to know:
i) When you enter the DrawingArea
ii) When you leave the DrawingArea
iii) The (x, y) at the DrawingArea and image pixel intensity
The problem I'm facing is, when you Zoom-out enough times, the image will look like this:
However, the 'mouse enter' and 'mouse leave' signals, aswell the 'mouse motion' one are sent like the DrawingArea still has the same size it was created with. Please hover the mouse over the image and outside the image but inside the DrawingArea to see the output in your terminal.
I would like the signals not to send themselves when hovering outside the image. This is, adapt the DrawingArea size to the displayed image size, if possible.
I've used gtk_window_resize() as it is mentioned here: Cannot reduce size of gtk window programatically
def on_drawing_area_draw(self, drawable, cairo_context):
pixbuf = self.displayed_image
# New line. Get DrawingArea's window and resize it.
drawable.get_window().resize(pixbuf.get_width(), pixbuf.get_height())
drawable.set_size_request(pixbuf.get_width(), pixbuf.get_height())
Gdk.cairo_set_source_pixbuf(cairo_context, pixbuf, 0, 0)
cairo_context.paint()

Get clicked chess piece from an SVG chessboard

I am developing a chess GUI in Python 3.6.3 using PyQt5 5.9.1 (GUI framework) and python-chess 0.21.1 (chess library) on Windows 10. I want to get the value of a piece that was clicked on an SVG chessboard (provided by python-chess) so that I can then move that piece to another square.
After the first left mouse click and getting the piece, I want to get the second left mouse click from the user and get the square that the user clicked on. Then my chess GUI must move the piece from originating square to the target square.
So, here's my complete working code so far. Any hints or actual code additions are very welcome.
import chess
import chess.svg
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtWidgets import QApplication, QWidget
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Chess Titan")
self.setGeometry(300, 300, 800, 800)
self.widgetSvg = QSvgWidget(parent=self)
self.widgetSvg.setGeometry(10, 10, 600, 600)
self.chessboard = chess.Board()
#pyqtSlot(QWidget)
def mousePressEvent(self, event):
if event.buttons() == Qt.LeftButton:
## How to get the clicked SVG chess piece?
# Envoke the paint event.
self.update()
#pyqtSlot(QWidget)
def paintEvent(self, event):
self.chessboardSvg = chess.svg.board(self.chessboard).encode("UTF-8")
self.widgetSvg.load(self.chessboardSvg)
if __name__ == "__main__":
chessTitan = QApplication([])
window = MainWindow()
window.show()
chessTitan.exec()
If size of chessboard is known, you can find the coordinates of the mouseclick from event.pos() resp.event.x(), event.y() depending on marginwidth and squaresize, see chess.svg.py line 129 ff.
edit Nov 25: event.pos() is in this example in MainWindow coordinates, to find the coordinates on chessboard all must be calculated from top left corner represented by self.svgX and self.svgY:
import chess
import chess.svg
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtWidgets import QApplication, QWidget
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Chess Titan")
self.setGeometry(300, 300, 800, 800)
self.widgetSvg = QSvgWidget(parent=self)
self.svgX = 50 # top left x-pos of chessboard
self.svgY = 50 # top left y-pos of chessboard
self.cbSize = 600 # size of chessboard
self.widgetSvg.setGeometry(self.svgX,self.svgY, self.cbSize, self.cbSize)
self.coordinates = True
# see chess.svg.py line 129
self.margin = 0.05*self.cbSize if self.coordinates == True else 0
self.squareSize = (self.cbSize - 2 * self.margin) / 8.0
self.chessboard = chess.Board()
self.pieceToMove = [None, None]
#pyqtSlot(QWidget)
def mousePressEvent(self, event):
if self.svgX < event.x() <= self.svgX + self.cbSize and self.svgY < event.y() <= self.svgY + self.cbSize: # mouse on chessboard
if event.buttons() == Qt.LeftButton:
# if the click is on chessBoard only
if self.svgX + self.margin < event.x() < self.svgX + self.cbSize - self.margin and self.svgY + self.margin < event.y() < self.svgY + self.cbSize - self.margin:
file = int((event.x() - (self.svgX + self.margin))/self.squareSize)
rank = 7 - int((event.y() - (self.svgY + self.margin))/self.squareSize)
square = chess.square(file, rank) # chess.sqare.mirror() if white is on top
piece = self.chessboard.piece_at(square)
coordinates = '{}{}'.format(chr(file + 97), str(rank +1))
if self.pieceToMove[0] is not None:
move = chess.Move.from_uci('{}{}'.format(self.pieceToMove[1], coordinates))
self.chessboard.push(move)
print(self.chessboard.fen())
piece = None
coordinates= None
self.pieceToMove = [piece, coordinates]
else:
print('coordinates clicked')
# Envoke the paint event.
self.update()
else:
QWidget.mousePressEvent(self, event)
#pyqtSlot(QWidget)
def paintEvent(self, event):
self.chessboardSvg = chess.svg.board(self.chessboard, size = self.cbSize, coordinates = self.coordinates).encode("UTF-8")
self.widgetSvg.load(self.chessboardSvg)
if __name__ == "__main__":
chessTitan = QApplication([])
window = MainWindow()
window.show()
chessTitan.exec()
move white and black pieces alternating, they change the color if the same color is moved twice.
Below is the Python, PyQt5 and python-chess code for a fully functional chess GUI that has legal move detection built in, so chess piece movement behaves according to the rules of chess.
#! /usr/bin/env python
"""
This module is the execution point of the chess GUI application.
"""
import sys
import chess
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtWidgets import QApplication, QWidget
class MainWindow(QWidget):
"""
Create a surface for the chessboard.
"""
def __init__(self):
"""
Initialize the chessboard.
"""
super().__init__()
self.setWindowTitle("Chess GUI")
self.setGeometry(300, 300, 800, 800)
self.widgetSvg = QSvgWidget(parent=self)
self.widgetSvg.setGeometry(10, 10, 600, 600)
self.boardSize = min(self.widgetSvg.width(),
self.widgetSvg.height())
self.coordinates = True
self.margin = 0.05 * self.boardSize if self.coordinates else 0
self.squareSize = (self.boardSize - 2 * self.margin) / 8.0
self.pieceToMove = [None, None]
self.board = chess.Board()
self.drawBoard()
#pyqtSlot(QWidget)
def mousePressEvent(self, event):
"""
Handle left mouse clicks and enable moving chess pieces by
clicking on a chess piece and then the target square.
Moves must be made according to the rules of chess because
illegal moves are suppressed.
"""
if event.x() <= self.boardSize and event.y() <= self.boardSize:
if event.buttons() == Qt.LeftButton:
if self.margin < event.x() < self.boardSize - self.margin and self.margin < event.y() < self.boardSize - self.margin:
file = int((event.x() - self.margin) / self.squareSize)
rank = 7 - int((event.y() - self.margin) / self.squareSize)
square = chess.square(file, rank)
piece = self.board.piece_at(square)
coordinates = "{}{}".format(chr(file + 97), str(rank + 1))
if self.pieceToMove[0] is not None:
move = chess.Move.from_uci("{}{}".format(self.pieceToMove[1], coordinates))
if move in self.board.legal_moves:
self.board.push(move)
piece = None
coordinates = None
self.pieceToMove = [piece, coordinates]
self.drawBoard()
def drawBoard(self):
"""
Draw a chessboard with the starting position and then redraw
it for every new move.
"""
self.boardSvg = self.board._repr_svg_().encode("UTF-8")
self.drawBoardSvg = self.widgetSvg.load(self.boardSvg)
return self.drawBoardSvg
if __name__ == "__main__":
chessGui = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(chessGui.exec_())
a_manthey_67 and BoĊĦtjan Mejak, I've combined features from both of your solutions:
https://github.com/vtad4f/chess-ui/blob/master/board.py
The full version integrates AI player(s) with your board UI:
Run make to build https://github.com/vtad4f/chess-ai/
Run main.py to play a game https://github.com/vtad4f/chess-ui/

Resources