UPDATE:我已经更新了代码,可以显示桌面通知(见下文)。
实现像小部件一样的桌面感知烤面包机并非不可能,但会带来一些与平台相关的问题。另一方面,客户端更容易。
我创建了一个小类,它能够根据当前小部件的顶级窗口显示通知,并且可以设置消息文本、图标以及通知是否可由用户关闭。我还添加了一个漂亮的不透明动画,这在此类系统中很常见。
它的主要用途是基于静态方法,类似于 QMessageBox 的用途,但也可以通过添加其他功能以类似的方式实现。
UPDATE
我意识到制作桌面通知并不是that很难(但是跨平台开发需要一些小心,我将把它留给程序员)。
以下是updated允许使用的代码None
作为类的父级,使通知成为桌面小部件而不是现有 Qt 小部件的子小部件。如果您正在阅读本文并且对此类功能不感兴趣,只需检查原始(稍微简单的)代码的编辑历史记录即可。
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_())