Files
ant-vision-inspector/gui/main_window.py
2026-06-10 16:18:41 +09:00

362 lines
13 KiB
Python

# 메인 윈도우 — 1920x1080 전체화면, 다크 테마, 4탭 네비게이션
from PyQt5.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QStackedWidget, QLabel, QSizePolicy,
)
from PyQt5.QtCore import Qt, QPoint, QRect, QElapsedTimer
from PyQt5.QtGui import QPainter, QPolygon, QColor, QFont
from gui.pages.settings_page import SettingsPage
from gui.pages.register_page import RegisterPage
from gui.pages.inspect_page import InspectPage
from gui.pages.retrain_page import RetrainPage
from ai.detector import Detector
from logic.pattern_matcher import PatternMatcher
from db.sql_client import SQLClient
_DOT_OK = ("background:#22cc55; border-radius:7px;"
"min-width:14px; max-width:14px; min-height:14px; max-height:14px;")
_DOT_FAIL = ("background:#cc2222; border-radius:7px;"
"min-width:14px; max-width:14px; min-height:14px; max-height:14px;")
class ChevronTabButton(QPushButton):
# 우측이 ">" 화살표 모양인 breadcrumb/stepper 스타일 탭 버튼
CHEVRON_W = 26 # 화살표 뾰족한 부분 너비(px)
COLOR_ACTIVE_BG = QColor("#0055cc")
COLOR_ACTIVE_FG = QColor("#ffffff")
COLOR_HOVER_BG = QColor("#2e2e2e")
COLOR_HOVER_FG = QColor("#ffffff")
COLOR_IDLE_BG = QColor("#222222")
COLOR_IDLE_FG = QColor("#aaaaaa")
def __init__(self, text: str, is_first: bool = False,
is_last: bool = False, parent=None):
super().__init__(text, parent)
self._is_first = is_first
self._is_last = is_last
self._active = False
self._hover = False
self.setMinimumHeight(60)
self.setCursor(Qt.PointingHandCursor)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setFlat(True)
self.setAttribute(Qt.WA_Hover, True)
def setActive(self, active: bool):
self._active = active
self.update()
def enterEvent(self, event):
self._hover = True
self.update()
super().enterEvent(event)
def leaveEvent(self, event):
self._hover = False
self.update()
super().leaveEvent(event)
def paintEvent(self, event):
if self._active:
bg, fg = self.COLOR_ACTIVE_BG, self.COLOR_ACTIVE_FG
elif self._hover:
bg, fg = self.COLOR_HOVER_BG, self.COLOR_HOVER_FG
else:
bg, fg = self.COLOR_IDLE_BG, self.COLOR_IDLE_FG
w, h = self.width(), self.height()
cw = self.CHEVRON_W
left_cw = 0 if self._is_first else cw
right_cw = 0 if self._is_last else cw
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
p.setPen(Qt.NoPen)
p.setBrush(bg)
# 시계방향: 좌상 → 우상 → (우측 ▶) → 우하 → 좌하 → (좌측 V)
pts = [QPoint(0, 0)]
if self._is_last:
pts += [QPoint(w, 0), QPoint(w, h)]
else:
pts += [QPoint(w - cw, 0), QPoint(w, h // 2), QPoint(w - cw, h)]
pts.append(QPoint(0, h))
if not self._is_first:
pts.append(QPoint(cw, h // 2))
p.drawPolygon(QPolygon(pts))
p.setPen(fg)
font = QFont(self.font())
font.setPointSize(18)
font.setBold(self._active)
p.setFont(font)
text_rect = QRect(left_cw, 0, w - left_cw - right_cw, h)
p.drawText(text_rect, Qt.AlignCenter, self.text())
class MainWindow(QMainWindow):
def __init__(self, insight_cam, basler_cam, config: dict, plc_client=None):
super().__init__()
self.insight = insight_cam
self.basler = basler_cam
self.config = config
self.detector = Detector()
self.matcher = PatternMatcher()
self.matcher.load() # 앱 시작 시 저장된 패턴 자동 로드
self.db_client = SQLClient()
self.plc_client = plc_client
self.setWindowTitle("비전 검사 시스템")
self.showFullScreen()
# 재학습 탭 연속 클릭(창 최소화 단축) 감지용
self._retrain_click_timer = QElapsedTimer()
self._retrain_click_timer.start()
self._build_ui()
self._switch_tab(0)
self._auto_connect_db()
self.update_connection_status()
# ================================================================== #
# UI 구성
# ================================================================== #
def _build_ui(self):
root = QWidget()
self.setCentralWidget(root)
layout = QVBoxLayout(root)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self._build_tab_bar())
layout.addWidget(self._build_stack(), stretch=1)
layout.addWidget(self._build_status_bar())
def _build_tab_bar(self) -> QWidget:
bar = QWidget()
bar.setFixedHeight(60)
# 화살표 모양 사이의 빈 삼각형 영역에서 보일 배경
bar.setStyleSheet("background:#1a1a1a;")
row = QHBoxLayout(bar)
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(0)
labels = ["환경설정", "제품 등록", "검사", "재학습"]
self._tab_btns = []
for i, label in enumerate(labels):
btn = ChevronTabButton(
label,
is_first=(i == 0),
is_last=(i == len(labels) - 1),
)
btn.clicked.connect(lambda _, idx=i: self._switch_tab(idx))
self._tab_btns.append(btn)
row.addWidget(btn)
return bar
def _build_stack(self) -> QStackedWidget:
self._stack = QStackedWidget()
# belt_delay = 거리 / 속도 (config 기본값 사용)
_conv = self.config.get("conveyor", {})
_dist = _conv.get("distance_cm", 100.0)
_speed = _conv.get("speed_cms", 30.0)
_belt_delay = _dist / _speed if _speed > 0 else 3.33
self._settings_page = SettingsPage(
self.insight, self.basler, self.config,
detector=self.detector,
update_insight_cb=self.update_insight,
update_basler_cb=self.update_basler,
update_detector_cb=self.update_detector,
update_db_cb=self.update_db,
update_plc_cb=self.update_plc,
plc_client=self.plc_client,
)
self._settings_page.cognex_status_changed.connect(
lambda _: self.update_connection_status()
)
self._settings_page.basler_status_changed.connect(
lambda _: self.update_connection_status()
)
self._settings_page.plc_status_changed.connect(
lambda _: self.update_connection_status()
)
self._register_page = RegisterPage(
self.insight, matcher=self.matcher, db_client=self.db_client
)
self._inspect_page = InspectPage(
self.insight, self.basler,
detector=self.detector,
belt_delay=_belt_delay,
)
self._inspect_page.update_matcher(self.matcher)
self._settings_page.belt_settings_changed.connect(
self._inspect_page.update_belt_delay
)
self._pages = [
self._settings_page,
self._register_page,
self._inspect_page,
RetrainPage(),
]
for page in self._pages:
self._stack.addWidget(page)
return self._stack
def _build_status_bar(self) -> QWidget:
bar = QWidget()
bar.setFixedHeight(36)
bar.setStyleSheet("background:#111111;")
row = QHBoxLayout(bar)
row.setContentsMargins(16, 0, 16, 0)
row.setSpacing(20)
self._dot_cognex = self._make_dot(self.insight.is_connected())
self._dot_basler = self._make_dot(
self.basler.is_connected() if self.basler else False
)
self._dot_db = self._make_dot(False)
self._dot_plc = self._make_dot(
bool(self.plc_client and self.plc_client.is_connected())
)
self._lbl_cognex = QLabel("코그넥스")
self._lbl_basler = QLabel("Basler")
self._lbl_db = QLabel("DB")
self._lbl_plc = QLabel("PLC")
for lbl in (self._lbl_cognex, self._lbl_basler, self._lbl_db, self._lbl_plc):
lbl.setStyleSheet("color:#aaaaaa; font-size:13px;")
row.addWidget(self._dot_cognex)
row.addWidget(self._lbl_cognex)
row.addWidget(self._dot_basler)
row.addWidget(self._lbl_basler)
row.addWidget(self._dot_db)
row.addWidget(self._lbl_db)
row.addWidget(self._dot_plc)
row.addWidget(self._lbl_plc)
row.addStretch()
return bar
@staticmethod
def _make_dot(connected: bool) -> QLabel:
dot = QLabel()
dot.setStyleSheet(_DOT_OK if connected else _DOT_FAIL)
return dot
# ================================================================== #
# 탭 전환
# ================================================================== #
def _switch_tab(self, idx: int):
# 재학습 탭(3)을 600ms 이내에 연속으로 두 번 클릭하면 창 최소화
if idx == 3:
if (self._stack.currentIndex() == 3
and self._retrain_click_timer.elapsed() < 600):
self.showMinimized()
self._retrain_click_timer.restart()
return
self._retrain_click_timer.restart()
self._stack.setCurrentIndex(idx)
for i, btn in enumerate(self._tab_btns):
btn.setActive(i == idx)
if idx == 0:
self._settings_page._sync_connection_status()
self.update_connection_status()
# ================================================================== #
# 코그넥스 인스턴스 교체 (SettingsPage 연결 성공 시 호출)
# ================================================================== #
def update_insight(self, new_insight):
self.insight = new_insight
self._register_page._insight = new_insight
self._inspect_page.update_insight(new_insight)
def update_basler(self, new_basler):
self.basler = new_basler
self._inspect_page.update_basler(new_basler)
def update_detector(self, new_detector):
self.detector = new_detector
if hasattr(self, "_inspect_page"):
self._inspect_page.update_detector(new_detector)
def update_db(self, db_client):
self.db_client = db_client
connected = db_client is not None and db_client.is_connected()
if hasattr(self, "_register_page"):
self._register_page.update_db(db_client)
if hasattr(self, "_settings_page"):
self._settings_page._db_client = db_client
self._settings_page._set_db_connected(connected)
self.update_connection_status()
def update_plc(self, plc_client):
self.plc_client = plc_client
connected = plc_client is not None and plc_client.is_connected()
if hasattr(self, "_settings_page"):
self._settings_page._plc_client = plc_client
self._settings_page._set_plc_connected(connected)
self.update_connection_status()
def _auto_connect_db(self):
"""앱 시작 시 config.json DB 접속 정보로 자동 연결 시도."""
db_cfg = self.config.get("db", {})
server = db_cfg.get("server", "").strip()
database = db_cfg.get("database", "").strip()
username = db_cfg.get("username", "").strip()
password = db_cfg.get("password", "")
if not server or not database:
return
ok = self.db_client.connect(server, database, username, password)
if ok:
self.update_db(self.db_client)
# ================================================================== #
# 실제 연결 상태를 읽어 상태바 전체 갱신
# ================================================================== #
def update_connection_status(self):
cognex_ok = bool(self.insight and self.insight.is_connected())
self._dot_cognex.setStyleSheet(_DOT_OK if cognex_ok else _DOT_FAIL)
self._lbl_cognex.setText("코그넥스 연결됨" if cognex_ok else "코그넥스 연결 안됨")
self._lbl_cognex.setStyleSheet(
"color:#1D9E75; font-size:13px; font-weight:bold;" if cognex_ok
else "color:#aaaaaa; font-size:13px;"
)
basler_ok = bool(self.basler and self.basler.is_connected())
self._dot_basler.setStyleSheet(_DOT_OK if basler_ok else _DOT_FAIL)
self._lbl_basler.setText("Basler 연결됨" if basler_ok else "Basler 연결 안됨")
self._lbl_basler.setStyleSheet(
"color:#1D9E75; font-size:13px; font-weight:bold;" if basler_ok
else "color:#aaaaaa; font-size:13px;"
)
db_ok = bool(self.db_client and self.db_client.is_connected())
self._dot_db.setStyleSheet(_DOT_OK if db_ok else _DOT_FAIL)
self._lbl_db.setText("DB 연결됨" if db_ok else "DB 연결 안됨")
self._lbl_db.setStyleSheet(
"color:#1D9E75; font-size:13px; font-weight:bold;" if db_ok
else "color:#aaaaaa; font-size:13px;"
)
plc_ok = bool(self.plc_client and self.plc_client.is_connected())
self._dot_plc.setStyleSheet(_DOT_OK if plc_ok else _DOT_FAIL)
self._lbl_plc.setText("PLC 연결됨" if plc_ok else "PLC 연결 안됨")
self._lbl_plc.setStyleSheet(
"color:#1D9E75; font-size:13px; font-weight:bold;" if plc_ok
else "color:#aaaaaa; font-size:13px;"
)