diff --git a/build.spec b/build.spec index 98b939d..0b84b55 100644 --- a/build.spec +++ b/build.spec @@ -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, ) diff --git a/main.py b/main.py index 05154bc..552a3f2 100644 --- a/main.py +++ b/main.py @@ -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_()) diff --git a/resources/icon.ico b/resources/icon.ico new file mode 100644 index 0000000..21d8a7f Binary files /dev/null and b/resources/icon.ico differ diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000..56c75e9 Binary files /dev/null and b/resources/icon.png differ diff --git a/scripts/gen_icon.py b/scripts/gen_icon.py new file mode 100644 index 0000000..ff22593 --- /dev/null +++ b/scripts/gen_icon.py @@ -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() diff --git a/scripts/resources/icon.ico b/scripts/resources/icon.ico new file mode 100644 index 0000000..21d8a7f Binary files /dev/null and b/scripts/resources/icon.ico differ diff --git a/scripts/resources/icon.png b/scripts/resources/icon.png new file mode 100644 index 0000000..56c75e9 Binary files /dev/null and b/scripts/resources/icon.png differ