Files
cmbot/src/app/main_window.py
T

355 lines
11 KiB
Python
Raw Normal View History

import logging
2026-06-15 15:53:01 +08:00
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QHBoxLayout,
QLabel,
QMainWindow,
QPushButton,
QScrollArea,
QSplitter,
QTabBar,
QVBoxLayout,
QWidget,
)
2026-06-15 15:53:01 +08:00
from version import APP_NAME, APP_VERSION
from app.widgets.export_panel import ExportPanel
from app.widgets.image_canvas import ImageCanvas
from app.widgets.image_list_panel import ImageListPanel
from app.widgets.template_panel import TemplatePanel
from app.widgets.transform_panel import TransformPanel
logger = logging.getLogger(__name__)
# Reference widths from docs/07-ui-design.md §3
_LEFT_WIDTH = 374
_RIGHT_WIDTH = 318
_QUEUE_HEIGHT = 188
_HEADER_HEIGHT = 40
_TABBAR_HEIGHT = 34
2026-06-15 15:53:01 +08:00
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("{} v{}".format(APP_NAME, APP_VERSION))
self.resize(1280, 720)
self.setMinimumSize(900, 600)
2026-06-15 15:53:01 +08:00
self._build_ui()
2026-06-15 15:53:01 +08:00
self.statusBar().showMessage("就绪")
logger.info("MainWindow initialized")
# ------------------------------------------------------------------
# Layout construction
# ------------------------------------------------------------------
def _build_ui(self):
root = QWidget()
root.setObjectName("root")
layout = QVBoxLayout(root)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self._create_header())
layout.addWidget(self._create_tab_bar())
layout.addWidget(self._create_work_area(), stretch=1)
layout.addWidget(self._create_queue_panel())
self.setCentralWidget(root)
self._apply_stylesheet()
def _create_header(self):
"""Top header: app name, version label, settings button."""
header = QWidget()
header.setObjectName("header")
header.setFixedHeight(_HEADER_HEIGHT)
row = QHBoxLayout(header)
row.setContentsMargins(12, 0, 12, 0)
row.setSpacing(6)
title = QLabel(APP_NAME)
title.setObjectName("appTitle")
version = QLabel("v{}".format(APP_VERSION))
version.setObjectName("appVersion")
version.setAlignment(Qt.AlignVCenter)
settings_btn = QPushButton("设置")
settings_btn.setObjectName("settingsBtn")
settings_btn.setFixedWidth(60)
row.addWidget(title)
row.addWidget(version)
row.addStretch()
row.addWidget(settings_btn)
return header
def _create_tab_bar(self):
"""Workflow step selector (QTabBar only — no swappable pane)."""
container = QWidget()
container.setObjectName("tabBarContainer")
container.setFixedHeight(_TABBAR_HEIGHT)
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self._tab_bar = QTabBar()
self._tab_bar.setObjectName("flowTabBar")
self._tab_bar.addTab("1 添加印花")
self._tab_bar.addTab("2 AI 穿搭")
self._tab_bar.addTab("3 导出上架")
# Only first tab is implemented in phase 1
self._tab_bar.setTabEnabled(1, False)
self._tab_bar.setTabEnabled(2, False)
self._tab_bar.currentChanged.connect(self._on_tab_changed)
layout.addWidget(self._tab_bar)
layout.addStretch()
return container
def _create_work_area(self):
"""Horizontal splitter: left material panel | canvas | right params."""
splitter = QSplitter(Qt.Horizontal)
splitter.setObjectName("workSplitter")
splitter.setHandleWidth(1)
# Left: material lists (~374 px); task 8 will fill ImageListPanel
self.image_list_panel = ImageListPanel()
self.image_list_panel.setMinimumWidth(200)
splitter.addWidget(self.image_list_panel)
# Center: canvas (QGraphicsView, stretches); task 9 will expand
self.image_canvas = ImageCanvas()
self.image_canvas.setMinimumWidth(400)
splitter.addWidget(self.image_canvas)
# Right: scrollable params column (~318 px)
right_scroll = self._create_right_panel()
right_scroll.setMinimumWidth(200)
splitter.addWidget(right_scroll)
splitter.setSizes([_LEFT_WIDTH, 800, _RIGHT_WIDTH])
splitter.setStretchFactor(0, 0)
splitter.setStretchFactor(1, 1)
splitter.setStretchFactor(2, 0)
self._work_splitter = splitter
return splitter
def _create_right_panel(self):
"""Scroll area containing template, transform and export panels."""
scroll = QScrollArea()
scroll.setObjectName("rightScroll")
scroll.setWidgetResizable(True)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
inner = QWidget()
inner.setObjectName("rightInner")
col = QVBoxLayout(inner)
col.setContentsMargins(0, 0, 0, 0)
col.setSpacing(0)
self.template_panel = TemplatePanel()
self.transform_panel = TransformPanel()
self.export_panel = ExportPanel()
col.addWidget(self.template_panel)
col.addWidget(self.transform_panel)
col.addWidget(self.export_panel)
col.addStretch()
scroll.setWidget(inner)
return scroll
def _create_queue_panel(self):
"""Bottom queue container with collapsible body."""
self._queue_container = QWidget()
self._queue_container.setObjectName("queueContainer")
self._queue_container.setFixedHeight(_QUEUE_HEIGHT)
col = QVBoxLayout(self._queue_container)
col.setContentsMargins(0, 0, 0, 0)
col.setSpacing(0)
col.addWidget(self._create_queue_header())
col.addWidget(self._create_queue_body(), stretch=1)
return self._queue_container
def _create_queue_header(self):
header = QWidget()
header.setObjectName("queueHeader")
header.setFixedHeight(36)
row = QHBoxLayout(header)
row.setContentsMargins(12, 0, 12, 0)
row.setSpacing(6)
title = QLabel("合成队列")
title.setObjectName("queueTitle")
self._collapse_btn = QPushButton("▲")
self._collapse_btn.setObjectName("collapseBtn")
self._collapse_btn.setFixedWidth(28)
self._collapse_btn.clicked.connect(self._toggle_queue)
row.addWidget(title)
row.addStretch()
row.addWidget(self._collapse_btn)
return header
def _create_queue_body(self):
self._queue_body = QWidget()
self._queue_body.setObjectName("queueBody")
body_layout = QVBoxLayout(self._queue_body)
body_layout.setContentsMargins(12, 8, 12, 8)
placeholder = QLabel("合成队列(实现中)")
placeholder.setAlignment(Qt.AlignCenter)
placeholder.setObjectName("queuePlaceholder")
body_layout.addWidget(placeholder)
self._queue_expanded = True
return self._queue_body
# ------------------------------------------------------------------
# Slots
# ------------------------------------------------------------------
def _on_tab_changed(self, index):
"""Keep focus on tab 0; notify user for unimplemented tabs."""
if index != 0:
self._tab_bar.setCurrentIndex(0)
self.statusBar().showMessage("该功能暂未开放")
def _toggle_queue(self):
self._queue_expanded = not self._queue_expanded
self._queue_body.setVisible(self._queue_expanded)
if self._queue_expanded:
self._queue_container.setFixedHeight(_QUEUE_HEIGHT)
self._collapse_btn.setText("▲")
else:
self._queue_container.setFixedHeight(36)
self._collapse_btn.setText("▼")
# ------------------------------------------------------------------
# Style (ref: docs/07-ui-design.md §10)
# ------------------------------------------------------------------
def _apply_stylesheet(self):
self.setStyleSheet("""
QMainWindow, #root {
background-color: #f0f0f0;
}
/* Header */
#header {
background-color: #ffffff;
border-bottom: 1px solid #d6d6d6;
}
#appTitle {
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
font-size: 14px;
font-weight: bold;
color: #1a1a1a;
}
#appVersion {
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
font-size: 11px;
color: #888888;
}
#settingsBtn {
border: 1px solid #d6d6d6;
border-radius: 3px;
padding: 3px 8px;
background: #f7f7f7;
font-size: 12px;
}
#settingsBtn:hover { background: #e8e8e8; }
/* Flow tab bar */
#tabBarContainer {
background-color: #f0f0f0;
border-bottom: 1px solid #d6d6d6;
}
QTabBar#flowTabBar::tab {
padding: 6px 28px;
border: none;
border-bottom: 2px solid transparent;
background: transparent;
color: #555555;
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
font-size: 13px;
}
QTabBar#flowTabBar::tab:selected {
color: #0078d4;
border-bottom: 2px solid #0078d4;
}
QTabBar#flowTabBar::tab:!selected:!disabled:hover {
color: #0078d4;
background: #e8f0fb;
}
QTabBar#flowTabBar::tab:disabled {
color: #bbbbbb;
}
/* Work splitter */
QSplitter#workSplitter::handle {
background-color: #d6d6d6;
width: 1px;
}
/* Right scroll */
#rightScroll {
border: none;
background-color: #f7f7f7;
}
#rightInner {
background-color: #f7f7f7;
}
/* Queue */
#queueContainer {
background-color: #ffffff;
border-top: 1px solid #d6d6d6;
}
#queueHeader {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d6d6;
}
#queueTitle {
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
font-size: 13px;
font-weight: bold;
color: #1a1a1a;
}
#collapseBtn {
border: none;
background: transparent;
color: #555555;
font-size: 10px;
}
#collapseBtn:hover { color: #0078d4; }
#queueBody { background-color: #ffffff; }
#queuePlaceholder {
color: #aaaaaa;
font-size: 12px;
}
/* Status bar */
QStatusBar {
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
font-size: 12px;
background-color: #f0f0f0;
border-top: 1px solid #d6d6d6;
}
""")