# LaserTank 窗口缩放功能修改记录

## 说明

原来的 LaserTank 只有三个固定尺寸：Small、Medium、Large。问题是——谁知道这三个到底多大？我想要的是**等比例缩放**，从 ×1 到 ×10，想放多大放多大，就像浏览器缩放一样直观。

原来的设计是在 `SetGameSize()` 里写死三组数据，完全没有扩展性。既然底层每个单元格是 32×32 像素，那缩放就是 `32 × N`，没理由写死。

## 核心思路

游戏区域是 16×16 的单元格网格，基准像素 32×32（×1 大小）。

```
单元格像素  = 32 × scale
游戏板总像素 = 16 × 32 × scale = 512 × scale
```

最关键的一个设计：菜单 ID 从 130 到 139 连续编号，这样 `scale = ID - 129`，一行代码就搞定，不需要 switch 或者 if-else 判断。这个小技巧让 `LTANK.C` 里处理菜单命令的代码非常干净。

## 改了哪些文件

### 1. `lt32l_us.inc` — 菜单资源

把原来的三行菜单项：

```c
MENUITEM "&Small",  120
MENUITEM "&Medium", 121, CHECKED
MENUITEM "&Large",  122
```

换成了十行：

```c
MENUITEM "&x1",  130, CHECKED
MENUITEM "&x2",  131
...
MENUITEM "&x10", 139
```

`CHECKED` 写在 `x1` 那行，表示默认选中 ×1（也就是原来的 Medium 大小）。`&x1` 里的 `&` 是快捷键标记，Alt+S 再按 X 就能打开 Size 菜单。

### 2. `LTANK.H` — 头文件

只加了一行注释：

```c
#define psSize "Size"  // Scale factor: 1-10 (x1 to x10)
```

`psSize` 是 INI 文件里的键名，原来就有的，不用改名字，兼容性没问题。

### 3. `LTANK.C` — 主窗口过程

原来处理 Small/Medium/Large 是三个独立的 case，各自调 `SetGameSize()`。现在改成这样：

```c
case 130:  // x1
case 131:  // x2
...
case 139:  // x10
    SetGameSize(LOWORD(wparam) - 129);
    InvalidateRect(Window, NULL, TRUE);
    return(0);
```

10 个 case 贯穿（fall-through），统一用 `ID - 129` 算出 scale。写完之后觉得这比原来的写法清爽太多了。

### 4. `LTANK2.C` — `SetGameSize()` 函数（核心改动）

这是本次修改最重要的函数，完全重写。逻辑分几步：

**第 1 步：参数钳制**

```c
if (scale < 1) scale = 1;
if (scale > 10) scale = 10;
```

防止 INI 文件被手动改坏。

**第 2 步：更新菜单勾选**

```c
SizeMenu = GetSubMenu(MMenu, 2);  // Option 菜单是第2个（从0数）
if (SizeMenu) {
    for (i = 130; i <= 139; i++)
        CheckMenuItem(SizeMenu, i, 0);      // 全部取消勾选
    CheckMenuItem(SizeMenu, 129 + scale, MF_CHECKED);  // 勾选当前项
}
```

这里有个小坑：`CheckMenuItem` 第三个参数传 `0` 是取消勾选（等价于 `MF_UNCHECKED`），传 `MF_CHECKED` 是添加勾选。lcc 的头文件里 `MF_CHECKED = 0x00000008`，直接写 `0` 反而更简洁。

**第 3 步：存 INI**

```c
itoa(scale, temps, 10);
WritePrivateProfileString("SCREEN", psSize, temps, INIFile);
```

下次启动程序时，`GetPrivateProfileInt("SCREEN", psSize, 1, INIFile)` 读回来，直接恢复到上次的大小。

**第 4 步：重载图形**

```c
if (GFXOn) GFXKill();   // 先卸掉旧的
SpBm_Width  = 32 * scale;
SpBm_Height = 32 * scale;
// ... 计算各种位置 ...
GFXInit();               // 重新加载
```

`GFXInit()` 会根据 `SpBm_Width / SpBm_Height` 加载对应尺寸的位图。如果自定义了皮肤（GFX），也会按新的尺寸重新加载。

**第 5 步：重算所有位置**

这是最繁琐的部分。缩放之后，以下几样东西都要跟着变：

| 变量                | 计算公式                             | 说明                          |
| ------------------- | ------------------------------------ | ----------------------------- |
| `SpBm_Width/Height` | `32 × scale`                         | 核心，所有渲染都依赖这个      |
| `ContXPos`          | `XOffset + 16×SpBm_Width + 40`       | 右侧控制面板的 X 坐标         |
| `LaserOffset`       | `13 × scale`                         | 激光绘制的偏移量（基准 13px） |
| 窗口宽度            | `XOffset + 16×SpBm_Width + 40 + 190` | 游戏板 + 间距 + 控制面板      |
| 窗口高度            | `YOffset + 16×SpBm_Height + 50`      | 游戏板 + 底部留白             |

编辑器模式下的按钮位置也要跟着重设，`SetWindowPos(BT1, ...)`、`SetWindowPos(BT2, ...)` 一路写下去，有点烦但没捷径。

编辑器还有一个细节：`EditBMWidth`（编辑器里每行显示的位图个数）要根据缩放级别动态调，放太大了一行显示不下：

```c
if (scale <= 4) EditBMWidth = 6;
else if (scale <= 6) EditBMWidth = 5;
else if (scale <= 8) EditBMWidth = 4;
else EditBMWidth = 3;
```

## 缩放尺寸对照

| 菜单 | 单元格  | 游戏板    | 激光偏移 |
| ---- | ------- | --------- | -------- |
| ×1   | 32×32   | 512×512   | 13       |
| ×2   | 64×64   | 1024×1024 | 26       |
| ×5   | 160×160 | 2560×2560 | 65       |
| ×10  | 320×320 | 5120×5120 | 130      |

×10 的时候游戏板有 5120 像素宽，4K 显示器刚好满屏……再大就超出屏幕了，所以上限设在 ×10 是合理的。

## 编译

用 lcc-Win32 编译（装在 `C:\lcc`）：

```bash
cd D:\LaserTank\lcc
C:\lcc\bin\make.exe clean
C:\lcc\bin\make.exe
```

结果：**0 errors，67 warnings**。

Warning 全是原代码的问题（`Missing prototype for 'xxx'`），和这次修改无关。lcc 对函数原型要求比较松，能正常链接。

编译出来的 exe 在 `D:\LaserTank\Setups\Files\lasertank.exe`，约 300KB。

## 顺便记录几个 API 的用法

这次改代码的时候把几个 Win32 API 的用法搞清楚了，记一下免得下次又去查：

**`CheckMenuItem()`**

```c
CheckMenuItem(hMenu, ID, MF_CHECKED);    // 打勾
CheckMenuItem(hMenu, ID, MF_UNCHECKED); // 去勾
// 或者简写：
CheckMenuItem(hMenu, ID, 0);           // 去勾（0 = MF_UNCHECKED）
```

**`SetWindowPos()`**

```c
// 只改大小，不改位置：
SetWindowPos(hwnd, 0, 0, 0, newWidth, newHeight, SWP_NOMOVE | SWP_NOZORDER);
// SWP_NOMOVE  → 忽略 x,y 参数
// SWP_NOZORDER → 忽略 Z 序参数
```

**INI 文件读写**

```c
// 读整数（键不存在时返回默认值 1）
int scale = GetPrivateProfileInt("SCREEN", "Size", 1, INIFile);

// 写字符串（需要先 itoa 转换）
char temps[11];
itoa(scale, temps, 10);
WritePrivateProfileString("SCREEN", "Size", temps, INIFile);
```

## 还存在的问题

- [ ] 窗口标题栏和边框尺寸没有参与缩放计算，×10 的时候游戏板比窗口大，需要自己拖边框
- [ ] 编辑器按钮在 ×10 下可能超出屏幕，没有做边界检查
- [ ] `EditBMWidth` 的几个阈值是拍脑袋定的，可能没有严格测试过每种情况

不过基本功能已经能用了，剩下的以后有需要再改。

---

_2026-05-17_
