OpenCV-PyQT项目实战(12)项目案例08:多线程视频播放
欢迎关注『OpenCV-PyQT项目实战 @ Youcans』系列,持续更新中
OpenCV-PyQT项目实战(1)安装与环境配置
OpenCV-PyQT项目实战(2)QtDesigner 和 PyUIC 快速入门
OpenCV-PyQT项目实战(3)信号与槽机制
OpenCV-PyQT项目实战(4)OpenCV 与PyQt的图像转换
OpenCV-PyQT项目实战(5)项目案例01:图像模糊
OpenCV-PyQT项目实战(6)项目案例02:滚动条应用
OpenCV-PyQT项目实战(7)项目案例03:鼠标框选
OpenCV-PyQT项目实战(8)项目案例04:鼠标定位
OpenCV-PyQT项目实战(9)项目案例04:视频播放
OpenCV-PyQT项目实战(10)项目案例06:键盘事件与视频抓拍
OpenCV-PyQT项目实战(11)项目案例07:摄像头操作与拍摄视频
OpenCV-PyQT项目实战(12)项目案例08:多线程视频播放
文章目录
- OpenCV-PyQT项目实战(12)项目案例08:多线程视频播放
- 1. 线程处理
- 1.1 线程与进程
- 1.2 为什么使用多线程
- 1.3 单线程与多线程的基本例程
- 2. Python自定义线程
- 2.1 线程的创建步骤
- 2.2 通过线程类 threading.Thread() 创建线程对象
- 2.4 继承 threading.Thread 来自定义线程类
- 2.5 使用 moveToThread 方法
- 3. 多线程视频解码和视频播放
- 3.1 自定义线程类
- 3.2 视频解码子程序
- 3.3 视频播放子程序
- 3.4 主程序
- 3.5 UI 界面 uiDemo13.ui
- 4. 项目实战:PyQt多线程视频处理
- 4.1 程序说明
- 4.2 完整例程
OpenCV-PyQT项目实战(12)项目案例08:多线程视频播放
在之前的案例中,我们使用 QTime 定时器和 QThread 的方式来控制 QLabel 中的图像更新,实现视频播放或实时监控。但是,如果需要同时播放多路视频,或者在视频播放的同时进行图像处理,这种处理方法容易产生卡顿或阻塞,因此需要使用多线程解决这个问题。
1. 线程处理
1.1 线程与进程
进程(Process)是操作系统进行资源分配和调度运行的基本单位,可以理解为操作系统中正在执行的程序。每个应用程序都有一个自己的进程。
线程是一个基本的CPU执行单元,是程序执行的最小单元。 线程自己不拥有系统资源,必须依托于进程存活。一个线程是一个execution context,即一个CPU执行时所需要的一串指令。
每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。
- 线程与进程的区别:
- 线程必须在某个进程中执行。
- 一个进程可包含多个线程,其中有且只有一个主线程。
- 多线程共享同个地址空间、打开的文件以及其他资源。
- 多进程共享物理内存、磁盘、打印机以及其他资源。
1.2 为什么使用多线程
线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄和其他进程应有的状态。
因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程之中拥有独立的内存单元,而多个线程共享内存,从而极大的提升了程序的运行效率。
线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性,多个线程共享一个进程的虚拟空间。线程的共享环境包括进程代码段、进程的共有数据等,利用这些共享的数据,线程之间很容易实现通信。
操作系统在创建进程时,必须为改进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。因此,使用多线程来实现并发比使用多进程的性能高得要多。
多线程(multithreading)是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
线程从创建到消亡的过程为:
- new(新建):新创建的线程经过初始化后,进入Runnable状态。
- Runnable(就绪):等待线程调度。调度后进入运行状态。
- Running(运行)。
- Blocked(阻塞):暂停运行,解除阻塞后进入Runnable状态重新等待调度。
- Dead(消亡):线程方法执行完毕返回或者异常终止。
从运行状态(Running)进入阻塞状态(Blocked)有三种情况:
- 同步:线程中获取同步锁,但是资源已经被其他线程锁定时,进入Locked状态,直到该资源可获取(获取的顺序由Lock队列控制)
- 睡眠:线程运行sleep()或join()方法后,线程进入Sleeping状态。区别在于sleep等待固定的时间,而join是等待子线程执行完。当然join也可以指定一个“超时时间”。从语义上来说,如果两个线程a,b, 在a中调用b.join(),相当于合并(join)成一个线程。最常见的情况是在主线程中join所有的子线程。
- 等待:线程中执行wait()方法后,线程进入Waiting状态,等待其他线程的通知(notify)。
1.3 单线程与多线程的基本例程
例程1:单线程
import timedef sayHello():print("Hello")time.sleep(1) if __name__ == '__main__':for i in range(10):sayHello()
运行时间为:10秒。
例程2:多线程
通过多线程可以完成多任务。
import time,threadingdef sayHello():print("Hello")time.sleep(1)if __name__ == '__main__':for i in range(10):# 创建子线程对象thread_obj = threading.Thread(target=sayHello)# 启动子线程对象thread_obj.start()
运行时间为:1秒。
例程3:线程的执行顺序
import time,threadingdef sing():for i in range(3):print("is singing... %d" % i)time.sleep(1)def dance():for i in range(3):print("is dancing... %d" % i)time.sleep(1)if __name__ == '__main__':print("The main thread starts to execute")t1 = threading.Thread(target=sing)t2 = threading.Thread(target=dance)t1.start()t2.start()print("Main thread execution completed")
运行结果:
The main thread starts to execute
is singing... 0
is dancing... 0
Main thread execution completed
is dancing... 1is singing... 1is singing... 2
is dancing... 2
Process finished with exit code 0
主线程执行完毕后,还要等待所有子线程执行完毕后才结束程序运行。
2. Python自定义线程
2.1 线程的创建步骤
(1) 导入线程模块import threading
(2) 通过线程类创建进程对象线程对象 = threading.Thread(target = 任务名)
(3) 启动线程执行任务线程对象.start()
2.2 通过线程类 threading.Thread() 创建线程对象
语法:
thread.Thread(group=Nore,targt=None,args=(),kwargs={},*,daemon=None)
参数说明:
- group:必须为None,于ThreadGroup类相关,一般不使用。
- target:线程调用的对象,就是目标函数。
- name:为线程起这个名字。默认是Tread-x,x是序号,由1开始,第一个创建的线程名字就是Tread-1。
- args:为目标函数传递关键字参数,元组。
- kwargs:为目标函数传递关键字参数,字典。
- daemon:用来设置线程是否随主线程退出而退出。
例程4:子线程的创建与开启
线程对象 = threading.Thread(target=任务名)
import threadingdef run1(n):print("current task:run1")def run2(n):print("current task:run2")if __name__ == "__main__":t1 = threading.Thread(target=run1) # 创建子线程1t2 = threading.Thread(target=run2) # 创建子线程2t1.start() # 开启线程 t1t2.start() # 开启线程 t2
例程5:给线程执行的任务传递参数
线程可以以元组args 的方式或字典 kwargs 的方式给执行的任务传递参数。
线程对象 = threading.Thread(target=任务名, args=(arg1,…))
线程对象 = threading.Thread(target=任务名, kwargs={k1:num1,…})
import threadingdef run1(n):print("current task:", n)def run2(n):print("current task:", n)if __name__ == "__main__":t1 = threading.Thread(target=run1, args=("thread 1",)) # 创建子线程1t2 = threading.Thread(target=run2, args=("thread 2",)) # 创建子线程2t1.start() # 开启线程 t1t2.start() # 开启线程 t2print("end")
2.4 继承 threading.Thread 来自定义线程类
例程6:自定义线程类 MyThread
import threading
import timeclass MyThread(threading.Thread):def __init__(self, num):threading.Thread.__init__(self)self.num = numdef run(self):print("current task:", self.num)time.sleep(self.num)if __name__ == "__main__":t1 = MyThread(100) # 创建子线程1t2 = MyThread(200) # 创建子线程2t1.start() # 开启线程 t1t2.start() # 开启线程 t2print("end")
2.5 使用 moveToThread 方法
moveToThread 函数给多个任务(如显示多个界面)各分配一个线程去执行,避免自定义多个类继承自 QThread类,从而可以避免冗余。
使用moveToThread函数的流程:
- 创建一个类继承自QObject类或其子类,在其中定义所要执行的多个任务。
- 任务通过moveToThread指定所要执行的线程。
- 线程通过start启动。
- 通过信号与槽机制触发线程的执行。
该方法通过创建一个线程,将创建的线程与类方法进行绑定来实现,相当于在线程中操作类方法。
例程6:自定义线程类 MyThread
# coding:UTF-8
from PyQt5 import QtWidgets, QtCore
import sys
from PyQt5.QtCore import *
import time# 继承 QObject
class Runthread(QtCore.QObject):# 通过类成员对象定义信号对象signal = pyqtSignal(str)def __init__(self):super(Runthread, self).__init__()self.flag = Truedef __del__(self):print ">>> __del__"def run(self):i = 0while self.flag:time.sleep(1)if i <= 100:self.signal.emit(str(i)) # 注意这里与_signal = pyqtSignal(str)中的类型相同i += 1print ">>> run end: "class Example(QtWidgets.QWidget):# 通过类成员对象定义信号对象_startThread = pyqtSignal()def __init__(self):super(Example, self).__init__()# 按钮初始化self.button_start = QtWidgets.QPushButton('开始', self)self.button_stop = QtWidgets.QPushButton('停止', self)self.button_start.move(60, 80)self.button_stop.move(160, 80)self.button_start.clicked.connect(self.start) # 绑定多线程触发事件self.button_stop.clicked.connect(self.stop) # 绑定多线程触发事件# 进度条设置self.pbar = QtWidgets.QProgressBar(self)self.pbar.setGeometry(50, 50, 210, 25)self.pbar.setValue(0)# 窗口初始化self.setGeometry(300, 300, 300, 200)self.show()self.myT = Runthread() # 创建线程对象self.thread = QThread(self) # 初始化QThread子线程# 把自定义线程加入到QThread子线程中self.myT.moveToThread(self.thread)self._startThread.connect(self.myT.run) # 只能通过信号-槽启动线程处理函数self.myT.signal.connect(self.call_backlog)def start(self):if self.thread.isRunning(): # 如果该线程正在运行,则不再重新启动return# 先启动QThread子线程self.myT.flag = Trueself.thread.start()# 发送信号,启动线程处理函数# 不能直接调用,否则会导致线程处理函数和主线程是在同一个线程,同样操作不了主界面self._startThread.emit()def stop(self):if not self.thread.isRunning(): # 如果该线程已经结束,则不再重新关闭returnself.myT.flag = Falseself.stop_thread()def call_backlog(self, msg):self.pbar.setValue(int(msg)) # 将线程的参数传入进度条def stop_thread(self):print ">>> stop_thread... "if not self.thread.isRunning():returnself.thread.quit() # 退出self.thread.wait() # 回收资源print ">>> stop_thread end... "if __name__ == "__main__":app = QtWidgets.QApplication(sys.argv)myshow = Example()myshow.show()sys.exit(app.exec_())
3. 多线程视频解码和视频播放
3.1 自定义线程类
def openVideo(self): # 导入视频文件,点击 btn_1 触发if self.btn_1.text() == "打开视频":# 打开视频文件self.videoPath, _ = QFileDialog.getOpenFileName(self, "Open Video", "../images/", "*.mp4 *.avi *.flv")print("Open Video: ", self.videoPath)# 实例化 cvDecode 类self.decodework = cvDecode()self.decodework.start()self.decodework.threadFlag = Trueself.decodework.changeFlag = Trueself.decodework.videoPath = r"" + self.videoPath# 实例化 playVedio 类self.playwork = playVedio()self.playwork.playLabel = self.label_1 # 设置显示控件 label_1self.playwork.threadFlag = Trueself.playwork.playFlag = True # 控制标识:播放# 创建视频播放线程self.playthread = QThread()self.playwork.moveToThread(self.playthread)self.playthread.started.connect(self.playwork.play) # 线程与类方法进行绑定self.playthread.start() # 启动视频播放线程# 视频/摄像头,准备播放self.btn_1.setText("关闭视频")self.btn_2.setEnabled(True) # "播放"按钮 可用self.btn_3.setEnabled(True) # "抓拍"按钮 可用else:self.closeEvent(self.close) # 关闭线程self.btn_1.setText("打开视频")self.btn_2.setText("播放视频")self.btn_2.setEnabled(False) # "播放"按钮 不可用self.btn_3.setEnabled(False) # "抓拍"按钮 不可用
3.2 视频解码子程序
class cvDecode(QThread): # 视频解码def __init__(self):super(cvDecode, self).__init__()self.threadFlag = False # 控制线程退出self.videoPath = "" # 视频文件路径Pself.changeFlag = 0 # 判断视频文件路径是否更改self.cap = cv.VideoCapture()def run(self):while self.threadFlag: # 线程开启状态if self.changeFlag == 1 and self.videoPath !="":self.changeFlag = 0self.cap = cv.VideoCapture(r""+self.videoPath)if self.videoPath !="":if self.cap.isOpened():ret, frame = self.cap.read()time.sleep(0.01) # 读取时间控制,读取视频文件取 0.01,读取实时摄像取 0.001if frame is None: # 控制循环播放self.cap = cv.VideoCapture(r"" + self.videoPath)if ret:Decode2Play.put(frame) # 解码后的数据放到队列中del frame # 释放资源else:# 控制重连self.cap = cv.VideoCapture(r"" + self.videoPath)time.sleep(0.01)
3.3 视频播放子程序
class playVedio(QObject): # 视频播放类def __init__(self):super(playVedio, self).__init__()self.playLabel = QLabel() # 初始化QLabel对象self.threadFlag = False # 控制线程退出self.playFlag = False # 控制播放/暂停标识def play(self):while self.threadFlag: # 线程开启状态if not Decode2Play.empty():self.frame = Decode2Play.get() # 从队列中读取视频帧if self.playFlag: # 当前状态播放image = cv.resize(self.frame, (400, 320)) # 调整为显示尺寸# qImg = self.cvToQImage(frame) # OpenCV 转为 PyQt 图像格式qImg = QImage(image, image.shape[1], image.shape[0], QImage.Format_RGB888).rgbSwapped()self.playLabel.setPixmap(QPixmap.fromImage(qImg)) # 图像在QLabel上展示time.sleep(0.001)
3.4 主程序
class MyMainWindow(QMainWindow, Ui_MainWindow): # 继承 QMainWindow 类和 Ui_MainWindow 界面类def __init__(self, parent=None):super(MyMainWindow, self).__init__(parent) # 初始化父类self.setupUi(self) # 继承 Ui_MainWindow 界面类# 菜单栏self.actionOpen.triggered.connect(self.openVideo) # 连接并执行 openSlot 子程序self.actionHelp.triggered.connect(self.trigger_actHelp) # 连接并执行 trigger_actHelp 子程序self.actionQuit.triggered.connect(self.close) # 连接并执行 trigger_actHelp 子程序## # 通过 connect 建立信号/槽连接,点击按钮事件发射 triggered 信号,执行相应的子程序 click_pushButtonself.btn_1.clicked.connect(self.openVideo) # 打开视频文件self.btn_2.clicked.connect(self.playPause) # 播放/暂停视频self.btn_3.clicked.connect(self.captureFrame) # 抓拍视频图像self.btn_4.clicked.connect(self.imageProcess) # 图像处理程序self.btn_5.clicked.connect(self.close) # 点击关闭按钮触发:关闭程序# self.timerCam.timeout.connect(self.refreshFrame) # 计时器结束时调用槽函数刷新当前帧# 初始化self.btn_2.setEnabled(False) # "播放"按钮 不可用self.btn_3.setEnabled(False) # "抓拍"按钮 不可用self.btn_4.setEnabled(False) # "处理"按钮 不可用
3.5 UI 界面 uiDemo13.ui
本例的 UI 继承自 uiDemo4.ui ,并进行修改如下:
完成了本项目的图形界面设计,将其保存为 uiDemo13.ui文件。
在 PyCharm中,使用 PyUIC 将选中的 uiDemo13.ui 文件转换为 .py 文件,就得到了 uiDemo13.py 文件。
4. 项目实战:PyQt多线程视频处理
4.1 程序说明
(1)“打开视频”按钮用于从文件夹选择播放的视频文件。
导入视频前,“暂停播放”、”抓拍图像“、”处理图像“ 按钮都不可用。
(2)“播放”按钮用于播放打开的视频文件,播放结束后自动关闭。
导入视频后开始播放,“暂停播放”、”抓拍图像“按钮可用,”处理图像“不可用。
(3)“暂停/播放”按钮用于暂停/播放视频文件。按钮初始显示为“暂停播放”,按下“暂停”按钮后暂停播放,按钮显示切换为“播放视频”;再次按下“播放”按钮后继续播放,按钮显示切换为“暂停播放”。
(4)”抓拍图像“按钮用于抓拍图像,并显示在窗口右侧的显示控件 Label_2。左侧窗口的视频播放不受影响。
抓拍图像完成后,“图像处理”按钮可用。
(5)”图像处理“按钮用于处理所抓拍图像,并将处理后图像显示在窗口右侧的显示控件 Label_2。
为了简化例程,本例中的图像处理仅将抓拍图像转换为灰度图像进行显示。在实际应用中,也可以根据需要编写图像处理程序。
由于采用多线程处理机制,不论图像处理程序耗时如何,都不会影响左侧窗口中的视频播放。
4.2 完整例程
# OpenCVPyqt13.py
# Demo05 of GUI by PyQt5
# Copyright 2023 Youcans, XUPT
# Crated:2023-03-02import sys, time
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QObject, QThread, Qt
from PyQt5.QtGui import *
import cv2 as cv
from queue import Queue
from uiDemo13 import Ui_MainWindow # 导入 uiDemo10.py 中的 Ui_MainWindow 界面类
Decode2Play = Queue()class cvDecode(QThread): # 视频解码def __init__(self):super(cvDecode, self).__init__()self.threadFlag = False # 控制线程退出self.videoPath = "" # 视频文件路径Pself.changeFlag = 0 # 判断视频文件路径是否更改self.cap = cv.VideoCapture()def run(self):while self.threadFlag: # 线程开启状态if self.changeFlag == 1 and self.videoPath !="":self.changeFlag = 0self.cap = cv.VideoCapture(r""+self.videoPath)if self.videoPath !="":if self.cap.isOpened():ret, frame = self.cap.read()time.sleep(0.01) # 读取时间控制,读取视频文件取 0.01,读取实时摄像取 0.001if frame is None: # 控制循环播放self.cap = cv.VideoCapture(r"" + self.videoPath)if ret:Decode2Play.put(frame) # 解码后的数据放到队列中del frame # 释放资源else:# 控制重连self.cap = cv.VideoCapture(r"" + self.videoPath)time.sleep(0.01)class playVedio(QObject): # 视频播放类def __init__(self):super(playVedio, self).__init__()self.playLabel = QLabel() # 初始化QLabel对象self.threadFlag = False # 控制线程退出self.playFlag = False # 控制播放/暂停标识def play(self):while self.threadFlag: # 线程开启状态if not Decode2Play.empty():self.frame = Decode2Play.get() # 从队列中读取视频帧if self.playFlag: # 当前状态播放image = cv.resize(self.frame, (400, 320)) # 调整为显示尺寸# qImg = self.cvToQImage(frame) # OpenCV 转为 PyQt 图像格式qImg = QImage(image, image.shape[1], image.shape[0], QImage.Format_RGB888).rgbSwapped()self.playLabel.setPixmap(QPixmap.fromImage(qImg)) # 图像在QLabel上展示time.sleep(0.001)class MyMainWindow(QMainWindow, Ui_MainWindow): # 继承 QMainWindow 类和 Ui_MainWindow 界面类def __init__(self, parent=None):super(MyMainWindow, self).__init__(parent) # 初始化父类self.setupUi(self) # 继承 Ui_MainWindow 界面类# 菜单栏self.actionOpen.triggered.connect(self.openVideo) # 连接并执行 openSlot 子程序self.actionHelp.triggered.connect(self.trigger_actHelp) # 连接并执行 trigger_actHelp 子程序self.actionQuit.triggered.connect(self.close) # 连接并执行 trigger_actHelp 子程序## # 通过 connect 建立信号/槽连接,点击按钮事件发射 triggered 信号,执行相应的子程序 click_pushButtonself.btn_1.clicked.connect(self.openVideo) # 打开视频文件self.btn_2.clicked.connect(self.playPause) # 播放/暂停视频self.btn_3.clicked.connect(self.captureFrame) # 抓拍视频图像self.btn_4.clicked.connect(self.imageProcess) # 图像处理程序self.btn_5.clicked.connect(self.close) # 点击关闭按钮触发:关闭程序# self.timerCam.timeout.connect(self.refreshFrame) # 计时器结束时调用槽函数刷新当前帧# 初始化self.btn_2.setEnabled(False) # "播放"按钮 不可用self.btn_3.setEnabled(False) # "抓拍"按钮 不可用self.btn_4.setEnabled(False) # "处理"按钮 不可用def openVideo(self): # 导入视频文件,点击 btn_1 触发if self.btn_1.text() == "打开视频":# 打开视频文件self.videoPath, _ = QFileDialog.getOpenFileName(self, "Open Video", "../images/", "*.mp4 *.avi *.mov")print("Open Video: ", self.videoPath)# 实例化 cvDecode 类self.decodework = cvDecode()self.decodework.start()self.decodework.threadFlag = Trueself.decodework.changeFlag = Trueself.decodework.videoPath = r"" + self.videoPath# 实例化 playVedio 类self.playwork = playVedio()self.playwork.playLabel = self.label_1 # 设置显示控件 label_1self.playwork.threadFlag = Trueself.playwork.playFlag = True # 控制标识:播放# 创建视频播放线程self.playthread = QThread()self.playwork.moveToThread(self.playthread)self.playthread.started.connect(self.playwork.play) # 线程与类方法进行绑定self.playthread.start() # 启动视频播放线程# 视频/摄像头,准备播放self.btn_1.setText("关闭视频")self.btn_2.setEnabled(True) # "播放"按钮 可用self.btn_3.setEnabled(True) # "抓拍"按钮 可用else:self.closeEvent(self.close) # 关闭线程self.btn_1.setText("打开视频")self.btn_2.setText("播放视频")self.btn_2.setEnabled(False) # "播放"按钮 不可用self.btn_3.setEnabled(False) # "抓拍"按钮 不可用def playPause(self): # 暂停/播放控制,点击 btn_2 触发if self.btn_2.text() == "暂停播放":self.playwork.playFlag = False # 控制标识:暂停self.btn_2.setText("播放视频")else:self.btn_2.setText("暂停播放")self.playwork.playFlag = True # 控制标识:播放def captureFrame(self): # 抓拍视频图像,点击 btn_3 触发wLabel, hLabel = 400, 320self.image = cv.resize(self.playwork.frame, (wLabel, hLabel)) # 调整为显示尺寸qImg = QImage(self.image, wLabel, hLabel, QImage.Format_RGB888).rgbSwapped() # OpenCV 转为 PyQt 图像格式self.label_2.setPixmap((QPixmap.fromImage(qImg))) # 加载 PyQt 图像self.btn_4.setEnabled(True) # "处理"按钮 可用def imageProcess(self): # 抓拍视频图像,点击 btn_4 触发gray = cv.cvtColor(self.image, cv.COLOR_BGR2GRAY) # 转为灰度图像row, col, pix = gray.shape[0], gray.shape[1], gray.strides[0]qImg = QImage(gray.data, col, row, pix, QImage.Format_Indexed8)# qImg = QImage(self.image, row, col, QImage.Format_RGB888) # OpenCV 转为 PyQt 图像格式self.label_2.setPixmap((QPixmap.fromImage(qImg))) # 加载 PyQt 图像def closeEvent(self, event): # 关闭线程print("关闭线程") # 先退出循环才能关闭线程if self.decodework.isRunning(): # 关闭解码self.decodework.threadFlag = Falseself.decodework.quit()if self.playthread.isRunning(): # 关闭播放线程self.playwork.threadFlag = Falseself.playthread.quit()def refreshShow(self, img, label): # 刷新显示图像qImg = self.cvToQImage(img) # OpenCV 转为 PyQt 图像格式label.setPixmap((QPixmap.fromImage(qImg))) # 加载 PyQt 图像returndef trigger_actHelp(self): # 动作 actHelp 触发QMessageBox.about(self, "About","""多线程视频播放器 v1.0\nCopyright YouCans, XUPT 2023""")returnif __name__ == '__main__':app = QApplication(sys.argv) # 在 QApplication 方法中使用,创建应用程序对象myWin = MyMainWindow() # 实例化 MyMainWindow 类,创建主窗口myWin.show() # 在桌面显示控件 myWinsys.exit(app.exec_()) # 结束进程,退出程序
【本节完】
版权声明:
Copyright 2023 youcans, XUPT
Crated:2023-03-02
相关文章:

OpenCV-PyQT项目实战(12)项目案例08:多线程视频播放
欢迎关注『OpenCV-PyQT项目实战 Youcans』系列,持续更新中 OpenCV-PyQT项目实战(1)安装与环境配置 OpenCV-PyQT项目实战(2)QtDesigner 和 PyUIC 快速入门 OpenCV-PyQT项目实战(3)信号与槽机制 …...

面向对象设计模式:结构型模式之装饰器模式
文章目录一、引入二、装饰器模式2.1 Intent 意图2.2 Applicability 适用性2.3 类图2.4 优缺点2.5 应用实例:Java IO 类2.6 应用实例:咖啡馆订购系统一、引入 咖啡馆订购系统 Initial 初始 4 种咖啡 House blend (混合咖啡)Dark Roast (深度烘培)Decaf (…...

Unity iOS 无服务器做一个排行榜 GameCenter
排行榜需求解决方案一(嗯目前只有一)UnityEngine.SocialPlatformsiOS GameCenterAppStoreConnect配置Unity 调用(如果使用GameCenter系统的面板,看到这里就可以了)坑(需要获取数据做自定义面板的看这里)iOS代码Unity 代码吐槽需求 需求:接入…...

现在招个会自动化测试的人是真难呀~你会个锤子的自动化测试
现在招个会自动化测试的人是真难呀~ 前一段时间公司计划要招2个自动化测试到岗,同事面试了十几个来应聘的人,发现一个很奇怪的现象,在面试的时候,如果问的是框架API、脚本编写这些问题,基本上所有人都能对答如流&…...
OracleDatabase——数据库表空间dmp导出与导入
由于公司的程序一直部署在客户现场内网,内网调试难度高,一般是有备份还原数据库的需求,这里简记备份(导出)数据库dmp文件与恢复(导入)的步骤。 一、导出dmp文件 exp与expdp命令异同 相同点&a…...

20张图带你彻底了解ReentrantLock加锁解锁的原理
哈喽大家好,我是阿Q。 最近是上班忙项目,下班带娃,忙的不可开交,连摸鱼的时间都没有了。今天趁假期用图解的方式从源码角度给大家说一下ReentrantLock加锁解锁的全过程。系好安全带,发车了。 简单使用 在聊它的源码…...
Dockerfile构建Springboot镜像
Dockerfile构建Springboot镜像 文章目录 Dockerfile构建Springboot镜像 简介实例演示 前期准备 Docker环境Springboot项目Dockerfile文件 Windows 要求构建镜像启动测试 Linux 要求构建镜像启动测试 简介 容器技术大流行的时代,也是docker大流行的时代。 此文…...

从深分页查询到覆盖索引
最近看到一道面试题,如何优化深分页查询 最简单的例子是 select * from web_bill_main limit 30000,10;分页达到30000行,需要把前面29999行都过滤掉,才能找到这10条数据 所以整体时间花了80ms(工具显示时间) 我当时的第一反应是࿰…...

Go语言学习的第三天--下部分(Gin框架的基础了解)
每天都会分享Go的知识,喜欢的朋友关注一下。每天的学习分成两部分基础(必要的,基础不牢地动山摇),另一部分是Go的一些框架知识(会不定时发布,因为小Wei也是一名搬砖人)。但是可以保证…...

JDK的动态代理(powernode 文档)(内含源代码)
JDK的动态代理(powernode 文档)(内含源代码) 源代码下载链接地址:https://download.csdn.net/download/weixin_46411355/87546086 一、动态代理 目录JDK的动态代理(powernode 文档)࿰…...

第1章 多线程基础
第1章 多线程基础 1.1.2 线程与进程的关系 进程可以看成是线程的容器,而线程又可以看成是进程中的执行路径。 1.2 多线程启动 线程有两种启动方式:实现Runnable接口;继承Thread类并重写run()方法。 执行进程中的任务时才会产生线程&a…...

Linux基本指令(一)
文章目录文件操作文档操作系统管理网络通信备份压缩Ctrl Alt T 打开终端 文件操作 1.复制文件 cp afile bfile (将名为afile的文件复制到名为bfile的文件夹中,如果bfile文件不存在,系统将会创建此文件,如果bfile文件已经存在&a…...
el-dialog子组件在mounted周期内获取不到dom?
el-dialog子组件在mounted周期内获取不到dom?一、问题描述二、分析原因三、猜测正常父子组件在mounted生命周期内可以获得dom 父created—子created—子mounted—父mounted----子updated—父updated 一、问题描述 ** el-dialog控制显示隐藏是css控制的display&…...
第九章 opengl之光照(光照贴图)
OpenGL光照贴图漫反射贴图镜面光贴图光照贴图 一个物体的不同部分是不同的材质,那么会有不同的环境光和漫反射颜色表现。 漫反射贴图 原理就是:纹理。 是对同样的原理使用了不同的名字:其实都是使用一张覆盖物体的图像,让我们能…...

JDK动态代理(powernode CD2207 video)(内含教学视频+源代码)
JDK动态代理(powernode CD2207 video)(内含教学视频源代码) 教学视频原代码下载链接地址:https://download.csdn.net/download/weixin_46411355/87545977 目录JDK动态代理(powernode CD2207 video…...

【Linux】Sudo的隐晦bug引发的一次业务问题排查
Sudo的隐晦bug引发的一次业务问题排查写在前面问题描述问题排查高负载现象排查日志排查跟踪任务调度过程Sudo引发的问题手动复现问题分析处理方案写在前面 记录一次生产环境sudo启动进程频繁被Kill且不报错的异常处理过程,如果遇到同样的问题只想要解决方案&#x…...

Java VisualVM 安装 Visual GC 插件图文教程
文章目录1. 通过运行打开 Java VisualVM 监控工具2. 菜单栏初始视图说明3. 工具插件菜单说明4. 手工安装插件5. 重启监控工具查看 Visual GC1. 通过运行打开 Java VisualVM 监控工具 首先确保已安装 Java 环境,如此处安装版本 JDK 1.8.0_161 C:\Users\niaonao>j…...

【C语言】详解静态变量static
关键字static 在C语言中:static是用来修饰变量和函数的static主要作用为:1. 修饰局部变量-静态局部变量 2. 修饰全局变量-静态全局变量3. 修饰函数-静态函数在讲解静态变量之前,我们应该了解静态变量和其他变量的区别: 修饰局部变量 //代码1 #include &l…...

SpringBoot整合ElasticSearch实现模糊查询,排序,分页,高亮
目录 前言 1.框架集成-SpringData-整体介绍 1.1Spring Data Elasticsearch 介绍 2.框架集成Spring Data Elasticsearch 2.1版本说明 2.2.idea创建一个springboot项目 2.3.导入依懒 2.3.增加配置文件 2.4Spring Boot 主程序。 2.5.数据实体类 2.6.配置类 2.7.DAO 数据…...

YARN基本架构
主要由ResourceManager、NodeManager、ApplicationMaster和Container等组件构成,如图所YA示。 ResourceManager(RM) RM是全局资源管理器,负责整个系统的资源管理和分配 主要由两个组件构成:Scheduler调度器和应用程序…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...

2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...
ffmpeg(四):滤镜命令
FFmpeg 的滤镜命令是用于音视频处理中的强大工具,可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下: ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜: ffmpeg…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...

【Redis】笔记|第8节|大厂高并发缓存架构实战与优化
缓存架构 代码结构 代码详情 功能点: 多级缓存,先查本地缓存,再查Redis,最后才查数据库热点数据重建逻辑使用分布式锁,二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...