目录
一、前言
二、桌面摄像头软件
2.1、下载项目
2.2、功能介绍
三、打包工具(nuitka)
四、项目文件复制(我全部合到一个文件里面了)
五、结语
一、前言
看见b站的向军大叔用electron制作了一个桌面摄像头软件
但是,我不怎么熟悉前端,自己就用pyside6简单制作一个
二、桌面摄像头软件
2.1、下载项目
软件下载地址:(下载后,双击即可运行——60MB左右)
https://wwm.lanzout.com/ibIEL1q0xt8d
密码:eg34
工程文件下载地址:
camera-python.zip - 蓝奏云
启动项目:
# 下载包 pip install -r requirements.txt# 运行项目 python main.py
2.2、功能介绍
启动后,会自动打开默认摄像头
基础操作
左键长按:拖拽移动
滚轮上下滑动:放大和缩小摄像头画面
右键设置操作
选择边框颜色(rgb格式)
选择摄像头(自由切换)
窗口变形(正方形窗口和圆形窗口的切换)
隐藏
退出
系统托盘(可右键选择隐藏或出现,以及退出)
三、打包工具(nuitka)
Python——Windows使用Nuitka2.0打包(保姆级教程)-CSDN博客
我的打包命令:
nuitka --mingw64 --show-progress --standalone --disable-console --enable-plugin=pyside6 --plugin-enable=numpy --onefile --remove-output --windows-icon-from-ico=logo.ico camera.py
四、项目文件复制(我全部合到一个文件里面了)
# -*- coding: utf-8 -*- # @Author : pan import time import cv2 import numpy as npfrom PySide6.QtCore import (Qt, QTimer, QPropertyAnimation, QEasingCurve,QParallelAnimationGroup, QThread, QMutex,Signal, Slot, QCoreApplication, QDate, QDateTime,QLocale, QMetaObject, QObject, QPoint, QRect,QSize, QTime, QUrl, QAbstractAnimation, QEvent) from PySide6.QtGui import (QPixmap, QPainter, QColor, QFontMetrics, QPen,QWheelEvent, QCursor, QAction, QImage, QPainterPath,QBrush, QConicalGradient, QFont, QFontDatabase,QGradient, QIcon, QKeySequence, QLinearGradient,QRadialGradient, QTransform) from PySide6.QtWidgets import (QApplication, QWidget, QLabel, QMenu, QDialog,QVBoxLayout, QLineEdit, QPushButton, QMessageBox,QInputDialog, QFrame, QHBoxLayout, QLayout,QSizePolicy, QSpacerItem, QSystemTrayIcon)# 提示组件 class Toast(QWidget):def __init__(self,text: str,duration: int = 3000,parent: QWidget = None,):super().__init__(parent)self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)self.setAttribute(Qt.WA_TranslucentBackground, True)self.duration = durationlabel = QLabel(self)label.setText(text)label.setStyleSheet("""background-color: rgba(60, 179, 113, 0.8);color: white;font-size: 16px;padding: 12px;border-radius: 4px;""")label.setAlignment(Qt.AlignCenter)fm = QFontMetrics(label.font())width = fm.boundingRect(text).width() + 80# 高度与宽度label.setFixedWidth(width)label.setFixedHeight(40)self.setGeometry(*self.calculatePosition(label.sizeHint()))self.fadeIn()self.animationTimer = QTimer()self.animationTimer.timeout.connect(self.fadeOut)self.animationTimer.start(self.duration)def calculatePosition(self, sizeHint):if self.parent() is not None:# 如果存在父窗口,计算使Toast窗口相对于父窗口居中的位置x = self.parent().width() / 2 - sizeHint.width()/2y = 10else:# 如果没有父窗口,计算使Toast窗口相对于屏幕居中的位置desktopRect = QApplication.primaryScreen().availableGeometry()x = (desktopRect.width() - sizeHint.width()) // 2y = (desktopRect.height() - sizeHint.height()) // 2return x, y, sizeHint.width(), sizeHint.height()def fadeIn(self):# 创建并设置淡入动画fadeInAnimation = QPropertyAnimation(self, b"windowOpacity", self)fadeInAnimation.setStartValue(0)fadeInAnimation.setEndValue(1)fadeInAnimation.setDuration(500)fadeInAnimation.finished.connect(lambda: print('加载成功'))# 启动淡入动画fadeInAnimation.start()print('in')# 淡出动画def fadeOut(self):print('out')# 停止计时器self.animationTimer.stop()# 断开计时器的超时信号与当前方法的连接self.animationTimer.timeout.disconnect(self.fadeOut)# 创建并设置并行动画组self.parallelAnimation = QParallelAnimationGroup()# 创建并设置不透明度动画self.opacityAnimation = QPropertyAnimation(self, b"windowOpacity")self.opacityAnimation.setStartValue(1.0)self.opacityAnimation.setEndValue(0.0)self.opacityAnimation.setDuration(500)# 创建并设置位置动画self.yAnimation = QPropertyAnimation(self, b"geometry")targetY = self.y() - 10self.yAnimation.setStartValue(self.geometry())self.yAnimation.setEndValue(self.geometry().translated(0, targetY))self.yAnimation.setDuration(500)self.yAnimation.setEasingCurve(QEasingCurve.OutCubic)# 将动画添加到并行动画组中self.parallelAnimation.addAnimation(self.opacityAnimation)self.parallelAnimation.addAnimation(self.yAnimation)# 连接并行动画组的完成信号与关闭窗口的槽self.parallelAnimation.finished.connect(self.close)# 启动动画组self.parallelAnimation.start(QAbstractAnimation.DeleteWhenStopped)def paintEvent(self, event):painter = QPainter(self)painter.fillRect(self.rect(), QColor(0, 0, 0, 0))def mousePressEvent(self, event):pass# 弹窗 输入框 class Ui_InputColor(QWidget):textEntered = Signal(str) # 定义一个带有字符串参数的信号def __init__(self):super().__init__()self.drag_position = Nonedef setupUi(self, Form):Form.setObjectName("Form") # 直接设置对象名称Form.resize(200, 70)# 设置背景颜色和透明度Form.setWindowFlags(Qt.FramelessWindowHint) # 设置无边框窗口Form.setAttribute(Qt.WA_TranslucentBackground) # 设置窗口透明self.bg_gray = QFrame(Form)self.bg_gray.setObjectName(u"bg_gray")self.bg_gray.setGeometry(QRect(0, 0, 200, 65))self.bg_gray.setStyleSheet(u".QFrame{\n" " background-color: rgb(234,236,243);\n" " border-radius:5px;\n" "}")self.bg_gray.setFrameShape(QFrame.StyledPanel)self.bg_gray.setFrameShadow(QFrame.Raised)self.verticalLayout_3 = QVBoxLayout(self.bg_gray)self.verticalLayout_3.setSpacing(0)self.verticalLayout_3.setObjectName(u"verticalLayout_3")self.verticalLayout_3.setContentsMargins(10, 11, 4, 11)self.verticalLayout_2 = QVBoxLayout()# 设置右侧边距为5pxself.verticalLayout_2.setContentsMargins(0, 0, 7, 0)self.verticalLayout_2.setObjectName(u"verticalLayout_2")self.window_top = QHBoxLayout()self.window_top.setSpacing(10)self.window_top.setObjectName(u"window_top")self.window_top.setSizeConstraint(QLayout.SetFixedSize)self.window_top.setContentsMargins(0, 0, 0, 0)self.text_title = QLabel(self.bg_gray)self.text_title.setObjectName(u"text_title")self.text_title.setStyleSheet(u" font-size: 9pt;\n" " font-family: \"\u5fae\u8f6f\u96c5\u9ed1\";\n" " color: #333;\n" "font-weight: bold;")self.window_top.addWidget(self.text_title)self.h_spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)self.window_top.addItem(self.h_spacer)self.btn_min = QPushButton(self.bg_gray)self.btn_min.setObjectName(u"btn_min")sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)sizePolicy.setHorizontalStretch(0)sizePolicy.setVerticalStretch(0)sizePolicy.setHeightForWidth(self.btn_min.sizePolicy().hasHeightForWidth())self.btn_min.setSizePolicy(sizePolicy)self.btn_min.setMaximumSize(QSize(10, 10))self.btn_min.setStyleSheet(u"QPushButton {\n" " background-color: #07BB2C;\n" " border: 2px solid #07BB2C;\n" " color: #3498db;\n" " padding: 1px;\n" " border-radius: 5px;\n" "}\n" " \n" "QPushButton:hover {\n" " background-color: #09ED3A;\n" " color: #ffffff;\n" "}")self.window_top.addWidget(self.btn_min)self.btn_max = QPushButton(self.bg_gray)self.btn_max.setObjectName(u"btn_max")self.btn_max.setMaximumSize(QSize(10, 10))self.btn_max.setStyleSheet(u"QPushButton {\n" " background-color: #FFB206;\n" " border: 2px solid #FFB206;\n" " color: #3498db;\n" " padding: 1px;\n" " border-radius: 5px;\n" "}\n" " \n" "QPushButton:hover {\n" " background-color: #FFC033;\n" " color: #ffffff;\n" "}")self.window_top.addWidget(self.btn_max)self.btn_stop = QPushButton(self.bg_gray)self.btn_stop.setObjectName(u"btn_stop")self.btn_stop.clicked.connect(self.hide) # 关联关闭窗口事件self.btn_stop.setMaximumSize(QSize(10, 10))self.btn_stop.setStyleSheet(u"\n" "\n" "QPushButton {\n" " background-color: #EE514A;\n" " border: 2px solid #EE514A;\n" " color: #3498db;\n" " padding: 1px;\n" " border-radius: 5;\n" "}\n" " \n" "QPushButton:hover {\n" " background-color: #F1756F;\n" " color: #ffffff;\n" "}")self.window_top.addWidget(self.btn_stop)self.verticalLayout_2.addLayout(self.window_top)self.horizontalLayout = QHBoxLayout()self.horizontalLayout.setObjectName(u"horizontalLayout")self.lineEdit = QLineEdit(self.bg_gray)self.lineEdit.setObjectName(u"lineEdit")self.horizontalLayout.addWidget(self.lineEdit)self.b_submit = QPushButton(self.bg_gray)self.b_submit.clicked.connect(self.emitSignal) # 点击按钮时连接到发射信号的方法self.b_submit.setObjectName(u"b_submit")self.b_submit.setMinimumSize(QSize(0, 0))self.b_submit.setStyleSheet(u" QPushButton {\n" " border-style: solid;\n" " border-width: 2px;\n" " border-radius: 15px;\n" " border-color: rgb(88, 180, 107);\n" " font-size: 7pt;\n" " font-weight: bold;\n" " padding: 2px;\n" " background-color: rgb(88, 180, 107);\n" " color: rgb(255, 255, 255);\n" " }\n" " QPushButton:hover {\n" " border-color: rgb(100, 100, 100);\n" " background-color: rgb(183, 255, 189);\n" " color: rgb(88, 180, 107);\n" " }\n" " QPushButton:pressed {\n" " border-color: rgb(42, 42, 42);\n" " background-color: rgb(160, 255, 163);\n" " color: rgb(88, 180, 107);\n" " }")self.horizontalLayout.addWidget(self.b_submit)self.verticalLayout_2.addLayout(self.horizontalLayout)self.verticalLayout_3.addLayout(self.verticalLayout_2)self.retranslateUi(Form)QMetaObject.connectSlotsByName(Form)# setupUi@Slot()def emitSignal(self):text = self.lineEdit.text()self.textEntered.emit(text) # 发射信号,并传递lineEdit中的文本def retranslateUi(self, Form):Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None))self.text_title.setText(QCoreApplication.translate("Form", u"\u8bf7\u8f93\u5165\u8272\u53f7", None))self.btn_min.setText("")self.btn_max.setText("")self.btn_stop.setText("")self.lineEdit.setPlaceholderText(QCoreApplication.translate("Form", u"255,255,255", None))self.b_submit.setText(QCoreApplication.translate("Form", u"\u786e\u8ba4", None))# retranslateUidef mousePressEvent(self, event):if event.button() == Qt.LeftButton:self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()event.accept()def mouseMoveEvent(self, event):if event.buttons() == Qt.LeftButton and self.drag_position:self.move(event.globalPosition().toPoint() - self.drag_position)event.accept()def mouseReleaseEvent(self, event):if event.button() == Qt.LeftButton:self.drag_position = Noneevent.accept()# 摄像头检测类 class Camera:def __init__(self, cam_preset_num=5):self.cam_preset_num = cam_preset_numdef get_cam_num(self):cnt = 0devices = []for device in range(0, self.cam_preset_num):stream = cv2.VideoCapture(device, cv2.CAP_DSHOW)grabbed = stream.grab()stream.release()if not grabbed:continueelse:cnt = cnt + 1devices.append(device)return cnt, devices# 摄像头线程 class CameraThread(QThread):change_pixmap_signal = Signal(np.ndarray)def __init__(self, device_id=0):super().__init__()self.device_id = device_idself.is_paused = Falseself.mutex = QMutex()self.exiting = Falseself.cap = Nonedef run(self):self.cap = cv2.VideoCapture(self.device_id)while not self.exiting:self.mutex.lock()if not self.is_paused:ret, frame = self.cap.read()if ret:self.change_pixmap_signal.emit(frame)else:breakself.mutex.unlock()self.cap.release()self.cap = Nonedef set_device_id(self, device_id):self.device_id = device_iddef pause(self):self.mutex.lock()self.is_paused = Trueself.mutex.unlock()def resume(self):self.mutex.lock()self.is_paused = Falseself.mutex.unlock()def stop(self):self.exiting = Trueself.wait()def set_device_id(self, device_id):self.device_id = device_iddef stop(self):self.exiting = True# 主窗口 class MyWindow(QWidget):def __init__(self,text: str,parent: QWidget = None,):super().__init__(parent)# 设置窗口属性self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)self.setAttribute(Qt.WA_TranslucentBackground, True)# 窗口大小self.size = 200self.color = "255,255,255"self.window_shape = 0 # 0是圆# 子线程self.camera_thread = CameraThread()self.camera_thread.change_pixmap_signal.connect(self.image_data_slot)self.camera_thread.start()self.select_camera = Falseself.input_color_window = None# 创建标签并设置样式self.label = QLabel(self)self.label.setText(text)self.reset_style() # 样式刷新self.label.setAlignment(Qt.AlignCenter)# 设置标签尺寸self.label.setAlignment(Qt.AlignCenter)self.label.setFixedSize(self.size, self.size) # 设置标签的宽度和高度# 设置窗口位置self.setGeometry(*self.calculatePosition(self.label.sizeHint()))self.fadeIn()def fadeIn(self):# 创建并设置淡入动画fadeInAnimation = QPropertyAnimation(self, b"windowOpacity", self)fadeInAnimation.setStartValue(0)fadeInAnimation.setEndValue(1)fadeInAnimation.setDuration(500)fadeInAnimation.finished.connect(lambda: print('窗口 加载成功'))# 启动淡入动画fadeInAnimation.start()# 出现在屏幕居中的位置def calculatePosition(self, sizeHint):desktopRect = QApplication.primaryScreen().availableGeometry()x = (desktopRect.width() - sizeHint.width()) // 2y = (desktopRect.height() - sizeHint.height()) // 2return x, y, sizeHint.width(), sizeHint.height()def paintEvent(self, event):painter = QPainter(self)painter.fillRect(self.rect(), QColor(0, 0, 0, 0))def image_data_slot(self, image_data):if self.select_camera is True:self.label.clear()self.label.setText('请重新选择摄像头')returnself.image = cv2.cvtColor(image_data, cv2.COLOR_BGR2RGB)height, width, channel = self.image.shapebytesPerLine = 3 * widthimage = QImage(self.image.data, width, height, bytesPerLine, QImage.Format_RGB888)# 将图片先裁剪为正方形width = image.width()height = image.height()size = min(width, height)square_image = image.copy((width - size) // 2, (height - size) // 2, size, size)# 计算缩放比例new_size = self.size - 10label_size = self.label.sizeHint()scaled_image = square_image.scaled(new_size, new_size, Qt.KeepAspectRatio)# 将 QImage 转换为 QPixmappixmap = QPixmap.fromImage(scaled_image)# 创建一个圆形路径path = QPainterPath()path.addEllipse(0, 0, new_size, new_size)# 创建一个与 scaled_image 大小相同的的 QPixmapresult_pixmap = QPixmap(new_size, new_size)result_pixmap.fill(Qt.transparent)# 在 result_pixmap 上绘制圆形图像painter = QPainter(result_pixmap)if self.window_shape == 0:painter.setClipPath(path) # 设置裁剪路径painter.setRenderHint(QPainter.Antialiasing) # 设置抗锯齿painter.drawPixmap(0, 0, pixmap) # 绘制原始图像painter.end()# 在 label 上显示圆形图像self.label.setPixmap(result_pixmap)def reset_style(self, flag=1):try:if flag == 0:r = 0else:r = self.size / 2self.label.setStyleSheet(f"""font-family: Arial, sans-serif; /* 设置字体 */font-size: 12pt; /* 设置字体大小 */background-color: rgb({self.color}); /* 设置背景颜色为红色 */padding: 0px; border-radius: {r}; /* 设置圆角大小为宽度的一半 */""")except Exception as e:print(e)def mousePressEvent(self, event):if event.button() == Qt.LeftButton:self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()event.accept()# 右键 打开菜单if event.button() == Qt.RightButton:menu = QMenu(self)set_color_action = menu.addAction("设置边框色")set_camera_action = menu.addAction("选择摄像头")set_camera_shape = menu.addAction('圆形窗口') if self.window_shape == 1 else menu.addAction('正方形窗口')set_hide_action = menu.addAction("隐藏")set_quit_action = menu.addAction("退出")# 修改 exec_ 弃用警告action = menu.exec(self.mapToGlobal(event.position().toPoint()))if action == set_color_action:# 打开窗口 Ui_InputColorself.open_input_color_window()# 获取目前所有摄像头的设备号,然后把设备号弄成一个菜单,让用户选择if action == set_camera_action:self.select_camera = True# 如果有摄像头在运行if self.camera_thread.cap is not None:# 这里给我绘制一个标签,等待2s再消失self.toast = Toast("正在关闭摄像头中", 1000, self)self.toast.show()time.sleep(1)self.camera_thread.stop() # 停止摄像头播放return# 检测摄像头可用设备,把设备号弄成一个菜单,让用户选择# 获取本地摄像头数量_, cams = Camera().get_cam_num()popMenu = QMenu()popMenu.setFixedWidth(110)popMenu.setStyleSheet('''QMenu {font-size: 9px;font-family: "Microsoft YaHei UI";font-weight: light;color:white;padding-left: 5px;padding-right: 5px;padding-top: 4px;padding-bottom: 4px;border-style: solid;border-width: 0px;border-color: rgba(255, 212, 255, 255);border-radius: 3px;background-color: rgba(16,155,226,50);}''')for cam in cams:action = QAction(f'{cam} 号摄像头')popMenu.addAction(action)pos = QCursor.pos()action = popMenu.exec(pos)# 设置摄像头来源if action:str_temp = ''selected_stream_source = str_temp.join(filter(str.isdigit, action.text())) # 获取摄像头号,去除非数字字符self.toast = Toast(f"摄像头设备是:{selected_stream_source}", 1000, self)self.toast.show()self.select_camera = Falseself.camera_thread = CameraThread()self.camera_thread.device_id = int(selected_stream_source)self.camera_thread.change_pixmap_signal.connect(self.image_data_slot)self.camera_thread.start()# 切换窗口if action == set_camera_shape:#if self.window_shape == 1:self.window_shape = 0 # 切换为圆形self.reset_style() # 切换为圆形else:self.window_shape = 1self.reset_style(flag=0) # 背景 变成正方形# 隐藏if action == set_hide_action:self.hide()# 退出if action == set_quit_action:self.close()# 颜色设置def colorEntered(self, text):print(1)# TODO 数据处理self.color = textself.reset_style()self.input_color_window.hide()# 打开窗口 Ui_InputColordef open_input_color_window(self):if self.input_color_window is None: # 只有当窗口对象尚未创建时才创建它self.input_color_window = Ui_InputColor()self.input_color_window.setupUi(self.input_color_window)self.input_color_window.setWindowFlag(Qt.FramelessWindowHint) # 设置窗口标志:隐藏窗口边框self.input_color_window.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) # 设置窗口标志位self.input_color_window.textEntered.connect(self.colorEntered) # 连接子窗口的信号和父窗口的槽# 窗口显示在父窗口的中心self.input_color_window.move(self.x() + (self.width() - self.input_color_window.width()) / 2,self.y() + (self.height() - self.input_color_window.height()) / 2)self.input_color_window.show()def mouseMoveEvent(self, event):# 左键 移动创建if event.buttons() == Qt.LeftButton:self.move(event.globalPosition().toPoint() - self.drag_position)event.accept()def mouseReleaseEvent(self, event):self.drag_position = None# 窗口大小变换def row_event(self, up=0, down=0):self.size = self.size + upself.size = self.size - downif self.window_shape == 1:self.reset_style(flag=0) # 背景 变成正方形else:self.reset_style() # 切换为圆形self.label.setFixedSize(self.size, self.size) # 设置标签的宽度和高度self.setFixedSize(self.size, self.size)def event(self, event):if event.type() == QWheelEvent.Wheel:# 阻止默认的滚动行为event.ignore()# 获取滚动方向和距离delta = event.angleDelta().y() / 120# 根据滚动方向和距离执行相应操作if delta < 0:# 向下滚动(触发两次 因为背景上有bugself.row_event(down=1)self.row_event(down=1)else:# 向上滚动(触发两次 因为背景上有bugself.row_event(up=1)self.row_event(up=1)return Truereturn super().event(event)if __name__ == "__main__":app = QApplication([])# 创建系统托盘图标tray_icon = QSystemTrayIcon(QIcon("logo.jpg"), parent=app)tray_icon.setToolTip("Camera By Pan")# 创建并设置托盘图标的右键菜单menu = QMenu()show_action = menu.addAction("出现") # 让window窗口出现hide_action = menu.addAction("隐藏") # 让window窗口隐藏exit_action = menu.addAction("退出")tray_icon.setContextMenu(menu)# 显示托盘图标tray_icon.show()window = MyWindow("Camera Software By Pan")# 使窗口不在任务栏出现window.setWindowFlags(window.windowFlags() | Qt.Tool) # 或者使用 Qt.SubWindow,根据你的需求和测试结果选择# 注意:# 使用Qt.Tool标志会使得窗口不在任务栏显示,这对于某些辅助工具或临时窗口是有用的。# 然而,这种改变也可能影响窗口的关闭行为和资源管理,尤其是在与系统托盘图标交互的上下文中。# 这导致我在关闭子窗口的时候,会导致程序崩溃!!!# 这导致我在关闭子窗口的时候,会导致程序崩溃!!!# 这导致我在关闭子窗口的时候,会导致程序崩溃!!!window.show()# 连接信号到槽函数exit_action.triggered.connect(app.quit)show_action.triggered.connect(window.show)hide_action.triggered.connect(window.hide)app.exec()
五、结语
- 如果你有任何问题,欢迎在评论区留言,我会尽快回复。
- 很久没有碰pyside6,而且学的很浅显,写了屎山代码
- 目前只能保证功能实现,里面还有一些小bug
- 但是不影响正常使用,就这样吧~
PS:其实我只是想自己拿来用用