DataFlowing / collector /ui /camera_widget.py
stouchinglhh
collector: ๅŒๆญฅ้‡‡้›†ๅทฅ็จ‹ v1.2.1๏ผˆๅ…จ้ƒจ LFS ๆจกๅผๆ–‡ไปถๆŒ‡้’ˆๅŒ–๏ผŒไธŽ Space ็Žฐๆœ‰ๆ–‡ไปถๅˆๅนถ๏ผ‰
d646dd7
Raw History Blame Contribute Delete
15.5 kB
"""
ๆ‘„ๅƒๆœบ็”ป้ขๆŽงไปถ โ€”โ€” ๆ˜พ็คบไธ€่ทฏๆ‘„ๅƒๆœบ็š„ๅฎžๆ—ถ็”ป้ขๅ’Œๅฝ•ๅˆถๆŽงๅˆถๆŒ‰้’ฎใ€‚
็”ป้ขๆ”ฏๆŒ้ผ ๆ ‡ๆ“ไฝœ:
- ๆปš่ฝฎ็ผฉๆ”พ๏ผˆไปฅ็”ป้ขไธญๅฟƒไธบๅŸบๅ‡†๏ผ‰
- ๅทฆ้”ฎๆ‹–ๆ‹ฝๅนณ็งป
- ๅŒๅ‡ป่ฟ˜ๅŽŸ๏ผˆ้‡็ฝฎ็ผฉๆ”พๅ’Œๅนณ็งป๏ผ‰
"""
from __future__ import annotations
from typing import Optional
import cv2
import numpy as np
from PyQt5.QtCore import Qt, pyqtSignal, QPointF, QRectF
from PyQt5.QtGui import (
QImage, QPixmap, QFont, QColor, QPainter,
)
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFrame, QSizePolicy, QToolButton,
)
from config import settings
from config.i18n import tr, lang_manager
from core.helpers import format_duration
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๅฏ็ผฉๆ”พๆ‹–ๆ‹ฝ็š„่ง†้ข‘ๆ˜พ็คบๆŽงไปถ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
class ZoomableVideoWidget(QWidget):
"""
ๆ”ฏๆŒ้ผ ๆ ‡ไบคไบ’็š„่ง†้ข‘็”ป้ขๆŽงไปถใ€‚
ๆ“ไฝœๆ–นๅผ:
- ๆปš่ฝฎ โ†’ ไปฅ็”ป้ขไธญๅฟƒไธบๅŸบๅ‡†็ผฉๆ”พ๏ผˆ1.1ร— / 0.9ร— ๆฏ็บง๏ผ‰
- ๆ‹–ๆ‹ฝ โ†’ ๅนณ็งป็”ป้ข
- ๅŒๅ‡ป โ†’ ่ฟ˜ๅŽŸๅˆฐ้€‚ๅบ”็ช—ๅฃๅคงๅฐ
็ผฉๆ”พ่Œƒๅ›ด: 0.1ร— ~ 10ร—
"""
ZOOM_MIN = 0.25
ZOOM_MAX = 8.0
ZOOM_STEP = 1.08 # ๆฏ็บงๆปš่ฝฎๅ€็އ๏ผˆๆ›ดๅนณๆป‘๏ผ‰
def __init__(self, parent: QWidget = None):
super().__init__(parent)
self._pixmap: Optional[QPixmap] = None
self._zoom = 1.0
self._offset = QPointF(0, 0)
self._dragging = False
self._last_mouse_pos = QPointF()
self._status_text = tr("ๆ— ไฟกๅท")
self._has_frame = False
# ็ผฉๆ”พ็”ต้‡ๆ็คบ๏ผˆๅณไธ‹่ง’็Ÿญๆš‚ๆ˜พ็คบ๏ผ‰
self._show_zoom_hint = False
self._zoom_hint_timer = 0
self.setMouseTracking(True)
self.setCursor(Qt.OpenHandCursor)
self.setMinimumSize(160, 120)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setStyleSheet("background:#000000; border:none;")
# โ”€โ”€ ๅ…ฌๅผ€ๆŽฅๅฃ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def reset_view(self):
"""Reset zoom and pan to default."""
self._zoom = 1.0
self._offset = QPointF(0, 0)
self.update()
def set_frame(self, bgr_frame: np.ndarray, flip_vertical: bool = False):
"""ไผ ๅ…ฅ BGR ๅธงๅนถ่งฆๅ‘้‡็ป˜ใ€‚
flip_vertical: ๆ˜ฏๅฆไธŠไธ‹็ฟป่ฝฌ็”ป้ข๏ผˆๆ‘„ๅƒๆœบ่ง†้ข‘้œ€่ฆ๏ผŒไผ ๆ„Ÿๅ™จๆธฒๆŸ“ไธ้œ€่ฆ๏ผ‰ใ€‚
"""
try:
if flip_vertical:
bgr_frame = cv2.flip(bgr_frame, 0)
# BGR โ†’ RGB ๅŽ็”จ Format_RGB888๏ผˆFormat_BGR888 ๅœจ Qt x86 ไธŠ็บข่“ไบ’ๆข๏ผ‰
rgb_frame = cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_frame.shape
qimg = QImage(rgb_frame.data, w, h, w * ch, QImage.Format_RGB888)
self._pixmap = QPixmap.fromImage(qimg)
self._has_frame = True
except Exception:
pass
if self._show_zoom_hint:
self._zoom_hint_timer -= 1
if self._zoom_hint_timer <= 0:
self._show_zoom_hint = False
self.update()
def set_status_text(self, text: str):
"""่ฎพ็ฝฎๆ— ็”ป้ขๆ—ถๆ˜พ็คบ็š„ๆ–‡ๅญ—ใ€‚"""
self._status_text = text
self._has_frame = False
self._pixmap = None
self.update()
def reset_view(self):
"""่ฟ˜ๅŽŸ็ผฉๆ”พๅ’Œๅนณ็งปๅˆฐๅˆๅง‹็Šถๆ€ใ€‚"""
self._zoom = 1.0
self._offset = QPointF(0, 0)
self.update()
# โ”€โ”€ ็ป˜ๅˆถ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def paintEvent(self, _event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
ww = self.width()
wh = self.height()
if self._pixmap is None or not self._has_frame:
painter.fillRect(self.rect(), Qt.black)
painter.setPen(QColor("#888888"))
painter.setFont(QFont("Microsoft YaHei", 12))
painter.drawText(self.rect(), Qt.AlignCenter, self._status_text)
return
pw = self._pixmap.width()
ph = self._pixmap.height()
base_scale = min(ww / pw, wh / ph) if pw > 0 and ph > 0 else 1.0
scale = base_scale * self._zoom
sw = pw * scale
sh = ph * scale
cx = (ww - sw) / 2.0 + self._offset.x()
cy = (wh - sh) / 2.0 + self._offset.y()
target = QRectF(cx, cy, sw, sh)
source = QRectF(0, 0, pw, ph)
painter.drawPixmap(target, self._pixmap, source)
# ็Ÿญๆš‚็š„็ผฉๆ”พ็™พๅˆ†ๆฏ”ๆ็คบ๏ผˆๅณไธ‹่ง’๏ผ‰
if self._show_zoom_hint:
pct = int(self._zoom * 100)
hint = f"{pct}%"
painter.setPen(Qt.NoPen)
painter.setBrush(QColor(0, 0, 0, 160))
fm = painter.fontMetrics()
tw = fm.horizontalAdvance(hint) + 16
th = fm.height() + 8
painter.drawRoundedRect(
QRectF(ww - tw - 12, wh - th - 12, tw, th), 6, 6
)
painter.setPen(QColor("#FFFFFF"))
painter.setFont(QFont("Segoe UI", 11, QFont.Bold))
painter.drawText(
QRectF(ww - tw - 12, wh - th - 12, tw, th),
Qt.AlignCenter, hint
)
# โ”€โ”€ ้ผ ๆ ‡ไบ‹ไปถ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def wheelEvent(self, event):
"""ๆปš่ฝฎ็ผฉๆ”พ โ€”โ€” ไปฅ้ผ ๆ ‡ไฝ็ฝฎไธบๅŸบๅ‡†็‚น็ผฉๆ”พใ€‚"""
if self._pixmap is None or not self._has_frame:
return
mouse_pos = QPointF(event.pos())
old_zoom = self._zoom
if event.angleDelta().y() > 0:
self._zoom = min(self.ZOOM_MAX, self._zoom * self.ZOOM_STEP)
else:
self._zoom = max(self.ZOOM_MIN, self._zoom / self.ZOOM_STEP)
if abs(self._zoom - old_zoom) < 1e-6:
return
# ไปฅ้ผ ๆ ‡ไฝ็ฝฎไธบๅŸบๅ‡†่ฐƒๆ•ดๅ็งป
ratio = self._zoom / old_zoom
self._offset = mouse_pos - ratio * (mouse_pos - self._offset)
# ๆ˜พ็คบ็ผฉๆ”พ็™พๅˆ†ๆฏ”ๆ็คบ๏ผˆๆŒ็ปญ็บฆ 1 ็ง’๏ผ‰
self._show_zoom_hint = True
self._zoom_hint_timer = 30 # ~30 ๅธง โ‰ˆ 1 ็ง’
self.update()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self._dragging = True
self._last_mouse_pos = QPointF(event.pos())
self.setCursor(Qt.ClosedHandCursor)
event.accept()
def mouseMoveEvent(self, event):
if self._dragging:
delta = QPointF(event.pos()) - self._last_mouse_pos
self._offset += delta
self._last_mouse_pos = QPointF(event.pos())
self.update()
event.accept()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton and self._dragging:
self._dragging = False
self.setCursor(Qt.OpenHandCursor)
event.accept()
def mouseDoubleClickEvent(self, event):
"""ๅŒๅ‡ป่ฟ˜ๅŽŸ็ผฉๆ”พๅ’Œๅนณ็งปใ€‚"""
if event.button() == Qt.LeftButton:
self.reset_view()
event.accept()
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๆ‘„ๅƒๆœบ้ขๆฟ๏ผˆ็ป„ๅˆ่ง†้ข‘ๆŽงไปถ + ๅฝ•ๅˆถๆŽงๅˆถๆ ๏ผ‰
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
class CameraWidget(QFrame):
"""
ๅ•่ทฏๆ‘„ๅƒๆœบ้ขๆฟ๏ผš่ง†้ข‘็”ป้ข + ็Šถๆ€่ฆ†็›–ๆกใ€‚
ไฟกๅท
----
remove_requested(str) โ€” ่ฏทๆฑ‚็งป้™คๆญคๆ‘„ๅƒๆœบ
exposure_clicked(str) โ€” ็‚นๅ‡ปๆ›ๅ…‰ๆŒ‰้’ฎ๏ผˆไธป็ช—ๅฃๆŒ‰ๆงฝไฝๆ‰€ๅฑž่ฎพๅค‡ๅผนๅฏน่ฏๆก†๏ผ‰
"""
remove_requested = pyqtSignal(str)
exposure_clicked = pyqtSignal(str)
def __init__(self, slot_id: str, camera_name: str = "",
parent: QWidget = None):
super().__init__(parent)
self.slot_id = slot_id
self.camera_name = camera_name or f"Camera {slot_id}"
self._fps = 0.0
self._setup_ui()
# ็›‘ๅฌ่ฏญ่จ€ๅˆ‡ๆข
lang_manager.language_changed.connect(self._refresh_texts)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# UI ๆž„ๅปบ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def _setup_ui(self):
self.setMinimumSize(settings.FEED_MIN_WIDTH, settings.FEED_MIN_HEIGHT)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setStyleSheet(f"background:{settings.COLOR_BG_WIDGET}; "
f"border:1px solid {settings.COLOR_BORDER};")
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(2, 2, 2, 2)
main_layout.setSpacing(0)
# โ”€โ”€ ่ง†้ข‘ๆ˜พ็คบๅŒบๅŸŸ๏ผˆๅฏ็ผฉๆ”พๆ‹–ๆ‹ฝ๏ผ‰ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
self.video_widget = ZoomableVideoWidget()
main_layout.addWidget(self.video_widget, 1)
# โ”€โ”€ ่ฆ†็›–ๆก๏ผˆ่ง†้ข‘็”ป้ข้กถ้ƒจ๏ผŒๅง‹็ปˆๅ›บๅฎšๅœจๅทฆไธŠ่ง’๏ผ‰ โ”€โ”€โ”€
self.overlay = QFrame(self.video_widget)
self.overlay.setStyleSheet(
"background:rgba(0,0,0,160); border-radius:4px;"
)
self.overlay.setFixedHeight(28)
# ๆณจๆ„๏ผš่ฆ†็›–ๆก็ปไธ่ƒฝ่ฎพ WA_TransparentForMouseEventsโ€”โ€”Qt ่ฏญไน‰ๆ˜ฏ
# ใ€Œ่ฏฅๆŽงไปถๅŠๅ…ถๅ…จ้ƒจๅญๆŽงไปถไธๆŽฅๆ”ถ้ผ ๆ ‡ใ€๏ผŒไผš่ฟž โ˜€ ๆŒ‰้’ฎไธ€่ตทๅฑ่”ฝใ€‚
# ไฟกๆฏๆกๆ‹–ๆ‹ฝ็”ฑ CameraGrid ็š„ไบ‹ไปถ่ฟ‡ๆปคๅ™จ็ปŸไธ€ๆ‹ฆๆˆช๏ผŒๆ— ้œ€็ฉฟ้€ใ€‚
overlay_layout = QHBoxLayout(self.overlay)
overlay_layout.setContentsMargins(8, 0, 8, 0)
overlay_layout.setSpacing(12)
self.name_label = QLabel(self.camera_name)
self.name_label.setStyleSheet(
"color:#FFFFFF; font-weight:bold; font-size:11px; background:transparent;"
)
overlay_layout.addWidget(self.name_label)
# โ˜€ ๆ›ๅ…‰ๆŒ‰้’ฎ๏ผˆ้ป˜่ฎค้š่—๏ผ›ไธป็ช—ๅฃๅชๅฏน่ฎพๅค‡็š„"ไธปๆงฝไฝ"ๆ˜พ็คบใ€‚
# _no_drag ๅฑžๆ€ง่ฎฉ็ฝ‘ๆ ผๆ‹–ๆ‹ฝ่ฟ‡ๆปคๅ™จๆ”พ่กŒ๏ผŒๆŒ‰ไธ‹ไธไผš่งฆๅ‘็”ป้ขๆ‹–ๆ‹ฝ/ๅŒๅ‡ป่ฟ˜ๅŽŸ๏ผ‰
self.exposure_btn = QToolButton()
self.exposure_btn.setText("โ˜€")
self.exposure_btn.setFixedSize(24, 22)
self.exposure_btn.setCursor(Qt.PointingHandCursor)
self.exposure_btn.setToolTip(tr("ๆ›ๅ…‰่ฎพ็ฝฎ"))
self.exposure_btn._no_drag = True
self.exposure_btn.setStyleSheet(
"QToolButton{background:transparent; color:#FFD54F; border:none;"
"font-size:13px; padding:0;}"
"QToolButton:hover{background:rgba(255,255,255,40); border-radius:3px;}"
"QToolButton:disabled{color:#666666;}"
)
self.exposure_btn.clicked.connect(
lambda: self.exposure_clicked.emit(self.slot_id))
self.exposure_btn.setVisible(False)
overlay_layout.addWidget(self.exposure_btn)
overlay_layout.addStretch()
self.fps_label = QLabel("FPS: --")
self.fps_label.setStyleSheet(
"color:#AAAAAA; font-size:10px; background:transparent;"
)
overlay_layout.addWidget(self.fps_label)
self.state_dot = QLabel("โ—")
self.state_dot.setStyleSheet(
f"color:{settings.COLOR_STOPPED}; font-size:10px; background:transparent;"
)
overlay_layout.addWidget(self.state_dot)
self.overlay.move(4, 4)
self.overlay.setFixedWidth(260)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๅ…ฌๅผ€ๆŽฅๅฃ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def update_frame(self, bgr_frame: np.ndarray):
"""็”จ็›ธๆœบ้‡‡้›†็š„ BGR ๅธงๆ›ดๆ–ฐ่ง†้ข‘ๆ˜พ็คบ๏ผˆไธŠไธ‹็ฟป่ฝฌ้€‚้…ๆ‘„ๅƒๅคดๅฎ‰่ฃ…ๆ–นๅ‘๏ผ‰ใ€‚"""
self.video_widget.set_frame(bgr_frame, flip_vertical=True)
def update_fps(self, fps: float):
"""ๆ›ดๆ–ฐ FPS ๆ˜พ็คบใ€‚"""
self._fps = fps
self.fps_label.setText(f"FPS: {fps:.0f}")
def set_frame_number(self, n: int):
"""ๅ›žๆ”พ็”จ๏ผšๅœจไฟกๆฏๆกๅณไพงๆ˜พ็คบๅฝ“ๅ‰ๅธงๅท๏ผˆๅค็”จ FPS ๆ ‡็ญพไฝ๏ผ‰ใ€‚"""
self.fps_label.setText(f"#{n}")
def set_camera_state(self, state: str):
"""ๆ นๆฎ็›ธๆœบ็Šถๆ€ๆ›ดๆ–ฐๆŒ‡็คบ็ฏ้ขœ่‰ฒๅ’Œ่ง†้ข‘ๅŒบๅŸŸๆ–‡ๅญ—ใ€‚"""
if state == "recording":
self.state_dot.setStyleSheet(
f"color:{settings.COLOR_RECORDING}; font-size:10px; background:transparent;"
)
elif state == "error":
self.state_dot.setStyleSheet(
f"color:{settings.COLOR_ABNORMAL}; font-size:10px; background:transparent;"
)
elif state == "disconnected":
self.state_dot.setStyleSheet("color:#BDBDBD; font-size:10px; background:transparent;")
self.video_widget.set_status_text(tr("ๅทฒๆ–ญๅผ€"))
else:
self.state_dot.setStyleSheet(
f"color:{settings.COLOR_STOPPED}; font-size:10px; background:transparent;"
)
if state == "idle":
self.video_widget.set_status_text(tr("็ญ‰ๅพ…ไธญโ€ฆ"))
# โ”€โ”€ ๆ›ๅ…‰ๅ…ฅๅฃ๏ผˆโ˜€ ๆŒ‰้’ฎ๏ผ‰ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def set_exposure_button_visible(self, visible: bool):
"""ๆ˜พ็คบ/้š่—ๆ›ๅ…‰ๆŒ‰้’ฎ๏ผˆๅชๅฏน่ฎพๅค‡็š„"ไธปๆงฝไฝ"ๆ˜พ็คบ๏ผ‰ใ€‚"""
self.exposure_btn.setVisible(visible)
def set_exposure_enabled(self, enabled: bool):
"""ๅฏ็”จ/็ฆ็”จๆ›ๅ…‰ๆŒ‰้’ฎ๏ผˆๅฝ•ๅˆถไธญ็ฆ็”จ๏ผ‰ใ€‚"""
self.exposure_btn.setEnabled(enabled)
self.exposure_btn.setToolTip(
tr("ๆ›ๅ…‰่ฎพ็ฝฎ") if enabled else tr("ๅฝ•ๅˆถไธญไธๅฏ่ฐƒๆ•ดๆ›ๅ…‰"))
def _refresh_texts(self, _lang: str = ""):
"""่ฏญ่จ€ๅˆ‡ๆขๆ—ถๅˆทๆ–ฐๆ–‡ๅญ—ใ€‚"""
self.exposure_btn.setToolTip(
tr("ๆ›ๅ…‰่ฎพ็ฝฎ") if self.exposure_btn.isEnabled()
else tr("ๅฝ•ๅˆถไธญไธๅฏ่ฐƒๆ•ดๆ›ๅ…‰"))
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ไบ‹ไปถ้‡ๅ†™
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def resizeEvent(self, event):
"""็ช—ๅฃๅคงๅฐๅ˜ๅŒ–ๆ—ถ่ฐƒๆ•ด่ฆ†็›–ๆกไฝ็ฝฎๅ’Œๅฎฝๅบฆใ€‚"""
super().resizeEvent(event)
self.overlay.setFixedWidth(min(self.video_widget.width() - 8, 300))
self.overlay.move(4, 4)