pyqt5

pyqt5

这两个礼拜做一个报文发送的后台守护程序,用到了pyqt5做用户界面。

1.简单窗口

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow


def main():
    # 生成QApplication主程序
    app = QApplication(sys.argv)

    # 生成窗口类实例
    main_window = QMainWindow()
    # 设置窗口标题
    main_window.setWindowTitle('测试程序')
    # 设置窗口大小
    main_window.resize(800, 600)
    # 显示窗口
    main_window.show()

    # 进入QApplication的事件循环
    sys.exit(app.exec_())
    pass


if __name__ == '__main__':
    main()

 

2.ui分离

这个就不写了,网上多的是。用 QtDesginer 生成 ui 文件,然后用 pyuic5 转换成 ui 类的 py 文件(vscode 插件 Qt for Python 也可以实现),在主程序中导入该文件并继承该 ui 类

3.信号与槽的参数

在定义信号的时候,就要确定信号带的参数类型与个数。 比如说:

sig_no_param = pyqtSignal()

sig_one_param = pyqtSignal(int)

sig_two_param = pyqtSingal(int, str)

与这些信号连接的槽函数也需要接受相应的参数。

但是有一种情况,信号的参数与槽函数的参数不一致怎么办?比如说信号是不带参数的,但是想将其连接到一个带参数的函数。要解决这个问题,可以考虑用lambda表达式。

Qwidget.event.connect(lambda: slot_func(args))

4.将日志输出到文本框

import logging
from PyQt5.QtGui import QColor


class QTextBrowserLoggingHandler(logging.Handler):
    """
    将日志输出到QTextBrowser控件的logging handler
    """

    def __init__(self, text_browser):
        """
        构造函数
        :param text_browser: 要显示日志信息的文本框实例
        :type text_browser: QTextEdit
        """
        super(QTextBrowserLoggingHandler, self).__init__()
        self.text_browser = text_browser
        self.default_text_color = text_browser.textColor()
        self.default_formatter = logging.Formatter('%(asctime)s %(levelname)5s '
                                                   '%(name)s.%(funcName)s[t%(thread)d] - %(message)s')
        self.setFormatter(self.default_formatter)
        pass

    def emit(self, record):
        # 不建议用 setTextColor() 再 append() 这种方法,
        # 因为它在 TextBrowser 窗口刷新时候似乎会有颜色冲突。
        # 会导致如果你选择某些已输出的文本,而此时文本又在持续更新,那么被选中的文本的颜色就可能错乱。
        # if 'ERROR' == record.levelname or 'CRITICAL' == record.levelname:
        #     self.text_browser.setTextColor(QColor('red'))
        # elif 'WARNING' == record.levelname:
        #     self.text_browser.setTextColor(QColor('magenta'))
        # elif 'INFO' == record.levelname:
        #     self.text_browser.setTextColor(self.default_text_color)
        # else:
        #     self.text_browser.setTextColor(QColor('darkGray'))
        # msg = self.format(record)
        # self.text_browser.append(msg)

        # 所以更稳定的方法应该是 直接追加 html 格式的文本
        msg = self.format(record)
        if 'ERROR' == record.levelname or 'CRITICAL' == record.levelname:
            self.text_browser.append('<span style="color: red">' + msg + '</span>')
        elif 'WARNING' == record.levelname:
            self.text_browser.append('<span style="color: magenta">' + msg + '</span>')
        elif 'INFO' == record.levelname:
            self.text_browser.append('<span style="color: black">' + msg + '</span>')
        else:
            self.text_browser.append('<span style="color: darkgray">' + msg + '</span>')
        pass

 

那么在窗口类就可以通过给logger添加该handler来输出到文本框ui。

# 1.新建一个logger对象
self.display_logger = logging.getLogger('TextBrowser_logger')

# 2.新建一个handler实例给logger(由于handler需要绑定到一个QTextEdit实例,所以需要在ui装载后调用该构造函数)
self.display_logger.addHandler(QTextBrowserLoggingHandler(self.textBrowser_logger))

# 3.设置该logger不将日志传递给父logger(默认是要传递给父logger的,这会导致父logger也输出日志)
self.display_logger.propagate = False

# 调用该logger写日志,将会显示到ui对象self.textBrowser_logger上。
self.display_logger.info('display log in ui')

 

5.ui是线程不安全的

不要在子线程试图改变ui,应该在子线程中发射信号,由主线程的槽函数捕获后修改ui状态。

比如说主线程有一个display_logger是绑定到QTextBrower控件的日志对象,将日志输出到该控件显示。

如果子线程也想输出日志到ui控件上,非多线程的做法是可以通过display_logger.getChild()来获得一个子logger输出日志。而多线程的情况下,这样做虽然也可以正常输出,但是在控制台会发现有QT的报错:

所以正确做法应该是在子线程定义信号:

# 定义信号
sig_display_log = pyqtSignal(int, str)

# 发射信号
self.sig_display_log.emit(logging.INFO, '打印info日志')
self.sig_display_log.emit(logging.ERROR, '打印error日志')

主线程将该信号连接到Logger的log(lvl, msg)函数:

self.thread_aaa.sig_display_log.connect(self.display_logger.log)

 

6.窗口最小化,由系统托盘退出

import sys
import logging
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QMainWindow, QSystemTrayIcon, QMenu, QAction, QMessageBox


class MyWindow(QMainWindow):
    """"""
    
    def __init__(self):
        """构造函数"""
        super(MyWindow, self).__init__()

        # 设置logger
        self.logger = logging.getLogger(self.__class__.__name__)
        self.logger.debug('Init a %s instance' % self.__class__.__name__)

        # 设置窗口标题
        self.setWindowTitle('测试程序')
        # 设置窗口大小
        self.resize(800, 600)

        # 设置窗口图标
        self.logo_icon = QIcon('./ui/logo_icon.png')
        self.setWindowIcon(self.logo_icon)

        # 设置系统托盘的右键菜单
        self.tray_menu = QMenu(self)
        action_quit = QAction('退出', self, triggered=self.slot_quit)
        action_show = QAction('显示', self, triggered=self.show)
        self.tray_menu.addAction(action_show)
        self.tray_menu.addSeparator()
        self.tray_menu.addAction(action_quit)
        # 设置系统托盘
        self.tray_icon = QSystemTrayIcon(self)
        self.tray_icon.setIcon(self.logo_icon)
        self.tray_icon.setContextMenu(self.tray_menu)
        # 设置系统托盘的点击事件
        self.tray_icon.activated.connect(self.slot_tray_icon_event)
        # 如果不主动show,系统托盘只会显示图标但不响应事件
        self.tray_icon.show()

        pass

    def slot_tray_icon_event(self, reason):
        """
        响应系统托盘的点击事件
        :param reason: 表示系统托盘的点击事件,1为右键点击,2为左键双击,3为左键单击,4为中键点击
        :type reason: enum QSystemTrayIcon::ActivationReason
        :return:
        """
        if reason == QSystemTrayIcon.Trigger or reason == QSystemTrayIcon.DoubleClick:
            self.logger.debug('左键点击系统托盘图标')
            self.show()
        elif reason == QSystemTrayIcon.Context:
            self.logger.debug('右键点击系统托盘图标')
        pass

    def slot_quit(self):
        reply = QMessageBox.question(self, '提示', '确认退出程序?', QMessageBox.Yes | QMessageBox.Cancel)
        if reply == QMessageBox.Yes:
            self.logger.debug('confirm to quit app')
            self.tray_icon.hide()
            QApplication.quit()
        else:
            self.logger.debug('cancel quit')
        pass

    def closeEvent(self, event):
        """重载closeEvent函数,关闭窗口后程序最小化到系统托盘"""
        self.logger.debug('close main window? no, just hide it')
        self.hide()
        event.ignore()
        pass


def main():
    # 生成QApplication主程序
    app = QApplication(sys.argv)
    # 设置即使所有窗口都关闭也不退出程序
    app.setQuitOnLastWindowClosed(False)

    # 生成窗口类实例
    main_window = MyWindow()
    # 显示窗口
    main_window.show()

    # 进入QApplication的事件循环
    sys.exit(app.exec_())
    pass


if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG, format="%(asctime)s [%(threadName)s] %(levelname)5s: %(message)s")
    main()

7.设置QTextEdit focus无边框

程序的用户界面中用到了QTextBrowser以及QLineEdit,每当控件聚焦的时候,都会出现蓝色的边框,特别扰人。查了网上说的各种办法,最多人说的是设置focus事件的qss。

QApplication.setStyleSheet('*:focus{border-width:1px; border-style:solid; border-color:#666666;}')

但这其实还有一点问题,因为我们不知道这些输入框在未聚焦时候的border-color默认值是多少,#66666只是我大概模拟的,细看还是有颜色区别的。而且这并没有阻止QT给聚焦输入框画蓝色边框的动作,只是被这个qss给覆盖了罢了。

实际上,QTextBrowser会有聚焦时候的蓝色边框,是因为它继承的QFrame类的默认frameShape=QFrame::StyledPanel。可以在qtdesigner上将其改为QFrame::Box。或者在代码中调用qtextbrowser.setFrameShape(QFrame.Box)即可,这样就不会在聚焦的时候给QTextEdit画上蓝色边框了。

而对于QLineEdit,它并没有继承QFrame类,但是它有一个frame属性,表示是否要给自己绘制边框,默认是打开的。可以通过QLineEdit.setFrame(False)将其关闭,但是这样该lineedit就没有边框了,只有一块白色,可以通过qss再给他设置border,这样它的border就固定了,不会因为聚焦而出现高亮的蓝色边框。

8. Single Instance QApplication

可以使用QT的共享内存来实现程序的单实例运行。当程序运行时候,先申请一段名字为key的共享内存,如果此时系统已经存在该名字的共享内存,说明已经有一个该程序的实例在运行,提示用户。

#!/usr/bin/env python

import sys
import logging
from PyQt5.QtCore import QSharedMemory, QSystemSemaphore
from PyQt5.QtWidgets import QApplication, QMessageBox

def check_app_instance_exist():
    # 使用全局变量关联共享内存,而不是局部变量
    # 局部变量会在函数结束后销毁并detach,如果此时共享内存的attached process为0,windows系统会自动销毁共享内存
    global shared_memory

    # 使用初始资源为1的信号量进行进程间互斥,
    # 防止多个进程同时执行后续的QSharedMemory.create()操作
    # 注意互斥QSemaphore的key不能与QSharedMemory的key同名
    semaphore = QSystemSemaphore('SendWorker_semaphore', initialValue=1)
    semaphore.acquire()
    logging.debug('获取到互斥信号量')

    # unix类系统的在程序崩溃的时候不会自动释放共享内存
    # 所以这里要多执行一次检查是否有遗留的共享内存并释放之。
    shared_memory = QSharedMemory('SendWorker_singleton')
    if shared_memory.attach():
        logging.debug('已存在共享内存段(可能是历史遗留)')
        shared_memory.detach()
        logging.debug('释放共享内存段')
    else:
        logging.debug('不存在共享内存段')

    if shared_memory.attach():
        logging.debug('共享内存段依然存在,说明已有同一程序的实例在运行')
        exist = True
    else:
        logging.debug('不存在共享内存段,创建之')
        shared_memory.create(1)
        exist = False

    # 释放互斥信号量
    semaphore.release()
    logging.debug('互斥信号量已释放')

    if shared_memory.size() == 0:
        logging.error('共享内存段异常!')

    return exist


if __name__ == '__main__':
    app = QApplication(sys.argv)
    if check_app_instance_exist():
        QMessageBox.warning(None, '提示', '该程序已经运行', QMessageBox.Ok)
        sys.exit(1)
    else:
        QMessageBox.information(None, '提示', '启动程序', QMessageBox.Ok)
        sys.exit(0)

9. 使用系统默认图标

logo_icon = QIcon('./ui/logo_icon.png')
if logo_icon.pixmap(11, 11).isNull():
    # 判断QIcon是否有图,不能用它自己的QIcon.isNull()
    self.logger.warning('无法载入logo_icon.png图标!')
    
    # 从当前程序的QStyle中获取系统标准图标
    logo_icon = QApplication.style().standardIcon(QStyle.SP_ArrowRight)

self.setWindowIcon(logo_icon)

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *

TOC