这两个礼拜做一个报文发送的后台守护程序,用到了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)