feat(brand): add application logo for exe and window icon

新增品牌标识并接入运行时与打包流程。

- `scripts/gen_logo.py` 从单一几何定义同时产出 SVG 母版、多尺寸 ICO 和
  PNG 预览,避免母版与位图漂移;改样式改脚本后重新生成即可。
- 标记为暖橙圆角徽章 + 白色上升箭头(提升并发布)+ 四角星芒(AI 生成),
  刻意避开蝦皮官方购物袋标识,避免暗示官方关联。
- ICO 含 16/20/24/32/40/48/64/128/256 九档,每档从矢量几何独立渲染而非
  由大图缩放,保证任务栏与资源管理器小尺寸清晰。
- `app/assets` 作为可导入包提供无 Qt 依赖的路径解析,兼容源码树、
  PyInstaller onedir 的 _MEIPASS 与 exe 同级目录三种布局。
- `widgets.app_icon()` 在 Qt 缺失或资源未生成时返回 None,调用方容错;
  在 QApplication 上设置图标以便任务栏、对话框和消息框一并继承,
  MainWindow 另行设置以覆盖直接构造窗口的测试路径。
- 两个 spec 均加 `icon=`,主 spec 收集资源到 datas 并在资源缺失时直接
  报错提示先跑生成脚本。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
chengma
2026-07-20 16:39:18 +08:00
co-authored by Claude Fable 5
parent 6a0cd1c763
commit 049d4b6bbc
11 changed files with 291 additions and 1 deletions
+5
View File
@@ -122,6 +122,11 @@ def main() -> int:
return 1
_ensure_offscreen_for_headless_tests()
app = QApplication.instance() or QApplication(sys.argv)
# Set on the application so the taskbar button, dialogs and message boxes
# all inherit it, not just the main window.
startup_icon = app_icon()
if startup_icon is not None:
app.setWindowIcon(startup_icon)
health_context = update_health.context_from_argv(
sys.argv,
appconfig.app_base_dir(),
+3
View File
@@ -98,6 +98,9 @@ class MainWindow(QMainWindow):
or appconfig.ai_models_config_path(self.config)
)
self.setWindowTitle(display_name())
window_icon = app_icon()
if window_icon is not None:
self.setWindowIcon(window_icon)
_fit_and_center_window(self)
self.setStyleSheet(BUTTON_BASE_STYLE)
self._settings_tab_index = TAB_TITLES.index("设置")
+18
View File
@@ -8,6 +8,8 @@ import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from .. import assets
try:
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QTimer
from PySide6.QtGui import QColor, QIcon, QImage, QPainter, QPixmap
@@ -136,6 +138,22 @@ QPushButton:disabled {{
_RawQMessageBox = QMessageBox if QT_IMPORT_ERROR is None else None
def app_icon():
"""Application icon, or None when Qt is unavailable or the file is missing.
Callers must tolerate None: the icon is a packaged asset, and a source
checkout that has not run scripts/gen_logo.py still has to start.
"""
if QT_IMPORT_ERROR is not None:
return None
path = assets.icon_path() or assets.icon_png_path()
if not path:
return None
icon = QIcon(path)
return None if icon.isNull() else icon
class _GuiAttributeProxy:
def __init__(self, name, fallback):
self._name = name