【python+pyqt5】B站直播弹幕姬

文章目录

  • 前言
  • 1.日志对象
  • 2.获取弹幕
  • 3.qt窗口
    • 窗口间传递信号
    • 主窗口
    • 设置窗口
    • 弹幕展示窗口
    • 托盘
  • 4.主函数
  • 5.最终成果及使用方法
  • 6.开源地址

前言

这个软件是基于我半年多前写的一个小小小软件(https://www.bilibili.com/video/BV1zN411Q7u4)的一个大更新,不过其实相当于重新写了一个就是了www
完整代码我已经开源到gitee和github上了,并且软件的使用方法已经发到b站,链接在文末,欢迎大家一起学习讨论。

1.日志对象

软件有日志都很常见了,既可以了解到现在程序运行在哪,也可以很方便地定位到出错代码。
写在所有代码之前作为全局变量也是为了各个代码模块方便调用。

'''
日志对象 
'''
if not os.path.exists('log'):os.mkdir('log')def creatLogger():logger = logging.getLogger('mylogger')logger.setLevel(logging.DEBUG)# 全部日志处理器rf_handler = logging.handlers.TimedRotatingFileHandler('log/all.log', when='midnight', interval=1, backupCount=7,atTime=datetime.time(0, 0, 0, 0))rf_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))# error日志处理器f_handler = logging.FileHandler('log/error.log')f_handler.setLevel(logging.ERROR)f_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(filename)s[:%(lineno)d] - %(message)s"))# 加入loggerlogger.addHandler(rf_handler)logger.addHandler(f_handler)# 返回设置好的logger对象return logger# 实例化
logger = creatLogger()

2.获取弹幕

获取b站直播弹幕用到了websocket协议,使用到了aiowebsocket这个类。详情可以看我上一篇文章(AioWebSocket实现python异步接收B站直播弹幕),已经讲得挺详细的了(大概),将文章中的代码封装成一个类就可以拿来用了,这里就只列出函数定义头了。

class BiliSocket():def __init__(self):async def startup(self,roomid):def getRealRoomid(self,url):async def check2close(self):def close(self):async def sendHeartBeat(self, websocket):async def receDM(self, websocket):def printDM(self, data):def DANMU_handle(self,data):

所不同的是,我新加入了几个函数。

  • 有些直播间地址栏的房间号是3位数及以下的,这些不是真实房间号,是获取不了弹幕的。所以getRealRoomid()用来获取真实房间号。
  • DANMU_handle()这个方法是对弹幕的处理,包括对用户输入的关键词进行筛选,及筛选后将弹幕传到展示窗口。
  • 软件运行时,主线程要给窗口刷新,不能用来接收弹幕,所以接收弹幕只能放在子线程。而子线程怎么关闭呢?我上网找过很多方法:一开始在aiowebsocket这个类里找到一个close_connection的方法,但这个方法好像不能帮我结束线程;后来用loop.stop()结束了循环并且用join()对子线程进行阻塞,等待子线程退出,这样其实也是可行的,但有时候又不行,反而会因为阻塞时间太长导致主窗口主线程无响应。后来我找到可以通过抛出异常来使线程退出,于是我写了如下代码:
async def check2close(self):'''循环判断关闭标志位closeFlag是否为真若为真则抛出异常来结束该子线程一定要设置休眠否则占据资源导致卡死'''while True:await asyncio.sleep(0.2)if self.__closeFlag == True:raise KeyboardInterruptdef close(self):'''通过设置标志位来结束线程'''self.__closeFlag = True

定义一个标志位__closeFlag,子线程通过扫描它来判断自己是否该退出,若是则抛出异常来退出该线程。完美解决!

3.qt窗口

前端方面用到的是pyqt5。之前我也用过tkinter(python自带的一个gui库)来实现过类似的应用,那是我半年前写的。但我发现tkinter功能太少,界面太简陋,不好操作,所以换成了比较多人用的gui,也就是pyqt5。但qt资料大多是c++的,转换到python不是一件容易事,对于我这种新手来说真的很不容易(所以为什么要用python来写啊哈哈)。
首先安装

pip install PyQt5
pip install PyQt5-tools

导入pyqt5模块(这是懒人一键全部导入,也可以一个一个模块导入)
其他模块就不赘述了,用到再导入就好。

from PyQt5 import QtCore
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.uic import loadUi

在pyqt5安装目录下可以找到qt designer,这个图形化软件可以帮助我们设计出想要的窗口。
在这里插入图片描述

窗口间传递信号

有时候,多个窗口之间要互相传递信息,这时候可以利用qt的信号和槽来实现。
举个例子

# 定义一种信号,两个参数 类型分别是: 整数 和 字符串
# 调用 emit方法 发信号时,传入参数 必须是这里指定的 参数类型
text_print = QtCore.pyqtSignal(int, str)

发送信号

text_print.emit(1,'abc')

接收信号

def printFun(num,string):print(num,string)text_print.connect(printFun)

而我很多时候需要传递不同种参数,需要多个信号变量,所以直接定义一个信号类并实例化,作为全局变量。这样就可以让不同的类之间相互交流了,不是继承自QObject的类也可以用。

'''
自定义信号源对象类型,一定要继承自 QObject
'''
class MySignals(QObject):# 定义一种信号,两个参数 类型分别是: 整数 和 字符串# 调用 emit方法 发信号时,传入参数 必须是这里指定的 参数类型text_print = QtCore.pyqtSignal(int, str)# 还可以定义其他种类的信号my_Signal = QtCore.pyqtSignal(str)new_comment = QtCore.pyqtSignal(bool,str)otherChange = QtCore.pyqtSignal(str,str)sizeChange = QtCore.pyqtSignal(str,int)fontChange = QtCore.pyqtSignal(QFont)# 实例化
global_ms = MySignals()

主窗口

主窗口比较简单,简单写写就行。

class MainWindow(QMainWindow):def __init__(self, parent=None):super(MainWindow, self).__init__(parent)loadUi('ui/MainWindow.ui', self)# 标题、图标self.setWindowTitle('主窗口')self.setWindowIcon(QIcon(iconPath))# 禁止最大化按钮self.setWindowFlags(Qt.WindowMinimizeButtonHint|Qt.WindowCloseButtonHint)# ui按钮链接self.Ui_init()def Ui_init(self):self.openSettingWinButton.triggered.connect(lambda: global_ms.my_Signal.emit('setting'))self.openAboutWinButton.triggered.connect(lambda: global_ms.my_Signal.emit('about'))self.startButton.clicked.connect(lambda: global_ms.my_Signal.emit('start'))self.closeButton.clicked.connect(lambda: global_ms.my_Signal.emit('close'))

设置窗口

设置窗口主要是对展示窗口进行一些参数的设置,比如文字大小、颜色、字体等。重点和难点应该是参数发生变化后如何与展示窗口进行交流并及时调整。

class ConfigWindow(QWidget):def __init__(self, parent=None):super(ConfigWindow, self).__init__(parent)loadUi('ui/ConfigWindow.ui', self)# 标题、图标self.setWindowTitle('设置')self.setWindowIcon(QIcon(iconPath))

这里我采用的是(QLineEdit+QPushButton )和QSpinBox 实现参数的改变。将QLineEdit设置为只读,当用户点击按钮修改参数时,程序会修改QLineEdit的内容;QSpinBox 则是直接改变内容的。当QLineEdit或QSpinBox的值发生变化时,发出信号并传递需要修改的参数,展示信号接收后作出相应操作。
下面的是窗口初始化部分代码,将QLineEdit或QSpinBox的值发生变化时与函数绑定,将参数发射出去。(接收部分详见展示窗口)

# 发送者id颜色
self.userColorView.setText(initusercolor)
self.userColorButton.clicked.connect(self.getUserColor)
self.userColorView.textChanged.connect(self.UserColorChange
# 字体大小
self.charactersSize.setValue(initsize)
self.charactersSize.valueChanged.connect(self.getSize)
def getUserColor(self):c = QColorDialog.getColor()colorName = c.name()# getColor对话框点击取消时会返回缺省值#000000# 为避免这种情况直接ban掉#000000,未找到更好方案if colorName != '#000000':# print(colorName)self.userColorView.setText(colorName)else:msg_box = QMessageBox(QMessageBox.Warning, '提示', '#000000禁止设置,黑色请尝试#000001等')msg_box.exec_()def UserColorChange(self,colorName):self.usercolor = colorNameglobal_ms.otherChange.emit('userColor',colorName)def getSize(self,size):self.size = sizeglobal_ms.sizeChange.emit('charactersSize',self.size)

设置窗口还有一个功能,就是将参数导入、导出,不用每次都手动设置。因为我不熟悉配置文件的操作,不知道我这种存储方式用那种文件比较合适,所以用了我最熟悉的excel表。如果各位有什么好推荐的话欢迎向我提出。
一开始我是打算打开一个文件浏览框让用户选择路径和文件名,但发现这样不好操作,因为做不到将用户设置的路径保存下来,下次自动导入(不可能为了它新建一个文件来保存吧,这样也不保险)。最保险的方法就是自己在代码里设定好路径,每次到这个路径下去找就行了。

'''
配置文件路径
'''
iconPath = os.getcwd() + r'\config\icon.jpg'
settingPath = os.getcwd() + r'\config\BiliComment.xlsx'
def GetOpenXlsxPath(self):logger.info('***尝试导入')if os.path.exists(settingPath):self.ImportXlsx()else:logger.warning('***文件不存在')msg_box = QMessageBox(QMessageBox.Warning, '提示', f'{settingPath}不存在!请先导出配置')msg_box.exec_()def GetSaveXlsxPath(self):logger.info('***尝试导出')self.SaveXlsx()def ImportXlsx(self):# 从文件中读取参数try:self.listWidget1.clear()self.listWidget2.clear()self.listWidget3.clear()self.listWidget4.clear()# 只读模式下读取速度更快,但没有columns这个属性wb = openpyxl.load_workbook(settingPath)ws = wb.activefor column in ws.columns:if column[0].value == 'white':self.ImportAdd(column,self.listWidget1,whiteUser)elif column[0].value == 'black':self.ImportAdd(column,self.listWidget2,blackUser)elif column[0].value == 'kw':self.ImportAdd(column,self.listWidget3,keywords)elif column[0].value == 'filterKW':self.ImportAdd(column,self.listWidget4,filterKWs)elif column[0].value == 'size':self.charactersSize.setValue(column[1].value)self.LineNumber.setValue(column[2].value)self.LineHeight.setValue(column[3].value)elif column[0].value == 'color':self.userColorView.setText(column[1].value)self.comColorView.setText(column[2].value)elif column[0].value == 'font':self.fontComboBox.setCurrentFont(QFont(column[1].value))elif column[0].value == 'background':filePath = column[1].valueif filePath != None and os.path.exists(filePath):self.backgroundView.setText(filePath)else:logger.warning('图片修改失败。原因:图片不存在')msg_box = QMessageBox(QMessageBox.Warning, '提示', '图片修改失败。原因:图片不存在')msg_box.exec_()else:continuelogger.info('***导入成功')msg_box = QMessageBox(QMessageBox.Warning, '提示', f'导入成功: {settingPath}')msg_box.exec_()except Exception as e:logger.error(u'***导入时出现异常:{}'.format(e))msg_box = QMessageBox(QMessageBox.Warning, '警告', '导入时出现异常')msg_box.exec_()def ImportAdd(self,column,listWidget,List):for cell in column[1:]:value = str(cell.value)if value != 'None':listWidget.addItem(value)List.append(value)

弹幕展示窗口

展示窗口是最复杂的,花了我很长时间找资料和解决办法。

class DisplayWindow(QDockWidget):def __init__(self, parent=None):super(DisplayWindow, self).__init__(parent)loadUi('ui/DisplayWindow.ui', self)self.setWindowFlags(Qt.WindowStaysOnTopHint|Qt.FramelessWindowHint|Qt.Tool)  # 置顶、无边框、隐藏任务栏self.setAttribute(Qt.WA_TranslucentBackground)    # 窗体背景透明self.setMouseTracking(True)  # 设置widget鼠标跟踪
  1. 【窗口中间层】首先是文字显示。
    为了无边框后可以移动窗口(移动窗口的方法一会儿讲到),我一开始用的是QLabel来设置文字,因为我发现鼠标在QLabel上是不会变成其他样式,而其他输入控件上鼠标会变成输入样式,导致无法移动。但QLabel有个致命缺点就是无法换行,导致我要添加多个QLabel来实现多行文本。而且当设置文字样式(比如文字描边后)也会出现无法移动窗口的情况。不仅占用资源多还容易出错。
    这时候我想到,既然鼠标在QLabel上不受影响,那如果在顶层铺一层QLabel,不设置文字(相当于透明),下面的控件会不会受影响呢?事实证明,覆盖下面控件后,鼠标就不受下面控件影响了,终于可以愉快的移动窗口了!
    于是下面的文字显示控件换成了QPlainTextEdit。这个控件既可以方便的换行、设置最大行数,又可以设置不同部分的文字颜色。这样,弹幕发送者与弹幕内容的文字颜色分开,就可以更直观了!
# 中间层初始化
self.comWidget = QWidget()
self.gridLayout.addWidget(self.comWidget, 0, 26, 11, 11)
self.comVerticalLayout = QVBoxLayout(self.comWidget)
self.comVerticalLayout.setObjectName("comVerticalLayout")
# 添加textEdit用于显示弹幕,并初始化
self.textEdit = QPlainTextEdit(self.comWidget)
self.textEdit.setReadOnly(True)
self.textEdit.setUndoRedoEnabled(False)
self.textEdit.setMaximumBlockCount(initlinenum)
font = QFont(initfont)
font.setWordSpacing(20)
self.textEdit.setFont(font)
self.textEdit.setStyleSheet(f"font-size:{initsize}px;border: none; background-color: transparent; font-weight: bold;")
self.comVerticalLayout.addWidget(self.textEdit)
# 指针
self.FontFormat = QTextCharFormat()
self.BlockFormat = QTextBlockFormat()
self.tc = self.textEdit.textCursor()

将弹幕加入QPlainTextEdit时我发现只能在主线程中操作,如果在子线程中会有显示延迟的情况。不过这也没有什么办法

def addComment(self,bool,msg):'''这里必须在主线程里添加,否则显示会延迟,但消息多时可能会导致窗口无响应。装在列表里for循坏也不行,目前还没找到更好的方法'''if bool:# 设置发送者id样式self.FontFormat.setForeground(self.Userfont)self.textEdit.mergeCurrentCharFormat(self.FontFormat)self.textEdit.appendPlainText("".join(msg.split(': ')[0]) + ": ")# 设置弹幕内容样式self.FontFormat.setForeground(self.Comfont)self.textEdit.mergeCurrentCharFormat(self.FontFormat)self.tc.movePosition(QTextCursor.End)self.textEdit.insertPlainText("".join(msg.split(': ')[1]))# 刷新,可有可无QApplication.processEvents()
  1. 【窗口顶层】然后是置顶、无边框后的移动处理和改变大小处理。
    窗口移动和改变大小可以合在一起说,因为他们都是利用了鼠标操作函数mousePressEventmouseMoveEventmouseReleaseEvent。只要我们重写这三个函数,就可以实现这些功能了。
    窗口移动很简单,只需要在发生鼠标点击事件mousePressEvent后跟踪坐标并计算移动后坐标值,将窗口移动至此坐标就行。
    但改变大小有点麻烦,我需要划分一定的区域,在这部分区域内才允许改变窗口大小,并且改变鼠标样式。所以本来直接在顶层铺一个label,我改成了在顶层铺一个QWidget,并在QWidget里面添加了3个QLabel,分别在如下位置:
    在这里插入图片描述
    然后,设置当鼠标在这3个QLabel上面时改变鼠标样式,提醒用户这些位置可以改变窗口大小。效果如下图:
    在这里插入图片描述
    这部分代码如下:
def _initDrag(self):    # 初始化部分# 设置鼠标跟踪判断扳机默认值self._move_drag = Falseself._corner_drag = Falseself._bottom_drag = Falseself._right_drag = False# 判断鼠标位置切换鼠标手势self.cornerLabel.setCursor(Qt.SizeFDiagCursor)self.bottomLabel.setCursor(Qt.SizeVerCursor)self.rightLabel.setCursor(Qt.SizeHorCursor)def resizeEvent(self, QResizeEvent):# 自定义窗口调整大小事件# 改变窗口大小的三个坐标范围ran = 30self._right_rect = [QPoint(x, y) for x in range(self.width() - ran, self.width() )for y in range(1, self.height()-ran)]self._bottom_rect = [QPoint(x, y) for x in range(1, self.width() - ran)for y in range(self.height() - ran, self.height())]self._corner_rect = [QPoint(x, y) for x in range(self.width() - ran, self.width() )for y in range(self.height() - ran, self.height() )]def mousePressEvent(self, event):# 重写鼠标点击的事件if (event.button() == Qt.LeftButton) and (event.pos() in self._corner_rect):# 鼠标左键点击右下角边界区域self._corner_drag = Trueevent.accept()elif (event.button() == Qt.LeftButton) and (event.pos() in self._right_rect):# 鼠标左键点击右侧边界区域self._right_drag = Trueevent.accept()elif (event.button() == Qt.LeftButton) and (event.pos() in self._bottom_rect):# 鼠标左键点击下侧边界区域self._bottom_drag = Trueevent.accept()elif (event.button() == Qt.LeftButton) and (event.y() < self.height()-30):# 鼠标左键点击其他位置self._move_drag = Trueself.move_DragPosition = event.globalPos() - self.pos()event.accept()def mouseMoveEvent(self, QMouseEvent):# 当鼠标左键点击不放及满足点击区域的要求后,分别实现不同的窗口调整# 没有定义左方和上方相关的5个方向,主要是因为实现起来不难,但是效果很差,拖放的时候窗口闪烁,再研究研究是否有更好的实现if Qt.LeftButton and self._right_drag:# 右侧调整窗口宽度self.resize(QMouseEvent.pos().x(), self.height())QMouseEvent.accept()elif Qt.LeftButton and self._bottom_drag:# 下侧调整窗口高度self.resize(self.width(), QMouseEvent.pos().y())QMouseEvent.accept()elif Qt.LeftButton and self._corner_drag:# 右下角同时调整高度和宽度self.resize(QMouseEvent.pos().x(), QMouseEvent.pos().y())QMouseEvent.accept()elif Qt.LeftButton and self._move_drag:# 其他位置拖放窗口位置self.move(QMouseEvent.globalPos() - self.move_DragPosition)QMouseEvent.accept()def mouseReleaseEvent(self, QMouseEvent):# 鼠标释放后,各扳机复位self._move_drag = Falseself._corner_drag = Falseself._bottom_drag = Falseself._right_drag = False
  1. 【窗口底层】然后是无边框后的背景处理。
    因为我想要实现改变窗口透明度的功能,但控件透明度不变(不然文字也看不清了),所以找了很多代码,例如下面的。
MainWindow.setWindowOpacity(0.85)  # 设置窗口透明度
MainWindow.setAttribute(QtCore.Qt.WA_TranslucentBackground)  # 设置窗口背景透明

但这些都只能改变整个窗口包括控件透明度。但我需要的是仅仅只是背景的改变。于是我想到,在文字控件的下面再铺一层QLabel,让这个QLabel显示图片,并且自己在ps里制作几张不同透明度的图片,分别让他显示出来:
在这里插入图片描述
搞定!窗口初始化代码如下:

# 底层label,设置背景图片
self.backLabel = QLabel()
self.backLabel.setObjectName("backLabel")
pix = QPixmap(initbackground)
self.backLabel.setPixmap(pix)
self.backLabel.setScaledContents(True)
self.gridLayout.addWidget(self.backLabel, 0, 26, 11, 11)

注意一定要铺在上层文字控件的同一位置(0, 26, 11, 11),否则这个Label会被挤到一边去。也正是这个原因我在qt designer里弄不到理想的效果,所以只能自己在代码里实现了。
后续要改变透明度的话,只需要将图片路径传进QPixmap再调用setPixmap就可以了。除了白底图片,其他任意图片也是可以的。
4. 【信号连接】
初始化

def SignalConnect_init(self):global_ms.new_comment.connect(self.addComment)global_ms.sizeChange.connect(self.modifySize)global_ms.fontChange.connect(self.modifyFont)global_ms.otherChange.connect(self.modifyOther)

以下代码以modifySize()为例:

def modifySize(self,type,value):'''多线程减轻主线程压力,防止窗口无响应'''def aa():try:if type == 'charactersSize':self.textEdit.setStyleSheet(f"font-size:{value}px;font-weight:bold;border: none; background-color: transparent;")logger.info('修改 文字大小 成功')elif type == 'lineNum':self.textEdit.setMaximumBlockCount(value)logger.info('修改 行数 成功')elif type == 'lineHeight':self.BlockFormat.setLineHeight(value, QTextBlockFormat.FixedHeight)self.tc.setBlockFormat(self.BlockFormat)self.textEdit.setTextCursor(self.tc)logger.info('修改 行高 成功')except Exception as e:logger.error(u'修改参数失败:{}'.format(e))threading.Thread(target=aa).start()

添加弹幕因为显示问题不得不在主线程中进行,但修改参数没有这些问题,所以在子线程中修改就行,可以减少主线程压力,防止窗口无响应。

托盘

托盘这里我采用的方法是继承QObject类。因为像MainWindow他有一个菜单栏QMenu,那我只要模仿他,创建一个QMenu,再将动作QAction加进去,就变成了一个托盘菜单选项了。

class TUOPAN(QObject):def __init__(self):super(TUOPAN, self).__init__()self.Ui_init()def Ui_init(self):# -------------------- 托盘开始 ----------------# 在系统托盘处显示图标self.tp = QSystemTrayIcon(self)self.tp.setIcon(QIcon('./config/icon.jpg'))# 设置系统托盘图标的菜单self.a1 = QAction('&主窗口', triggered=lambda:global_ms.my_Signal.emit('MainWindow'))self.a2 = QAction('&开始/更新', triggered=lambda:global_ms.my_Signal.emit('start'))self.a3 = QAction('&关闭', triggered=lambda:global_ms.my_Signal.emit('close'))self.a4 = QAction('&设置', triggered=lambda:global_ms.my_Signal.emit('setting'))self.a5 = QAction('&退出', triggered=lambda:global_ms.my_Signal.emit('exit'))  # 直接退出可以用qApp.quitself.tpMenu = QMenu()self.tpMenu.addAction(self.a1)self.tpMenu.addAction(self.a2)self.tpMenu.addAction(self.a3)self.tpMenu.addAction(self.a4)self.tpMenu.addAction(self.a5)self.tp.setContextMenu(self.tpMenu)# 点击活动连接到函数处理self.tp.activated.connect(self.act)# 不调用show不会显示系统托盘self.tp.show()# -------------------- 托盘结束 ------------------def act(self, reason):# 鼠标点击icon传递的信号会带有一个整形的值,1是表示单击右键,2是双击,3是单击左键,4是用鼠标中键点击if reason == 2:global_ms.my_Signal.emit('MainWindow')

4.主函数

上面已经完成了各个功能模块的类的代码,接下来只需要把他们联系起来

class BiliDanmuji():def __init__(self):# 主要对象self.app = QApplication(sys.argv)self.app.setQuitOnLastWindowClosed(False)  # 最小化托盘用,关闭所有窗口也不结束程序self.scan() # 扫描文件夹,若不存在报错self.w = MainWindow()   # 主窗口self.settingWindow = ConfigWindow() # 设置窗口self.display = DisplayWindow()  # 弹幕展示窗口self.about = AboutInfo()  # 关于窗口self.tuopan = TUOPAN()  # 托盘对象# 爬虫对象self.bilisocket = BiliSocket()# 开始运行、弹幕窗口隐藏标志self.startFlag = Falseself.isHide = True# 接受子窗口传回来的信号  然后调用主界面的函数global_ms.my_Signal.connect(self.SignalHandle)# 写日志logger.info('----------------------初始化成功-----------------------')

点击(开始/更新)按钮执行的函数:先判断输入房间号是否合法;然后判断是否已经开始,若已经开始则为更新效果(先执行关闭操作再重新开始)。将获取弹幕的操作放到子线程。

def start_run(self):try:# 获取房间号text = self.w.roomidEdit.text()if text != '' and text.isdigit():if self.startFlag == False:self.startFlag = Trueelse:print(self.SocketTread.is_alive())if self.SocketTread.is_alive():self.close_com()self.display.clearWindow()self.bilisocket.comList.clear()self.display.show()self.isHide = Falseloop = asyncio.new_event_loop()self.SocketTread = threading.Thread(target=self.asyncTreadfun, args=(loop, text),name='SocketTread')self.SocketTread.daemon = True  # 守护线程self.SocketTread.start()logger.info(f'开启/更新成功,当前房间:{text}')else:self.startFlag = Falselogger.info('输入房间号有误')msg_box = QMessageBox(QMessageBox.Warning, '提示', '请输入正确房间号')msg_box.exec_()except Exception as e:self.startFlag = Falseself.isHide = Truelogger.error(u'开启/更新出错:{}'.format(e))def asyncTreadfun(self,new_loop,roomid):try:asyncio.set_event_loop(new_loop)self.loop = asyncio.get_event_loop()task = asyncio.ensure_future(self.bilisocket.startup(roomid))self.loop.run_until_complete(asyncio.wait([task]))except RuntimeError as e:logger.warning(u'loop循环未完成退出(若是关闭时为正常现象)。错误信息:{}'.format(e))

点击(关闭)按钮执行的函数:调用BiliSocket类的自定义close方法,并且等待它抛出异常,从而达到结束线程的效果。

def close_com(self):if self.startFlag == True:self.display.hide()self.isHide = Truetry:logger.info('///尝试关闭循环')self.bilisocket.close()# 等待抛出异常,抛出后线程自动结束time.sleep(0.3)logger.info(f'loop状态:{self.loop.is_running()}{self.loop.is_closed()} / 'f'SocketTread状态:{self.SocketTread.is_alive()}')logger.info('///循环关闭成功')except NotImplementedError as e:logger.error(u'///关闭循环出错:{}'.format(e))

接收信号并处理

def SignalHandle(self,value):if value == 'closeWin':print('子窗口被关闭')elif value == 'MainWindow':self.w.show()elif value == 'start':self.start_run()elif value == 'close':self.close_com()elif value == 'setting':self.settingWindow.show()elif value == 'about':self.about.show()elif value == 'WebSocketError':self.close_com()msg_box = QMessageBox(QMessageBox.Warning, '警告', '获取弹幕失败')msg_box.exec_()elif value == 'exit':self.quitApp()

退出软件

def quitApp(self):print('托盘关闭')if self.isHide == False:self.close_com()# 关闭窗体程序QCoreApplication.instance().quit()self.tuopan.tp.setVisible(False)logger.info('----------------------程序正常退出-----------------------')sys.exit(0)

写上循环指令

def run(self):try:# 显示主窗口,开始处理窗口事件self.w.show()sys.exit(self.app.exec_())except:logger.critical('**********************程序异常退出************************')

最后,只需要在其他地方实例化这个类,并且调用它的run方法,就可以运行整个程序啦!

danmuji = BiliDanmuji()
danmuji.run()

5.最终成果及使用方法

b站教程:https://www.bilibili.com/video/BV1LP4y177sa

6.开源地址

gitee:https://gitee.com/huihui486/bilibili-danmuji
github:https://github.com/huihui486/bilibili-danmuji

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/25129.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

B站有哪些程序员大牛up主?

B站有哪些程序员大牛up主 接下来分享一波有利于大家 面试 和 提高自己技术 的B站程序员up主 &#xff1a;B站&#xff0c;想必大家都再熟悉不过 &#xff1a;B站是啥&#xff1f;说清楚点 &#xff1a;就是哔哩哔哩啊&#xff1b; &#xff1a;哦 &#xff1a;你在b站上学习吗…

B 站崩了,受害程序员聊聊

非吃瓜&#xff0c;B 站事件始末分析 防治技术分享 大家好&#xff0c;我是鱼皮&#xff0c;昨天小破站崩了的事情相信很多朋友都听说了。 这要是搁以前&#xff0c;不爱吃瓜的我根本不会去关注这种事&#xff0c;崩了就崩了呗&#xff0c;反正天塌下来有程序员大佬们扛着&am…

B站发帖软件哪个好用?好用的哔哩哔哩发帖工具

B站发帖软件哪个好用?好用的哔哩哔哩发帖工具#发帖软件#哔哩哔哩发帖#视频发布软件 登录成功之后&#xff0c;进入到这样一个界面&#xff0c;默认情况下是这个样子的&#xff0c;我们在这里输入一下我们的一个文件夹的路径&#xff0c;输入到这里&#xff0c;点击添加账号&a…

B站一键查询视频/分P视频/番剧的弹幕发送者

近几年因为B站的门槛越来越低&#xff0c;时常会看到一些让人眼前一亮的弹幕。直接举报拉黑这种做法显然已经太便宜了、、 网上大部分查询工具时效性已经过了或者有局限性&#xff0c;所以我打算自己写一款能通过视频、分P视频、番剧来查询弹幕发送者的工具&#xff0c;输入视频…

新开源项目(solidjs-use)随想录

大厂技术 高级前端 Node进阶 点击上方 程序员成长指北&#xff0c;关注公众号 回复1&#xff0c;加入高级Node交流群 前言 如果你是 React 技术栈&#xff0c;就会发现其对新手其实是不太友好的&#xff0c;会导致新人写出很多重复渲染的组件和 BUG&#xff0c;而且排查难度高…

Semantic Kernel 知多少 | 开启面向AI编程新篇章

引言 在ChatGPT 火热的当下, 即使没有上手亲自体验&#xff0c;想必也对ChatGPT的强大略有耳闻。当一些人在对ChatGPT犹犹豫豫之时&#xff0c;一些敏锐的企业主和开发者们已经急不可耐得开展基于ChatGPT模型AI应用的落地探索。 因此&#xff0c;可以明确预见的是&#xff0c;「…

Semantic Kernel 知多少 | 开启面向 AI 编程新篇章

在 ChatGPT 火热的当下, 即使没有上手亲自体验&#xff0c;想必也对 ChatGPT 的强大略有耳闻。当一些人在对 ChatGPT 犹犹豫豫之时&#xff0c;一些敏锐的企业主和开发者们已经急不可耐地开展基于 ChatGPT 模型 AI 应用的落地探索。 因此&#xff0c;可以明确预见的是&#xf…

我做了个GPT3键盘,用了两个月发现它有点傻

自 ChatGPT 出世&#xff0c;各类文本类AI产品层出不穷。甚至接连几日&#xff0c;Producthunt 上新品过半都是AI相关。 这其中部分原因是 OpenAI 公司开放的 GPT3 1API 接口十分易用。只要一个简单的文本请求&#xff0c;就能将现有产品加入AI功能。例如&#xff0c;Notion、…

使用ChatGPT帮你提升代码编写能力

第一次ChatGPT代码沟通评审 import org.apache.http.*; import org.apache.http.client.*; import org.apache.http.client.methods.*; import org.apache.http.conn.ssl.*; import org.apache.http.entity.*; import org.apache.http.impl.client.*; import org.apache.http.s…

设计模式(七)门面模式(Facade Pattern 外观模式)

一、模式定义 门面模式(Facade Pattern)&#xff1a;外部与一个子系统的通信必须通过一个统一的外观对象进行&#xff0c;为子系统中的一组接口提供一个一致的界面&#xff0c;外观模式定义了一个高层接口&#xff0c;这个接口使得这一子系统更加容易使用。门面模式又称为外观…

html实现牌匾效果,4款店面牌匾设计效果图 店铺门头亚克力牌匾样式制作设计图...

4款店面牌匾设计效果图 店铺门头亚克力牌匾样式制作设计图 店铺门头亚克力牌匾样式设计很规整自然&#xff0c;同时这种材质也是很受现代人的欢迎&#xff0c;酒红色的色调很自然&#xff0c;醒目的视觉冲击也很不错&#xff0c;中央是点名和logo的设计&#xff0c;再加上图示&…

设计模式 -- 门面模式

前言 月是一轮明镜,晶莹剔透,代表着一张白纸(啥也不懂) 央是一片海洋,海乃百川,代表着一块海绵(吸纳万物) 泽是一柄利剑,千锤百炼,代表着千百锤炼(输入输出) 月央泽,学习的一种过程,从白纸->吸收各种知识->不断输入输出变成自己的内容 希望大家一起坚持这个过程,也同…

市场主流智能语音音箱对话系统哪个做的更好?

市场主流智能语音音箱对话系统哪个做的更好&#xff1f; 如何评价智能语音音箱对话系统的好与坏呢&#xff1f;智能音箱的对话技能如何实现&#xff1f;评价指标又有哪些呢&#xff1f;带着一连串的问题&#xff0c;小君来说说自己的理解。首先&#xff0c;智能音箱的对话技能…

语音聊天app开发——语音聊天室系统如何开发

网络直播行业近些年算得上是多元化发展&#xff0c;各个互联网平台陆续入驻&#xff0c;开发自身的短视频直播平台&#xff0c;像百度&#xff0c;腾讯&#xff0c;阿里等&#xff0c;直播也多种渠道发展&#xff0c;1对多视频直播&#xff0c;1对1直播&#xff0c;视频语音多人…

C语言实现扫雷游戏完整代码

文章目录 游戏整体框架游戏具体功能及实现整体代码 一、雷盘的定义 1.雷盘的定义 对于扫雷游戏&#xff0c;我们遇到的第一个问题就是&#xff1a;应该如何表示扫雷的雷盘及如何存放布雷、排雷的数据&#xff1b;我们发现&#xff0c;二维数组可以很好的解决这个问题。 #inc…

Java扫雷全代码

Java极致还原XP系统经典扫雷 前言 最近疫情在家&#xff0c;没有工作上的996压迫着&#xff0c;使我倍感无聊&#xff0c;不知这满头秀发该如何消耗。   闲逛着游戏社区&#xff0c;常常回想起和朋友一起通宵玩游戏的那种快感。   一款扫雷游戏使我眼前一亮&#xff0c;他…

编写代码实现简单的扫雷游戏

扫雷 菜单 比较简单&#xff0c;代码如下 void menu() {printf("*******************\n");printf("**** 1. play ****\n");printf("**** 0. exit ****\n");printf("*******************\n"); }效果如图 然后就是根据不同的输入…

【C语言】扫雷游戏详解及完整代码

文章目录 前言一、程序环境配置二、各种功能的实现以及逻辑关系的整理2.1 创建游戏初始界面&#xff08;进入\退出 游戏&#xff09;2.2 创建并初始化二维数组board[][] mine[][] (board存放棋盘的信息 mine存放雷的信息)2.3 初始化棋盘2.4 打印棋盘2.5 设置雷区2.6 扫雷 三、完…

互联网最值得加入的 173 家国企名单

大家好&#xff01;我是韩老师。 今年的就业相比以往是难了不少&#xff0c;感受到的人都懂。有一位学妹毕业后在互联网公司工作了两年多&#xff0c;受到的业绩考核压力越来越大&#xff0c;萌发了跳去国企的念头&#xff0c;和她通话聊了挺久。 就是这次的起因&#xff0c;给…

苹果,王炸产品来了!下一个 iPhone 诞生了?

推荐阅读&#xff1a; 《实名举报&#xff01;》 《简单&#xff0c;聊两句。》 1 科技界春晚 知道为什么&#xff0c;总称苹果发布会为科技界的春晚吗&#xff1f; 因为苹果总是可以结合最新工艺、制造、科技&#xff0c;打造出一个跨越时代的产品&#xff0c;或者说可以称之为…