feat: application icon for Windows taskbar + exe
- scripts/gen_icon.py: generates 256x256 SSH client icon (dark terminal window with >_ prompt and SSH connection nodes), outputs multi-size ICO (16-256px) + PNG - resources/icon.ico + icon.png: generated icon files - main.py: _get_icon_path() searches _MEIPASS (onefile), exe dir, source dir; sets app + window icon - build.spec: bundles icon files via datas=, sets exe icon=icon.ico - Windows taskbar will now show the custom icon instead of default PyInstaller icon
This commit is contained in:
+3
-3
@@ -31,8 +31,8 @@ a = Analysis(
|
||||
pathex=[str(ROOT)],
|
||||
binaries=[],
|
||||
datas=[
|
||||
# 如果后续要加资源文件(图标、配置模板),放这里
|
||||
# ('resources/icon.ico', 'resources'),
|
||||
('resources/icon.png', 'resources'),
|
||||
('resources/icon.ico', 'resources'),
|
||||
],
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
@@ -80,5 +80,5 @@ exe = EXE(
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
# icon='resources/icon.ico' if Path('resources/icon.ico').exists() else None,
|
||||
icon='resources/icon.ico' if Path('resources/icon.ico').exists() else None,
|
||||
)
|
||||
|
||||
@@ -20,11 +20,35 @@ _setup_path()
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtGui import QFont, QIcon, QPixmap
|
||||
|
||||
from ui.main_window import MainWindow, APP_NAME
|
||||
|
||||
|
||||
def _get_icon_path() -> str:
|
||||
"""找到 icon.ico / icon.png 的路径"""
|
||||
# 搜索目录列表:源码运行 / PyInstaller onefile / PyInstaller onedir
|
||||
search_dirs = []
|
||||
if getattr(sys, "frozen", False):
|
||||
# PyInstaller onefile: 资源解压到 _MEIPASS
|
||||
meipass = getattr(sys, "_MEIPASS", None)
|
||||
if meipass:
|
||||
search_dirs.append(Path(meipass))
|
||||
search_dirs.append(Path(meipass) / "resources")
|
||||
# exe 同目录(onedir 模式或用户手动放)
|
||||
search_dirs.append(Path(sys.executable).parent)
|
||||
search_dirs.append(Path(sys.executable).parent / "resources")
|
||||
else:
|
||||
search_dirs.append(Path(__file__).parent)
|
||||
search_dirs.append(Path(__file__).parent / "resources")
|
||||
for d in search_dirs:
|
||||
for name in ("icon.png", "icon.ico"):
|
||||
p = d / name
|
||||
if p.exists():
|
||||
return str(p)
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
# Windows 上高 DPI 适配
|
||||
if hasattr(Qt, "AA_EnableHighDpiScaling"):
|
||||
@@ -39,7 +63,14 @@ def main():
|
||||
f = QFont("Consolas", 10)
|
||||
app.setFont(f)
|
||||
|
||||
# 设置应用图标(任务栏 + 窗口标题栏)
|
||||
icon_path = _get_icon_path()
|
||||
if icon_path:
|
||||
app.setWindowIcon(QIcon(icon_path))
|
||||
|
||||
w = MainWindow()
|
||||
if icon_path:
|
||||
w.setWindowIcon(QIcon(icon_path))
|
||||
w.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
生成 SSHClient 应用图标
|
||||
- 深色圆角方块背景
|
||||
- 终端窗口 + >_ 提示符 + SSH 连接线
|
||||
输出: resources/icon.ico (多尺寸) + resources/icon.png (256x256)
|
||||
"""
|
||||
import os
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
OUT_DIR = os.path.join(os.path.dirname(__file__), "resources")
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
|
||||
SIZE = 256
|
||||
|
||||
def draw_icon(size: int) -> Image.Image:
|
||||
"""绘制指定尺寸的图标"""
|
||||
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
|
||||
pad = int(size * 0.06)
|
||||
rect = [pad, pad, size - pad, size - pad]
|
||||
radius = int(size * 0.18)
|
||||
|
||||
# 背景渐变(模拟,用两层半透明)
|
||||
d.rounded_rectangle(rect, radius=radius, fill=(30, 30, 46, 255))
|
||||
|
||||
# 上半部分亮色渐变
|
||||
inner = [pad + 4, pad + 4, size - pad - 4, size // 2]
|
||||
d.rounded_rectangle(inner, radius=radius - 2, fill=(40, 40, 60, 255))
|
||||
|
||||
# 终端窗口
|
||||
tw_margin = int(size * 0.16)
|
||||
tw_top = int(size * 0.20)
|
||||
tw_bottom = int(size * 0.78)
|
||||
tw_left = tw_margin
|
||||
tw_right = size - tw_margin
|
||||
tw_radius = int(size * 0.04)
|
||||
|
||||
# 终端窗口阴影
|
||||
shadow_offset = max(2, size // 64)
|
||||
d.rounded_rectangle(
|
||||
[tw_left + shadow_offset, tw_top + shadow_offset,
|
||||
tw_right + shadow_offset, tw_bottom + shadow_offset],
|
||||
radius=tw_radius, fill=(0, 0, 0, 60)
|
||||
)
|
||||
|
||||
# 终端窗口背景
|
||||
d.rounded_rectangle(
|
||||
[tw_left, tw_top, tw_right, tw_bottom],
|
||||
radius=tw_radius, fill=(12, 12, 20, 255)
|
||||
)
|
||||
|
||||
# 终端标题栏
|
||||
tb_height = int(size * 0.055)
|
||||
d.rounded_rectangle(
|
||||
[tw_left, tw_top, tw_right, tw_top + tb_height],
|
||||
radius=tw_radius, fill=(50, 50, 70, 255)
|
||||
)
|
||||
# 底部直角覆盖(标题栏只有上面圆角)
|
||||
d.rectangle(
|
||||
[tw_left, tw_top + tb_height - tw_radius, tw_right, tw_top + tb_height],
|
||||
fill=(50, 50, 70, 255)
|
||||
)
|
||||
|
||||
# 三个小圆点(macOS 风格窗口按钮)
|
||||
dot_r = max(2, int(size * 0.016))
|
||||
dot_y = tw_top + tb_height // 2
|
||||
dot_start = tw_left + int(size * 0.04)
|
||||
dot_gap = int(size * 0.045)
|
||||
for i, color in enumerate([(239, 83, 80), (255, 167, 38), (102, 187, 106)]):
|
||||
cx = dot_start + i * dot_gap
|
||||
d.ellipse([cx - dot_r, dot_y - dot_r, cx + dot_r, dot_y + dot_r], fill=color)
|
||||
|
||||
# >_ 提示符
|
||||
cx = tw_left + int(size * 0.06)
|
||||
cy = tw_top + tb_height + int(size * 0.10)
|
||||
line_w = max(2, int(size * 0.022))
|
||||
arrow_len = int(size * 0.08)
|
||||
gap = int(size * 0.02)
|
||||
|
||||
# > 符号
|
||||
d.line([cx, cy, cx + arrow_len, cy + arrow_len // 2],
|
||||
fill=(102, 187, 106), width=line_w)
|
||||
d.line([cx, cy + arrow_len, cx + arrow_len, cy + arrow_len // 2],
|
||||
fill=(102, 187, 106), width=line_w)
|
||||
|
||||
# _ 光标
|
||||
cursor_x = cx + arrow_len + gap
|
||||
cursor_w = int(size * 0.06)
|
||||
cursor_h = max(2, int(size * 0.018))
|
||||
d.rounded_rectangle(
|
||||
[cursor_x, cy + arrow_len // 2 - cursor_h // 2,
|
||||
cursor_x + cursor_w, cy + arrow_len // 2 + cursor_h // 2],
|
||||
radius=max(1, cursor_h // 3), fill=(137, 180, 250)
|
||||
)
|
||||
|
||||
# SSH 连接线(底部装饰)
|
||||
line_y = tw_bottom + int(size * 0.05)
|
||||
if line_y < size - pad:
|
||||
# 连接节点
|
||||
node_r = max(2, int(size * 0.012))
|
||||
n1 = (tw_left + int(size * 0.04), line_y)
|
||||
n2 = (tw_right - int(size * 0.04), line_y)
|
||||
d.ellipse([n1[0]-node_r, n1[1]-node_r, n1[0]+node_r, n1[1]+node_r],
|
||||
fill=(137, 180, 250))
|
||||
d.ellipse([n2[0]-node_r, n2[1]-node_r, n2[0]+node_r, n2[1]+node_r],
|
||||
fill=(137, 180, 250))
|
||||
d.line([n1[0]+node_r, n1[1], n2[0]-node_r, n2[1]],
|
||||
fill=(137, 180, 250, 160), width=max(1, line_w // 2))
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def main():
|
||||
# 生成 256x256 主图
|
||||
icon = draw_icon(SIZE)
|
||||
png_path = os.path.join(OUT_DIR, "icon.png")
|
||||
icon.save(png_path)
|
||||
print(f"Saved {png_path} ({SIZE}x{SIZE})")
|
||||
|
||||
# 生成多尺寸 ICO(Pillow 自动从 256x256 缩放到各尺寸)
|
||||
sizes = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
|
||||
ico_path = os.path.join(OUT_DIR, "icon.ico")
|
||||
icon.save(ico_path, format="ICO", sizes=sizes)
|
||||
print(f"Saved {ico_path} (sizes: {[s[0] for s in sizes]})")
|
||||
print("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Reference in New Issue
Block a user