# 介面與輸入

> 畫板上的按鈕、選項卡片能點，滑鼠停在上面會自動醒目顯示；登記熱鍵、點地面選位置、讀取滑鼠指著哪裡、知道本機玩家選取了誰。點擊、熱鍵、放技能、聊天全文、玩家離開，都會進入事件流。

來源: https://war3ai.com/zh-tw/docs/ui-input/

[畫板](https://war3ai.com/zh-tw/docs/canvas/) 畫出來的東西，現在**能點了**。執行環境接管遊戲視窗的輸入，外部程式可以：

| 能力 | 一句話 | 遊戲收得到嗎 |
|---|---|---|
| **可點擊的畫板項目** | 按鈕、選項卡片、面板：點中時發出 `ui.click`，滑鼠停在上面自動醒目顯示 | 點在按鈕上的那一下**收不到** |
| **熱鍵** | 登記 `F5`、`ctrl+shift+Q` 這樣的組合，按下時發出 `hotkey` | 可選擇吞掉（連同它產生的字元） |
| **點地面** | 點在世界上時發出 `mouse.world`，帶有地面座標 | 可選擇吞掉（「點一個位置放塔」） |
| **滑鼠位置** | 每幀更新：螢幕像素、指著的地面點、懸停的畫板項目 | —— |
| **選取** | 本機玩家選取了誰；一有變化就發出 `selection.changed` | —— |

全部都是**本機輸入 + 本機繪製**：不進入指令流，多人局也安全。但如果回呼裡改動了世界（刷單位、改屬性），那還是只能用於單人局。

## Python：g.ui

```python
ui = g.ui                                                   # 第一次使用時，執行環境接管視窗輸入
ui.button("shop", "買一瓶藥水（50 金）", screen=(40, 300), on_click=lambda g, ev: buy(g))
c = ui.choice("升級了！選一個獎勵", [("力量 +5", "更耐打"), ("攻速 +20%", "輸出更高"), ("召喚狼", "多一個幫手")],
              pause=True, on_pick=lambda g, i: give(g, i))  # 螢幕中間一排卡片；pause=True 選擇時遊戲暫停
i = c.wait(timeout=30)                                      # 也可以阻塞等待（期間照樣處理事件，不會遺失）
ui.hotkey("F5", lambda g, ev: g.say(hero, "收到！"))        # 預設吞掉
ui.hotkey("ctrl+shift+Q", on_press=..., swallow=False)
ui.mouse(on_click, capture=True, buttons=("left", "right"))  # 擷取地面點擊：左鍵、右鍵都回報，並吞掉
xy = ui.pick_point("點地面：塔要蓋在哪？")                  # 阻塞版：下一次左鍵點地面 -> (x, y)；Esc 或逾時 -> None
ui.cursor()                                                 # {'screen': (x, y), 'world': (x, y, z) 或 None, 'hover': 'shop'}
ui.toast("第 3 波來了！", seconds=3)
ui.close()                                                  # 收起自己的控制項和熱鍵；沒有別的程式在用輸入時才交還視窗輸入
g.close()                                                   # 或者整個中斷連線（也可以寫 with Game(...) as g:）
```

回呼的參數是 `(g, ev)`，在你呼叫 `g.events()` 時觸發 —— Bot 和[玩法模組](https://war3ai.com/zh-tw/docs/mods/)的執行器每拍都會呼叫。沒有給回呼的點擊會進入 `ui.clicks`。回呼裡拋出例外只會記入日誌，不影響其他回呼和事件。

也可以直接使用畫板底層：`g.canvas.text(..., clickable=True, hover=顏色)`，點擊從事件流裡接收，`ev.key` 就是繪製時給的 key。畫可點擊的元素會自動開啟輸入，不必先碰 `g.ui`。

**熱鍵寫法**：`F1` ~ `F24`、`A` ~ `Z`、`0` ~ `9`、`numpad0` ~ `numpad9`、`space enter esc tab backspace insert delete home end pageup pagedown left up right down`，前面可以加上 `ctrl+`、`shift+`、`alt+`。

> **注意**
>
> 不帶修飾鍵的字母和數字會和聊天輸入、遊戲快速鍵互相衝突。優先使用 F5 ~ F8 這類遊戲沒占用的鍵，或者組合鍵。

## 新事件

`g.events()` 裡多了這些（完整欄位見 [W3P 協定](https://war3ai.com/zh-tw/docs/protocol/)）：

| kind | 什麼時候 | 便捷欄位 |
|---|---|---|
| `ui.click` | 可互動的畫板項目被點擊 | `.key` 畫板 key、`.button`（`'left'` / `'right'`）、`.mods` 修飾鍵 |
| `ui.hover` | 滑鼠移入 / 移出畫板項目 | `.key`（移出時是 `None`） |
| `hotkey` | 登記的熱鍵被按下 | `.key` 熱鍵寫法、`.mods` |
| `mouse.world` | 開啟地面點擊後，點在世界上 | `.x .y` 地面座標、`.button`、`.value`（1 = 被吞掉了） |
| `selection.changed` | 本機玩家的選取變了 | 用 `g.selection()` 取得單位 |
| `spell.cast` | 單位放了技能（技能開始冷卻） | `.spell` 四字碼、`.b` 等級、`.value` 冷卻秒數、`.x .y` 施法點 |
| `message` | 螢幕訊息框裡出現一條 | `.text` 全文、`.frame` 哪個框、`.chat`（是聊天時） |
| `player.left` | 玩家離開或被判定落敗移除 | `.player` |
| `game.ended` | 離開對局 | —— |

## 聊天和螢幕訊息

玩家在聊天框輸入的文字，直接從 `message` 事件的 `.chat` 讀取：

```python
for ev in g.events():
    if ev.kind == "message" and ev.chat and ev.chat["text"] == "-follow":
        ...                                    # ev.chat = {'channel': '所有人', 'sender': '玩家名稱', 'text': '-follow'}
```

`g.messages()` 另有一份獨立的游標：遊戲提示（「需要更多的農場」「不能在那裡建造」）也在裡面。寫 Bot 時用它，就知道一條命令為什麼沒做成。

## 從其他語言使用

- **閘道**：`ui.button`、`ui.choice`、`ui.hotkey`、`ui.mouse`、`ui.cursor` 這些方法在 [閘道](https://war3ai.com/zh-tw/docs/gateway/) 上以同名提供。遠端無法傳入回呼函式，點擊和熱鍵從事件推送裡接收（`ui.click` 事件帶有 `key`）。
- **直接寫入共用記憶體**：先送出語意命令 `input_enable`（W3P 操作碼 74），執行環境開始接管輸入；在輸入區塊 `Local\War3Input_<pid>` 裡，你寫入熱鍵表和滑鼠開關，它回寫滑鼠位置、指著的地面點和懸停項目。畫板項目的旗標位元 `0x40` 表示「可互動」。配置見 [W3P 協定](https://war3ai.com/zh-tw/docs/protocol/)。

## 好幾個程式同時使用

模組、遠見、MCP、閘道的每個工作階段可能同時在一局遊戲裡放按鈕、登記熱鍵，彼此互不干擾：

- 每個程式登記自己的熱鍵和地面點擊開關，SDK 把大家的合併成一張表交給執行環境。同一個鍵只留一條，事件發給所有人，各自依鍵認出自己的熱鍵；
- `ui.close()` 只撤掉自己的，最後一個程式離開了才交還視窗輸入；
- 程式被強制結束、來不及收尾：執行環境每 2 秒檢查一次，登記過的程式全都結束了，就清掉它們留下的熱鍵和地面點擊攔截，它們畫的按鈕也不再攔截點擊。

## 實測

2026-09-25，測試實例上的實機核對 16/16：

- 點按鈕 → `ui.click` + 回呼，執行環境的攔截計數 +1（遊戲沒收到這一下）；點在按鈕外面不觸發；
- F6 → `hotkey`；點地面 → `mouse.world`（被吞掉）；
- 刷出一個聖騎士、選取它 → `selection.changed`，`g.selection()` 對得上；放神聖護甲 → `spell.cast('AHds', 1, 35.0)`；
- 地圖文字 → `message`；聊天 → `message`，`.chat` 解析出發言者和內容；
- 判定電腦落敗 → `player.left`；結束對局 → `game.ended`。

真人用滑鼠點按鈕、懸停醒目顯示也逐一看過。

## 邊界與注意事項

- **位置用的是真實滑鼠**：遊戲本身依系統游標讀取位置，所以懸停、`cursor()` 反映的是真實滑鼠。攔截只管按鍵。
- **畫在滑鼠游標下面**：魔獸把游標當作畫面的一部分，每幀畫進去。畫板和頭頂氣泡都在遊戲畫游標那一步之前繪製：蓋住遊戲介面，被游標蓋住。只有這一幀沒畫游標（隱藏或過場動畫）時，才退回到最後一步繪製。
- **系統縮放**：自己寫測試、用視窗訊息投遞點擊時，未宣告 DPI 感知的行程送過去的座標會被系統放大（150% 縮放時實測 ×1.5）。測試程式要先宣告 DPI 感知。真人點擊不受影響。
- **第一次要預熱字型**，約 1 秒。這段期間按鈕還沒畫出來，點不中。
- **不在對局裡時不回報地面點擊**：主選單和結算畫面上 `mouse.world` 不發出，也不吞掉。
- 按下時被吞掉的那一下，放開前切換到別的程式或滑鼠拖出了視窗，也會復位，不會連下一次放開也吞掉。
- 1.27 沒有建立新遊戲介面框架的函式（1.31 才有）：這裡的按鈕、卡片都是執行環境畫的，樣式隨你定，但不會出現在遊戲本身的選單層級裡。
