How to use matplotlib blitting to add matplot.patches to an matplotlib plot in wxPython? - python-3.x

I am making a plot using the matplotlib library and showing it in my wxPython GUI. I am plotting a massive amount of data points from a LIDAR instrument. The thing is, I would like to draw rectangles in this plot to indicate interesting areas. But when I draw a rectangle on the same axes as the plot, the whole plot gets replotted which takes lots of time. This is because of the self.canvas.draw(), a function which replots everything.
The code gets displayed as follows in the GUI:
Printscreen of GUI
Here is a minimal working example of the problem. U can draw rectangles by holding the right mouse button. Once you plot the NetCDF data using the button on the left, the drawing of rectangles gets really slow. I tried some things with blitting using the examples provided by ImportanceOfBeingErnest but after a lot of tries, I still have not managed to get it to work.
To make the minimal working example work, you will have to specify the path to the NetCDF file under the plot_Data() function. I provided the NetCDF file which to download here:
Download NetCDF file
How can I blit the self.square to the self.canvas in the onselect function?
import netCDF4 as nc
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
import matplotlib.pyplot as plt
import matplotlib.colors as colors
import matplotlib.widgets
import time
import wx
class rightPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
self.initiate_Matplotlib_Plot_Canvas()
self.add_Matplotlib_Widgets()
def initiate_Matplotlib_Plot_Canvas(self):
self.figure = Figure()
self.axes = self.figure.add_subplot(111)
self.colorbar = None
self.canvas = FigureCanvas(self, -1, self.figure)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.canvas, proportion=1, flag=wx.ALL | wx.GROW)
self.SetSizer(self.sizer)
self.Fit()
self.canvas.draw()
def add_Matplotlib_Widgets(self):
self.rectangleSelector = matplotlib.widgets.RectangleSelector(self.axes, self.onselect,
drawtype="box", useblit=True,
button=[3], interactive=False
)
def onselect(self, eclick, erelease):
tstart = time.time()
x1, y1 = eclick.xdata, eclick.ydata
x2, y2 = erelease.xdata, erelease.ydata
height = y2-y1
width = x2-x1
self.square = matplotlib.patches.Rectangle((x1,y1), width,
height, angle=0.0, edgecolor='red',
fill=False
#blit=True gives Unknown property blit
)
self.axes.add_patch(self.square)
self.canvas.draw()
# =============================================================================
# self.background = self.canvas.copy_from_bbox(self.axes.bbox)
#
#
# self.canvas.restore_region(self.background)
#
# self.axes.draw_artist(self.square)
#
# self.canvas.blit(self.axes.bbox)
# =============================================================================
tend = time.time()
print("Took " + str(tend-tstart) + " sec")
def plot_Data(self):
"""This function gets called by the leftPanel onUpdatePlot. This updates
the plot to the set variables from the widgets"""
path = "C:\\Users\\TEST_DATA\\cesar_uvlidar_backscatter_la1_t30s_v1.0_20100501.nc"
nc_data = self.NetCDF_READ(path)
print("plotting......")
vmin_value = 10**2
vmax_value = 10**-5
combo_value = nc_data['perp_beta']
self.axes.clear()
plot_object = self.axes.pcolormesh(combo_value.T, cmap='rainbow',
norm=colors.LogNorm(vmin=vmin_value, vmax=vmax_value))
self.axes.set_title("Insert title here")
if self.colorbar is None:
self.colorbar = self.figure.colorbar(plot_object)
else:
self.colorbar.update_normal(plot_object)
self.colorbar.update_normal(plot_object)
print('canvas draw..............')
self.canvas.draw()
print("plotting succesfull")
###############################################################################
###############################################################################
"""BELOW HERE IS JUST DATA MANAGEMENT AND FRAME/PANEL INIT"""
###############################################################################
###############################################################################
def NetCDF_READ(self, path):
in_nc = nc.Dataset(path)
list_of_keys = in_nc.variables.keys()
nc_data = {} #Create an empty dictionary to store NetCDF variables
for item in list_of_keys:
variable_shape = in_nc.variables[item].shape
variable_dimensions = len(variable_shape)
if variable_dimensions > 1:
nc_data[item] = in_nc.variables[item][...] #Adding netCDF variables to dictonary
return nc_data
class leftPanel(wx.Panel):
def __init__(self, parent, mainPanel):
wx.Panel.__init__(self, parent)
button = wx.Button(self, -1, label="PRESS TO PLOT")
button.Bind(wx.EVT_BUTTON, self.onButton)
self.mainPanel = mainPanel
def onButton(self, event):
self.mainPanel.rightPanel.plot_Data()
class MainPanel(wx.Panel):
def __init__(self, parent):
"""Initializing the mainPanel. This class is called by the frame."""
wx.Panel.__init__(self, parent)
self.SetBackgroundColour('red')
"""Acquire the width and height of the monitor"""
width, height = wx.GetDisplaySize()
"""Split mainpanel into two sections"""
self.vSplitter = wx.SplitterWindow(self, size=(width,(height-100)))
self.leftPanel = leftPanel(self.vSplitter, self)
self.rightPanel = rightPanel(self.vSplitter)
self.vSplitter.SplitVertically(self.leftPanel, self.rightPanel,102)
class UV_Lidar(wx.Frame):
"""Uppermost class. This class contains everything and calls everything.
It is the container around the mainClass, which on its turn is the container around
the leftPanel class and the rightPanel class. This class generates the menubar, menu items,
toolbar and toolbar items"""
def __init__(self, parent, id):
print("UV-lidar> Initializing GUI...")
wx.Frame.__init__(self, parent, id, 'UV-lidar application')
self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
self.mainPanel = MainPanel(self)
def OnCloseWindow(self, event):
self.Destroy()
if __name__ == '__main__':
app = wx.App()
frame = UV_Lidar(parent=None, id=-1)
frame.Show()
print("UV-lidar> ")
print("UV-lidar> Initializing GUI OK")
app.MainLoop()

I have found the solution myself:
In order to blit a matplotlib patch, you will have to first add the patch to the axes. Then draw the patch on the axes and then you can blit the patch to the canvas.
square = matplotlib.patches.Rectangle((x1,y1), width,
height, angle=0.0, edgecolor='red',
fill=False)
self.axes.add_patch(square)
self.axes.draw_artist(square)
self.canvas.blit(self.axes.bbox)
If you do not want to use self.canvas.draw but still use matplotlib widgets which have useblit=True, you can save the plot as a background image: self.background = self.canvas.copy_from_bbox(self.axes.bbox) and restore it later by using: self.canvas.restore_region(self.background). This is a lot faster than drawing everything over!
When using the matplotlib's RectangleSelector widget with useblit=True, it will create another background instance variable, which interferes with your own background instance variable. To fix this problem, you will have to set the background instance variable of the RectangleSelector widget to be equal to your own background instance variable. However, this should only be done after the RectangleSelector widget is no longer active. Otherwise it will save some of the drawing animation to the background. So once the RectangleSelector has become inactive, you can update its background using: self.rectangleSelector.background = self.background
The code that had to be edited is given below. wx.CallLater(0, lambda: self.tbd(square)) is used so that the background instance variable of the RectangleSelector widget is updated only when it has become inactive.
def add_Matplotlib_Widgets(self):
"""Calling these instances creates another self.background in memory. Because the widget classes
restores their self-made background after the widget closes it interferes with the restoring of
our leftPanel self.background. In order to compesate for this problem, all background instances
should be equal to eachother. They are made equal in the update_All_Background_Instances(self)
function"""
"""Creating a widget that serves as the selector to draw a square on the plot"""
self.rectangleSelector = matplotlib.widgets.RectangleSelector(self.axes, self.onselect,
drawtype="box", useblit=True,
button=[3], interactive=False
)
def onselect(self, eclick, erelease):
self.tstart = time.time()
x1, y1 = eclick.xdata, eclick.ydata
x2, y2 = erelease.xdata, erelease.ydata
height = y2-y1
width = x2-x1
square = matplotlib.patches.Rectangle((x1,y1), width,
height, angle=0.0, edgecolor='red',
fill=False
#blit=True gives Unknown property blit
)
"""In order to keep the right background and not save any rectangle drawing animations
on the background, the RectangleSelector widget has to be closed first before saving
or restoring the background"""
wx.CallLater(0, lambda: self.tbd(square))
def tbd(self, square):
"""leftPanel background is restored"""
self.canvas.restore_region(self.background)
self.axes.add_patch(square)
self.axes.draw_artist(square)
self.canvas.blit(self.axes.bbox)
"""leftPanel background is updated"""
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
"""Setting all backgrounds equal to the leftPanel self.background"""
self.update_All_Background_Instances()
print('Took '+ str(time.time()-self.tstart) + ' s')
def update_All_Background_Instances(self):
"""This function sets all of the background instance variables equal
to the lefPanel self.background instance variable"""
self.rectangleSelector.background = self.background

Related

Plot text in 3d-plot that does not scale or move

Hello Pyqtgraph community,
I want to be able to create a "fixed" text window in a 3D interactive plot generated in PyQtGraph.
This text window will contain simulation-related information and should be visible at all times, regardless if you zoom in/out or pan to the left or right; and the location of the window should not change.
So far all the solutions I have found, create a text object that moves as the scaling of the axes changes. For example, the code below prints text on 3D axis, but once you zoom in/out the text moves all over the place. Any ideas would be greatly appreciated.
Thanks in advance
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl
from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem
class GLTextItem(GLGraphicsItem):
"""
Class for plotting text on a GLWidget
"""
def __init__(self, X=None, Y=None, Z=None, text=None):
GLGraphicsItem.__init__(self)
self.setGLOptions('translucent')
self.text = text
self.X = X
self.Y = Y
self.Z = Z
def setGLViewWidget(self, GLViewWidget):
self.GLViewWidget = GLViewWidget
def setText(self, text):
self.text = text
self.update()
def setX(self, X):
self.X = X
self.update()
def setY(self, Y):
self.Y = Y
self.update()
def setZ(self, Z):
self.Z = Z
self.update()
def paint(self):
self.GLViewWidget.qglColor(QtCore.Qt.white)
self.GLViewWidget.renderText(self.X, self.Y, self.Z, self.text)
if __name__ == '__main__':
# Create app
app = QtGui.QApplication([])
w1 = gl.GLViewWidget()
w1.resize(800, 800)
w1.show()
w1.setWindowTitle('Earth 3D')
gl_txt = GLTextItem(10, 10, 10, 'Sample test')
gl_txt.setGLViewWidget(w1)
w1.addItem(gl_txt)
while w1.isVisible():
app.processEvents()
So I was finally able to find a solution. What needs to be done is the following:
Subclass the GLViewWidget
From the derived class, overload the paintGL() so that it uses the member function renderText() to render text on the screen every time the paingGL() is called.
renderText() is overloaded to support both absolute screen coordinates, as well as axis-based coordinates:
i) renderText(int x, int y, const QString &str, const QFont &font = QFont()): plot based on (x, y) window coordinates
ii) renderText(double x, double y, double z, const QString &str, const QFont &font = QFont()): plot on (x, y, z) scene coordinates
You might want to use the QtGui.QFontMetrics() class to get the dimensions of the rendered text so you can place it in a location that makes sense for your application, as indicated in the code below.
from pyqtgraph.opengl import GLViewWidget
import pyqtgraph.opengl as gl
from PyQt5.QtGui import QColor
from pyqtgraph.Qt import QtCore, QtGui
class GLView(GLViewWidget):
"""
I have implemented my own GLViewWidget
"""
def __init__(self, parent=None):
super().__init__(parent)
def paintGL(self, *args, **kwds):
# Call parent's paintGL()
GLViewWidget.paintGL(self, *args, **kwds)
# select font
font = QtGui.QFont()
font.setFamily("Tahoma")
font.setPixelSize(21)
font.setBold(True)
title_str = 'Screen Coordinates'
metrics = QtGui.QFontMetrics(font)
m = metrics.boundingRect(title_str)
width = m.width()
height = m.height()
# Get window dimensions to center text
scrn_sz_width = self.size().width()
scrn_sz_height = self.size().height()
# Render text with screen based coordinates
self.qglColor(QColor(255,255,0,255))
self.renderText((scrn_sz_width-width)/2, height+5, title_str, font)
# Render text using Axis-based coordinates
self.qglColor(QColor(255, 0, 0, 255))
self.renderText(0, 0, 0, 'Axis-Based Coordinates')
if __name__ == '__main__':
# Create app
app = QtGui.QApplication([])
w = GLView()
w.resize(800, 800)
w.show()
w.setWindowTitle('Earth 3D')
w.setCameraPosition(distance=20)
g = gl.GLGridItem()
w.addItem(g)
while w.isVisible():
app.processEvents()

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

How to work with a isometric / orthogonal view in Kivy?

The image below is a GridLayout 10 x 10 with buttons.
I'd like to create the same Grid but in an isometric / orthogonal 2d view.
It means that every button, instead of being a square, it might be like a Rhombus, as the image below:
How can I do this?
I don't think you can actually do a 3D rotation on kivy UIX widgets, but you can do 2D rotations, and scaling. Here is an example of an App that does it in the build() method:
from kivy.app import App
from kivy.graphics.context_instructions import PushMatrix, Rotate, Scale, PopMatrix
from kivy.properties import BooleanProperty
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
import numpy as np
def matrixToNumpy(mat):
a = []
for i in range(4):
b = []
for j in range(4):
b.append(mat[i*4+j])
a.append(b)
npmat = np.mat(a)
return npmat
class MyButton(Button):
def on_touch_down(self, touch):
if not self.parent.touched:
self.parent.touched = True
if self.parent.mat is None:
scale = matrixToNumpy(self.parent.sca.matrix)
rotate = matrixToNumpy(self.parent.rot.matrix)
self.parent.mat = np.matmul(rotate, scale)
self.parent.inv_mat = self.parent.mat.I
npTouch = np.mat([touch.x, touch.y, 0, 1.0])
convTouch = np.matmul(npTouch, self.parent.inv_mat)
touch.x = convTouch[0,0]
touch.y = convTouch[0,1]
return super(MyButton, self).on_touch_down(touch)
def on_touch_up(self, touch):
self.parent.touched = False
return super(MyButton, self).on_touch_up(touch)
class MyGridLayout(GridLayout):
touched = BooleanProperty(False)
def __init__(self, **kwargs):
super(MyGridLayout, self).__init__(**kwargs)
self.mat = None
self.inv_mat = None
class MyApp(App):
def build(self):
layout = MyGridLayout(cols=10)
with layout.canvas.before:
PushMatrix()
layout.sca = Scale(1.0, 0.5, 1.0)
layout.rot = Rotate(angle=45, axis=(0,0,1), origin=(400,300,0))
with layout.canvas.after:
PopMatrix()
for i in range (1, 101):
layout.add_widget(MyButton(text=str(i)))
return layout
MyApp().run()
I suspect that with clever application of those two kivy.graphics.context_instructions, you can simulate what you want. The PushMatrix() and PopMatrix() confine the effects of the Scale and Rotate to just the MyGridLayout. You should be able to adjust these values using the layout.sca and layout.rot references.
I noticed after my original answer that the Buttons looked good, but no longer worked. I added a bunch of code to address that issue. All the numpy matrix stuff is just to get the mouse press position into the same coordinates as the MyGridLayout. Unfortunately, applying Canvas scaling and rotations isn't automatically taken into account by the Kivy events, so the additional code is necessary.
Here is what it looks like:

Set dimensions of an SVG image in fullscreen using QSvgWidget in PyQT?

I am lost with my PyQT programm. I want to display SVG image on full screen, but I need to be able to set the scale and position of the image and rest of the screen fill with black colour.
I use QSvgWidget and regular QWidget on top of each other, but It is not a good solution, because it runs as two processes and two separate windows.
Could you tell me how to make this with only one widget
import sys, os
from PyQt4 import QtGui, QtSvg
class Display(QtSvg.QSvgWidget):
def __init__(self, parent=None):
super(Display, self).__init__(parent)
def main():
app = QtGui.QApplication(sys.argv)
black = QtGui.QWidget() #setting background with widget
black.showFullScreen()
form = Display()
form.setWindowTitle("Display SVG Layer")
form.showFullScreen()
form.setStyleSheet("background-color:black;")
form.load("E:\example.svg")
form.move(100,0)
form.resize(1900,1000)
app.exec_()
if __name__ == '__main__':
main()
perhaps You can use QGraphicsView and QGraphicsScene:
class MyGraphicsView(QGraphicsView):
def __init__(self, w, h, parent=None):
QGraphicsView.__init__(self, parent)
self.setGeometry(0, 0, w, h) # screen size
class MyGraphicsScene(QGraphicsScene):
def __init__(self, w, h, parent = None):
QGraphicsScene.__init__(self,parent)
self.setSceneRect(0, 0, w, h) # screen size
self.backgroundPen = QPen(QColor(Qt.black))
self.backgroundBrush = QBrush(QColor(Qt.black))
self.textPen = QPen(QColor(Qt.lightGray))
self.textPen.setWidth(1)
self.textBrush = QBrush(QColor(Qt.lightGray))
self.textFont = QFont("Helvetica", 14, )
# paint the background
self.addRect(0,0,self.width(), self.height(), self.backgroundPen, self.backgroundBrush)
# paint the svg-title
self.svgTitle = self.addSimpleText('Display SVG Layer', self.textFont)
self.svgTitle.setPen(self.textPen)
self.svgTitle.setBrush(self.textBrush)
self.svgTitle.setPos(200,75)
# paint the svg
self.svgItem = QGraphicsSvgItem('./example.svg')
'''
edit:
if necessary, get the size of the svgItem to calculate
scale factor and position
'''
self.svgSize = self.svgItem.renderer().defaultSize()
self.svgItem.setScale(0.25) # scale the svg to an appropriate size
self.addItem(self.svgItem)
self.svgItem.setPos(200, 125)
if __name__ == '__main__':
app = QApplication(sys.argv)
screen_size = app.primaryScreen().size()
width = screen_size.width()
height = screen_size.height()
graphicsScene = MyGraphicsScene(width, height)
graphicsView = MyGraphicsView(width, height)
graphicsView.setScene(graphicsScene)
graphicsView.show()
app.exec_()

Resources