v2.1.1: 加自定义图标(多尺寸 ICO 嵌入 exe)+ 版本号常量化 + 产品栏右上角显示版本号
This commit is contained in:
parent
57cd353a78
commit
580236efdb
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ __pycache__/
|
|||||||
debug.log
|
debug.log
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
icon.png
|
||||||
|
|||||||
12
README.md
12
README.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
ESP32-S3 类产品的产线入库测试夹具。Python + tkinter 编写,打包为单文件 `.exe`,仓库人员双击即用,不需要装 Python。
|
ESP32-S3 类产品的产线入库测试夹具。Python + tkinter 编写,打包为单文件 `.exe`,仓库人员双击即用,不需要装 Python。
|
||||||
|
|
||||||
**版本**:v2.1.0
|
**版本**:v2.1.1
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -207,7 +207,15 @@ seaborn bokeh cv2 tables numba llvmlite torch tensorflow sklearn
|
|||||||
|
|
||||||
## 七、版本说明
|
## 七、版本说明
|
||||||
|
|
||||||
### v2.1.0(当前)
|
### v2.1.1(当前)
|
||||||
|
|
||||||
|
- 新增自定义图标:蓝色渐变 + 白色 IC 芯片轮廓 + 绿色对勾,多尺寸(16/24/32/48/64/128/256)嵌入 exe,窗口标题栏也用同图标
|
||||||
|
- 加 `APP_VERSION` 常量,更新一处即可全局同步
|
||||||
|
- 产品栏右上角显示版本号(深灰加粗),方便仓库人员一眼看清当前版本
|
||||||
|
- 窗口标题不再带版本号(重复)
|
||||||
|
- 新增 `generate_icon.py` 独立脚本,手工写多帧 ICO(绕开 PIL writer 只保留单帧的 bug)
|
||||||
|
|
||||||
|
### v2.1.0
|
||||||
|
|
||||||
**核心改动**(相对 v2.0.0):
|
**核心改动**(相对 v2.0.0):
|
||||||
- **SN 不再被 FAIL 占用**:FAIL 行 SN 列留空,PASS 才占编号
|
- **SN 不再被 FAIL 占用**:FAIL 行 SN 列留空,PASS 才占编号
|
||||||
|
|||||||
109
generate_icon.py
Normal file
109
generate_icon.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
"""一次性脚本:生成多尺寸 icon.ico(芯片轮廓 + 绿色对勾,蓝色 Material 风格)。
|
||||||
|
|
||||||
|
生成正确的多帧 ICO 文件(手工写 ICONDIR + PNG 帧),避免 PIL 的 ICO writer 只保留单帧的 bug。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
|
||||||
|
def make_base(size: int) -> Image.Image:
|
||||||
|
"""画一张 size×size 的图标 base image。"""
|
||||||
|
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# 圆角方形背景 + 垂直蓝色渐变
|
||||||
|
for y in range(size):
|
||||||
|
t = y / size
|
||||||
|
r = int(25 + (66 - 25) * t)
|
||||||
|
g = int(118 + (165 - 118) * t)
|
||||||
|
b = int(210 + (245 - 210) * t)
|
||||||
|
draw.rectangle((0, y, size, y + 1), fill=(r, g, b, 255))
|
||||||
|
|
||||||
|
# 圆角裁切
|
||||||
|
mask = Image.new("L", (size, size), 0)
|
||||||
|
ImageDraw.Draw(mask).rounded_rectangle(
|
||||||
|
(0, 0, size - 1, size - 1), radius=int(size * 0.19), fill=255)
|
||||||
|
img.putalpha(mask)
|
||||||
|
|
||||||
|
# IC 芯片轮廓
|
||||||
|
s = size
|
||||||
|
chip_box = (int(s * 0.265), int(s * 0.352), int(s * 0.735), int(s * 0.648))
|
||||||
|
line_w = max(2, int(s * 0.024))
|
||||||
|
draw.rectangle(chip_box, outline=(255, 255, 255), width=line_w)
|
||||||
|
|
||||||
|
pin_w = max(2, int(s * 0.024))
|
||||||
|
pin_l = max(2, int(s * 0.047))
|
||||||
|
|
||||||
|
cx_left, cy_top, cx_right, cy_bot = chip_box
|
||||||
|
chip_w = cx_right - cx_left
|
||||||
|
chip_h = cy_bot - cy_top
|
||||||
|
|
||||||
|
# 上下引脚(5 根)
|
||||||
|
for i in range(5):
|
||||||
|
x = cx_left + int(chip_w * (0.117 + 0.192 * i))
|
||||||
|
draw.rectangle((x - pin_w // 2, cy_top - pin_l, x + pin_w // 2, cy_top),
|
||||||
|
fill=(255, 255, 255))
|
||||||
|
draw.rectangle((x - pin_w // 2, cy_bot, x + pin_w // 2, cy_bot + pin_l),
|
||||||
|
fill=(255, 255, 255))
|
||||||
|
|
||||||
|
# 左右引脚(3 根)
|
||||||
|
for i in range(3):
|
||||||
|
y = cy_top + int(chip_h * (0.185 + 0.302 * i))
|
||||||
|
draw.rectangle((cx_left - pin_l, y - pin_w // 2, cx_left, y + pin_w // 2),
|
||||||
|
fill=(255, 255, 255))
|
||||||
|
draw.rectangle((cx_right, y - pin_w // 2, cx_right + pin_l, y + pin_w // 2),
|
||||||
|
fill=(255, 255, 255))
|
||||||
|
|
||||||
|
# 中心:绿色对勾
|
||||||
|
check_pts = [
|
||||||
|
(int(s * 0.359), int(s * 0.492)),
|
||||||
|
(int(s * 0.453), int(s * 0.578)),
|
||||||
|
(int(s * 0.648), int(s * 0.383)),
|
||||||
|
]
|
||||||
|
check_w_max = max(3, int(s * 0.055))
|
||||||
|
for w in range(check_w_max, max(2, check_w_max - 6), -2):
|
||||||
|
draw.line(check_pts, fill=(76, 175, 80, 255), width=w, joint="curve")
|
||||||
|
# 白色细描边
|
||||||
|
draw.line(check_pts, fill=(255, 255, 255, 220), width=max(1, int(s * 0.008)), joint="curve")
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def write_ico(path: Path, sizes):
|
||||||
|
"""手工构造一个多帧 ICO 文件(每帧用 PNG 编码,Windows Vista+ 通用)。"""
|
||||||
|
images_png = []
|
||||||
|
for sz in sizes:
|
||||||
|
img = make_base(sz)
|
||||||
|
buf = BytesIO()
|
||||||
|
img.save(buf, format="PNG")
|
||||||
|
images_png.append((sz, buf.getvalue()))
|
||||||
|
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
# ICONDIR
|
||||||
|
f.write(struct.pack("<HHH", 0, 1, len(images_png)))
|
||||||
|
# ICONDIRENTRY × n
|
||||||
|
offset = 6 + 16 * len(images_png)
|
||||||
|
for sz, data in images_png:
|
||||||
|
w = sz if sz < 256 else 0 # 256 用 0 表示
|
||||||
|
f.write(struct.pack("<BBBBHHII",
|
||||||
|
w, w, 0, 0,
|
||||||
|
1, 32,
|
||||||
|
len(data), offset))
|
||||||
|
offset += len(data)
|
||||||
|
# 图像数据
|
||||||
|
for _, data in images_png:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sizes = [16, 24, 32, 48, 64, 128, 256]
|
||||||
|
out = Path(__file__).parent / "icon.ico"
|
||||||
|
write_ico(out, sizes)
|
||||||
|
print(f"saved: {out} {out.stat().st_size} bytes (frames: {len(sizes)})")
|
||||||
|
|
||||||
|
# 顺便存一张大图 png 方便预览
|
||||||
|
make_base(512).save(Path(__file__).parent / "icon.png", format="PNG")
|
||||||
19
test_jig.py
19
test_jig.py
@ -29,6 +29,8 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_OPENPYXL = False
|
HAS_OPENPYXL = False
|
||||||
|
|
||||||
|
APP_VERSION = "v2.1.1"
|
||||||
|
|
||||||
if getattr(sys, "frozen", False):
|
if getattr(sys, "frozen", False):
|
||||||
BASE = Path(sys.executable).parent
|
BASE = Path(sys.executable).parent
|
||||||
else:
|
else:
|
||||||
@ -610,7 +612,8 @@ class QuickItemEditor(tk.Toplevel):
|
|||||||
class TestJig:
|
class TestJig:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("产品-硬件入库测试工具 v2.1.0")
|
self.root.title("产品-硬件入库测试工具")
|
||||||
|
self._set_window_icon()
|
||||||
self.root.geometry("1240x820")
|
self.root.geometry("1240x820")
|
||||||
|
|
||||||
ensure_default_profile()
|
ensure_default_profile()
|
||||||
@ -665,6 +668,8 @@ class TestJig:
|
|||||||
command=self._export_profile).pack(side="left", padx=4)
|
command=self._export_profile).pack(side="left", padx=4)
|
||||||
ttk.Button(prod_bar, text="📥 导入 JSON",
|
ttk.Button(prod_bar, text="📥 导入 JSON",
|
||||||
command=self._import_profile).pack(side="left", padx=4)
|
command=self._import_profile).pack(side="left", padx=4)
|
||||||
|
ttk.Label(prod_bar, text=APP_VERSION, foreground="#888",
|
||||||
|
font=("Segoe UI", 10, "bold")).pack(side="right", padx=8)
|
||||||
|
|
||||||
# 第二条:串口
|
# 第二条:串口
|
||||||
ser_outer = ttk.Frame(self.root, padding=(8, 3, 8, 3))
|
ser_outer = ttk.Frame(self.root, padding=(8, 3, 8, 3))
|
||||||
@ -1965,6 +1970,18 @@ class TestJig:
|
|||||||
self.settings["auto_clear_log_on_disconnect"] = self.auto_clear_log_var.get()
|
self.settings["auto_clear_log_on_disconnect"] = self.auto_clear_log_var.get()
|
||||||
save_settings(self.settings)
|
save_settings(self.settings)
|
||||||
|
|
||||||
|
def _set_window_icon(self):
|
||||||
|
"""设置窗口标题栏图标(兼容 PyInstaller --onefile 打包后从临时目录读取)"""
|
||||||
|
try:
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
ico = Path(sys._MEIPASS) / "icon.ico"
|
||||||
|
else:
|
||||||
|
ico = Path(__file__).parent / "icon.ico"
|
||||||
|
if ico.exists():
|
||||||
|
self.root.iconbitmap(str(ico))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _on_close(self):
|
def _on_close(self):
|
||||||
self._sync_sn_to_profile()
|
self._sync_sn_to_profile()
|
||||||
save_profile(self.profile)
|
save_profile(self.profile)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user