From 580236efdb6080dfcbdc22558966c8f6033ed591 Mon Sep 17 00:00:00 2001 From: VALM-Labs <135186421+VALM-Labs@users.noreply.github.com> Date: Wed, 20 May 2026 16:48:10 +0800 Subject: [PATCH] =?UTF-8?q?v2.1.1:=20=E5=8A=A0=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=EF=BC=88=E5=A4=9A=E5=B0=BA=E5=AF=B8=20ICO=20?= =?UTF-8?q?=E5=B5=8C=E5=85=A5=20exe=EF=BC=89+=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E5=B8=B8=E9=87=8F=E5=8C=96=20+=20=E4=BA=A7=E5=93=81?= =?UTF-8?q?=E6=A0=8F=E5=8F=B3=E4=B8=8A=E8=A7=92=E6=98=BE=E7=A4=BA=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 12 +++++- generate_icon.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++ icon.ico | Bin 0 -> 6941 bytes test_jig.py | 19 ++++++++- 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 generate_icon.py create mode 100644 icon.ico diff --git a/.gitignore b/.gitignore index 47275e5..1eabbe7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__/ debug.log .vscode/ .idea/ +icon.png diff --git a/README.md b/README.md index 95e1ae1..e5daa81 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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): - **SN 不再被 FAIL 占用**:FAIL 行 SN 列留空,PASS 才占编号 diff --git a/generate_icon.py b/generate_icon.py new file mode 100644 index 0000000..01f52d4 --- /dev/null +++ b/generate_icon.py @@ -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("Jw zDN?0{4hjfJ69@#zyQtrr|9|Gq`|eD#yLZbuXV2OFow)!20cZde3IHh@V1odFHvj;f zoPT9^P<{md(CEK%J;;-x03adpSN;m}AK(Ch!Tgo4zyKhE1^@_zzj6r`0CXb3fZ%f! zKON-p;8;-xdK&b|6Gs|)Elo8;kb-Xk6oMN3=iQ`OgJeugP4$NF%gm=W^q~@{@SsV* zs05}cDZs)T)7S zy(fmxUp?P{qKc*W<3N}4h;`zyB~RUlhb0d5!0IOc+T!>Y6@a!0@sebZy3q7uJ~Fwf z^XU^p>e$X$0!(09O4Qs&_jD?P2F?zRx}cdK)n4Y#?Aq3>+Cl|Y{_-fgNeSY1=4&1X zjse>I9>^6{^Dn%oO)xP2kX|&O=FZs?c)u1$*=_In__nw(nNX2T$hvT=e3NnZ80MQW z+XF1PPC)C5o?780Y{>s3Dd08$oPUxs<)cIbb13^aDYgkBH6ID^!}A6-63v_<(b3TZ z(wx-TACe^!l9Dq?CJ_6!XP_#rzusz=r#Q{$Lbg&R9%<^q#uuV zD)70+w`>25`C9m%E8_MrS253J^STutWO}~b+BgbfJ7I^#8mK2;mW{hTe5j<^J!1GX`mu~wNt z{XnX-{Bt4R*G=lC(!vi(@R35>-UjY*S5L?C>NLIMv4=>V)t~_sg z=jM$&UYE)<`_f;cX}xZVWy`*L&01lFO?^YHgs{=f050Rl`N^J=s;tM12xH9b%!|0F zxVYfG`%-igeFe?ISK!}LM1fNS2A4nK4=VplzR{o_{U_h|{QZA}`F`*>-z!PIS6d`l z!zuR`F6VH(&P$oeeCT#7O~)f5qMRGE z`NCj>d=wT_6qX3F^rqRHH=`A|Z19yt8N7y_z1zS29rJ-cy;~-T?sqf$Yx@i?;)fH$ zgOx9@b!N26d{+; zTTBDQM~VbWFCWJ>Tj4@HX0-X>fiFqwn?{a-D5bKYzHE%M{P)R1TJDJLYPa*NupNza z>VU=a&V<9`B!5<367N063lYiMEZ7nm?g?FI%P)uGr?saVrOdA$^G33uMRf?PQn^k{ zhK@JO5q2J?&k)B*R4LYug=|XJ0sm$9z@`L9{Mo%*(A@|CfIj=TdnPV=L<|bYvWqG} zNk_rp<-$&9{2yt~@kmQEUvc@CE}HS~rv`MIQ*DMv6K&ww^f(n>-hQT!*FL6!y<36l z{ToF`)>U7hA6;KN74#O?w~zm7TB}cCM8uExoZc%#ai9V(1>aF)y&zeDpqimaugukP z&d4BQ-hO*VA4zxHafd1#5FCajhf=>5`BmTO?&OayeNH9e*y)e$>v@7EnvQ>;ybmik3~H1|gVSbB?6e_Owh`W08;FB$rqbSKRd=4`#TJg#mmQw13lA z_%qWJcbdFRr+8ok1H>xQ1y`PMe9Dwn-T<(NyVJB7()6glSM-Fx!3#_CF*phxv~_+z zG4D=NYVc;kE`gU)eJMBm>wW&RLW6e@_edB4J^8^^&9lz81^|4w%iCryB=X2jzL15R z6isN?gTsZ?+DW$#+RF1mHJTZ?aF#`{z?U`=oBmzj0=~cqE;80Bx@XAmH}=h}7FE4X z(_uh9zQN-@?SmN9$W#?^0TjAV^EQU{9JAFTKuXBd6}6F&yyb(_Im|p zg$fCrROWnyOP(8k6|NNacG3j`SV|u%%+-kEWu=xij*&TWBl|RfoZrFc@mGRlzZq1$ zc5;!^5eI1P8F&@#?J@Q$-5M$dy+IcNd!VmIhvuK$Ub~0%r4M z5Oe7}-?>+1RD1=Slt_V&fkb}u^8a$0F`ypv=QO`_^q2qu7~Q|qY`WswFTtu*xWORN zqh6Cxz%p}vjq};UJ;1KN2hOa*q@!6}nEzce=i}XsW1I-Vx4J5=aPez+c(&e*bj(Vb zZYuwk`zv#MX(_~|rwX_2R?lZO-g)tq9d-IAoZm*V;G<9umGR)V|_}uN|8L|gD$N--3vM?#3F}zdtN{qu`=bl|$r!dO?{30H5x&#?E zqEl@JzrE?F@zZ*N{4_+^D$KyY_gOW5I@A4wk=8(ckNy;KHZa5O?quRlYlMwsgiJvJ zj(EE*htZk(At}T8_zjDzow^bG9k>l~!fY3Oxc z1A+09A*sD1jd)n?^Cj7=k%4iEYa`WLbU=COQ6wYnGssdBFLW)A;wP7!JXcG59ftgb z-u+e7qFGb2i7bE$)Z}q#gm`b*vsDR2d^x1!t4YpzB*{WBQ^bVlUH-YtqS zG$i$Zy#h3DgvfXyA8^#@KuOj^>wIkGSWoSz&k1_Bb1;TAm4eIMud78?2m` z8axh2h&d-nYvZx9Z-wQ%;6T+iGB(p06=J_e>zTK7yIh4q*brDm5>owly$7A{S zyxS=NA!X})L|}u^kM}bOgx0j#*7-`DxrN7%w3+IDJWJT_4ai{9fcCUsXPyp{S4FO< z)j;R-Uix0@DHBR&=GsatFVGkfFb~C*E+jqJrXcU~0-p0LR;iOrhLsl*Z{ALMbB{%W zHe{J9s*LuO$!&41#WVC;Ta*7G>)W z0HE^wS462~*D*EL*6#d$27?4|w^#Jf9s*6aYEG7+GG=0iLNjiKhNq5=;9?2~h6K@U zxuTA;BihiABt+1SiNZRbfMxFPm+QOu_)K3P=k87)#dMtFQqH-Z^msclmsQkh0~sH7 zlVKXTk9Ku~EHY9T3N&w`2VWVY)!1Z0*A#PE5plw|cxE=|&6(FgP-7zItnpho)$oPcXBe- zpJM&_QBh5O-qeHC)$YJs5O#EGqM=%S(vuPHRilYPpN4avB-r*R7_UznN!SO>-{bS= zh8J1x+mv-}(xg7SK*2H6=4guGJ2z>#Cxle!^@2Dzmh<$ilTjkCg7}3FXNj?AYi8>X zw;6ip#7pA{f=VZ9r+>uu(>vW_hz8E|Mir6MF79WAdE|8?50=u;4EE2kFzMr4`6h*!*7mX~0Li;VQAo=H#RN+`S-QPB)VF4P5x zzfTi7>}YXvlDczF40=z3T?LDUoTvuP>J-Ov>bXYS-L7(m?s-+xe+)A67`A$#YBrJ? zf*Z=NQPRwJcocMd@y;PRZTy@~`#^R-Z;zLaMzmf~@wQ1KssBq{`CVBC+^RGq_NlSN zN!2Ipl=T%27QWXa>nY*=0BN%$uVVZB2zS5XOWhE(_I}x#+E!%IQmh#x$6#z`=y*c$ z>%Fo7a@oZ!ylIYJ5T>->N}D@xoT#7g5XHONZzua<;B<~+<2x*vF1+Fm2b`6?mD=u% z?D6|c)6L|UjoDVtq7*!3esiouCTvp(pIlL$k(t0Z*&%$zZr-6SYNI+XW1QGMHlen^ zJ0kh0qS`p5guwX}jpnN!tV^jWWU*par3T`Z5q-g^M-1zw#}t^#p7Zp7`+CyaR<(cT zMP$Qso{RdO?hE9>C9k)ucF#PCVp*3>bt0ma7DR$I1r8-5D+b?}aSk+HO&78Mr20hY z(3#WR%tcI;`hM}Cz`VTblQW0A&YaF>1){uM=5igdeUH`DW+`5t!@6~kicuftC53g| z)H_BU$GL>!&E3d~LR_`E6-$t^sS3CJv=rO&T~fdGk}=2THJgU@y-$I2_dnksp*j1g zKRcM0`{3#B?rP|{axQ%ixOvZ3g!TzXNK<1*L6bc+m^b#HyQRuX z=Rl?195h*43TU}61hBKOt*=dXcBhZ|N zIUaEd6{h{FHp`Dt#R@$|7hSa6p-k5-3JbdGc;mZ=ggPwkhJw7T78O6rx41XQdDfz7 z5s60s{Pd2V!kBKFbYli4eeD0=vB3`v;Fp3w9oyV9#~uKX_WwHe_|ju8jpNi)FTRbz z!nnMLmOdU6M4&#24BP*QZ1VX0#eRi-6OwfNUPMrJ7~#!OufsACOr zY}@6)4}bOhS&Y!RI9fS%SvfU^+S_wg;+TrpnScsU4q!UM)vn99Pz%!(ad^At^k+#Z z3NL88wCqxYMDH66-pqp&qk%BqpT!>RvKG{=_?!-P{$O2opqcT$Q)*X5-Vt;60`tO5 z45krNWutsPuNvxK~Q+ktn!i6EUk57Jmf+CNeF3$v86()B@anh zYmpP_@*MJkLxn4x;Zv0=(H+e?0sJU<#X>CFJC8%l~O!jRC8nS7TdYF zq!EhoKmGrhc@|55_Q?H_>}?Bsime?S2-Yn;_|fFIs6H^WY4t0~6{_UBTd9$mVnF|I z#bb;F-7h&8KFD8Jj_i4*akx;m0 zh2O~ysySRgxwzC#dA=gAO#U#57 z-iQ=hn(-b!qSS93u5Vl1RVy7ci>ycG;s2FI1dvHtHHQ5w~8{oNLT82 z2`1^~isJR;Ll1$KoM3H0VMu0QH|X(-5a8w1zb1R&S7Cd+m8I+YqTVn#$gUEes3L~Zh{0(G97wwZV6+= z_7=3r$6JDae_NR#53L7wA_zAJ|w62UB)wEs7rH$7)j_GH$JWG zyw>bN`6HQ%~WI-R2KEvwo(L*Dv4+(f~3;fDkt*dXwH&P)c6(cIR z@}G;|jIO`?RBC8sZ`LmlP6xSX?@JbuNJCZ0@Z~SRo}JVWyW_%C<;lNdX~Ny(LM&}p zpLTF(CGd2H7w9rQr;Lb=mTIP~r7KqP@58bHFXwB+SJ{KdrX7aQf^+q@tY#`F{m$+d z-QQWIOi!zm^IV>Di|&7jo#%`RV3rtq+Z#^4C~wLgzZ-{n>KCj_jK8q2@l1Dn%S*AX zD#rhK@ygJ~rrl;y|C^d~Uq8Av4?20eQ`DDamVeFDQWks1XWDWXhomxzM*T_h1fKkJ zP@(b^4qt0ZL2Gy2uK;3ry2mxZ-|#g8p|T_hDE%x28C{UMs8mh7V>vZLkrjeA>tDYK zv9GjK@Gb10?YuHYA$OfPoUQpD`vvoO-k>rSIcm^Vp>wO-C$SWG&P89!pfUH4@0;0@)hY`JwLxZYAqktZO%Ea0ON>4L0; zt6oO%uyi9G=vUW{-@o@A1Xdn4R$uB7-WS+u-=Ta41Iq9d+NFh{y6Uik7JW@{W)*gO zu5B&U!RAKr(J;#Nm%5n)~Flwl`adl=hNz zUBycBkI_{VujA9@avbs_1mas-J!4mz4X#L*NkbKp0#KHqdQCpVTSbwa?VB`sr#f%U zqglT%X8^d2=s4jNsl#kiY#hE2W`}`#ezkH3TfYevnrnXF_9i5Y z8tfnJ9Kp?X-nZdZ|0Ylhg5hz{6I|&ZM2dx7#)7f`V}1l^|G~Q>d^^IBafZKG2x8#> z!kP{MHK@0thmLfbVgA1hxTgXjmS+;NPZRq5dJ_TMi??&Lji?v$e|_y~33nlp(^5-E zsJE*^XLA8)U5Waf0^+g=>=O2oIv@;CGld&RV?0@PgyXz3FybrFNGLO8=)O)SchK(skg z3$q3NIVk)lrIu~@EqWmL{tNI1Bh5^xc24->qRilY8n{0!_mNo7^jQ;V9~BOjzX^$k zXG9mwUhL|9_`B*965=IJ{CshTUNxFJ!_0(*_$eZ|lNKXe;&5*0Orv5F{E5Cp!;PAH za3;`Uy==(JY$>1)>#-D1>cdU28J{9-`KMl9a$>+f8{x>|#56^1GE-p_uR-G2tR&A_ t0Aig!VIhs2Y>fN0rpU(HE8Lla5~sgCz{OD@{(W6$nS$T@{^#}d{{TTFSKa^s literal 0 HcmV?d00001 diff --git a/test_jig.py b/test_jig.py index a1e74f4..c7dcf17 100644 --- a/test_jig.py +++ b/test_jig.py @@ -29,6 +29,8 @@ try: except ImportError: HAS_OPENPYXL = False +APP_VERSION = "v2.1.1" + if getattr(sys, "frozen", False): BASE = Path(sys.executable).parent else: @@ -610,7 +612,8 @@ class QuickItemEditor(tk.Toplevel): class TestJig: def __init__(self, root): self.root = root - self.root.title("产品-硬件入库测试工具 v2.1.0") + self.root.title("产品-硬件入库测试工具") + self._set_window_icon() self.root.geometry("1240x820") ensure_default_profile() @@ -665,6 +668,8 @@ class TestJig: command=self._export_profile).pack(side="left", padx=4) ttk.Button(prod_bar, text="📥 导入 JSON", 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)) @@ -1965,6 +1970,18 @@ class TestJig: self.settings["auto_clear_log_on_disconnect"] = self.auto_clear_log_var.get() 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): self._sync_sn_to_profile() save_profile(self.profile)