diff --git a/src/app/main_window.py b/src/app/main_window.py index e187778..b330ee5 100644 --- a/src/app/main_window.py +++ b/src/app/main_window.py @@ -1,7 +1,33 @@ +import logging + from PySide6.QtCore import Qt -from PySide6.QtWidgets import QLabel, QMainWindow +from PySide6.QtWidgets import ( + QHBoxLayout, + QLabel, + QMainWindow, + QPushButton, + QScrollArea, + QSplitter, + QTabBar, + QVBoxLayout, + QWidget, +) 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 class MainWindow(QMainWindow): @@ -9,9 +35,320 @@ class MainWindow(QMainWindow): super().__init__() self.setWindowTitle("{} v{}".format(APP_NAME, APP_VERSION)) self.resize(1280, 720) + self.setMinimumSize(900, 600) - label = QLabel("自动合成印花服饰效果图工具\n\n工程骨架已创建") - label.setAlignment(Qt.AlignCenter) - self.setCentralWidget(label) - + self._build_ui() 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; + } + """) diff --git a/tasks.md b/tasks.md index 0fe40bc..8ce89f1 100644 --- a/tasks.md +++ b/tasks.md @@ -264,21 +264,21 @@ 任务: -- [ ] 先读取 `src/app/main_window.py` 现有内容 -- [ ] 将 `src/app/main_window.py` 从最小窗口扩展为主布局 -- [ ] 实现顶部标题和版本号 -- [ ] 实现流程页签区域 -- [ ] 实现左侧素材栏容器 -- [ ] 实现中间预览区容器 -- [ ] 实现右侧参数栏容器 -- [ ] 实现底部合成队列容器 -- [ ] 实现底部状态栏 +- [x] 先读取 `src/app/main_window.py` 现有内容 +- [x] 将 `src/app/main_window.py` 从最小窗口扩展为主布局 +- [x] 实现顶部标题和版本号 +- [x] 实现流程页签区域 +- [x] 实现左侧素材栏容器 +- [x] 实现中间预览区容器 +- [x] 实现右侧参数栏容器 +- [x] 实现底部合成队列容器 +- [x] 实现底部状态栏 验收: -- [ ] 窗口标题显示 `APP_NAME` 和 `APP_VERSION` -- [ ] UI 布局接近 `docs/ui-v1.png` -- [ ] 不把所有业务逻辑塞进 `main_window.py` +- [x] 窗口标题显示 `APP_NAME` 和 `APP_VERSION` +- [x] UI 布局接近 `docs/ui-v1.png` +- [x] 不把所有业务逻辑塞进 `main_window.py` ## 8. 素材列表 UI