目錄
一、前言
二、桌面攝像頭軟件
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:其實我只是想自己拿來用用