前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >PC端自动化测试实战教程-6-pywinauto 打印和保存控件菜单树结构之ElementNotFoundError(详细教程)

PC端自动化测试实战教程-6-pywinauto 打印和保存控件菜单树结构之ElementNotFoundError(详细教程)

原创
作者头像
北京-宏哥
发布2025-01-23 22:30:50
发布2025-01-23 22:30:50
12200
代码可运行
举报
运行总次数:0
代码可运行

1.简介

其实前边的文章宏哥已经在控制台打印过控件菜单树结构,只是没有将其保存到文件中。只需要一个方法即可。在pywinauto中可以使用 print_control_identifiers() 方法打印控件菜单树结构,这对我们查找控件非常方便。宏哥今天将其单独拎出来是因为Windows10系统和Windows11系统会有一个坑,而且宏哥掉里边了,查了好多资料都没有找到解决办法,最后还好通过自己各种尝试将坑填平,成功爬出来了。其实前边已经遇到了打开记事本最后替换成了notepad++。今天跟随宏哥一步步入坑,然后再一步步填坑,最后成功解决。

2.控件操作

程序窗口中的内容,把它称之为控件,我们要对这个窗口的内容进行操作,就需要选择到对应的控件,获取所有控件我们可以通过print_control_identifiers()这个方法,来获取这个窗口下的直接子控件。因此我们为了清楚可以将控件的菜单结构树打印出来,一目了然。

3.起因

宏哥的台式电脑是Windows10系统的,但是宏哥的笔记本却是Windows11系统的。宏哥在学习和演示打印控件菜单树结构的时候,宏哥首先是在台式电脑(Win10系统)上操作和演示(打印记事本控件结构树)的,但是文章就写了一半,没有写完。这时候刚好由于出差宏哥只能被迫背上笔记本电脑(Win11系统),于是宏哥想要完成剩下的文章就继续将Windows10系统运行成功的代码,直接在Windows11系统上拷贝运行演示操作,结果报错了。。。。运行失败了,一时很懵,不知道如何解决,查了好多资料发现好多人都遇到同样的问题,但是就是没有给出解决办法,有的是提一句如何如何做,宏哥都一一试过了,都不行。就是这样就调入坑中了,要是一直在Windows10系统上操作演示或许就不会有这一篇文章,这一回事了。一切都是命啊,万般不由人,但是臣妾做到了。由于宏哥写文章的时候,手头还是没有Windows10,就网上找了一台免费微软提供类似win10系统,然后简单的搭建了一个环境给小伙伴或者童鞋们进行演示,有兴趣的自己可以试一下:实验 - 使用 Microsoft Office 集成 - Training | Microsoft Learn

4.Windows10系统

4.1代码设计

4.2参考代码

代码语言:javascript
代码运行次数:0
复制
# -*- coding:utf-8 -*-

# 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行

# 2.注释:包括记录创建时间,创建人,项目名称。
'''
Created on 2025-02-12
@author: 北京-宏哥
北京宏哥(微信搜索:北京宏哥,关注宏哥,提前解锁更多测试干货!)
Project: PC端自动化测试实战教程-6-pywinauto打印和保存控件菜单树结构(详细教程)
'''

# 3.导入模块
from pywinauto import Application
import time

# 通过窗口打开
app = Application('uia').start("notepad.exe")
win = app['Untitled - Notepad']
print(win)
print(app.process)
win.print_control_identifiers()

4.3运行代码

1.运行代码,右键Run'Test',就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的动作(启动记事本)。如下图所示:

5.Windows11系统

1.宏哥出差了,然后想也没想就将上边在Windows10系统运行成功的代码拷贝到笔记本Windows11系统上的Pycharm中进行运行,结果报错了:pywinauto.findwindows.ElementNotFoundError: {'best_match': 'Untitled - Notepad', 'backend': 'uia', 'process': 31680}

5.1运行代码

1.运行代码,右键Run'Test',就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的动作(启动记事本)。如下图所示:

5.2报错分析

1.宏哥眼睁睁地看记事本启动了,报错却告诉我找不到元素。这不是前后矛盾啊。因为代码中宏哥打印了启动记事本进程号是:28192,如下图所示:

2.然后宏哥查看笔记本电脑的任务管理器的记事本的进程号是:24196 ,如下图所示:

 3.宏哥再次用工具查看,进程号是:24196,如下图所示:

总结:工具查看和任务管理器查看的进程号(24196)相同,但是代码运行启动的进程号(28192)与它们的进程号(24196)不一样,所有才会报错找不到元素,这就可以说通了为啥报这个错。

6.填坑实践

6.1加等待

1.开始填坑,查了好多资料网上说,可能是由于代码运行的快,而PC端程序启动慢导致的,需要加等待,换句话说:应用程序可能需要一段时间才能完全初始化其窗口和UI元素。即便start()方法在内部尝试连接,但如果UI还未完全加载,后续立即进行窗口或控件查找可能失败。于是宏哥就加了等待的代码。

6.1.1代码设计

6.1.2参考代码

代码语言:javascript
代码运行次数:0
复制
# -*- coding:utf-8 -*-

# 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行

# 2.注释:包括记录创建时间,创建人,项目名称。
'''
Created on 2025-02-12
@author: 北京-宏哥
北京宏哥(微信搜索:北京宏哥,关注宏哥,提前解锁更多测试干货!)
Project: PC端自动化测试实战教程-6-pywinauto 打印和保存控件菜单树结构(详细教程)
'''

# 3.导入模块
from pywinauto import Application
import time

# 通过窗口打开
app = Application('uia').start("notepad.exe")
time.sleep(3)
win = app['无标题 - Notepad']
print(win)
print(app.process)
win.print_control_identifiers()

6.1.3运行代码

1.运行代码,右键Run'Test',就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的动作(启动记事本)。如下图所示:

6.2改路径

1.从上边看到我们失败了,然后宏哥继续查资料,又发现说是将start括号里写成路径的格式就可以。结果仍然是报一样的错误。如下图所示:

6.2.1参考代码

代码语言:javascript
代码运行次数:0
复制
# -*- coding:utf-8 -*-

# 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行

# 2.注释:包括记录创建时间,创建人,项目名称。
'''
Created on 2025-02-12
@author: 北京-宏哥
北京宏哥(微信搜索:北京宏哥,关注宏哥,提前解锁更多测试干货!)
Project: PC端自动化测试实战教程-6-pywinauto 打印和保存控件菜单树结构(详细教程)
'''

# 3.导入模块
from pywinauto import Application
import time

# 通过窗口打开
app = Application('uia').start("C:/Windows/notepad.exe")
time.sleep(3)
win = app['无标题 - Notepad']
print(win)
print(app.process)
win.print_control_identifiers()

6.3 connect()

手动调用connect()给予额外的时间缓冲,可能恰好让UI准备就绪。结果仍然是报一样的错误。

6.3.1参考代码

代码语言:javascript
代码运行次数:0
复制
# -*- coding:utf-8 -*-

# 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行

# 2.注释:包括记录创建时间,创建人,项目名称。
'''
Created on 2025-02-12
@author: 北京-宏哥
北京宏哥(微信搜索:北京宏哥,关注宏哥,提前解锁更多测试干货!)
Project: PC端自动化测试实战教程-6-pywinauto 打印和保存控件菜单树结构(详细教程)
'''

# 3.导入模块
from pywinauto import Application
import time

# 通过窗口打开
app = Application('uia').start("notepad.exe")

app = Application('uia').connect(class_name="Notepad")
win = app['无标题 - Notepad']
print(win)
print(app.process)
win.print_control_identifiers()

6.4 connect()和visible_only参数

手动调用connect()给予额外的时间缓冲,然后加上visible_only参数,这是宏哥自己想到的,因为在上边的报错中宏哥看到了visible_only参数,于是宏哥决定加上参数试一下。如下图所示:

6.4.1代码设计

6.4.2参考代码

代码语言:javascript
代码运行次数:0
复制
# -*- coding:utf-8 -*-

# 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行

# 2.注释:包括记录创建时间,创建人,项目名称。
'''
Created on 2025-02-12
@author: 北京-宏哥
北京宏哥(微信搜索:北京宏哥,关注宏哥,提前解锁更多测试干货!)
Project: PC端自动化测试实战教程-6-pywinauto 打印和保存控件菜单树结构(详细教程)
'''

# 3.导入模块
from pywinauto import Application
import time

# 通过窗口打开
app = Application('uia').start("notepad.exe")

app = Application('uia').connect(class_name="Notepad",visible_only=False)
win = app['无标题 - Notepad']
print(win)
print(app.process)
win.print_control_identifiers()

6.4.3运行代码

1.运行代码,右键Run'Test',就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的动作(启动记事本)。如下图所示:

6.5 connect()和等待

这个也是宏哥在一次偶然运行代码中发现的,因为宏哥忘记将等待的代码段注释掉,结果运行代码成功!哈哈~~,坑一下子就这样跳出来了,要问宏哥是什么原因,宏哥也是一脸懵,一头问号,反正不管怎么说,问题就这样得到解决了。

6.5.1代码设计

6.5.2参考代码

代码语言:javascript
代码运行次数:0
复制
# -*- coding:utf-8 -*-

# 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行

# 2.注释:包括记录创建时间,创建人,项目名称。
'''
Created on 2025-02-12
@author: 北京-宏哥
北京宏哥(微信搜索:北京宏哥,关注宏哥,提前解锁更多测试干货!)
Project: PC端自动化测试实战教程-6-pywinauto 打印和保存控件菜单树结构(详细教程)
'''

# 3.导入模块
from pywinauto import Application
import time

# 通过窗口打开
app = Application('uia').start("notepad.exe")
time.sleep(3)
app = Application('uia').connect(class_name="Notepad")
win = app['无标题 - Notepad']
print(win)
print(app.process)
win.print_control_identifiers()

6.5.3运行代码

1.运行代码,右键Run'Test',就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的动作(启动记事本)。如下图所示:

好了到此打印控件菜单结构树,就大功告成了,下一步我们只需要将其保存就可以了,灰常简单哦!!!

7.保存控件菜单结构树

7.1print_control_identifiers()源码

忙着解决问题,都没有来得及查看一下print_control_identifiers()的源码,如下:

代码语言:javascript
代码运行次数:0
复制
    def print_control_identifiers(self, depth=None, filename=None):
        """
        Prints the 'identifiers'

        Prints identifiers for the control and for its descendants to
        a depth of **depth** (the whole subtree if **None**).

        .. note:: The identifiers printed by this method have been made
               unique. So if you have 2 edit boxes, they won't both have "Edit"
               listed in their identifiers. In fact the first one can be
               referred to as "Edit", "Edit0", "Edit1" and the 2nd should be
               referred to as "Edit2".
        """
        if depth is None:
            depth = sys.maxsize
        # Wrap this control
        this_ctrl = self.__resolve_control(self.criteria)[-1]

        # Create a list of this control and all its descendants
        all_ctrls = [this_ctrl, ] + this_ctrl.descendants()

        # Create a list of all visible text controls
        txt_ctrls = [ctrl for ctrl in all_ctrls if ctrl.can_be_label and ctrl.is_visible() and ctrl.window_text()]

        # Build a dictionary of disambiguated list of control names
        name_ctrl_id_map = findbestmatch.UniqueDict()
        for index, ctrl in enumerate(all_ctrls):
            ctrl_names = findbestmatch.get_control_names(ctrl, all_ctrls, txt_ctrls)
            for name in ctrl_names:
                name_ctrl_id_map[name] = index

        # Swap it around so that we are mapped off the control indices
        ctrl_id_name_map = {}
        for name, index in name_ctrl_id_map.items():
            ctrl_id_name_map.setdefault(index, []).append(name)

        def print_identifiers(ctrls, current_depth=1, log_func=print):
            """Recursively print ids for ctrls and their descendants in a tree-like format"""
            if len(ctrls) == 0 or current_depth > depth:
                return

            indent = (current_depth - 1) * u"   | "
            for ctrl in ctrls:
                try:
                    ctrl_id = all_ctrls.index(ctrl)
                except ValueError:
                    continue
                ctrl_text = ctrl.window_text()
                if ctrl_text:
                    # transform multi-line text to one liner
                    ctrl_text = ctrl_text.replace('\n', r'\n').replace('\r', r'\r')

                output = indent + u'\n'
                output += indent + u"{class_name} - '{text}'    {rect}\n"\
                    "".format(class_name=ctrl.friendly_class_name(),
                              text=ctrl_text,
                              rect=ctrl.rectangle())
                output += indent + u'{}'.format(ctrl_id_name_map[ctrl_id])

                title = ctrl_text
                class_name = ctrl.class_name()
                auto_id = None
                control_type = None
                if hasattr(ctrl.element_info, 'automation_id'):
                    auto_id = ctrl.element_info.automation_id
                if hasattr(ctrl.element_info, 'control_type'):
                    control_type = ctrl.element_info.control_type
                    if control_type:
                        class_name = None  # no need for class_name if control_type exists
                    else:
                        control_type = None # if control_type is empty, still use class_name instead
                criteria_texts = []
                if title:
                    criteria_texts.append(u'title="{}"'.format(title))
                if class_name:
                    criteria_texts.append(u'class_name="{}"'.format(class_name))
                if auto_id:
                    criteria_texts.append(u'auto_id="{}"'.format(auto_id))
                if control_type:
                    criteria_texts.append(u'control_type="{}"'.format(control_type))
                if title or class_name or auto_id:
                    output += u'\n' + indent + u'child_window(' + u', '.join(criteria_texts) + u')'

                if six.PY3:
                    log_func(output)
                else:
                    log_func(output.encode(locale.getpreferredencoding(), errors='backslashreplace'))

                print_identifiers(ctrl.children(), current_depth + 1, log_func)

        if filename is None:
            print("Control Identifiers:")
            print_identifiers([this_ctrl, ])
        else:
            log_file = codecs.open(filename, "w", locale.getpreferredencoding())

            def log_func(msg):
                log_file.write(str(msg) + os.linesep)
            log_func("Control Identifiers:")
            print_identifiers([this_ctrl, ], log_func=log_func)
            log_file.close()

    print_ctrl_ids = print_control_identifiers
    dump_tree = print_control_identifiers

print_ctrl_ids 和 dump_tree 实现的功能与print_control_identifiers等价,都是调用的print_control_identifiers 方法。 用2个参数:

  • depth 查找框架深度,默认全部查找
  • filename 保存本地文件名称

7.2保存到本地文件

1.我们把打印的控件结构树内容保存到本地txt,这样查看更方便,直接CTRL+F查找即可。

7.2.1代码设计

7.2.2参考代码

代码语言:javascript
代码运行次数:0
复制
# -*- coding:utf-8 -*-

# 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行

# 2.注释:包括记录创建时间,创建人,项目名称。
'''
Created on 2025-02-12
@author: 北京-宏哥
北京宏哥(微信搜索:北京宏哥,关注宏哥,提前解锁更多测试干货!)
Project: PC端自动化测试实战教程-6-pywinauto 打印和保存控件菜单树结构(详细教程)
'''

# 3.导入模块
from pywinauto import Application
import time

# 通过窗口打开
app = Application('uia').start("notepad.exe")
time.sleep(3)
app = Application('uia').connect(class_name="Notepad")
win = app['无标题 - Notepad']
print(win)
print(app.process)
win.print_control_identifiers()
win.print_ctrl_ids(filename="bjhg.txt")

7.2.3运行代码

1.运行代码,右键Run'Test',就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的动作(启动记事本)。如下图所示:

3.在windows上运行后文件写入的中文内容有乱码,如下图所示:

4.重新设保存文件默认编码可以解决此问题。

7.2.4代码设计

7.2.5参考代码

代码语言:javascript
代码运行次数:0
复制
# -*- coding:utf-8 -*-

# 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行

# 2.注释:包括记录创建时间,创建人,项目名称。
'''
Created on 2025-02-12
@author: 北京-宏哥
北京宏哥(微信搜索:北京宏哥,关注宏哥,提前解锁更多测试干货!)
Project: PC端自动化测试实战教程-6-pywinauto 打印和保存控件菜单树结构(详细教程)
'''

# 3.导入模块
from pywinauto import Application
import time
import locale


def getpreferredencoding(do_setlocale = True):
    return "utf-8"


# 设置保存文件编码 "utf-8"
locale.getpreferredencoding = getpreferredencoding
print(locale.getpreferredencoding())

# 通过窗口打开
app = Application('uia').start("notepad.exe")
time.sleep(3)
app = Application('uia').connect(class_name="Notepad")
win = app['无标题 - Notepad']
print(win)
print(app.process)
win.print_control_identifiers()
win.print_ctrl_ids(filename="bjhg.txt")

7.2.6运行代码

1.运行代码,右键Run'Test',就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的动作(启动记事本)。如下图所示:

8.小结

 今天主要讲解和分享的是打印控件菜单结构树的方法:print_control_identifiers()在Windows10系统和Windows11系统上遇到的问题:pywinauto.findwindows.ElementNotFoundError: {'best_match': 'Untitled - Notepad', 'backend': 'uia', 'process': 31680} ,如何遇到,然后宏哥是怎么一步一步解决的,但是其中的原理宏哥还是有点懵,好在是问题暂时解决了。其实回过头来看走过的路,其中有一些走弯路了,当时由于宏哥急于解决问题,没有仔细思考,第一步已经发现进程号都不一样了,什么等待啊、改路径等等全是白扯,根本解决不了问题。现在问题解决了,按照解决问题思路宏哥倒退一下是原理:PC端程序启动后,慢慢在后台加载界面程序,这时宏哥加了等待,然后等待程序加载完成,宏哥然后连接这个加载好的应用程序,这样就确保PC端启动的程序和连接的程序一样(进程号一致),然后执行控件结构树的打印,就不会找不到了。因此等待和连接二者缺一不可,前边宏哥也将而这分开实践了,解决不了问题,仍然报错。二者结合问题解决。

好了,关于打印和保存控件菜单结构树以及不同操作系统遇到的问题,都得到完美解决,仅供参考学习,小伙伴或者童鞋们,有其他更好的解决办法可以给宏哥留言评论哈!时间不早了今天就分享到这里,感谢你耐心地阅读!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.简介
  • 2.控件操作
  • 3.起因
  • 4.Windows10系统
    • 4.1代码设计
    • 4.2参考代码
    • 4.3运行代码
  • 5.Windows11系统
    • 5.1运行代码
    • 5.2报错分析
  • 6.填坑实践
    • 6.1加等待
      • 6.1.1代码设计
      • 6.1.2参考代码
      • 6.1.3运行代码
    • 6.2改路径
      • 6.2.1参考代码
    • 6.3 connect()
      • 6.3.1参考代码
    • 6.4 connect()和visible_only参数
      • 6.4.1代码设计
      • 6.4.2参考代码
      • 6.4.3运行代码
    • 6.5 connect()和等待
      • 6.5.1代码设计
      • 6.5.2参考代码
      • 6.5.3运行代码
  • 7.保存控件菜单结构树
    • 7.1print_control_identifiers()源码
    • 7.2保存到本地文件
      • 7.2.1代码设计
      • 7.2.2参考代码
      • 7.2.3运行代码
      • 7.2.4代码设计
      • 7.2.5参考代码
      • 7.2.6运行代码
  • 8.小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档