systray

(You can also check this code in the repository)

import sys

from PySide6.QtWidgets import QApplication, QMessageBox, QSystemTrayIcon

from window import Window

if __name__ == "__main__":
    app = QApplication()

    if not QSystemTrayIcon.isSystemTrayAvailable():
        QMessageBox.critical(None, "Systray", "I couldn't detect any system tray on this system.")
        sys.exit(1)

    QApplication.setQuitOnLastWindowClosed(False)

    window = Window()
    window.show()
    sys.exit(app.exec())
from PySide6.QtCore import Slot
from PySide6.QtGui import QAction, QIcon
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDialog,
                               QGridLayout, QGroupBox, QHBoxLayout, QLabel,
                               QLineEdit, QMenu, QMessageBox, QPushButton,
                               QSpinBox, QStyle, QSystemTrayIcon, QTextEdit,
                               QVBoxLayout)

import rc_systray


class Window(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)

        self._icon_group_box = QGroupBox()
        self._icon_label = QLabel()
        self._icon_combo_box = QComboBox()
        self._show_icon_check_box = QCheckBox()

        self._message_group_box = QGroupBox()
        self._type_label = QLabel()
        self._duration_label = QLabel()
        self._duration_warning_label = QLabel()
        self._title_label = QLabel()
        self._body_label = QLabel()

        self._type_combo_box = QComboBox()
        self._duration_spin_box = QSpinBox()
        self._title_edit = QLineEdit()
        self._body_edit = QTextEdit()
        self._show_message_button = QPushButton()

        self._minimize_action = QAction()
        self._maximize_action = QAction()
        self._restore_action = QAction()
        self._quit_action = QAction()

        self._tray_icon = QSystemTrayIcon()
        self._tray_icon_menu = QMenu()

        self.create_icon_group_box()
        self.create_message_group_box()

        self._icon_label.setMinimumWidth(self._duration_label.sizeHint().width())

        self.create_actions()
        self.create_tray_icon()

        self._show_message_button.clicked.connect(self.show_message)
        self._show_icon_check_box.toggled.connect(self._tray_icon.setVisible)
        self._icon_combo_box.currentIndexChanged.connect(self.set_icon)
        self._tray_icon.messageClicked.connect(self.message_clicked)
        self._tray_icon.activated.connect(self.icon_activated)

        self._main_layout = QVBoxLayout()
        self._main_layout.addWidget(self._icon_group_box)
        self._main_layout.addWidget(self._message_group_box)
        self.setLayout(self._main_layout)

        self._icon_combo_box.setCurrentIndex(1)
        self._tray_icon.show()

        self.setWindowTitle("Systray")
        self.resize(400, 300)

    def setVisible(self, visible):
        self._minimize_action.setEnabled(visible)
        self._maximize_action.setEnabled(not self.isMaximized())
        self._restore_action.setEnabled(self.isMaximized() or not visible)
        super().setVisible(visible)

    def closeEvent(self, event):
        if not event.spontaneous() or not self.isVisible():
            return
        if self._tray_icon.isVisible():
            QMessageBox.information(self, "Systray",
                                    "The program will keep running in the system tray. "
                                    "To terminate the program, choose <b>Quit</b> in the context "
                                    "menu of the system tray entry.")
            self.hide()
            event.ignore()

    @Slot(int)
    def set_icon(self, index):
        icon = self._icon_combo_box.itemIcon(index)
        self._tray_icon.setIcon(icon)
        self.setWindowIcon(icon)
        self._tray_icon.setToolTip(self._icon_combo_box.itemText(index))

    @Slot(str)
    def icon_activated(self, reason):
        if reason == QSystemTrayIcon.Trigger:
            pass
        if reason == QSystemTrayIcon.DoubleClick:
            self._icon_combo_box.setCurrentIndex(
                (self._icon_combo_box.currentIndex() + 1) % self._icon_combo_box.count()
            )
        if reason == QSystemTrayIcon.MiddleClick:
            self.show_message()

    @Slot()
    def show_message(self):
        self._show_icon_check_box.setChecked(True)
        selected_icon = self._type_combo_box.itemData(self._type_combo_box.currentIndex())
        msg_icon = QSystemTrayIcon.MessageIcon(selected_icon)

        if selected_icon == -1:  # custom icon
            icon = QIcon(self._icon_combo_box.itemIcon(self._icon_combo_box.currentIndex()))
            self._tray_icon.showMessage(
                self._title_edit.text(),
                self._body_edit.toPlainText(),
                icon,
                self._duration_spin_box.value() * 1000,
            )
        else:
            self._tray_icon.showMessage(
                self._title_edit.text(),
                self._body_edit.toPlainText(),
                msg_icon,
                self._duration_spin_box.value() * 1000,
            )

    @Slot()
    def message_clicked(self):
        QMessageBox.information(None, "Systray",
                                "Sorry, I already gave what help I could.\n"
                                "Maybe you should try asking a human?")

    def create_icon_group_box(self):
        self._icon_group_box = QGroupBox("Tray Icon")

        self._icon_label = QLabel("Icon:")

        self._icon_combo_box = QComboBox()
        self._icon_combo_box.addItem(QIcon(":/images/bad.png"), "Bad")
        self._icon_combo_box.addItem(QIcon(":/images/heart.png"), "Heart")
        self._icon_combo_box.addItem(QIcon(":/images/trash.png"), "Trash")

        self._show_icon_check_box = QCheckBox("Show icon")
        self._show_icon_check_box.setChecked(True)

        icon_layout = QHBoxLayout()
        icon_layout.addWidget(self._icon_label)
        icon_layout.addWidget(self._icon_combo_box)
        icon_layout.addStretch()
        icon_layout.addWidget(self._show_icon_check_box)
        self._icon_group_box.setLayout(icon_layout)

    def create_message_group_box(self):
        self._message_group_box = QGroupBox("Balloon Message")

        self._type_label = QLabel("Type:")

        self._type_combo_box = QComboBox()
        self._type_combo_box.addItem("None", QSystemTrayIcon.NoIcon)
        self._type_combo_box.addItem(
            self.style().standardIcon(QStyle.SP_MessageBoxInformation),
            "Information",
            QSystemTrayIcon.Information,
        )
        self._type_combo_box.addItem(
            self.style().standardIcon(QStyle.SP_MessageBoxWarning),
            "Warning",
            QSystemTrayIcon.Warning,
        )
        self._type_combo_box.addItem(
            self.style().standardIcon(QStyle.SP_MessageBoxCritical),
            "Critical",
            QSystemTrayIcon.Critical,
        )
        self._type_combo_box.addItem(QIcon(), "Custom icon", -1)
        self._type_combo_box.setCurrentIndex(1)

        self._duration_label = QLabel("Duration:")

        self._duration_spin_box = QSpinBox()
        self._duration_spin_box.setRange(5, 60)
        self._duration_spin_box.setSuffix(" s")
        self._duration_spin_box.setValue(15)

        self._duration_warning_label = QLabel("(some systems might ignore this hint)")
        self._duration_warning_label.setIndent(10)

        self._title_label = QLabel("Title:")
        self._title_edit = QLineEdit("Cannot connect to network")
        self._body_label = QLabel("Body:")

        self._body_edit = QTextEdit()
        self._body_edit.setPlainText("Don't believe me. Honestly, I don't have a clue."
                                   "\nClick this balloon for details.")

        self._show_message_button = QPushButton("Show Message")
        self._show_message_button.setDefault(True)

        message_layout = QGridLayout()
        message_layout.addWidget(self._type_label, 0, 0)
        message_layout.addWidget(self._type_combo_box, 0, 1, 1, 2)
        message_layout.addWidget(self._duration_label, 1, 0)
        message_layout.addWidget(self._duration_spin_box, 1, 1)
        message_layout.addWidget(self._duration_warning_label, 1, 2, 1, 3)
        message_layout.addWidget(self._title_label, 2, 0)
        message_layout.addWidget(self._title_edit, 2, 1, 1, 4)
        message_layout.addWidget(self._body_label, 3, 0)
        message_layout.addWidget(self._body_edit, 3, 1, 2, 4)
        message_layout.addWidget(self._show_message_button, 5, 4)
        message_layout.setColumnStretch(3, 1)
        message_layout.setRowStretch(4, 1)
        self._message_group_box.setLayout(message_layout)

    def create_actions(self):
        self._minimize_action = QAction("Minimize", self)
        self._minimize_action.triggered.connect(self.hide)

        self._maximize_action = QAction("Maximize", self)
        self._maximize_action.triggered.connect(self.showMaximized)

        self._restore_action = QAction("Restore", self)
        self._restore_action.triggered.connect(self.showNormal)

        self._quit_action = QAction("Quit", self)
        self._quit_action.triggered.connect(qApp.quit)

    def create_tray_icon(self):
        self._tray_icon_menu = QMenu(self)
        self._tray_icon_menu.addAction(self._minimize_action)
        self._tray_icon_menu.addAction(self._maximize_action)
        self._tray_icon_menu.addAction(self._restore_action)
        self._tray_icon_menu.addSeparator()
        self._tray_icon_menu.addAction(self._quit_action)

        self._tray_icon = QSystemTrayIcon(self)
        self._tray_icon.setContextMenu(self._tray_icon_menu)
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/">
   <file>images/bad.png</file>
   <file>images/heart.png</file>
   <file>images/trash.png</file>
</qresource>
</RCC>