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:
co-authored by
Claude Fable 5
parent
6a0cd1c763
commit
049d4b6bbc
@@ -0,0 +1,57 @@
|
||||
"""Bundled static assets (application icon).
|
||||
|
||||
The icon ships inside the package so the same file backs the window icon at
|
||||
runtime and the executable icon at build time. Keep this module free of Qt
|
||||
imports: the packaging spec imports it while collecting data files.
|
||||
|
||||
Regenerate the files with ``py -3.10 scripts/gen_logo.py``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
ICON_NAME = "cmshopee.ico"
|
||||
ICON_PNG_NAME = "cmshopee-256.png"
|
||||
|
||||
_PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def _candidate_paths(name):
|
||||
"""Locations to probe, covering source tree and PyInstaller onedir."""
|
||||
|
||||
yield os.path.join(_PACKAGE_DIR, name)
|
||||
|
||||
bundle_dir = getattr(sys, "_MEIPASS", None)
|
||||
if bundle_dir:
|
||||
yield os.path.join(bundle_dir, "app", "assets", name)
|
||||
|
||||
if getattr(sys, "frozen", False):
|
||||
yield os.path.join(
|
||||
os.path.dirname(os.path.abspath(sys.executable)),
|
||||
"app",
|
||||
"assets",
|
||||
name,
|
||||
)
|
||||
|
||||
|
||||
def asset_path(name):
|
||||
"""Return an existing asset path, or None when it is not bundled."""
|
||||
|
||||
for candidate in _candidate_paths(name):
|
||||
if candidate and os.path.isfile(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def icon_path():
|
||||
"""Path to the multi-size Windows icon, or None when unavailable."""
|
||||
|
||||
return asset_path(ICON_NAME)
|
||||
|
||||
|
||||
def icon_png_path():
|
||||
"""Path to the 256px PNG fallback, or None when unavailable."""
|
||||
|
||||
return asset_path(ICON_PNG_NAME)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="蝦皮圈優化助手">
|
||||
<title>蝦皮圈優化助手</title>
|
||||
<defs>
|
||||
<linearGradient id="badge" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#F5793F"/>
|
||||
<stop offset="1" stop-color="#D6371C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="10" y="10" width="236" height="236" rx="54" fill="url(#badge)"/>
|
||||
<path d="M 112 58 L 56 116 L 168 116 Z" fill="#FFFFFF"/>
|
||||
<rect x="90" y="110" width="44" height="52" rx="10" fill="#FFFFFF"/>
|
||||
<rect x="56" y="178" width="112" height="26" rx="13" fill="#FFFFFF"/>
|
||||
<polygon points="194.0,40.0 201.8,66.2 228.0,74.0 201.8,81.8 194.0,108.0 186.2,81.8 160.0,74.0 186.2,66.2" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 777 B |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -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(),
|
||||
|
||||
@@ -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("设置")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user