# 控件获取 - chatautox 开发文档

# 控件获取

扩展开发时，第一步通常是**拿到目标控件的定位信息**（`Name`、`AutomationId`、`ClassName`、`ControlType`），再编写点击、输入等自动化逻辑。本章介绍 chatautox 提供的控件获取方式。

> 合规提示：下文示例中的窗口标题、控件 Name 等字符串，均来自 UIA/Win32 枚举结果，仅作技术演示。详见 [开发文档 — 商标与合规说明](/doc-site/dev.md#商标与合规说明)。

## 推荐流程

```
1. 导出控件树（dump / walk）
      ↓
2. 在树文本或 control_browser 中找到目标行
      ↓
3. 提取 Name / Aid / Class / ctrl_type
      ↓
4. 用 WinAuto.find() 验证能定位到控件
      ↓
5. 编写 click / input / get_list_items 等业务逻辑
```

## 方式一：导出控件树（调试首选）

### WinAuto — 通用任意窗口

```python
from chatautox import WinAuto

TARGET_CLASS = 'YourMainWndClass'  # 改为你的窗口 ClassName
TARGET_TITLES = ('目标应用', 'MyApp')
hwnd = WinAuto.find_main_hwnd(TARGET_CLASS, TARGET_TITLES, find_window_name=TARGET_TITLES[0])
auto = WinAuto(hwnd)

windows = auto.dump_windows_controls(
    max_windows=30,       # 最多导出几个顶级窗
    max_lines=800,        # 每个窗最多几行控件
    same_process_only=True,  # 仅同进程窗口
)
WinAuto.print_windows_dump(windows)
```

输出示例（窗口标题 `title=` / 控件 `Name=` 为系统返回值，非 chatautox 品牌用语）：

```
[window] hwnd=12345 pid=6789 class="YourMainWndClass" title="…" rect=[0, 0, 800, 600]
  WindowControl Name="…" Class="YourMainWndClass" Aid="" Rect=(0,0,800,600)
    PaneControl Name="" Class="" Aid="main_window" Rect=(0,0,800,600)
      ListControl Name="" Class="" Aid="session_list" Rect=(0,60,250,600)
        ListItemControl Name="示例会话" Class="ChatSessionCell" Aid="" Rect=(0,60,250,100)
      EditControl Name="" Class="ChatInputBox" Aid="chat_input" Rect=(250,550,780,590)
      ButtonControl Name="发送" Class="" Aid="" Rect=(780,550,800,590)
```

树文本每行格式：

```
{缩进}{ControlType} Name="..." Class="..." Aid="..." Rect=(left,top,right,bottom)
```

### 内置 IM 客户端 — 含子进程窗（高级）

部分 IM 客户端的笔记、合并消息等弹窗可能在**独立进程**。通过 `auto.im_client()` 获得客户端后，可调用 `DumpWindowsControls`（参数与 `WinAuto.dump_windows_controls` 类似）：

```python
im = auto.im_client(debug=False)

# 常用：主进程窗口
windows = im.DumpWindowsControls(
    max_windows=30,
    max_lines=800,
    same_process_only=True,
)
```

若需 **`restrict_pid` / `extra_hwnds`**（笔记子进程、不可见 HWND），走内部 API：

```python
# 指定 PID + 强制包含额外 HWND
windows = im._api.DumpWindowsControls(
    max_windows=30,
    max_lines=1000,
    same_process_only=True,
    restrict_pid=note_pid,
    extra_hwnds=[note_hwnd],
)
for w in windows:
    print(f'hwnd={w["hwnd"]} class={w["class_name"]} title={w["title"]}')
    print(w.get('tree', ''))
```

### GenericWnd — 结构化遍历（保留 ctrl 对象）

`control_browser` 控件浏览器底层使用此接口，返回带 **UIA 控件对象** 的列表：

```python
from chatautox.ui.window import GenericWnd

wnd = GenericWnd(hwnd=12345)
controls = wnd.dump_controls(max_windows=50, max_lines=800)

for item in controls:
    print(item['label'])       # 格式化标签行
    print(item['ctrl'])        # UIA 控件对象，可直接传给 WinAuto.click
    print(item['cx'], item['cy'])  # 中心坐标
    print(item['rect'], item['depth'])
```

`walk_controls()` 与 `dump_controls()` 类似，可单独遍历当前绑定窗口：

```python
items = wnd.walk_controls(max_depth=30, max_lines=800, time_budget=5.0)
```

### 解析树文本为结构化列表

若已有 `dump_windows_controls` 的返回值，可解析为扁平控件列表（不含 ctrl 对象）：

```python
from chatautox.ui.window import GenericWnd

parsed = GenericWnd.parse_controls(windows)
for p in parsed:
    print(p['label'], p['cx'], p['cy'], p.get('sender', ''))
```

***

## 方式二：按属性查找单个控件

### WinAuto.find — 推荐

在已绑定 HWND 的窗口下，按 UIA 属性查找**第一个**匹配控件：

```python
from chatautox import WinAuto

auto = WinAuto(hwnd)

ctrl = auto.find(
    ctrl_type='EditControl',    # UIA 类型名，空则泛型 Control
    name='发送',                 # Name 精确匹配
    aid='chat_input',           # AutomationId
    cls='ChatInputBox',         # ClassName
    depth=10,                   # 搜索深度，默认 10
)

if ctrl is None:
    print('未找到控件')
else:
    print(ctrl.Name, ctrl.AutomationId, ctrl.ClassName)
    auto.click(ctrl)
```

查找参数可任意组合，至少提供一个非空条件。常用 `ctrl_type`：

| ctrl\_type        | 说明        |
| ----------------- | --------- |
| `Control`         | 任意控件（最宽泛） |
| `ButtonControl`   | 按钮        |
| `EditControl`     | 输入框       |
| `ListControl`     | 列表        |
| `ListItemControl` | 列表项       |
| `TextControl`     | 文本        |
| `MenuItemControl` | 菜单项       |
| `PaneControl`     | 面板        |

### WinAuto.wait\_ctrl — 等待控件出现

弹窗、加载动画等场景，轮询直到控件出现或超时：

```python
ctrl = auto.wait_ctrl(
    name='另存为',
    ctrl_type='WindowControl',
    timeout=5.0,      # 最长等待秒数
    interval=0.2,     # 轮询间隔
)
```

### macro\_helper — 函数式

流程编排器生成代码常用：

```python
from chatautox.auto.macro_helper import get_hwnd, find_ctrl, ctrl_find

WIN_TITLE = '…'  # 替换为目标窗口在任务栏显示的实际标题
hwnd = get_hwnd(win_title=WIN_TITLE, win_class='')
ctrl = find_ctrl(win_title=WIN_TITLE, name='发送', ctrl_type='ButtonControl')
ctrl = ctrl_find(hwnd, aid='chat_input', ctrl_type='EditControl')
```

***

## 方式三：批量查找（多个匹配项）

### AppWindow — 返回列表

```python
from chatautox.auto.uibase import AppWindow

win = AppWindow.from_hwnd(hwnd)

# 按 ClassName 查找所有匹配
cells = win.find_by_class('ChatSessionCell', depth=15)

# 按 Name 查找（exact=True 精确，False 模糊包含）
items = win.find_by_name('示例会话', exact=True, depth=15)

# 按 AutomationId
inputs = win.find_by_automationid('chat_input', depth=10)

# 原生 UIA FindAll
root = win.root_control
all_btns = [c for c in root.FindAll(maxDepth=10)
            if c.ControlTypeName == 'ButtonControl']
```

### 遍历子控件

```python
list_ctrl = auto.find(ctrl_type='ListControl', aid='session_list')
children = list_ctrl.GetChildren()

for child in children:
    print(child.Name, child.ClassName, child.AutomationId)
```

***

## 方式四：列表项批量获取

适用于会话列表、联系人列表等 `ListControl`，一次性获取所有项的完整属性：

```python
items = auto.get_list_items(
    list_type='ListControl',
    list_aid='session_list',       # 列表 AutomationId
    item_cls='ChatSessionCell',    # 列表项 ClassName 过滤
    name_field='name',             # 提取名称字段：name / aid / text
    unread_pattern=r'\[(\d+)条\]',  # 未读数正则（可选）
    mute_keyword='消息免打扰',       # 免打扰关键词（可选）
    depth=15,
)

for item in items:
    print(item['name'])            # 显示名称
    print(item['unread'])          # 未读数
    print(item['mute'])            # 是否免打扰
    print(item['ctrl'])            # UIA 控件对象
    print(item['BoundingRectangle'])  # 位置
    print(item['AutomationId'])
```

`get_list_items` 返回的每项包含 `_dump_ctrl_props` 导出的完整属性：

| 字段                          | 说明                                          |
| --------------------------- | ------------------------------------------- |
| `Name` / `Text`             | 控件 Name 原文                                  |
| `AutomationId`              | AutomationId                                |
| `ClassName`                 | ClassName                                   |
| `ControlTypeName`           | 控件类型                                        |
| `BoundingRectangle`         | `{left, top, right, bottom, width, height}` |
| `IsEnabled` / `IsOffscreen` | 状态                                          |
| `ctrl`                      | UIA 控件对象（可直接操作）                             |
| `name` / `unread` / `mute`  | 列表语义字段                                      |

IM 客户端会话列表快捷方法（以 chatautox 默认适配目标为例）：

```python
sessions = auto.get_sessions(list_aid='session_list', item_cls='ChatSessionCell')
unread   = auto.get_unread_sessions()
found    = auto.find_session('示例会话')
```

***

## 方式五：control\_browser 可视化获取

项目内置 **control\_browser** / **control\_browser\_lite** 可视化调试面板，适合不熟悉代码的控件定位：

### 基本操作

1. 启动控件浏览器，**拾取**或绑定目标窗口（类名 + 标题，可重复执行、自动查找 HWND）
2. 按 **F5** 刷新控件树
3. 在左侧树中选中目标控件
4. 右键 → **控件详情** 查看完整属性
5. 右键 → **生成代码** 获取 `WinAuto` 调用代码

面板顶部可切换点击模式（UIA / PostMessage / 物理），与 `WinAuto` 的 `mode` 参数对应。

### control\_browser\_lite：流程录制与代码执行

lite 版右侧提供**代码编辑框**与**运行输出**面板：

1. 拾取窗口后自动生成绑定头（`WIN_CLASS` / `WIN_TITLES` / `auto = WinAuto(...)`）
2. 点击、发送、键盘操作，或「所有操作 → 消息列表处理」等，会**追加步骤**到编辑器，不覆盖已有代码
3. **▶ 执行**：在**独立子进程**中运行编辑器脚本（`-u` 无缓冲），`print` / `wxlog` 调试日志实时显示在「运行输出」
4. **⏹ 停止**：终止子进程（含 `GetNextNewMessage` 长时间阻塞时也可停止）；执行中 ▶ 禁用

「消息列表处理 → GetNextNewMessage 循环 / GetNewMessage 循环」模板含 **`auto.activate()` + `while True` 轮询**。首帧通常不交付历史消息，属正常行为。

消息 API 通过 **`auto.im_client()`** 创建（默认内置 IM 适配器；绑定控件浏览器时会自动查找 IM 主窗口），步骤模板会自动补上 `im = auto.im_client(...)`。

### 所有操作 → 注册别名（给控件起名字）

**一句话**：先把「怎么找到某个控件」记在一个**短名字**里，后面写流程只写名字，不用反复抄 `aid`、`name`、`depth`。

可以把它理解成：给按钮、输入框、会话列表**起外号**，脚本里用外号操作。

#### 什么时候用

| 情况                         | 要不要注册别名              |
| -------------------------- | -------------------- |
| 只点一次、试一下就完                 | 不用，直接「鼠标操作 → 左键单击」即可 |
| 同一条流程里**多次**用到输入框、发送钮、会话列表 | **建议用**              |
| 脚本很长，想改一处定位、全局生效           | **建议用**              |

和「列表操作」的区别：

- **列表操作**：代码里直接写 `list_aid='session_list'`，适合演示、写一次
- **注册别名**：先 `register('sessions', list_aid='session_list', ...)`，后面统一写 `'sessions'`，适合**多步骤流程**

#### 在 control\_browser\_lite 里怎么用

1. **拾取**目标窗口，左侧控件树 **F5** 刷新
2. 打开 **「所有操作」** → 分类选 **「注册别名」**
3. 按下面顺序操作（每一步点 **「生成代码」** 追加到右侧编辑器）：

| 步骤 | 选什么操作                   | 你要做什么                                                                            |
| -- | ----------------------- | -------------------------------------------------------------------------------- |
| ①  | **注册控件**                | 在树里**选中**输入框/按钮 → 生成 `auto.register('c', ...)` → 把 `'c'` 改成好记的名字，如 `'input_box'` |
| ②  | **注册列表**（可选）            | 参数填 `list_aid`，如 `'session_list'` → 别名 `'lst'` 表示左侧会话列表                          |
| ③  | **别名点击 / 别名输入 / 别名发消息** | 不再选控件，代码里只用 `'input_box'` 等名字                                                    |
| ④  | **代理链式发消息**（可选）         | 一行写完：`auto.use('input_box').click().input('你好').enter()`                         |

**注册列表** 时参数示例：`list_aid='session_list'`（左侧会话列表示例，以你控件树里看到的 AutomationId 为准）。

**注册控件** 前务必在树里选中目标；步骤预览里会出现 `target = auto.find(...)` + `register(...)`。

#### 生成代码长什么样

**注册一个输入框：**

```python
# 查找聊天输入框（Aid 来自控件树）
target = auto.find(aid='chat_input', depth=12)
auto.register('input_box', ctrl_type='EditControl', aid='chat_input', depth=12)
```

**注册左侧会话列表：**

```python
auto.register('sessions',
    list_type='ListControl',
    list_aid='session_list',
    item_cls='ChatSessionCell',
    unread_pattern=r'\[(\d+)条\]',
    mute_keyword='消息免打扰')
```

**后面只用别名（短、好读）：**

```python
auto.input_cfg('input_box', '你好')
auto.send_msg_cfg('input_box', '你好')      # 点击 + 输入 + 回车

auto.use('sessions').click_item('示例会话')
auto.use('sessions').click_unread(0)        # 点第一个未读会话

# 链式写法（和上面等价，一行串起来）
auto.use('input_box').click().input('你好').enter()
```

#### 「所有操作」里各项是干什么的

| 操作名                      | 作用                                       |
| ------------------------ | ---------------------------------------- |
| 注册控件                     | 把**当前选中控件**的定位写入别名                       |
| 注册列表                     | 把**列表**的 list\_aid 等写入别名                 |
| 读取配置 cfg                 | 查看别名里存了什么：`auto.cfg('input_box')`        |
| 别名查找                     | `ctrl = auto.find_cfg('input_box')`      |
| 别名点击 / 输入 / 发消息          | 用别名点击、打字、发消息                             |
| 别名取列表 / 点列表项 / 取未读 / 点未读 | 列表专用，不用每次写 list\_aid                     |
| 代理链式 ×                   | `auto.use('别名').click().input(...)` 链式写法 |

API 参数表见 [WinAuto — 控件别名](/doc-site/dev/winauto/alias.md#控件别名小白入门)。

#### 完整小流程示例（发送一条消息）

```python
auto = WinAuto(hwnd, mode='uia')
auto.activate()

# ① 注册（流程开头做一次即可）
auto.register('input_box', ctrl_type='EditControl', aid='chat_input', depth=12)

# ② 使用别名
auto.use('input_box').click().input('你好，这是测试').enter()
```

若还要先点某个会话，可先 **注册列表** + **代理点列表项**，或直接用「列表操作 → 点击会话」。

***

## 读取控件属性

找到控件后，可直接读 UIA 属性或借助封装：

```python
# 原生 UIA 属性
ctrl.Name
ctrl.AutomationId
ctrl.ClassName
ctrl.ControlTypeName
ctrl.BoundingRectangle   # left/top/right/bottom
ctrl.IsEnabled
ctrl.Exists(maxSearchSeconds=0)

# ElementAction 包装（uibase）
from chatautox.auto.uibase import ElementAction

elem = ElementAction(ctrl, hwnd=hwnd)
print(elem.name, elem.automation_id, elem.bbox, elem.center)
texts = elem.get_all_text()   # 子控件所有 Name
```

***

## 完整示例：从导出到操作

```python
from chatautox import WinAuto

TARGET_CLASS = 'YourMainWndClass'
TARGET_TITLES = ('目标应用', 'MyApp')
hwnd = WinAuto.find_main_hwnd(TARGET_CLASS, TARGET_TITLES, find_window_name=TARGET_TITLES[0])
auto = WinAuto(hwnd, mode='uia')

# 1. 导出控件树，找到 chat_input 的 Aid
WinAuto.print_windows_dump(auto.dump_windows_controls(max_lines=500))

# 2. 验证定位
input_ctrl = auto.find(ctrl_type='EditControl', aid='chat_input')
assert input_ctrl is not None, '输入框未找到'

# 3. 注册别名，后续脚本复用
auto.register('input_box', ctrl_type='EditControl', aid='chat_input')
auto.register('send_btn', ctrl_type='ButtonControl', name='发送')

# 4. 操作
auto.use('input_box').click().input('你好').enter()
# 或
auto.send_msg_cfg('input_box', '你好')
```

***

## 常见问题

### find 返回 None

- 检查 `depth` 是否足够（复杂界面建议 15\~20）
- 确认 `Name` 是否与控件树中**完全一致**（含空格、换行）
- 弹窗可能在子进程，需用 `DumpWindowsControls(restrict_pid=..., extra_hwnds=...)`
- 控件可能尚未渲染，改用 `wait_ctrl(timeout=5)`

### 控件树过大 / 卡顿

- 减小 `max_lines`（如 500\~800）
- 设置 `same_process_only=True` 过滤无关窗口
- 复杂界面建议 `max_windows=30`

### Name 含换行

列表项 Name 常含多行文本，`get_list_items` 会自动取第一行作为 `name` 字段；手动查找时建议用 `aid` 或 `cls` 定位。

[上一页WinAuto 总览](/doc-site/dev/winauto.md)[下一页构造与绑定](/doc-site/dev/winauto/bind.md)
