调试与示例

本章提供可直接复制运行的组合示例。每个示例都包含 print 调试输出,并在注释里标注典型控制台输出,方便对照排查。

合规提示:示例中的窗口标题、控件 Name 等字符串均来自 UIA/Win32 枚举,请按实际环境替换 WIN_CLASS / aid / 会话名。


通用脚本头(建议每个脚本开头都用)

from chatautox import WinAuto
import time

WIN_CLASS = 'YourMainWndClass'      # 改为你的窗口 ClassName
WIN_TITLES = ('目标应用', 'MyApp')     # 改为任务栏标题

def bind_auto(mode='uia'):
    hwnd = WinAuto.find_main_hwnd(WIN_CLASS, WIN_TITLES, find_window_name=WIN_TITLES[0])
    if not hwnd:
        raise RuntimeError(f'未找到窗口: class={WIN_CLASS} titles={WIN_TITLES}')
    auto = WinAuto(hwnd, mode=mode, delay=0.15)
    auto.activate()
    time.sleep(0.3)
    print(f'[bind] hwnd={hwnd} mode={mode}')
    return auto

def pp_ctrl(label, ctrl):
    """打印控件关键属性,定位失败时最有用。"""
    if ctrl is None:
        print(f'[{label}] ctrl=None')
        return
    try:
        r = ctrl.BoundingRectangle
        print(f'[{label}] type={ctrl.ControlTypeName} name={ctrl.Name!r} '
              f'aid={ctrl.AutomationId!r} class={ctrl.ClassName!r} rect={r}')
    except Exception as e:
        print(f'[{label}] 读取属性失败: {e}')

auto = bind_auto()

典型输出:

[bind] hwnd=123456 mode=uia

控件树导出

详见 控件获取 — 导出控件树

auto = bind_auto()

windows = auto.dump_windows_controls(
    max_windows=30,
    max_lines=800,
    same_process_only=True,
)
print(f'[dump] 共 {len(windows)} 个顶级窗口')
for i, w in enumerate(windows):
    print(f"  [{i}] hwnd={w['hwnd']} class={w['class_name']!r} title={w['title']!r}")
    # 在 tree 文本里搜 aid / Name
    for needle in ('session_list', 'chat_input', '发送'):
        if needle in (w.get('tree') or ''):
            print(f'      tree 含关键字: {needle!r}')

WinAuto.print_windows_dump(windows)   # 完整树打印到控制台

典型输出:

[dump] 共 1 个顶级窗口
  [0] hwnd=123456 class='YourMainWndClass' title='目标应用'
      tree 含关键字: 'session_list'
      tree 含关键字: 'chat_input'
[window] hwnd=123456 pid=6789 class="YourMainWndClass" title="目标应用" rect=[...]
  WindowControl Name="目标应用" ...
    ListControl Aid="session_list" ...

示例 1:find 定位 + 打印属性

auto = bind_auto()

ctrl = auto.find(ctrl_type='EditControl', aid='chat_input', depth=15)
pp_ctrl('chat_input', ctrl)

btn = auto.find(ctrl_type='ButtonControl', name='发送', depth=15)
pp_ctrl('send_btn', btn)

典型输出:

[bind] hwnd=123456 mode=uia
[chat_input] type=EditControl name='' aid='chat_input' class='ChatInputBox' rect=(250,550,780,590)
[send_btn] type=ButtonControl name='发送' aid='' class='' rect=(780,550,800,590)

ctrl=None,说明 aid / depth 不对,回到控件树导出核对。


示例 2:wait_ctrl 等待出现 + 超时提示

控件可能晚于窗口出现(加载动画、切换会话后输入框重建):

auto = bind_auto()

auto.click_session('示例会话')
time.sleep(0.5)

ctrl = auto.wait_ctrl(
    ctrl_type='EditControl',
    aid='chat_input',
    timeout=5.0,
    interval=0.2,
)
if ctrl is None:
    print('[wait_ctrl] 超时:5s 内未找到 chat_input')
else:
    pp_ctrl('chat_input after wait', ctrl)

典型输出:

[bind] hwnd=123456 mode=uia
[chat_input after wait] type=EditControl name='' aid='chat_input' class='ChatInputBox' rect=(...)

示例 3:注册别名 + 链式发送(带每步输出)

auto = bind_auto()

auto.register('input_box', ctrl_type='EditControl', aid='chat_input', depth=15)
auto.register('send_btn', ctrl_type='ButtonControl', name='发送', depth=15)
auto.register('sessions', list_type='ListControl', list_aid='session_list',
              item_cls='ChatSessionCell')

print('[register] 已注册: input_box, send_btn, sessions')

# 切会话
auto.click_list_cfg('sessions', '示例会话', exact=True)
time.sleep(0.4)
print('[click_list_cfg] 已点击会话: 示例会话')

# 链式:点击输入框 → 输入 → 回车
text = f'WinAuto 测试 {time.strftime("%H:%M:%S")}'
auto.use('input_box').click().input(text).enter()
print(f'[send] 已发送: {text!r}')

# 验证输入框是否清空(部分客户端发送后仍留字,仅作调试参考)
left = auto.copy(auto.find_cfg('input_box'), mode='uia')
print(f'[verify] 输入框剩余文字: {left!r}')

典型输出:

[bind] hwnd=123456 mode=uia
[register] 已注册: input_box, send_btn, sessions
[click_list_cfg] 已点击会话: 示例会话
[send] 已发送: 'WinAuto 测试 14:32:05'
[verify] 输入框剩余文字: ''

示例 4:会话列表遍历 + 未读统计

auto = bind_auto()

sessions = auto.get_sessions(list_aid='session_list')
print(f'[sessions] 共 {len(sessions)} 项')
for i, s in enumerate(sessions[:8]):          # 只打印前 8 条
    print(f"  [{i}] name={s.get('name')!r} unread={s.get('unread')} mute={s.get('mute')}")

unread = auto.get_unread_sessions()
print(f'[unread] 未读会话 {len(unread)} 个:')
for s in unread:
    print(f"  - {s.get('name')!r} ({s.get('unread')}条)")

典型输出:

[sessions] 共 42 项
  [0] name='示例会话' unread=0 mute=False
  [1] name='张三' unread=3 mute=False
  ...
[unread] 未读会话 2 个:
  - '张三' (3条)
  - '工作群' (12条)

示例 5:切换会话 + 读取当前聊天全部消息

auto = bind_auto()
im = auto.im_client(debug=False, auto_listen=False)

im.ChatWith('示例会话')
time.sleep(0.5)
info = im.ChatInfo()
print(f"[ChatInfo] {info}")

msgs = im.GetAllMessage(fetch_sender=False)
print(f'[GetAllMessage] 共 {len(msgs)} 条')
for msg in msgs[-5:]:                         # 最后 5 条
    print(f"  [{msg.attr}] type={msg.type} content={msg.content!r}")

典型输出:

[bind] hwnd=123456 mode=uia
[ChatInfo] {'chat_name': '示例会话', 'chat_type': 'friend', ...}
[GetAllMessage] 共 128 条
  [self] type=text content='你好'
  [friend] type=text content='收到'
  [self] type=image content='[图片]'
  ...

示例 6:轮询一轮新消息 + 打印 batch 结构

auto = bind_auto()
im = auto.im_client(debug=False, auto_listen=False)

def on_message(msg):
    print(f'  [callback] attr={msg.attr} type={msg.type} sender={msg.sender!r} '
          f'content={msg.content!r}')

print('[poll] 第 1 次(通常建立基线,msg 为空)')
batch = im.GetNextNewMessage(filter_mute=False, fetch_sender=False, callback=on_message)
print(f"  chat_name={batch.get('chat_name')!r} chat_type={batch.get('chat_type')!r} "
      f"msg_count={len(batch.get('msg') or [])}")

time.sleep(2)
print('[poll] 第 2 次(有新消息时 msg 非空)')
batch = im.GetNextNewMessage(filter_mute=False, fetch_sender=False, callback=on_message)
msgs = batch.get('msg') or []
print(f"  chat_name={batch.get('chat_name')!r} msg_count={len(msgs)}")
for msg in msgs:
    print(f"  [batch] attr={msg.attr} type={msg.type} content={msg.content!r}")

典型输出(第 1 次):

[poll] 第 1 次(通常建立基线,msg 为空)
  chat_name='' chat_type='' msg_count=0

典型输出(第 2 次,有人发消息后):

[poll] 第 2 次(有新消息时 msg 非空)
  [callback] attr=friend type=text sender='张三' content='在吗'
  chat_name='张三' msg_count=1
  [batch] attr=friend type=text content='在吗'

示例 6b:消息锚点(MakeMessageAnchor → 滚动后 ResolveMessageByAnchor)

处理 GetNextNewMessagemsg 时先生成锚点,中间滚动聊天区后仍可按锚点找回同一条(与内部 callback 队列重绑逻辑一致):

auto = bind_auto()
im = auto.im_client(debug=False, auto_listen=False)

_last_anchor = None

def on_message(msg):
    global _last_anchor
    _last_anchor = im.MakeMessageAnchor(msg)
    print(f'[anchor] {_last_anchor["summary"]}')
    print(f'  key={_last_anchor["key"]!r}')

batch = im.GetNextNewMessage(filter_mute=False, callback=on_message)
if not _last_anchor:
    print('[skip] 本轮无新消息')
else:
    # 模拟:滚动后 msg.control 可能已失效
    time.sleep(1)
    msg = im.ResolveMessageByAnchor(_last_anchor)
    if msg is None:
        print('[resolve] 视口内未找到(可能已滚出屏幕)')
    else:
        print(f'[resolve] 找回 attr={msg.attr} type={msg.type} content={msg.content!r}')
        msg.roll_into_view()

典型输出:

[anchor] [friend] text chat='示例会话' content='测试' rid=42.1234567.789...
  key=('示例会话', 'text', 'friend', '', '测试', 0, '42.1234567.789...')
[resolve] 找回 attr=friend type=text content='测试'

示例 7:输入框 copy + inspect_clipboard 对比

auto = bind_auto()
auto.click_session('示例会话')
time.sleep(0.4)

ctrl = auto.wait_ctrl(ctrl_type='EditControl', aid='chat_input', timeout=5.0)
if not ctrl:
    print('[copy] 未找到输入框,退出')
else:
    plain = auto.copy(ctrl, mode='uia')           # 纯文字 str
    rich = auto.copy(ctrl, mode='uia', items=True)  # 含 items 的 dict
    clip = auto.inspect_clipboard()

    print(f'[copy plain] {plain!r}')
    print(f'[copy items] success={rich.get("success")} item_count={rich.get("item_count")}')
    for item in rich.get('items') or []:
        print(f'  item type={item["type"]} content={item["content"]!r}')
    print('--- report ---')
    print(clip.get('report', ''))

典型输出:

[copy plain] '你好,这是测试文字'
[copy items] success=True item_count=1
  item type=text content='你好,这是测试文字'
--- report ---
✓ 剪贴板解析成功
  文字 x1: 你好,这是测试文字

示例 8:监听里读笔记(推荐)

聊天区笔记不要 find(name='[笔记]'),应通过消息 API 拿到 Message 后调用 get_content()

from chatautox.param import WxResponse

auto = bind_auto()
im = auto.im_client(debug=False, auto_listen=False)

def on_message(msg):
    if msg.attr != 'friend' or msg.type != 'note':
        return
    print(f'[note] 收到笔记: {msg.content!r}')
    result = msg.get_content(wait=3)
    if isinstance(result, WxResponse):
        print('[note] 失败:', result.get('message'))
        return
    for i, item in enumerate(result):
        print(f"  [{i}] {item if isinstance(item, dict) else item}")

# 单次:先打开有笔记的会话
im.ChatWith('示例会话')
time.sleep(0.5)
batch = im.GetNextNewMessage(filter_mute=False, callback=on_message)
print(f'[poll] batch msg_count={len(batch.get("msg") or [])}')

典型输出:

[note] 收到笔记: '[笔记]'
  [0] {'type': 'text', 'content': '标题一行'}
  [1] {'type': 'text', 'content': '正文...'}
  [2] {'type': 'image', 'content': 'C:\\...\\note_img.png'}
[poll] batch msg_count=1

示例 9:msg.control + 弹窗差集(进阶)

需要手写「点击气泡 → 等新弹窗 → 读内容」时,ctrl 仍来自消息列表:

auto = bind_auto()
im = auto.im_client(debug=False, auto_listen=False)
im.ChatWith('示例会话')
time.sleep(0.5)

msg = next((m for m in im.GetAllMessage() if m.type == 'note'), None)
if msg is None:
    print('[popup] 当前聊天无笔记消息')
else:
    POPUP_CLASSES = ['Chrome_WidgetWin_0', 'NoteWnd', 'ChatRecordWnd']
    before = auto.snapshot_popups(POPUP_CLASSES)
    print(f'[popup] 点击前弹窗数: {len(before)}')
    auto.click_bubble(msg.control, mode='uia')
    popup_hwnd = auto.wait_popup(POPUP_CLASSES, before, timeout=5.0)
    if not popup_hwnd:
        print('[popup] 5s 内未出现弹窗')
    else:
        print(f'[popup] 新弹窗 hwnd={popup_hwnd}')
        popup = WinAuto(popup_hwnd, mode='uia')
        popup.activate()
        body = popup.find(ctrl_type='DocumentControl', depth=25)
        pp_ctrl('popup body', body)
        result = popup.copy_content(body, mode='uia') if body else {'success': False}
        print(result.get('report', '无 report'))
        popup.close_window()
        print('[popup] 已关闭')

典型输出:

[popup] 点击前弹窗数: 0
[popup] 新弹窗 hwnd=789012
[popup body] type=EditControl name='' aid='' class='' rect=(...)
✓ 内容复制成功
  文字 x3 | 图片 x2
[popup] 已关闭

示例 10:键盘组合 + 剪贴板验证

auto = bind_auto()
ctrl = auto.wait_ctrl(ctrl_type='EditControl', aid='chat_input', timeout=5.0)

auto.click(ctrl)
auto.input('ABC测试123', ctrl=ctrl, paste=True)
print(f'[input] 已输入')

auto.hotkey(0x11, 0x41)    # Ctrl+A
auto.hotkey(0x11, 0x43)    # Ctrl+C
selected = auto.get_clip()
print(f'[hotkey copy] 剪贴板={selected!r}')

auto.hotkey(0x11, 0x41)
auto.keys('{Delete}', ctrl)
after_clear = auto.copy(ctrl, mode='uia')
print(f'[after clear] 输入框={after_clear!r}')

典型输出:

[input] 已输入
[hotkey copy] 剪贴板='ABC测试123'
[after clear] 输入框=''

示例 11:type_and_send + 滚动翻页组合

auto = bind_auto()
auto.click_session('示例会话')
time.sleep(0.4)

ctrl = auto.find(ctrl_type='EditControl', aid='chat_input')
auto.type_and_send('第一条', ctrl, mode='uia')
print('[type_and_send] 第一条已发')
time.sleep(0.3)

auto.type_and_send('第二条', ctrl, mode='uia')
print('[type_and_send] 第二条已发')

# 聊天区翻页(坐标按窗口大小调整)
auto.page_up(times=2)
print('[scroll] 上翻 2 页')
auto.page_down(times=2)
print('[scroll] 下翻 2 页')

示例 12:记事本完整流程(通用窗口)

不依赖 IM 客户端,适合验证 WinAuto 环境是否正常:

from chatautox import WinAuto, MODE_UIA

auto = WinAuto.from_main_hwnd('Notepad', '无标题 - Notepad', mode=MODE_UIA)
if not auto.hwnd:
    # 已有文件时标题可能带文件名
    auto = WinAuto.from_main_hwnd('Notepad', '无标题 - 记事本', mode=MODE_UIA)
auto.activate()
time.sleep(0.3)
print(f'[notepad] hwnd={auto.hwnd}')

auto.register('editor', ctrl_type='EditControl', depth=5)
auto.use('editor').click().input('Hello from WinAuto').sleep(0.3)
print('[notepad] 已输入文字')

saved = auto.keys('{Ctrl}s', auto.find_cfg('editor'), mode='uia')
clip_check = auto.copy(auto.find_cfg('editor'), mode='uia')
print(f'[notepad] 当前内容={clip_check!r}')

典型输出:

[notepad] hwnd=456789
[notepad] 已输入文字
[notepad] 当前内容='Hello from WinAuto'

示例 13:一键调试函数(复制到项目里反复用)

def debug_winauto_snapshot(auto, aids=('session_list', 'chat_input')):
    """绑定后快速检查:窗口信息 + 关键 aid 能否 find。"""
    print('=' * 50)
    print(f'hwnd={auto.hwnd} mode={auto.mode}')
    for aid in aids:
        c = auto.find(aid=aid, depth=20)
        pp_ctrl(aid, c)
    sessions = auto.get_sessions(list_aid='session_list') if 'session_list' in aids else []
    if sessions:
        print(f'sessions: total={len(sessions)} first={sessions[0].get("name")!r}')
    unread = auto.get_unread_sessions() if 'session_list' in aids else []
    if unread is not None:
        print(f'unread sessions: {len(unread)}')
    print('=' * 50)

auto = bind_auto()
debug_winauto_snapshot(auto)

典型输出:

==================================================
hwnd=123456 mode=uia
session_list type=ListControl name='' aid='session_list' ...
chat_input type=EditControl name='' aid='chat_input' ...
sessions: total=42 first='示例会话'
unread sessions: 2
==================================================

与 macro_helper 的关系

macro_helper 中的函数式接口(ctrl_clickctrl_inputget_hwnd 等)底层均委托给 WinAuto。Macro 与 OO 风格可混用,调试时建议统一加 print

# 函数式(macro_helper)
from chatautox.auto.macro_helper import get_hwnd, ctrl_click, ctrl_input
WIN_TITLE = '…'  # 目标窗口实际标题
hwnd = get_hwnd(win_title=WIN_TITLE)
print(f'[macro] hwnd={hwnd}')
ctrl_click(hwnd, name='发送')
print('[macro] ctrl_click 完成')

# 面向对象(推荐)
from chatautox import WinAuto
auto = WinAuto(hwnd)
auto.click(name='发送')
print('[winauto] click 完成')

调试排查速查

现象建议
find 返回 Nonedump_windows_controls,核对 aid/Name/depth
点击无反应mode='uia'mode='mouse'activate() 后再点
中文输入乱码/失败input(..., paste=True)mode='uia'
组合键无效post 模式用 hotkey(0x11, 0x43),勿用 keys('{Ctrl}c')
聊天区 find [笔记] 找不到气泡滚动 / 多条同名
消息轮询始终空首帧建基线正常
弹窗读不到ClassName 与 dump 不一致

更多 API 参数见各专题页:查找与点击剪贴板消息列表消息处理指南


API 参考

调试

✨dump_windows_controls

导出可见顶级窗口 UIA 树。

windows = auto.dump_windows_controls(same_process_only=True)
WinAuto.print_windows_dump(windows)

参数

参数名类型默认值描述
max_windowsint30最多导出窗口数
max_linesint800每个控件树最大行数
same_process_onlyboolTrue是否仅导出同进程窗口

返回值

  • 类型:list[dict]
  • 描述:每项 {hwnd, pid, class_name, title, rect, tree}

上一页控件别名下一页找图找色采样