feat: 增强便携版支持与配置管理
- 更新 .gitignore 文件,添加便携版分发目录和 Fody 生成的配置文件。 - 修改 README.md,详细说明便携版的构建与分发流程,强调单文件分发的优势。 - 在 Config.cs 中实现便携配置管理,优先使用 exe 同目录的 config.json,提升用户体验。 - 更新 ExcelService.cs,优化默认模板的导出逻辑,支持现场替换模板文件。 - 在 MainForm.cs 中加载应用图标,提升界面一致性。 - 调整 Program.cs,确保在单文件分发时正确加载 native DLL。
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,305 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成 TfsExcelImporter 的应用图标(app.ico / 预览图)。
|
||||
|
||||
纯标准库实现(本机没有 Pillow / numpy):用有向距离场(SDF)解析求覆盖率做
|
||||
抗锯齿,再手写 PNG / ICO 容器,不依赖任何第三方包。
|
||||
|
||||
设计:蓝色渐变圆角方块 + 白色线性「上升箭头」字形(导入语义,
|
||||
方向与界面「开始导入」按钮的 ArrowUp 一致)。按尺寸分档:
|
||||
- 大尺寸:加顶部高光与 1px 内描边(立体感)
|
||||
- 小尺寸(<=24):关掉高光/内描边、加粗描边并略微放大字形,避免糊成一团
|
||||
|
||||
用法:
|
||||
python tools/make_icon.py # 生成 app.ico + tools/icon_preview.png
|
||||
python tools/make_icon.py --only-preview # 只出预览图
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
# ----------------------------------------------------------------- 设计参数
|
||||
SIZES = [16, 20, 24, 32, 40, 48, 64, 128, 256]
|
||||
|
||||
COLOR_TOP = (0x44, 0x8E, 0xFA) # 顶部亮蓝
|
||||
COLOR_BOT = (0x18, 0x42, 0xC4) # 底部深蓝
|
||||
WHITE = (255, 255, 255)
|
||||
|
||||
TILE_MARGIN = 0.045
|
||||
TILE_RADIUS = 0.235
|
||||
|
||||
|
||||
def style(size):
|
||||
"""按尺寸分档的视觉参数。"""
|
||||
if size <= 20:
|
||||
return dict(stroke=0.150, gloss=0.00, rim=0.00, ring=True, grow=1.08)
|
||||
if size <= 24:
|
||||
return dict(stroke=0.138, gloss=0.00, rim=0.00, ring=True, grow=1.06)
|
||||
if size <= 40:
|
||||
return dict(stroke=0.118, gloss=0.09, rim=0.14, ring=False, grow=1.02)
|
||||
return dict(stroke=0.100, gloss=0.10, rim=0.17, ring=False, grow=1.00)
|
||||
|
||||
|
||||
# 字形骨架(归一化坐标,单位方格)。全部是圆头胶囊,端点已按描边半径内缩过,
|
||||
# 这样描边加粗时外形不会溢出画布。
|
||||
# 返回 (箭头胶囊组, 副元素胶囊组, 副元素描边系数);副元素为空表示只画箭头。
|
||||
def glyph_capsules(stroke, variant="arrow"):
|
||||
if variant == "arrow":
|
||||
# 只有箭头:上下留白对称(0.14~0.85),比例比带盒子时更大更饱满
|
||||
arrow = [
|
||||
(0.500, 0.270, 0.500, 0.790),
|
||||
(0.262, 0.418, 0.500, 0.196),
|
||||
(0.500, 0.196, 0.738, 0.418),
|
||||
]
|
||||
return arrow, [], 1.0
|
||||
shaft_end = 0.700 if variant == "tray" else 0.650
|
||||
arrow = [
|
||||
# 箭杆 + 箭头两撇(V 形,圆头在顶点自然并成箭头)
|
||||
(0.500, 0.235, 0.500, shaft_end),
|
||||
(0.330, 0.375, 0.500, 0.205),
|
||||
(0.500, 0.205, 0.670, 0.375),
|
||||
]
|
||||
if variant == "slot":
|
||||
# 单根加粗横条("槽/落点"),无竖壁,小尺寸更干净
|
||||
return arrow, [(0.250, 0.780, 0.750, 0.780)], 1.30
|
||||
# 托盘:底边 + 两侧竖壁;底边端点落在竖壁中心线上,角部由此自然圆接
|
||||
return arrow, [
|
||||
(0.300, 0.800, 0.700, 0.800),
|
||||
(0.300, 0.640, 0.300, 0.800),
|
||||
(0.700, 0.640, 0.700, 0.800),
|
||||
], 1.0
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- SDF 基础
|
||||
def sd_round_rect(x, y, cx, cy, hw, hh, r):
|
||||
dx = abs(x - cx) - (hw - r)
|
||||
dy = abs(y - cy) - (hh - r)
|
||||
ox = dx if dx > 0.0 else 0.0
|
||||
oy = dy if dy > 0.0 else 0.0
|
||||
inner = dx if dx > dy else dy
|
||||
if inner > 0.0:
|
||||
inner = 0.0
|
||||
return math.hypot(ox, oy) + inner - r
|
||||
|
||||
|
||||
def sd_seg(x, y, ax, ay, bx, by):
|
||||
pax, pay = x - ax, y - ay
|
||||
bax, bay = bx - ax, by - ay
|
||||
den = bax * bax + bay * bay
|
||||
t = 0.0 if den == 0.0 else max(0.0, min(1.0, (pax * bax + pay * bay) / den))
|
||||
return math.hypot(pax - bax * t, pay - bay * t)
|
||||
|
||||
|
||||
def clamp01(v):
|
||||
return 0.0 if v < 0.0 else (1.0 if v > 1.0 else v)
|
||||
|
||||
|
||||
def cov(d, size):
|
||||
"""距离场 -> 覆盖率(1px 过渡带)。"""
|
||||
return clamp01(0.5 - d * size)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- 单尺寸渲染
|
||||
def render(size, variant="tray"):
|
||||
st = style(size)
|
||||
# 只有箭头时没有「托盘」当配重,线条要更粗一点才站得住
|
||||
stroke = st["stroke"] + (0.016 if variant == "arrow" else 0.0)
|
||||
grow = st["grow"]
|
||||
arrow, tray, tray_w = glyph_capsules(stroke, variant)
|
||||
gr = stroke / 2.0
|
||||
# 大尺寸下托盘描边略细于箭头:用「粗细」而不是「透明度」做主次(透明度压在深蓝上会发灰)
|
||||
tray_k = tray_w * (1.0 if size <= 32 else 0.94)
|
||||
tray_tone = 1.0
|
||||
|
||||
hw = 0.5 - TILE_MARGIN
|
||||
rad = TILE_RADIUS
|
||||
inner_shrink = 1.0 / size # 内描边宽度 = 1px
|
||||
|
||||
buf = bytearray(size * size * 4)
|
||||
span = 1.0 - 2.0 * TILE_MARGIN
|
||||
|
||||
for py in range(size):
|
||||
v = (py + 0.5) / size
|
||||
# 字形整体缩放:把坐标拉回以 0.5 为心
|
||||
gv = 0.5 + (v - 0.5) / grow
|
||||
row_off = py * size * 4
|
||||
for px in range(size):
|
||||
u = (px + 0.5) / size
|
||||
|
||||
a = cov(sd_round_rect(u, v, 0.5, 0.5, hw, hw, rad), size)
|
||||
if a <= 0.0:
|
||||
continue
|
||||
|
||||
# 渐变(略偏对角线,避免纯竖条的死板)
|
||||
t = clamp01(((v - TILE_MARGIN) * 0.82 + (u - TILE_MARGIN) * 0.18) / span)
|
||||
cr = COLOR_TOP[0] + (COLOR_BOT[0] - COLOR_TOP[0]) * t
|
||||
cg = COLOR_TOP[1] + (COLOR_BOT[1] - COLOR_TOP[1]) * t
|
||||
cb = COLOR_TOP[2] + (COLOR_BOT[2] - COLOR_TOP[2]) * t
|
||||
|
||||
# 顶部高光
|
||||
if st["gloss"] > 0.0:
|
||||
g = st["gloss"] * clamp01(1.0 - (v - TILE_MARGIN) / 0.38)
|
||||
cr += (255.0 - cr) * g
|
||||
cg += (255.0 - cg) * g
|
||||
cb += (255.0 - cb) * g
|
||||
|
||||
# 1px 内描边(沿边缘的细亮线,让方块边界更挺)
|
||||
if st["rim"] > 0.0:
|
||||
d_in = sd_round_rect(u, v, 0.5, 0.5, hw - inner_shrink,
|
||||
hw - inner_shrink, rad - inner_shrink)
|
||||
band = a - cov(d_in, size)
|
||||
if band > 0.0:
|
||||
k = st["rim"] * band
|
||||
cr += (255.0 - cr) * k
|
||||
cg += (255.0 - cg) * k
|
||||
cb += (255.0 - cb) * k
|
||||
|
||||
# 字形:托盘(略细)+ 箭头(主调)
|
||||
gu = 0.5 + (u - 0.5) / grow
|
||||
for group, tone, k in ((tray, tray_tone, tray_k), (arrow, 1.0, 1.0)):
|
||||
d = 1e9
|
||||
rg = gr * k
|
||||
for (ax, ay, bx, by) in group:
|
||||
dd = sd_seg(gu, gv, ax, ay, bx, by) - rg
|
||||
if dd < d:
|
||||
d = dd
|
||||
ag = cov(d, size) * a * tone
|
||||
if ag > 0.0:
|
||||
cr += (WHITE[0] - cr) * ag
|
||||
cg += (WHITE[1] - cg) * ag
|
||||
cb += (WHITE[2] - cb) * ag
|
||||
|
||||
i = row_off + px * 4
|
||||
buf[i] = int(cr + 0.5)
|
||||
buf[i + 1] = int(cg + 0.5)
|
||||
buf[i + 2] = int(cb + 0.5)
|
||||
buf[i + 3] = int(a * 255.0 + 0.5)
|
||||
return buf
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- PNG / ICO
|
||||
def png_bytes(w, h, rgba):
|
||||
raw = bytearray()
|
||||
for y in range(h):
|
||||
raw.append(0)
|
||||
raw += rgba[y * w * 4:(y + 1) * w * 4]
|
||||
|
||||
def chunk(tag, data):
|
||||
return (struct.pack(">I", len(data)) + tag + data
|
||||
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
|
||||
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)
|
||||
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr)
|
||||
+ chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def dib_bytes(w, h, rgba):
|
||||
"""32bpp 自下而上的 BMP(ICO 里的 DIB,含 AND 掩码)。"""
|
||||
xor = bytearray()
|
||||
for y in range(h - 1, -1, -1):
|
||||
for x in range(w):
|
||||
i = (y * w + x) * 4
|
||||
xor += bytes((rgba[i + 2], rgba[i + 1], rgba[i], rgba[i + 3]))
|
||||
and_stride = ((w + 31) // 32) * 4
|
||||
and_mask = bytes(and_stride * h)
|
||||
hdr = struct.pack("<IiiHHIIiiII", 40, w, h * 2, 1, 32, 0,
|
||||
len(xor) + len(and_mask), 0, 0, 0, 0)
|
||||
return hdr + bytes(xor) + and_mask
|
||||
|
||||
|
||||
def ico_bytes(frames):
|
||||
n = len(frames)
|
||||
out = [struct.pack("<HHH", 0, 1, n)]
|
||||
offset = 6 + 16 * n
|
||||
body = b""
|
||||
for size, payload in frames:
|
||||
b = 0 if size >= 256 else size
|
||||
out.append(struct.pack("<BBBBHHII", b, b, 0, 0, 1, 32, len(payload),
|
||||
offset + len(body)))
|
||||
body += payload
|
||||
return b"".join(out) + body
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- 预览拼版
|
||||
def blit(dst, dw, dh, src, sw, sh, ox, oy, scale=1):
|
||||
for y in range(sh * scale):
|
||||
sy = y // scale
|
||||
dy = oy + y
|
||||
if dy >= dh:
|
||||
break
|
||||
for x in range(sw * scale):
|
||||
sx = x // scale
|
||||
dx = ox + x
|
||||
if dx >= dw:
|
||||
break
|
||||
si = (sy * sw + sx) * 4
|
||||
di = (dy * dw + dx) * 4
|
||||
dst[di:di + 4] = src[si:si + 4]
|
||||
|
||||
|
||||
def preview(renders, path):
|
||||
pad = 10
|
||||
big = renders[256]
|
||||
strip = SIZES[:-1] # 16..128
|
||||
zoom = 4
|
||||
strip_w = sum(s * zoom + pad for s in strip)
|
||||
w = max(256 + 2 * pad, strip_w + pad)
|
||||
h = 256 + strip_w * 0 + 64 + 128 * zoom + 3 * pad
|
||||
h = 256 + pad * 3 + 128 * zoom
|
||||
canvas = bytearray(w * h * 4)
|
||||
for i in range(w * h):
|
||||
canvas[i * 4 + 0] = 0xF3
|
||||
canvas[i * 4 + 1] = 0xF4
|
||||
canvas[i * 4 + 2] = 0xF6
|
||||
canvas[i * 4 + 3] = 0xFF
|
||||
blit(canvas, w, h, big, 256, 256, pad, pad, 1)
|
||||
x = pad
|
||||
y = 256 + pad * 2
|
||||
for s in strip:
|
||||
blit(canvas, w, h, renders[s], s, s, x, y + (128 - s) * zoom, zoom)
|
||||
x += s * zoom + pad
|
||||
with open(path, "wb") as f:
|
||||
f.write(png_bytes(w, h, canvas))
|
||||
return w, h
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- main
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--out", default="TfsExcelImporter/app.ico")
|
||||
ap.add_argument("--preview", default="tools/icon_preview.png")
|
||||
ap.add_argument("--only-preview", action="store_true")
|
||||
ap.add_argument("--variant", default="arrow", choices=["arrow", "tray", "slot"])
|
||||
args = ap.parse_args()
|
||||
|
||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
out_ico = args.out if os.path.isabs(args.out) else os.path.join(root, args.out)
|
||||
out_png = (args.preview if os.path.isabs(args.preview)
|
||||
else os.path.join(root, args.preview))
|
||||
if args.variant != "arrow":
|
||||
base, ext = os.path.splitext(out_png)
|
||||
out_png = "%s_%s%s" % (base, args.variant, ext)
|
||||
|
||||
renders = {}
|
||||
for s in SIZES:
|
||||
renders[s] = render(s, args.variant)
|
||||
print("rendered %dx%d" % (s, s))
|
||||
|
||||
frames = [(s, png_bytes(s, s, renders[s]) if s >= 256
|
||||
else dib_bytes(s, s, renders[s])) for s in SIZES]
|
||||
|
||||
if not args.only_preview:
|
||||
with open(out_ico, "wb") as f:
|
||||
f.write(ico_bytes(frames))
|
||||
print("wrote %s (%d bytes)" % (out_ico, os.path.getsize(out_ico)))
|
||||
|
||||
with open(os.path.splitext(out_png)[0] + "_256.png", "wb") as f:
|
||||
f.write(png_bytes(256, 256, renders[256]))
|
||||
w, h = preview(renders, out_png)
|
||||
print("wrote %s (%dx%d)" % (out_png, w, h))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""查 exe 里到底嵌了哪套图标(PE 资源解析),可导出最大帧为 PNG 核对。
|
||||
|
||||
用途:换图标后 Windows 仍显示旧图标时,先确认「exe 里是新图标」还是「外壳图标缓存没刷」。
|
||||
|
||||
python tools/pe_icon.py dist/TfsExcelImporter.exe [out_256.png]
|
||||
"""
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
|
||||
def u16(d, o):
|
||||
return struct.unpack_from("<H", d, o)[0]
|
||||
|
||||
|
||||
def u32(d, o):
|
||||
return struct.unpack_from("<I", d, o)[0]
|
||||
|
||||
|
||||
def parse_resources(path):
|
||||
d = open(path, "rb").read()
|
||||
pe = u32(d, 0x3C)
|
||||
if d[pe:pe + 4] != b"PE\0\0":
|
||||
raise SystemExit("不是 PE 文件")
|
||||
coff = pe + 4
|
||||
nsec = u16(d, coff + 2)
|
||||
opt_size = u16(d, coff + 16)
|
||||
opt = coff + 20
|
||||
magic = u16(d, opt)
|
||||
dd = opt + (96 if magic == 0x10B else 112)
|
||||
rsrc_rva = u32(d, dd + 2 * 8)
|
||||
|
||||
sec = opt + opt_size
|
||||
sections = []
|
||||
for i in range(nsec):
|
||||
s = sec + 40 * i
|
||||
vsize, vaddr, rawsize, rawptr = (u32(d, s + 8), u32(d, s + 12),
|
||||
u32(d, s + 16), u32(d, s + 20))
|
||||
sections.append((vaddr, vsize, rawptr, rawsize))
|
||||
|
||||
def rva2off(rva):
|
||||
for vaddr, vsize, rawptr, rawsize in sections:
|
||||
if vaddr <= rva < vaddr + max(vsize, rawsize):
|
||||
return rva - vaddr + rawptr
|
||||
raise ValueError("rva %#x 不在任何节里" % rva)
|
||||
|
||||
base = rva2off(rsrc_rva)
|
||||
out = []
|
||||
|
||||
def walk(off, path_ids):
|
||||
nnamed, nid = u16(d, base + off + 12), u16(d, base + off + 14)
|
||||
for i in range(nnamed + nid):
|
||||
e = base + off + 16 + 8 * i
|
||||
name_id, off_to = u32(d, e), u32(d, e + 4)
|
||||
if off_to & 0x80000000:
|
||||
walk(off_to & 0x7FFFFFFF, path_ids + [name_id])
|
||||
else:
|
||||
de = base + off_to
|
||||
rva, size = u32(d, de), u32(d, de + 4)
|
||||
o = rva2off(rva)
|
||||
out.append((path_ids + [name_id], d[o:o + size]))
|
||||
|
||||
walk(0, [])
|
||||
return out
|
||||
|
||||
|
||||
def png_write(path, w, h, rgba):
|
||||
raw = bytearray()
|
||||
for y in range(h):
|
||||
raw.append(0)
|
||||
raw += rgba[y * w * 4:(y + 1) * w * 4]
|
||||
|
||||
def chunk(tag, data):
|
||||
return (struct.pack(">I", len(data)) + tag + data
|
||||
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def dib_to_rgba(payload):
|
||||
bw, bh2 = struct.unpack_from("<ii", payload, 4)
|
||||
h = bh2 // 2
|
||||
px = payload[40:40 + bw * h * 4]
|
||||
out = bytearray(bw * h * 4)
|
||||
for y in range(h):
|
||||
sy = h - 1 - y
|
||||
for x in range(bw):
|
||||
si = (sy * bw + x) * 4
|
||||
di = (y * bw + x) * 4
|
||||
out[di] = px[si + 2]
|
||||
out[di + 1] = px[si + 1]
|
||||
out[di + 2] = px[si]
|
||||
out[di + 3] = px[si + 3]
|
||||
return bw, h, out
|
||||
|
||||
|
||||
def main():
|
||||
exe = sys.argv[1]
|
||||
out_png = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
res = parse_resources(exe)
|
||||
|
||||
groups = [(p, b) for p, b in res if p[0] == 14]
|
||||
icons = {(p[1] if len(p) > 1 else 0): b for p, b in res if p[0] == 3}
|
||||
print("%s: RT_GROUP_ICON=%d RT_ICON=%d" % (exe, len(groups), len(icons)))
|
||||
if not groups:
|
||||
print("!! exe 里没有图标资源(会用系统默认 exe 图标)")
|
||||
return
|
||||
|
||||
for path_ids, b in groups:
|
||||
cnt, = struct.unpack_from("<H", b, 4)
|
||||
print(" group %s: %d 帧" % (path_ids, cnt))
|
||||
frames = []
|
||||
for i in range(cnt):
|
||||
w, h, cc, resv, planes, bc, sz, idx = struct.unpack_from("<BBBBHHIH", b, 6 + 14 * i)
|
||||
frames.append((w or 256, idx, sz))
|
||||
print(" 尺寸:", [f[0] for f in frames])
|
||||
|
||||
if out_png:
|
||||
for size, idx, sz in frames:
|
||||
if size == 256:
|
||||
payload = icons.get(idx)
|
||||
if payload and payload[:8] == b"\x89PNG\r\n\x1a\n":
|
||||
open(out_png, "wb").write(payload)
|
||||
print(" 已导出 256 PNG 帧 ->", out_png)
|
||||
return
|
||||
for size, idx, sz in frames:
|
||||
if size == 48:
|
||||
w, h, rgba = dib_to_rgba(icons[idx])
|
||||
png_write(out_png, w, h, rgba)
|
||||
print(" 已导出 48 DIB 帧 ->", out_png)
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python3
|
||||
"""冒烟验证:直接拉起 Release exe,截取真实窗口(含标题栏图标),存 PNG。
|
||||
|
||||
用途:确认 ApplicationIcon 与 Form.Icon 都生效、且换图标后窗口能正常起来。
|
||||
纯标准库 + ctypes,不依赖任何第三方包。
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import zlib
|
||||
from ctypes import wintypes as wt
|
||||
|
||||
u32 = ctypes.WinDLL("user32", use_last_error=True)
|
||||
g32 = ctypes.WinDLL("gdi32", use_last_error=True)
|
||||
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
|
||||
u32.PrintWindow.argtypes = [wt.HWND, wt.HDC, wt.UINT]
|
||||
u32.GetWindowRect.argtypes = [wt.HWND, ctypes.POINTER(wt.RECT)]
|
||||
u32.GetWindowDC.argtypes = [wt.HWND]
|
||||
u32.GetWindowDC.restype = wt.HDC
|
||||
u32.GetWindowTextW.argtypes = [wt.HWND, wt.LPWSTR, ctypes.c_int]
|
||||
u32.GetWindowThreadProcessId.argtypes = [wt.HWND, ctypes.POINTER(wt.DWORD)]
|
||||
u32.IsWindowVisible.argtypes = [wt.HWND]
|
||||
u32.SendMessageW.argtypes = [wt.HWND, wt.UINT, wt.WPARAM, wt.LPARAM]
|
||||
u32.SendMessageW.restype = ctypes.c_void_p
|
||||
g32.CreateCompatibleDC.argtypes = [wt.HDC]
|
||||
g32.CreateCompatibleDC.restype = wt.HDC
|
||||
g32.CreateCompatibleBitmap.argtypes = [wt.HDC, ctypes.c_int, ctypes.c_int]
|
||||
g32.CreateCompatibleBitmap.restype = wt.HBITMAP
|
||||
g32.SelectObject.argtypes = [wt.HDC, wt.HGDIOBJ]
|
||||
g32.SelectObject.restype = wt.HGDIOBJ
|
||||
g32.GetDIBits.argtypes = [wt.HDC, wt.HBITMAP, wt.UINT, wt.UINT, ctypes.c_void_p,
|
||||
ctypes.c_void_p, wt.UINT]
|
||||
u32.ReleaseDC.argtypes = [wt.HWND, wt.HDC]
|
||||
u32.ReleaseDC.restype = ctypes.c_int
|
||||
u32.GetDC.argtypes = [wt.HWND]
|
||||
u32.GetDC.restype = wt.HDC
|
||||
u32.FillRect.argtypes = [wt.HDC, ctypes.POINTER(wt.RECT), wt.HBRUSH]
|
||||
g32.DeleteObject.argtypes = [wt.HGDIOBJ]
|
||||
g32.DeleteObject.restype = wt.BOOL
|
||||
g32.DeleteDC.argtypes = [wt.HDC]
|
||||
g32.DeleteDC.restype = wt.BOOL
|
||||
g32.CreateSolidBrush.argtypes = [wt.DWORD]
|
||||
g32.CreateSolidBrush.restype = wt.HBRUSH
|
||||
|
||||
|
||||
class BITMAPINFOHEADER(ctypes.Structure):
|
||||
_fields_ = [("biSize", wt.DWORD), ("biWidth", wt.LONG), ("biHeight", wt.LONG),
|
||||
("biPlanes", wt.WORD), ("biBitCount", wt.WORD),
|
||||
("biCompression", wt.DWORD), ("biSizeImage", wt.DWORD),
|
||||
("biXPelsPerMeter", wt.LONG), ("biYPelsPerMeter", wt.LONG),
|
||||
("biClrUsed", wt.DWORD), ("biClrImportant", wt.DWORD)]
|
||||
|
||||
|
||||
def bgra_to_rgba(buf):
|
||||
"""GetDIBits 给的是 BGRA,PNG 要 RGBA。"""
|
||||
out = bytearray(buf)
|
||||
for i in range(0, len(out), 4):
|
||||
out[i], out[i + 2] = out[i + 2], out[i]
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def png(path, w, h, rgba):
|
||||
raw = bytearray()
|
||||
for y in range(h):
|
||||
raw.append(0)
|
||||
raw += rgba[y * w * 4:(y + 1) * w * 4]
|
||||
|
||||
def chunk(tag, data):
|
||||
return (struct.pack(">I", len(data)) + tag + data
|
||||
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def find_window(pid, timeout=25.0):
|
||||
result = []
|
||||
|
||||
@ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM)
|
||||
def cb(hwnd, _):
|
||||
p = wt.DWORD()
|
||||
u32.GetWindowThreadProcessId(hwnd, ctypes.byref(p))
|
||||
if p.value != pid or not u32.IsWindowVisible(hwnd):
|
||||
return True
|
||||
n = u32.GetWindowTextLengthW(hwnd)
|
||||
if n <= 0:
|
||||
return True
|
||||
buf = ctypes.create_unicode_buffer(n + 2)
|
||||
u32.GetWindowTextW(hwnd, buf, n + 2)
|
||||
result.append((hwnd, buf.value))
|
||||
return False
|
||||
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
u32.EnumWindows(cb, 0)
|
||||
if result:
|
||||
return result[0]
|
||||
time.sleep(0.3)
|
||||
return None
|
||||
|
||||
|
||||
def grab(hwnd, path):
|
||||
r = wt.RECT()
|
||||
u32.GetWindowRect(hwnd, ctypes.byref(r))
|
||||
w, h = r.right - r.left, r.bottom - r.top
|
||||
if w <= 0 or h <= 0:
|
||||
raise RuntimeError("window rect empty")
|
||||
src = u32.GetWindowDC(hwnd)
|
||||
mem = g32.CreateCompatibleDC(src)
|
||||
bmp = g32.CreateCompatibleBitmap(src, w, h)
|
||||
old = g32.SelectObject(mem, bmp)
|
||||
ok = u32.PrintWindow(hwnd, mem, 2) # PW_RENDERFULLCONTENT
|
||||
bi = BITMAPINFOHEADER()
|
||||
bi.biSize = ctypes.sizeof(BITMAPINFOHEADER)
|
||||
bi.biWidth, bi.biHeight = w, -h
|
||||
bi.biPlanes, bi.biBitCount, bi.biCompression = 1, 32, 0
|
||||
buf = ctypes.create_string_buffer(w * h * 4)
|
||||
lines = g32.GetDIBits(mem, bmp, 0, h, buf, ctypes.byref(bi), 0)
|
||||
g32.SelectObject(mem, old)
|
||||
g32.DeleteObject(bmp)
|
||||
g32.DeleteDC(mem)
|
||||
u32.ReleaseDC(hwnd, src)
|
||||
rgba = bgra_to_rgba(buf.raw)
|
||||
png(path, w, h, rgba)
|
||||
return w, h, ok, lines, rgba
|
||||
|
||||
|
||||
def crop_zoom(src_png_w, src_png_h, rgba, cw, ch, scale, path, ox=0, oy=0):
|
||||
out_w, out_h = cw * scale, ch * scale
|
||||
dst = bytearray(out_w * out_h * 4)
|
||||
for y in range(out_h):
|
||||
sy = oy + y // scale
|
||||
for x in range(out_w):
|
||||
sx = ox + x // scale
|
||||
si = (sy * src_png_w + sx) * 4
|
||||
di = (y * out_w + x) * 4
|
||||
dst[di:di + 4] = rgba[si:si + 4]
|
||||
png(path, out_w, out_h, dst)
|
||||
|
||||
|
||||
def main():
|
||||
exe = sys.argv[1]
|
||||
out = sys.argv[2]
|
||||
icon_dump = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
|
||||
proc = subprocess.Popen([exe], cwd=os.path.dirname(exe))
|
||||
try:
|
||||
got = find_window(proc.pid)
|
||||
if not got:
|
||||
print("FAIL: 没找到窗口(可能是启动崩溃),进程存活 =", proc.poll() is None)
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
return 1
|
||||
hwnd, title = got
|
||||
print("window title:", title)
|
||||
|
||||
# WM_GETICON=0x7F, ICON_BIG=1 / ICON_SMALL=0
|
||||
big = u32.SendMessageW(hwnd, 0x7F, 1, 0)
|
||||
small = u32.SendMessageW(hwnd, 0x7F, 0, 0)
|
||||
print("WM_GETICON big=%#x small=%#x" % (big or 0, small or 0))
|
||||
|
||||
time.sleep(0.6)
|
||||
w, h, ok, lines, pixels = grab(hwnd, out)
|
||||
print("printed %dx%d ok=%s lines=%d -> %s" % (w, h, ok, lines, out))
|
||||
# 标题栏局部放大 4 倍,专门看图标
|
||||
crop_zoom(w, h, pixels, min(300, w), min(44, h), 4,
|
||||
os.path.splitext(out)[0] + "_titlebar.png", 0, 0)
|
||||
print("titlebar crop ->", os.path.splitext(out)[0] + "_titlebar.png")
|
||||
|
||||
if icon_dump and big:
|
||||
u32.DrawIconEx.argtypes = [wt.HDC, ctypes.c_int, ctypes.c_int, wt.HICON,
|
||||
ctypes.c_int, ctypes.c_int, wt.UINT,
|
||||
wt.HBRUSH, wt.UINT]
|
||||
src = u32.GetDC(None)
|
||||
mem = g32.CreateCompatibleDC(src)
|
||||
size = 32
|
||||
bmp = g32.CreateCompatibleBitmap(src, size, size)
|
||||
old = g32.SelectObject(mem, bmp)
|
||||
# 白底,避免透明区域看不清
|
||||
brush = g32.CreateSolidBrush(0x00FFFFFF)
|
||||
rect = wt.RECT(0, 0, size, size)
|
||||
u32.FillRect(mem, ctypes.byref(rect), brush)
|
||||
u32.DrawIconEx(mem, 0, 0, big, size, size, 0, None, 3) # DI_NORMAL
|
||||
bi = BITMAPINFOHEADER()
|
||||
bi.biSize = ctypes.sizeof(BITMAPINFOHEADER)
|
||||
bi.biWidth, bi.biHeight = size, -size
|
||||
bi.biPlanes, bi.biBitCount, bi.biCompression = 1, 32, 0
|
||||
buf2 = ctypes.create_string_buffer(size * size * 4)
|
||||
g32.GetDIBits(mem, bmp, 0, size, buf2, ctypes.byref(bi), 0)
|
||||
g32.SelectObject(mem, old)
|
||||
g32.DeleteObject(bmp)
|
||||
g32.DeleteObject(brush)
|
||||
g32.DeleteDC(mem)
|
||||
u32.ReleaseDC(None, src)
|
||||
png(icon_dump, size, size, bgra_to_rgba(buf2.raw))
|
||||
print("icon dumped ->", icon_dump)
|
||||
finally:
|
||||
time.sleep(0.2)
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user