手把手教学:用Python实现APP自动化测试(uiautomator2+weditor)



前言


  在移动应用测试领域,自动化技术正成为提升效率的核心利器。本文将系统介绍两款强大的Android自动化工具——uiautomator2(Python库)与weditor(可视化编辑器),通过环境搭建、工具联动、实战演示,带你快速掌握APP自动化测试全流程。无论你是测试工程师还是开发爱好者,都能轻松实现“自动打开APP→输入文本→点击发送”的完整自动化操作,告别重复劳动!

 

详细目录

一、uiautomator2 和 weditor 简介

1.1 uiautomator2介绍

1.2 weditor介绍

二、uiautomator2与weditor安装指南
 2.1 基础环境安装
  2.1.1 PyCharm安装(附官方地址)
  2.1.2 Python安装(附官方地址)

 2.2 ADB环境配置
  2.2.1 下载与解压
  2.2.2 系统环境变量配置
  2.2.3 环境验证与冲突解决

 2.3 工具库安装
  2.3.1 uiautomator2安装
  2.3.2 weditor安装
  2.3.3 设备初始化
  2.3.4 虚拟环境避坑指南

三、weditor实战:自动化消息发送
 3.1 环境准备
  3.1.1 ADB设备连接验证
  3.1.2 weditor连接设备(含报错解决方案)

 3.2 APP启动控制
  3.2.1 包名与Activity获取方法
  3.2.2 Python启动脚本编写

 3.3 消息发送自动化
  3.3.1 输入框定位与文本输入
  3.3.2 发送按钮定位与点击
  3.3.3 完整代码执行验证

四、最佳实践建议和示例

1. 基本脚本示例、尝试各种元素的使用方法
2. 常用API说明
3. 等待元素出现
4.处理弹窗
5. 异常处理
6.其他建议

五、weditor功能全解析

1 顶部工具栏

2 属性展示区

3 代码编辑区

4 设备操作区

5 报错解决方案

六、资源推荐

1. 官方文档:
2. 学习资源:
3. 社区支持:

 

 

 

 

一:uiautomator2 和 weditor 简介

1. uiautomator2介绍

uiautomator2 是一个基于 Android 系统自带的 UIAutomator 框架封装的 Python 库,主要用于 Android 设备的 UI 自动化测试。它可以模拟用户操作(如点击、滑动、输入文本等),获取应用界面元素信息,实现自动化流程(如 APP 功能测试、重复性操作脚本等)。

 

  • 1.1、实现逻辑:

底层基于 Android 系统的 UIAutomator 框架(Android 原生的 UI 自动化工具),通过 Python 封装了一系列 API,简化了使用难度。

  • 1.2、工作流程:
    • 1.2.1、在 PC 端通过 Python 脚本调用 uiautomator2 的 API;
    • 1.2.2、脚本通过 ADB(Android Debug Bridge)与 Android 设备通信,在设备上安装一个辅助服务(atx-agent);
    • 1.2.3、辅助服务接收指令并转化为 UIAutomator 可执行的操作,控制设备完成相应动作(如点击、输入等);
    • 1.2.4、设备的操作结果通过辅助服务返回给 PC 端的脚本。
    • 图示:

image

 

2. weditor介绍

weditor 是一个可视化的 UI 元素查看器和编辑器,主要配合 uiautomator2 或其他 Android 自动化工具使用。它可以实时显示 Android 设备的屏幕,并解析界面上的 UI 元素(如按钮、输入框等),方便开发者查看元素属性(如 ID、文本、坐标等),快速编写自动化脚本。

 

  • 2.1、实现逻辑:

本质是一个 Web 应用(基于 Python 的 Flask 框架),通过浏览器访问使用。weditor 是 uiautomator2 的辅助工具,解决了 UI 元素定位的可视化问题;uiautomator2 负责执行自动化操作,weditor 负责简化元素定位和脚本编写过程,两者结合可大幅提高 Android 自动化脚本的开发效率。

  • 2.2.、工作流程:
    • 2.2.1、启动 weditor 后,它会在本地启动一个 Web 服务,并通过 ADB 连接 Android 设备;
    • 2.2.2、实时截取设备屏幕并在浏览器中显示,同时通过 uiautomator2 或 Android 自带的 uiautomatorviewer 解析当前界面的 UI 元素结构;
    • 2.2.3、开发者可在浏览器中点击屏幕元素,查看其属性(如 resourceIdtextbounds 等),这些属性可直接用于 uiautomator2 脚本中定位元素;
    • 2.2.4、支持生成简单的操作代码(如点击、输入),方便快速编写自动化脚本。
    • 2.2.5、图示:weditor类似于浏览器的F12工具,都是获取界面的UI元素信息。下面两张图就很清晰的看到了两个工具的对比。

image

 

 

image

 

 

二、uiautomator2与weditor的安装。

1、安装Pycharm和Python

1.1、下载安装的流程本文不再详细阐述,如需要教程可以参考:https://www.cnblogs.com/xiaodi888/p/18733979

1.2、Python官网下载地址:https://www.python.org/downloads/

1.3、Pycharm官网下载地址:https://www.jetbrains.com.cn/pycharm/download/?section=windows

2、安装adb

由于uiautomator2与weditor都需依赖ADB与Android设备交互。所以我们的本机电脑是需要配置adb环境的。如果已经配置的可以忽略这一步。当然即使已经配置,也需要检查自己的adb配置情况。

安装步骤:

  • 2.1、下载对应系统的 Android SDK Platform Tools,下载链接:https://adbdownload.com/
  • 2.2、解压到任意目录如:C:\adb\adb.exe将该目录C:\adb\  在电脑设置添加到系统环境变量 PATH 中
    • 2.2.1、’Windows 系统:按下 Win+R 键打开运行窗口,输入 "sysdm.cpl" 并回车,在弹出的系统属性窗口中切换到 "高级" 选项卡,点击下方的 "环境变量" 按钮即可打开。
    • 2.2.2、macOS/Linux 系统:打开终端,直接编辑配置文件(如 macOS 的~/.bash_profile 或~/.zshrc,Linux 的~/.bashrc),通过 export 命令添加环境变量。
  • 2.3、终端输入 adb version 验证,显示版本即配置成功

        注意

    • 2.3.1、如果您已经安装过adb工具,也需要通过 where adb  命令,检查一下你是否只安装了一个adb,如果有2个或者更多,你需要删除多余的,否则后续,可能会报错,导致系统无法正确识别adb,
    •  2.3.2、还需检查你的adb是否是安卓官方的。因为某些开发平台,可能会有自己的adb工具,可以使用 adb version 命令,来检查你的adb是否为官方,名称是否是这样的:Android Debug Bridge version 1.0.41

3、安装uiautomator2与weditor

  • 3.1、在CMD命令行中以此输入以下命令来安装uiautomator2:
pip install -U uiautomator2  #安装最新版本
#或者指定安装版本:
pip install uiautomator2==3.4.0  #(作者使用的是这个版本,已验证可用)
  • 3.2、在CMD命令行中以此输入以下命令来安装weditor:
pip install weditor
#若在pip install weditor时出现如下报错: UnicodeDecodeError: gbk' codec can't decode byte 0xad in position 829: illegal multibyte sequence
可以尝试降版本安装:pip install weditor==0.6.4
  • 3.3、初始化设备,自动部署 atx-agent
    • 3.3.1、确保 Android 设备通过 USB 连接电脑,并开启 开发者模式 和 USB 调试。
    • 3.3.2、在CMD命令行执行设备初始化命令(会自动下载并部署 atx-agent 到设备):
python -m uiautomator2 init
    • 3.3.3、执行成功后,atx-agent 会被安装到设备的 /data/local/tmp 目录下。

注意:尽量不要在PyCharm终端控制台去执行下载命令,因为你很可能会把程序安装到虚拟环境中,而非全局安装,一旦安装到虚拟环境中,就不能在CMD中直接执行,需要先进入虚拟环境才能启动程序。当你的PyCharm终端控制台上出现(,venv)就表示当前终端已激活了名为 .venv 的虚拟环境。这是一个隔离的 Python 运行环境,可避免项目依赖冲突。激活后,安装的包会被放在该环境目录下,而非系统全局 Python 环境中。若要退出,可执行 deactivate 命令。如果已经安装到虚拟环境,要么卸载重装,要么假设 .venv 文件夹在 D:\my_project 目录下,在 CMD 中输入: cd /d D:\my_project  然后输入激活命令激活虚拟环境: .venv\Scripts\activate.bat  执行后,CMD 提示符前会显示 (.venv),表示已进入该虚拟环境。这时候才能去输入python命令。

 三、weditor的使用

1、weditor的启动方式有两种,

  • 1.1、简单的:直接在CMD中执行命令:即可在浏览器中打开Flask 框架的 WEB 网页界面。
python -m weditor
  • 1.2、便捷的:在CMD中,执行以下命令。 会在桌面创建一个快捷方式,省去了手动创建的步骤。后续直接在桌面启动即可。
weditor --shortcut

 

 

 

 2、写一个最简单代码:自动化给DeepSeek发送一句你好。

 

  • 2.1、先确保adb链接成功,且USB调试模式已经打开。可以执行 adb devices -l  命令确认设备正常连接。
  • 2.2、在weditor页面,左上方,点击Connect 按钮。直到按钮旁边出现一个绿色的小树叶,且左侧出现手机投屏页面,即代表可以正常使用。

    注意:如果weditor连接时出现报错:“AttributeError: 'Device' object has no attribute 'address'问题:”需要修改获取IP策略,涉及代码较多,我把代码放在了文章末尾。

image

 

  • 2.3、通过APP的包名以及Activity 名称启动要测试的目标APP及目标界面。
    • 2.3.1、此时我们先要获取到,要操作APP的包名以及Activity 名称,可以先在手机上打开目标 APP 至需要查看的界面,在 CMD 中输入: adb shell dumpsys window | findstr mCurrentFocus 如下,我先打开了deepseekAPP主页:
C:\Users\Administrator>adb shell dumpsys window | findstr mCurrentFocus
  mCurrentFocus=null
  mCurrentFocus=null
  mCurrentFocus=Window{3f3ffd5 u0 com.deepseek.chat/com.deepseek.chat.MainActivity}

最后一行 mCurrentFocus=Window{3f3ffd5 u0 com.deepseek.chat/com.deepseek.chat.MainActivity} 是关键信息:

com.deepseek.chat 是当前前台 APP 的包名(即应用的唯一标识)。

/ 后面的 com.deepseek.chat.MainActivity 是当前显示的Activity 名称(即该 APP 中正在运行的界面组件)。

         这说明此时的设备屏幕上正在显示的是 com.deepseek.chat 这个应用的 MainActivity 界面。
 
    • 2.3.2、我如果在python执行以下代码,就可以直接打开deepseek这个APP。
import uiautomator2 as u2

# 连接设备(默认连接已识别的设备,若多设备需指定设备序列号)
d = u2.connect()

# 打开目标APP(参数为APP的包名)若需要直接打开指定Activity(包名+Activity全称)
d.app_start("com.deepseek.chat", activity="com.deepseek.chat.MainActivity")

或者直接在weditor代码编辑框中,执行也是可以的:

image

 

 

 

  • 2.4、启动APP后,输入框中输入“你好”

接下来,我们先在左侧APP页面,用鼠标点击输入框,然后在中间属性区域点击Send Keys按钮,这个时候就会有一个弹窗,给我们输入文案,我这儿输入的是:“你好”,如下图可见,右侧代码编辑框,已经自动帮我生成了对应的输入代码,此时,如果我们运行代码,可以自动在输入框中,帮我们输入:“你好”。

注意:
如果要输入,是需要安装adb专用输入法的,正常情况下在前面初始化设备的时候,就会自动安装,如果没有安装去网上下载一下,然后需要在手机的输入法管理中启用adb 输入法,并把当前输入法设置为adb 输入法;

如果你发现代码无误,但是输入无法上屏,可以重启一下服务:在weditor页面右上角,有一个圆圈箭头图标;

image

 

  • 2.5、将消息:“你好”发送给deepseek。

我们运行代码以后,下图可见,你好,这俩字已经成功输入到输入框,这时候,我们需要点击发送按钮,将消息发送给deepseek。如下图,我们先鼠标在左边APP窗口中,点击发送按钮,然后在中间元素窗口中,选择Tab。这时候在右侧的代码编辑框中,可见已经自动生成了对应的代码,我们运行完整代码以后。就可以实现自动打开deepseekAPP ,自动发送“你好”。初步实现APP的UI自动化。

image

 

 

 

四、最佳实践建议和示例

1. 基本脚本示例、尝试各种元素的使用方法

创建一个Python文件:

import uiautomator2 as u2

# 连接设备(可以通过adb devices获取设备号)
d = u2.connect() # 默认连接第一个设备
# 或 d = u2.connect('设备序列号')

# 启动应用(以微信为例)
d.app_start("com.tencent.mm")

# 点击元素(使用weditor获取的定位表达式)
d(text="通讯录").click()

# 输入文本
d(resourceId="com.tencent.mm:id/ht").set_text("你好")

# 滑动操作
d.swipe(500, 1500, 500, 500, 0.5) # 从下往上滑动

# 返回键
d.press("back")

# 截图保存
d.screenshot("home.jpg")

# 关闭应用
d.app_stop("com.tencent.mm")

2. 常用API说明

- 元素定位:

d(text="设置") # 通过文本定位
d(description="设置") # 通过content-desc定位
d(resourceId="com.android.settings:id/title") # 通过resource-id定位
d(className="android.widget.TextView") # 通过类名定位

- 元素操作:

element.click() # 点击
element.long_click() # 长按
element.set_text("text") # 输入文本
element.clear_text() # 清除文本

- 全局操作:

d.press("home") # 主页键
d.press("back") # 返回键
d.swipe(x1, y1, x2, y2, duration) # 滑动
d.click(x, y) # 坐标点击

3. 等待元素出现

# 等待最多10秒直到元素出现
element = d(text="设置").wait(timeout=10.0)
if element:
element.click()
else:
print("元素未找到")

4.处理弹窗

# 检查并关闭可能的弹窗
if d(text="允许").exists:
d(text="允许").click()

5. 异常处理

from uiautomator2.exceptions import UiObjectNotFoundError

try:
d(text="不存在的元素").click()
except UiObjectNotFoundError as e:
print("元素未找到:", e)
# 可以在这里添加恢复逻辑

 6、其他建议

  • 6.1. 元素定位:

- 优先使用resource-id定位
- 避免使用绝对XPath
- 为关键元素添加自定义属性(需要开发配合)

  • 6.2. 脚本编写:

- 使用Page Object模式组织代码
- 将定位表达式与操作逻辑分离
- 添加详细的日志记录

  • 6.3. 执行环境:

- 使用固定分辨率的设备或模拟器
- 关闭不必要的动画(开发者选项中可以关闭)
- 保持测试环境干净

 

五、weditor的基础功能介绍

当我们通过 python -m weditor 命令或者桌面快捷方式启动weditor后,会在浏览器中打开weditor的网页界面。下面,我们详细介绍一下,页面上的各个模块功能和属性。

1、顶部工具栏区域:用于对设备的连接与操作:

image

 

1.1、Android 下拉框:

用于选择已连接到电脑的 系统设备若有多个设备接入,可在此切换,确保 weditor 识别并操作对应设备 。

1.2、输入框以及Connect 按钮:

可输入设备序列号或 IP 等信息,辅助精准识别设备,点击右侧 Connect 按钮可尝试建立与该设备的连接,连接成功后才能进行后续 UI 自动化相关操作。

1.3、Dump Hierarchy 按钮:

点击后,weditor 会去获取当前连接 Android 设备屏幕的 UI 层级结构信息,也就是把界面上各个控件的层级、属性等数据抓取下来,方便后续在左侧和中间区域展示、分析,

1.4、静态 / 实时切换开关:

静态模式时显示的是抓取瞬间的 UI 结构,后续设备界面变化,这里不会自动更新,适合分析固定时刻的界面,方便慢慢查看、调试元素定位表达式 。
实时模式开启后,weditor 会尝试实时同步设备界面的 UI 层级变化,设备操作导致界面改变时,这里能及时更新显示,便于观察动态交互过程中界面元素的变化,但相对会占用一定系统资源,且可能存在短暂延迟 。

 

2、中间属性展示区域:用户展示当前页面元素信息:

image

 

2.1、Clear Canvas 按钮:点击后,会清空中间区域展示的 UI 元素绘制的 “画布” 内容(比如之前 Dump Hierarchy 后显示的界面元素布局示意 ),方便重新抓取、展示新的 UI 层级结构 。

2.2、Tap Widget(Beta) 按钮:处于测试阶段的功能,点击后,会模拟在 Android 设备上对当前选中的 UI 元素执行 “点击(Tap)” 操作,用于快速测试元素定位是否准确,以及实际点击效果 。

2.3、Tap 按钮:同样是模拟对选中元素的点击操作,相对 Tap Widget(Beta) 可能是更成熟、稳定的点击触发方式,点击后,设备端对应元素位置会有点击反馈,可用于自动化测试中模拟用户点击交互 。

2.4、Send Keys 按钮:若当前选中元素是可输入文本的控件(比如输入框),点击该按钮,可尝试向其发送键盘输入内容(需提前在代码或交互区域配置好要发送的文本等 ),用于自动化填充文本信息 。

2.5、各属性介绍:

activity:显示当前元素所属的 Android 应用的 Activity 名称(这里是 MainActivity ),帮助识别元素所在的应用页面上下文 。
XPathLite:是一种简化的 XPath 表达式,用于定位该元素,可作为编写 UI 自动化脚本时元素定位的参考,复制该表达式,结合 uiautomator2 等库的 d.xpath() 方法就能操作对应元素 。
坐标 % pr:显示元素在屏幕上的坐标比例(这里 (0.322, 0.145) ),点击坐标值可查看具体像素坐标,也可用于基于坐标的点击、滑动等操作(不过一般优先用元素属性定位,坐标定位易受界面变化影响 )。
className:元素的类名(这里 android.widget.Button ),表明该元素是 Android 系统的按钮控件类型,编写自动化代码时,可结合类名 + 其他属性筛选元素 。
*index:元素在同层级兄弟元素中的索引位置(这里 0 ),当同层级有多个同类元素时,可借助索引精准定位 。
*text:元素上显示的文本内容(这里 开发者选项 ),常作为元素定位的重要依据,比如 d(text="开发者选项").click() 这样的代码就能操作该元素 。
*resourceId:元素的资源 ID(这里 com.github.uiautomator:id/development_settings ),是 Android 开发中给元素设置的唯一标识(理想情况下 ),用 resourceId 定位元素精准度高,像 d(resourceId="com.github.uiautomator:id/development_settings").click() 就是示例代码里操作该元素的方式 。
*package:元素所属应用的包名(这里 com.github.uiautomator ),用于区分不同应用的元素,编写跨应用操作脚本时,结合包名可避免混淆 。
*description:元素的描述信息(若有设置 ),部分场景下可辅助定位元素,比如一些自定义控件可能靠描述区分 。
*checkable、*clickable 等布尔属性:
*checkable(这里 false ):表示元素是否可被勾选(比如复选框、单选框等控件的属性 ),false 即不可勾选 。
*clickable(这里 true ):表示元素是否可被点击,true 说明能响应点击操作,也对应前面 Tap 等按钮可成功触发点击的依据 。
*enabled(true ):元素是否可用,true 表示可交互 。
*focusable(true ):元素是否可获取焦点 。
*focused(false ):元素当前是否处于焦点状态 。
*scrollable(false ):元素是否可滚动 。
*longClickable(false ):是否支持长按操作 。
*password(false ):是否是密码输入框类型 。
*selected(false ):元素是否被选中(比如选项卡、列表项选中状态 )。
# rect:元素在屏幕上的矩形区域坐标及尺寸信息(x:17, y:296; width:506; height:113 ),可用于基于坐标的精准操作,也能辅助判断元素在屏幕上的位置、大小 。
代码 展示区:会根据当前选中元素,自动生成一些示例代码(比如 d(resourceId="com.github.uiautomator:id/development_settings") ),方便直接复制到右侧代码编辑区,用于编写 UI 自动化脚本,减少手动编写定位表达式的工作量 。

 

 

3、右侧代码编辑与执行区域:用于编写 Python 脚本

image

 3.1、单行或选中运行 按钮:若光标在某一行代码,点击可单独执行该行代码,快速测试某一步操作(比如只测试 d(resourceId="com.github.uiautomator:id/development_settings").click() 这一行,看是否能成功点击对应元素 )。若选中多行代码,点击则执行选中的代码片段,用于调试部分脚本逻辑 。

3.2、重置代码 按钮:点击后,会清除当前的代码,慎点 。

3.3、运行 :点击可执行整个代码编辑区的脚本,按照编写的逻辑依次对 Android 设备进行操作,实现完整的 UI 自动化流程,执行过程中,下方 Console 区域会输出执行日志、报错信息等 。

3.4、复制按钮,可以复制当前编辑的代码。

3.5、Console 控制台区域:显示脚本执行的日志信息,包括代码执行的状态(比如 [Finished in 0.371s] 表示执行完成及耗时 )、打印的调试信息(若代码里有 print() 输出 )、报错堆栈(脚本执行出错时,会详细显示哪里出错、错误类型等 ),用于排查脚本问题、验证操作是否成功 。

3.6、Hierarchy 按钮:点击可切换查看 UI 层级结构的可视化展示,和左侧、中间区域的元素属性展示配合,更直观分析界面元素关系 。

 

4、底部及其他辅助区域:用于模拟 Android 设备的物理按键功能

image

4.1、POWER 模拟电源键(可锁屏、亮屏等 ),

4.2、Home 回到设备主屏幕,

4.3、Back 模拟返回操作,Menu 调出设备的菜单选项(若应用支持 ),方便在自动化脚本执行前后,或调试过程中,快速模拟用户对设备的基础按键交互 。

 

 

5、weditor 连接设备时出现了AttributeError: 'Device' object has no attribute 'address'问题的处理:

可以先尝试方案一:

先使电脑和移动设备保持同一局域网,
然后在C:\Users\Administrator\AppData\Local\Programs\Python\Python313\Lib\site-packages\weditor\web\handlers中(路径需要根据自己的实际路径去替换);
将page.py文件的第80行81行替换成下面这样,就行:

ws_addr = get_device(id).device.wlan_ip # yapf: disable
ret['screenWebSocketUrl']= ws_addr + "/minicap"

如果这个方案可以解决问题,那么就可以直接尝试方案2:将获取ip地址的代码换个写法:避免因为ip变更而重复修改page.py. 方案一的代码只要IP地址变更,就不行了。

将page.py文件的代码做如下全文替换:

# coding: utf-8
#

import base64
import io
import json
import os
import re  # 新增:导入re模块用于正则匹配
import traceback

import tornado
from logzero import logger
from PIL import Image
from tornado.escape import json_decode

from ..device import connect_device, get_device
from ..version import __version__


pathjoin = os.path.join


class BaseHandler(tornado.web.RequestHandler):
    def set_default_headers(self):
        self.set_header("Access-Control-Allow-Origin", "*")
        self.set_header("Access-Control-Allow-Headers", "x-requested-with")
        self.set_header("Access-Control-Allow-Credentials",
                        "true")  # allow cookie
        self.set_header('Access-Control-Allow-Methods',
                        'POST, GET, PUT, DELETE, OPTIONS')

    def options(self, *args):
        self.set_status(204)  # no body
        self.finish()

    def check_origin(self, origin):
        """ allow cors request """
        return True


class VersionHandler(BaseHandler):
    def get(self):
        ret = {
            'name': "weditor",
            'version': __version__,
        }
        self.write(ret)


class MainHandler(BaseHandler):
    def get(self):
        self.render("index.html")


class DeviceConnectHandler(BaseHandler):
    def post(self):
        platform = self.get_argument("platform").lower()
        device_url = self.get_argument("deviceUrl")

        try:
            id = connect_device(platform, device_url)
        except RuntimeError as e:
            self.set_status(410)  # 410 Gone
            self.write({
                "success": False,
                "description": str(e),
            })
        except Exception as e:
            logger.warning("device connect error: %s", e)
            self.set_status(410)  # 410 Gone
            self.write({
                "success": False,
                "description": traceback.format_exc(),
            })
        else:
            ret = {
                "deviceId": id,
                'success': True,
            }
            if platform == "android":
                # 方案二:通过adb命令动态获取设备IP地址
                # 执行adb命令获取wlan0的IP信息
                ip_info = os.popen('adb shell ip addr show wlan0 | findstr global')
                # 读取并处理命令输出
                info = " ".join(ip_info.readlines())
                # 使用正则表达式提取IP地址
                pattern = r'(\d+\.\d+\.\d+\.\d+)/24'  # 匹配IPv4地址格式
                match = re.search(pattern, info)
                if match:
                    ws_addr = match.group(1)  # 提取IP地址
                    ret['screenWebSocketUrl'] = f"ws://{ws_addr}/minicap"
                else:
                    # 如果获取IP失败,使用默认地址作为 fallback
                    ret['screenWebSocketUrl'] = "ws://127.0.0.1/minicap"
                    logger.warning("无法获取设备IP地址,使用默认地址")
            self.write(ret)


class DeviceHierarchyHandler(BaseHandler):
    def get(self, device_id):
        d = get_device(device_id)
        self.write(d.dump_hierarchy())


class DeviceHierarchyHandlerV2(BaseHandler):
    def get(self, device_id):
        d = get_device(device_id)
        self.write(d.dump_hierarchy2())


class WidgetPreviewHandler(BaseHandler):
    def get(self, id):
        self.render("widget_preview.html", id=id)


class DeviceWidgetListHandler(BaseHandler):
    __store_dir = os.path.expanduser("~/.weditor/widgets")

    def generate_id(self):
        os.makedirs(self.__store_dir, exist_ok=True)
        names = [
            name for name in os.listdir(self.__store_dir)
            if os.path.isdir(os.path.join(self.__store_dir, name))
        ]
        return "%05d" % (len(names) + 1)

    def get(self, widget_id: str):
        data_dir = os.path.join(self.__store_dir, widget_id)
        with open(pathjoin(data_dir, "hierarchy.xml"), "r",
                  encoding="utf-8") as f:
            hierarchy = f.read()

        with open(os.path.join(data_dir, "meta.json"), "rb") as f:
            meta_info = json.load(f)
            meta_info['hierarchy'] = hierarchy
            self.write(meta_info)

    def json_parse(self, source):
        with open(source, "r", encoding="utf-8") as f:
            return json.load(f)

    def put(self, widget_id: str):
        """ update widget data """
        data = json_decode(self.request.body)
        target_dir = os.path.join(self.__store_dir, widget_id)
        with open(pathjoin(target_dir, "hierarchy.xml"), "w",
                  encoding="utf-8") as f:
            f.write(data['hierarchy'])

        # update meta
        meta_path = pathjoin(target_dir, "meta.json")
        meta = self.json_parse(meta_path)
        meta["xpath"] = data['xpath']
        with open(meta_path, "w", encoding="utf-8") as f:
            f.write(json.dumps(meta, indent=4, ensure_ascii=False))

        self.write({
            "success": True,
            "description": f"widget {widget_id} updated",
        })

    def post(self):
        data = json_decode(self.request.body)
        widget_id = self.generate_id()
        target_dir = os.path.join(self.__store_dir, widget_id)
        os.makedirs(target_dir, exist_ok=True)

        image_fd = io.BytesIO(base64.b64decode(data['screenshot']))
        im = Image.open(image_fd)
        im.save(pathjoin(target_dir, "screenshot.jpg"))

        lx, ly, rx, ry = bounds = data['bounds']
        im.crop(bounds).save(pathjoin(target_dir, "template.jpg"))

        cx, cy = (lx + rx) // 2, (ly + ry) // 2
        # TODO(ssx): missing offset
        # pprint(data)
        widget_data = {
            "resource_id": data["resourceId"],
            "text": data['text'],
            "description": data["description"],
            "target_size": [rx - lx, ry - ly],
            "package": data["package"],
            "activity": data["activity"],
            "class_name": data['className'],
            "rect": dict(x=lx, y=ly, width=rx-lx, height=ry-ly),
            "window_size": data['windowSize'],
            "xpath": data['xpath'],
            "target_image": {
                "size": [rx - lx, ry - ly],
                "url": f"http://localhost:17310/widgets/{widget_id}/template.jpg",
            },
            "device_image": {
                "size": im.size,
                "url": f"http://localhost:17310/widgets/{widget_id}/screenshot.jpg",
            },
            # "hierarchy": data['hierarchy'],
        } # yapf: disable

        with open(pathjoin(target_dir, "meta.json"), "w",
                  encoding="utf-8") as f:
            json.dump(widget_data, f, ensure_ascii=False, indent=4)

        with open(pathjoin(target_dir, "hierarchy.xml"), "w",
                  encoding="utf-8") as f:
            f.write(data['hierarchy'])

        self.write({
            "success": True,
            "id": widget_id,
            "note": data['text'] or data['description'],  # 备注
            "data": widget_data,
        })


class DeviceScreenshotHandler(BaseHandler):
    def get(self, serial):
        logger.info("Serial: %s", serial)
        try:
            d = get_device(serial)
            buffer = io.BytesIO()
            d.screenshot().convert("RGB").save(buffer, format='JPEG')
            b64data = base64.b64encode(buffer.getvalue())
            response = {
                "type": "jpeg",
                "encoding": "base64",
                "data": b64data.decode('utf-8'),
            }
            self.write(response)
        except EnvironmentError as e:
            traceback.print_exc()
            self.set_status(430, "Environment Error")
            self.write({"description": str(e)})
        except RuntimeError as e:
            self.set_status(410)  # Gone
            self.write({"description": traceback.print_exc()})

 

 

 

六、资源推荐

1. 官方文档:

- uiautomator2 GitHub:https://github.com/openatx/uiautomator2
- weditor GitHub:https://github.com/openatx/weditor

2. 学习资源:

- Android UI Automator官方文档:https://developer.android.com/training/testing/ui-automator

- Python官方教程:https://docs.python.org/3/tutorial/

3. 社区支持:

- Stack Overflow:https://stackoverflow.com/
- GitHub Issues区报告问题

 

 

 

通过本教程,你应该已经掌握了在Windows上使用weditor和uiautomator2进行APP UI自动化的基本方法。实际项目中可能会遇到更多复杂情况,建议多实践并参考官方文档解决特定问题。

 

 


结语

 

亲爱的朋友:
      希望本文中描述的问题以及解决方案,可以帮助到您。当然,我们深知,问题和挑战总是层出不穷,新的情况也在不断涌现。如果读者朋友您有更好的方案,或者在实际应用中发现了文中的不足之处,请不吝分享您的宝贵建议。诚挚地邀请每一位读者加入我们的行列,共同完善这份教程。
    感谢您的阅读与支持!

Dear frends,

     We hope that the questions and solutions presented in this article can be of assistance to you. Of course, we are fully aware that problems and challenges are always emerging in an endless stream, and new situations are constantly arising. If you, our readers, have better solutions or have discovered any deficiencies in this article through practical application, please do not hesitate to share your valuable suggestions with us. We sincerely invite every reader to join us in continuously improving this tutorial.

Thank you for your reading and support!
See you,Parting is for better meeting!

 

 
posted @ 2025-08-08 12:09  xiaodi888  阅读(462)  评论(0)    收藏  举报