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

2022/1/16 14:04:15

本文主要是介绍【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"))
    # 加入logger
    logger.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 KeyboardInterrupt

def 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 = colorName
    global_ms.otherChange.emit('userColor',colorName)
    
def getSize(self,size):
	self.size = size
	global_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.active
        for 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].value
                if filePath != None and os.path.exists(filePath):
                    self.backgroundView.setText(filePath)
                else:
                    logger.warning('图片修改失败。原因:图片不存在')
                    msg_box = QMessageBox(QMessageBox.Warning, '提示', '图片修改失败。原因:图片不存在')
                    msg_box.exec_()
            else:
                continue
        logger.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 = False
    self._corner_drag = False
    self._bottom_drag = False
    self._right_drag = False
    # 判断鼠标位置切换鼠标手势
    self.cornerLabel.setCursor(Qt.SizeFDiagCursor)
    self.bottomLabel.setCursor(Qt.SizeVerCursor)
    self.rightLabel.setCursor(Qt.SizeHorCursor)

def resizeEvent(self, QResizeEvent):
    # 自定义窗口调整大小事件
    # 改变窗口大小的三个坐标范围
    ran = 30
    self._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 = True
        event.accept()
    elif (event.button() == Qt.LeftButton) and (event.pos() in self._right_rect):
        # 鼠标左键点击右侧边界区域
        self._right_drag = True
        event.accept()
    elif (event.button() == Qt.LeftButton) and (event.pos() in self._bottom_rect):
        # 鼠标左键点击下侧边界区域
        self._bottom_drag = True
        event.accept()
    elif (event.button() == Qt.LeftButton) and (event.y() < self.height()-30):
        # 鼠标左键点击其他位置
        self._move_drag = True
        self.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 = False
    self._corner_drag = False
    self._bottom_drag = False
    self._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.quit
        self.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 = False
        self.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 = True
            else:
                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 = False
            loop = 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 = False
            logger.info('输入房间号有误')
            msg_box = QMessageBox(QMessageBox.Warning, '提示', '请输入正确房间号')
            msg_box.exec_()
    except Exception as e:
        self.startFlag = False
        self.isHide = True
        logger.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 = True
        try:
            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



这篇关于【python+pyqt5】B站直播弹幕姬的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程