精华内容
下载资源
问答
  • Java桌面程序demo

    2019-02-17 20:52:29
    基于Java语言、Swing图形库开发的Java桌面示例程序小Demo
  • Java使用JFrame开发出漂亮的桌面程序附源码下载
  • Win7远程桌面程序完整包,64位,打包了所有相关程序;
  • 大家想到使用Java这门编程语言大多是脑海中是浮现的B/S开发,高并发、云计算、大数据等等,今天给大家带来的是Java不一样的使用方式——使用Java做PC桌面程序开发。 不同的是,之前Java是使用swing或者awt开发,...

    大家想到使用Java这门编程语言大多是脑海中是浮现的B/S开发,高并发、云计算、大数据等等,今天给大家带来的是Java不一样的使用方式——使用Java做PC桌面程序开发。

    不同的是,之前Java是使用swing或者awt开发,但是这次介绍给大家的是使用JavaFx做桌面程序开发。

    应该没有人使用过电脑版的QQ、酷狗、视频播放器等等软件,它们都有非常漂亮的UI,也就是界面。

    可是我们使用swing和awt开发出的桌面程序却是那么的“不好看”!所以今天就给大家介绍一个方便快捷且美观的Java桌面程序开发方式——JavaFx+JFoenix开发

    先看看一张效果图(来自JFoenix GitHub)

    如果你对这样美观的桌面程序感兴趣的话,请继续往下阅读..................

    JavaFx

    JavaFx使用方便快捷的Java代码配合FXML布局文件开发,做到视图和逻辑代码分开,形成一种方便、简洁、易懂的开发模式。

    • FXML文件用于布局
    • Controller用于编写FXML的初始化操作和逻辑操作(事件绑定、动态效果等等)
    • 使用CSS代码给FXML界面组件做效果

    如下图:

    • FXML
    • Controller 
    • CSS

    这样虽然开始简易、方便,但是界面依然不够美观漂亮。这时就需要使用到JFoenix。

    JFoenix

    JFoenix的github地址:https://github.com/jfoenixadmin/JFoenix    里边有demo、示例图等等

    官网地址  http://www.jfoenix.com/

    只需要在你的项目中使用JFoenix的jar包即可使用,然后在FXML设计器的Custom一栏下有JFOenix的所有组件。

    下边是JFoenix的界面效果图(来自GitHub),效果为Material Design风格:

    更多案例效果请看 https://github.com/jfoenixadmin/JFoenix

     

    熟练使用JavaFx+JFoenix,即可方便高效的开发出漂亮的Java桌面程序

    展开全文
  • windows桌面程序开发 大多数在Windows上工作的软件开发人员都有相当标准的工具集:代码编辑器或IDE; Git或其他版本控制系统; 消息传递客户端(Slack或其克隆之一),等等。 由于大多数开发人员工作流程都在线,...

    windows桌面程序开发

    大多数在Windows上工作的软件开发人员都有相当标准的工具集:代码编辑器或IDE; Git或其他版本控制系统; 消息传递客户端(Slack或其克隆之一),等等。 由于大多数开发人员工作流程都在线,因此Web浏览器消除了我们通常可能安装在桌面上的许多其他工具。

    但是,无论是用于管理日常工作流程还是用于创建与构建软件工作相邻的内容(例如,视频演练或屏幕录像),程序员都可以从中受益。 这里有六个这样的工具,应该在任何开发人员的桌面上占据一席之地。

    [ 同样在InfoWorld上:您将犯下的6个Git错误-及其解决方法 ]

    OBS工作室

    OBS Studio接近现场视频捕获和流传输的多合一解决方案,您无需付费即可获得。 该开源应用程序在抛光和灵活性方面可与许多商业应用程序相媲美。 它支持从单个窗口,整个桌面或屏幕的固定区域进行捕获,在视图之间进行即时切换以及从各种来源(静态图像,预捕获的电影,实时视频等)进行叠加。 并且它为音频捕获提供了同样灵活的支持。

    OBS Studio中的每个功能都可以连接到自定义热键,因此您可以启动程序,将其最小化并控制录制,而不必将程序弹出来(尽管这确实需要一些练习)。 一个小小的遗漏:捕获时无法对屏幕的某个区域进行交互式缩放,例如使用某种组合键和鼠标滚轮。 但是,考虑到OBS Studio给您多少价格,这几乎是无与伦比的。

    obs工作室 IDG

    使用OBS Studio设置视频捕获会话。 可以定义多个捕获方案,并在运行中进行切换,这样您就可以从使用网络摄像头的画中画(如此处所示)移动到特定窗口的缩放视图。

    同上

    Microsoft Windows中的剪贴板从未具有过很多用途,很大程度上是因为它一次只存储一个剪辑。 Windows 10最近向剪贴板添加了“历史记录”功能,但它仍然没有我们某些人所希望的功能强大(尽管“跨设备同步剪辑”功能漂亮)。

    同上为Windows剪贴板添加了大量的附加功能。 除了存储所需的剪辑外,您还可以键入搜索内容,保存常见的剪辑以供重复使用,允许剪辑在x天后过期,并执行特殊的粘贴功能,例如仅粘贴剪辑的纯文本版本。 我最喜欢的功能:如果要复制图像,则可以打开Ditto,将图像拖出片段列表,将其拖放到文件夹中,然后将其另存为PNG文件。 截屏并没有比这容易得多。

    同上 IDG

    从同上剪贴板中选择一个粘贴。 您可以键入以从Ditto中存储的所有可用片段中进行搜索。 此处显示的工具提示会在上下文中显示剪辑的全文。

    Unichars

    Windows另一个长期的痛点是不使用特殊键盘或其他一些便利而输入特殊字符(重音符号,数学符号等)。 Unichars使用一个强大的隐喻来键入特殊字符:组合键(Unix人士应该熟悉的东西)。 敲击撰写键(通常是Alt键之一,但您可以重新分配它),并且您可以在一个字符中组合一个或多个击键。

    例如,如果您将“ O”和双引号(“)组合在一起,则会得到一个带有变音符号(Ö)的O。您会从每个按键中获得实时反馈,因此您可以相对快速地弄清楚如何获得字符以及整个字符字母具有简单的前缀:例如,对大写和小写希腊文分别使用G和g,最后,Unichars是完全可定制的,因此您可以将键组合用于自己的字符或整个样板文字。程序本身尚未经过一段时间的更新,但在Windows 10上仍然可以可靠运行。

    单字符 IDG

    用Unichars键入特殊字符。 键入时会弹出工具提示,以指示哪些击键将生成哪些字符。 可以自定义组合键(用于触发特殊字符序列),如左侧程序主窗口中所示。

    KeePass

    有消息说,密码正逐渐被淘汰,并用更优雅的方式来进行身份验证,但是我们仍然是无密码世界中的一种方式。 在此之前,密码管理器将继续使在线生活变得更加轻松得多,那么为什么不选择免费的呢? 毕竟,开源项目KeePass仍然是最流行,得到广泛支持和可广泛配置的密码管理器之一。

    KeePass可以用作用户机密的通用加密存储库,而不仅仅是密码。 条目是版本控制的,因此,如果您需要挖掘旧版本的密码,可以在KeePass数据库历史记录中找到它。 另外,第三方开发人员还创建了许多有用的加载项,例如明显的密码生成器或Windows Hello集成。 但不管有没有附加组件,KeePass都非常有用。 例如,可以将条目配置为在按下热键时自动键入具有给定标题的窗口。

    存在多个KeePass版本。 有些用于其他操作系统,例如适用于Linux和MacOS的KeePassX。 其他是转载,例如用C ++而不是C#编写的KeePassXC或电子应用程序版本KeeWeb。 但是原始的KeePass对于Windows用户仍然很有吸引力。

    Keepass IDG

    KeePass密码输入。 KeePass为您提供了多种生成密码的方法,并以图形方式显示了密码的强度。

    Irfanview

    如果要对图像进行任何处理,则至少需要一个通用的图像管理和查看工具-比文件资源管理器更多,但比Adobe Photoshop少得多。 Irfanview达到了很好的平衡。 它加载速度足够快,可以用作现场图像预览器,而且还打包了许多其他有用的功能,而不会妨碍它们。

    缩略图浏览,批量图像转换和快捷编辑都在这里。 甚至还支持某些真正的奇特图像格式和扩展,例如多页TIFF,以及用于默认情况下不包含的所有内容的插件体系结构。 尽管Irfanview不是开源的,但在全球成千上万用户捐赠的支持下,它是免费使用的。

    irfanview IDG

    使用Irfanview查看图像。 可以从任何图像打开右侧的缩略图视图,使您可以轻松浏览同一文件夹中的其他图像。

    WinDirStat

    “我所有的磁盘空间都放在哪里?” 是开发人员问自己的第二个最普遍的问题(紧接在“还有咖啡吗?”之后)。 子目录和子目录可以填充各种数字垃圾-日志文件,临时文件,废弃的下载文件,从未正确卸载的软件,过时的备份(随便命名)。

    WinDirStat分析Microsoft Windows系统上的存储空间,并为您提供方便的图形概述,以显示正在占用的空间。 WinDirStat演示文稿的美妙之处在于,您一眼就能看到最大和最偏僻的空间用途,那就是更好地将它们挑选出来并更快地回收它们。 它自身的缺点是扫描驱动器可能要花几分钟,但是从扫描过程中获得的丰富见解值得等待。

    温迪司他 IDG

    WinDirStat分析系统驱动器。 每个矩形区域代表一个目录,使您可以快速浏览哪些目录占用了最多的空间。 突出显示的区域指示回收站中的内容,一段时间以来没有将其清空。

    翻译自: https://www.infoworld.com/article/3546013/6-windows-desktop-utilities-every-software-developer-needs.html

    windows桌面程序开发

    展开全文
  • 一、Visual Studio Installer 安装 C++ 桌面开发库、 二、Visual Studio 2019 创建 Windows 桌面程序



    之前曾使用 Visual Studio 2019 开发 CMake 项目 , 参考之前的博客 :





    一、Visual Studio Installer 安装 C++ 桌面开发库



    启动 Visual Studio 2019 , 点击 " 创建新项目 " ;

    在这里插入图片描述

    之前只使用 Visual Studio 2019 开发 CMake 项目 , 没有安装开发桌面程序的开发库 , 这里需要使用 Visual Studio Installer 安装开发库 ;

    在这里插入图片描述

    首先要安装开发相关的库 , 在 Visual Studio Installer 中 , 选择 " 使用 C++ 的桌面开发 " , 安装该开发库 ;

    在这里插入图片描述

    等待安装完毕 ;

    在这里插入图片描述





    二、Visual Studio 2019 创建 Windows 桌面程序



    选择创建 " Windows 桌面应用程序 " , 点击 " 下一步 " :

    在这里插入图片描述

    配置项目相关参数 , 名称 , 路径 等 :

    在这里插入图片描述

    点击 " 创建 " 按钮 , 即可创建 Windows 桌面应用程序 ;

    在这里插入图片描述

    运行程序 , 弹出空的界面 :

    在这里插入图片描述

    展开全文
  • Windows桌面程序自动化控制之uiautomation模块全面讲解

    万次阅读 多人点赞 2021-11-18 00:55:45
    uiautomation模块除了核心功能UI控件的控制、截图和数据提取外,还支持全局热键注册、剪切板操作和管理员权限提权。 这个库的部分功能超过好几个专门针对某个功能的库。

    大家好,我是小小明,经过2个月断断续续的整理,uiautomation模块桌面自动化控制的系统教程终于成型出炉了。下面是本文档的目录大纲,大家可以感受一下:

    简介

    功能介绍

    本文档大纲:

    image-20211118101343337

    可以看到uiautomation模块除了核心功能UI控件的控制截图数据提取外,还支持全局热键注册剪切板操作管理员权限提权

    在常规的模拟鼠标和键盘操作,我们一般使用pyautogui,uiautomation模块不仅能直接支持这些操作,还能通过控件定位方式直接定位到目标控件的位置,而不需要自己去获取对应坐标位置。uiautomation模块不仅支持任意坐标位置截图,还支持目标控件的截图,缺点在于截取产生的图片对象难以直接与PIL库配合,只能导出文件后让PIL图像处理库重新读取。对于能够获取到其ScrollItemPattern对象的控件还可以通过ScrollIntoView方法进行视图定位,与游览器的元素定位效果几乎一致。

    在常规的热键功能,我们一般使用pynput实现,但现在有了uiautomation模块,热键注册会比pynput更简单功能更强。uiautomation模块所支持的剪切板操作的功能也远远超过常规的专门用于剪切板复制粘贴的库。更牛的是uiautomation模块能直接支持让你的python程序实现管理员提权。

    基本上这个库的功能超过好几个专门针对某个功能的库。我们可以看看一下这个库自动化操作过程的动图效果:

    请添加图片描述

    请添加图片描述

    请添加图片描述

    请添加图片描述

    掌握这个框架之后,你能够实现的自动化效果远不止如此。

    这么优秀的框架你是否心动了呢?心动不如行动,学起来吧!!!

    基本原理

    uiautomation模块项目地址:https://github.com/yinkaisheng/Python-UIAutomation-for-Windows

    uiautomation是yinkaisheng业余时间开发一个模块。封装了微软UIAutomation API,支持自动化Win32,MFC,WPF,Modern UI(Metro UI), Qt, IE, Firefox(version<=56 or >=60), Chrome谷歌游览器和基于Electron开发的应用程序(加启动参数–force-renderer-accessibility也能支持UIAutomation被自动化).

    uiautomation只支持Python 3版本,依赖comtypes和typing这两个包,但Python不要使用3.7.6和3.8.1这两个版本,comtypes在这两个版本中不能正常工作(issue)。

    UIAutomation的工作原理:

    UIAutomation操作程序时会给程序发送WM_GETOBJECT消息,如果程序处理WM_GETOBJECT消息,实现UI Automation Provider,并调用函数
    UiaReturnRawElementProvider(HWND hwnd,WPARAM wparam,LPARAM lparam,IRawElementProviderSimple *el),此程序就支持UIAutomation。

    IRawElementProviderSimple 就是 UI Automation Provider,包含了控件的各种信息,如Name,ClassName,ContorlType,坐标等。

    UIAutomation 根据程序返回的 IRawElementProviderSimple,就能遍历程序的控件,得到控件各种属性,进行自动化操作。若程序没有处理WM_GETOBJECT或没有实现UIAutomation Provider,UIAutomation则无法识别这些程序内的控件,不支持自动化。
    很多DirectUI程序没有实现UIAutomation Provider,所以不支持自动化。

    关于各控件所支持的控件模式,可参考:

    https://docs.microsoft.com/zh-cn/windows/win32/winauto/uiauto-controlpatternmapping

    在使用uiautomation模块前需要先安装:

    pip install uiautomation
    

    安装后会在python安装目录下的Scripts目录下得到一个automation.py脚本,可以使用它来准确获取目标窗口的控件结构信息。

    automation.py脚本也可以从https://github.com/yinkaisheng/Python-UIAutomation-for-Windows/raw/master/automation.py下载。

    当然使用windows自带的inspect.exe图形化工具来观察控件的树形结构更加,通过everything可以很快在系统中找到该工具。

    ⚠️ :inspect.exe工具获取到的控件类型可能与automation.py脚本打印的结果不太一样,如果发现控件实际不存在,要以automation.py脚本打印的结果为准。

    控件控制入门:记事本操作

    控件分析与可用参数

    首先打开记事本窗口,并设置窗口前置:

    import subprocess
    import uiautomation as auto
    
    subprocess.Popen('notepad.exe')
    # 从桌面的第一层子控件中找到记事本程序的窗口WindowControl
    notepadWindow = auto.WindowControl(searchDepth=1, ClassName='Notepad')
    print(notepadWindow.Name)
    # 设置窗口前置
    notepadWindow.SetTopmost(True)
    

    运行上述代码后,会打开一个窗口前置的记事本程序。

    控件可用参数说明:

    • searchFromControl = None:从哪个控件开始查找,如果为None,从根控件Desktop开始查找
    • searchDepth = 0xFFFFFFFF: 搜索深度
    • searchInterval = SEARCH_INTERVAL:搜索间隔
    • foundIndex = 1 :搜索到的满足搜索条件的控件索引,索引从1开始
    • Name:控件名字
    • SubName :控件部分名字
    • RegexName:使用re.match匹配符合正则表达式的名字,Name,SubName,RegexName只能使用一个,不能同时使用
    • ClassName :类名字
    • AutomationId: 控件AutomationId
    • ControlType :控件类型
    • Depth:控件相对于searchFromControl的精确深度
    • Compare:自定义比较函数function(control: Control, depth: int)->bool

    searchDepth和Depth的区别:

    searchDepth在指定的深度范围内(包括1~searchDepth层中的所有子孙控件)搜索第一个满足搜索条件的控件

    Depth只在Depth所在的深度(如果Depth>1,排除1~searchDepth-1层中的所有子孙控件)搜索第一个满足搜索条件的控件

    为了进一步操作该程序,我们可以使用inspect.exe工具或automation.py脚本分析控件结构。

    通过inspect.exe工具分析控件时可以看到记事本的编辑区类型为DocumentControl

    image-20210921164143136

    但uiautomation实际使用该类型查找控件时却会找不到控件报错。

    下面我们使用automation.py脚本来分析目标窗口,我的Python安装目录为D:\Miniconda3所以automation.py脚本会存在于D:\Miniconda3\Scripts\automation.py

    查看帮助信息:

    >python D:\Miniconda3\Scripts\automation.py -h
    UIAutomation 2.0.15 (Python 3.7.4, 64 bit)
    usage
    -h      show command help
    -t      delay time, default 3 seconds, begin to enumerate after Value seconds, this must be an integer
            you can delay a few seconds and make a window active so automation can enumerate the active window
    -d      enumerate tree depth, this must be an integer, if it is null, enumerate the whole tree
    -r      enumerate from root:Desktop window, if it is null, enumerate from foreground window
    -f      enumerate from focused control, if it is null, enumerate from foreground window
    -c      enumerate the control under cursor, if depth is < 0, enumerate from its ancestor up to depth
    -a      show ancestors of the control under cursor
    -n      show control full name, if it is null, show first 30 characters of control's name in console,
            always show full name in log file @AutomationLog.txt
    -p      show process id of controls
    
    if UnicodeError or LookupError occurred when printing,
    try to change the active code page of console window by using chcp or see the log file @AutomationLog.txt
    chcp, get current active code page
    chcp 936, set active code page to gbk
    chcp 65001, set active code page to utf-8
    
    examples:
    automation.py -t3
    automation.py -t3 -r -d1 -m -n
    automation.py -c -t3
    

    下面为了后续命令简化一点,我先将automation.py文件复制到cmd所在的当前目录。

    执行以下命令:

    python automation.py -t1 -d1
    

    立马在1秒内将鼠标激活记事本窗口,可以看到控制台打印:

    image-20210921170002665

    可以看到第一个控件的类型为EditControl

    下面将鼠标移动到记事本的编辑框内之后,执行:

    python automation.py -t0 -c
    

    就得到编辑器的全部子控件信息:

    image-20210921170648813

    下面我们使用uiautomation向记事本输入文本。

    首先获取输入框:

    edit = notepadWindow.EditControl()
    

    方法1-使用EditControl支持的ValuePattern:

    edit.GetValuePattern().SetValue('方法1')
    

    该方法直接修改编辑框的文本内容。

    方法2-发送按键指令输入文本:

    edit.SendKeys('方法2')
    

    该方法的输入效果比较像打字机输入。

    方法3-复制文本后到剪切板粘贴:

    auto.SetClipboardText("方法3")
    edit.SendKeys('{Ctrl}v')
    

    获取当前编辑框中的文本:

    print(edit.GetValuePattern().Value)
    

    最后我们点击标题栏的关闭按钮(可以通过索引或名称查找目标按钮):

    # 通过标题栏第三个按钮找到关闭按钮
    notepadWindow.TitleBarControl(Depth=1).ButtonControl(foundIndex=3).Click()
    

    或:

    # 通过标题栏查找名称为关闭的按钮
    notepadWindow.TitleBarControl(Depth=1).ButtonControl(searchDepth=1, Name='关闭').Click()
    

    最后保存并关闭:

    notepadWindow.TitleBarControl(Depth=1).ButtonControl(searchDepth=1, Name='关闭').Click()
    # 确认保存
    auto.SendKeys('{ALT}s')
    # 输入文件名,并快捷键点击保存
    auto.SendKeys('自动保存{ALT}s')
    # 如果弹出文件名冲突提示,则确认覆盖
    auto.SendKeys('{ALT}y')
    

    完整代码:

    import subprocess
    import uiautomation as auto
    
    subprocess.Popen('notepad.exe')
    # 首先从桌面的第一层子控件中找到记事本程序的窗口WindowControl,再从这个窗口查找子控件
    notepadWindow = auto.WindowControl(searchDepth=1, ClassName='Notepad')
    print(notepadWindow.Name)
    # 设置窗口前置
    notepadWindow.SetTopmost(True)
    # 输入文本
    edit = notepadWindow.EditControl()
    auto.SetClipboardText("WIN98中的“98”是什么意思?")
    edit.SendKeys('{Ctrl}v')
    # 获取文本
    print("编辑框内容:",edit.GetValuePattern().Value)
    # 通过标题栏查找名称为关闭的按钮
    notepadWindow.TitleBarControl(Depth=1).ButtonControl(searchDepth=1, Name='关闭').Click()
    # 确认保存
    auto.SendKeys('{ALT}s')
    # 输入文件名,并快捷键点击保存
    auto.SendKeys('自动保存{ALT}s')
    # 如果弹出文件名冲突提示,则确认覆盖
    auto.SendKeys('{ALT}y')
    

    控件延迟搜索机制

    底层COM对象方法:

    ⚠️ Control.Element返回IUIAutomation底层COM对象IUIAutomationElement, 基本上Control的所有属性或方法都是通过调用IUIAutomationElement COM API和Win32 API实现的。

    延迟搜索控件:

    当我们创建一个Control对象时,uiautomation并不会马上开始搜索控件,而是当使用其属性或方法,并且内部的Control.Element是None时uiautomation才开始搜索控件。如果在uiautomation.TIME_OUT_SECOND(默认为10)秒内找不到控件,uiautomation就会抛出一个LookupError异常。

    也可以调用Control.Refind立马或重新开始搜索控件,例如:

    edit = notepadWindow.EditControl()
    edit.Refind()
    
    True
    

    但是当控件不存在时,则会报出错误。

    为了避免函数最终抛出异常,可以调用Control.Exists(maxSearchSeconds, searchIntervalSeconds, printIfNotExist)检查目标控件是否存在:

    edit = notepadWindow.EditControl()
    edit.Exists()
    
    True
    

    Control.Refind和Control.Exists均会使Control.Element无效并触发重新搜索逻辑。

    另一种检查目标控件是否存在的方法是auto.WaitForExist(control, timeout)

    下面继续以记事本为对象演示这个机制。首先打开第一个记事本并获取输入控件:

    import subprocess
    import uiautomation as auto
    auto.uiautomation.SetGlobalSearchTimeout(2)  # 设置全局搜索超时时间为2秒
    
    subprocess.Popen('notepad.exe')
    window = auto.WindowControl(searchDepth=1, ClassName='Notepad')
    # 创建控件对象时并不会开始搜索控件
    edit = window.EditControl()
    

    此时,控件window和edit还没有开始搜索,内部Control.Element的值为None。

    第一次调用SendKeys时,才开始搜索控件window和edit:

    # 第一次调用SendKeys时, 才开始搜索控件window和edit
    edit.SendKeys('第一次调用')
    

    搜索完毕后,才会开始执行发送按键方法,此时Control.Element有效。

    第二次调用SendKeys不会触发搜索(Control.Element不为None):

    edit.SendKeys('第二次调用')
    

    然后我们清空输入的内容,并关闭刚才打开的记事本:

    edit.GetValuePattern().SetValue('')
    window.GetWindowPattern().Close()
    

    关闭后,此时虽然window和edit有值却已经无效了。

    再次打开记事本,就必须重新搜索控件(否则无法操作新启动的窗口):

    subprocess.Popen('notepad.exe')  # 运行第二个Notepad
    window.Refind()  # 必须重新搜索
    edit.Refind()  # 必须重新搜索
    

    然后可以将其关闭:

    window.GetWindowPattern().Close()
    

    window和edit的Element再次失效。

    Exists方法则既可以判断控件是否存在,也可以触发重新搜索:

    subprocess.Popen('notepad.exe')  # 运行第三个Notepad
    if window.Exists(maxSearchSeconds=1.5, searchIntervalSeconds=0.3):  # 触发重新搜索
        if edit.Exists(maxSearchSeconds=1.5):  # 触发重新搜索
            edit.SendKeys('third notepad')  # 之前的Exists保证edit.Element有效
            edit.SendKeys('{Ctrl}a{Del}')
        window.GetWindowPattern().Close()
    else:
        print("窗口1.5秒内未找到")
    

    示例:连续打开三个记事本并关闭

    完整代码与注释:

    import subprocess
    import uiautomation as auto
    # 设置全局搜索超时2秒
    auto.uiautomation.SetGlobalSearchTimeout(2)  
    
    subprocess.Popen('notepad.exe')
    window = auto.WindowControl(searchDepth=1, ClassName='Notepad')
    window.SetTopmost(True)
    edit = window.EditControl()
    # 当第一次调用SendKeys时, uiautomation开始在2秒内搜索控件window和edit
    # 因为SendKeys内部会间接调用Control.Element并且Control.Element值是None
    # 如果在15秒内找不到window和edit,会抛出LookupError异常
    try:
        edit.SendKeys('第一个记事本程序')
    except LookupError as ex:
        print("第一个记事本窗口在2秒内未找到")
    
    # 第二次调用SendKeys不会触发搜索, 之前的调用保证Control.Element有效
    edit.SendKeys('{Ctrl}a{Del}')
    # 关闭第一个Notepad, window和edit的Element虽然有值,但是无效了
    window.GetWindowPattern().Close()
    
    subprocess.Popen('notepad.exe')  # 运行第二个Notepad
    window.Refind()  # 必须重新搜索
    edit.Refind()  # 必须重新搜索
    edit.SendKeys('第二个记事本程序')
    edit.SendKeys('{Ctrl}a{Del}')
    window.GetWindowPattern().Close()  # 关闭第二个Notepad, window和edit的Element虽然有值,但是再次无效了
    
    subprocess.Popen('notepad.exe')  # 运行第三个Notepad
    if window.Exists(3, 1):  # 触发重新搜索
        if edit.Exists(3):  # 触发重新搜索
            edit.SendKeys('第三个记事本程序')  # 之前的Exists保证edit.Element有效
            edit.SendKeys('{Ctrl}a{Del}')
        window.GetWindowPattern().Close()
    else:
        print("第三个记事本程序在三秒内不存在")
    

    UIAutomation的常见功能

    导包:

    import uiautomation as auto
    

    基本方法

    显示桌面(相当于点击桌面右下角的按钮):

    auto.ShowDesktop()
    

    获取uiautomation已运行的时间:

    auto.ProcessTime()
    

    判断两个控件是否一致:

    auto.ControlsAreSame(control1, control2)
    

    鼠标点击指定坐标:

    auto.Click(x, y)
    

    右键单击:

    auto.RightClick(x, y)
    

    鼠标拖拽(鼠标从(x1,y1)位置按下鼠标拖动到(x2,y2)位置):

    auto.DragDrop(x1, y1, x2, y2, moveSpeed=1)
    

    moveSpeed参数决定了移动的速度。

    关于鼠标滚轮操作详见后面的实例,被找到的控件调用鼠标点击方法不需要传入坐标。

    获取窗口对象

    获取桌面对象:

    c = auto.GetRootControl()
    

    返回运行当前python程序的控制台窗口对象:

    cmdWindow = auto.GetConsoleWindow()
    

    没有找到则返回None。

    获取当前鼠标位置对应的窗口:

    c = auto.ControlFromCursor().GetTopLevelControl()
    

    ControlFromCursor返回了当前鼠标位置的控件,GetTopLevelControl获取了该控件对应的顶级窗口对象。

    获取当前激活窗口对应的对象:

    c = auto.GetForegroundControl().GetTopLevelControl()
    

    控件查找方法

    获取所有的子控件:

    control.GetChildren()
    

    获取首个子控件:

    control.GetFirstChildControl()
    

    获取最后一个子控件:

    control.GetLastChildControl()
    

    获取下一个兄弟控件:

    control.GetNextSiblingControl()
    

    获取前一个兄弟控件:

    control.GetPreviousSiblingControl()
    

    获取父控件:

    control.GetParentControl()
    

    获取顶层窗口控件:

    control.GetTopLevelControl()
    

    获取满足指定条件的祖先控件:

    control.GetAncestorControl(func)
    

    传入的函数参数要求:function(control: Control, depth: int) -> bool

    当函数返回True时表示找到控件并返回,例如以下方法几乎可以得与GetTopLevelControl()相同的结果:

    control.GetAncestorControl(lambda c, d: isinstance(c, auto.WindowControl))
    

    窗口属性调整

    假设获取到一个窗口对象:

    win = auto.ControlFromCursor().GetTopLevelControl()
    

    获取本地窗口句柄:

    win.NativeWindowHandle
    

    根据本地窗口句柄获取窗口控件对象:

    win2 = auto.ControlFromHandle(win.NativeWindowHandle)
    

    经测试,对象一致:

    auto.ControlsAreSame(win, win2)
    
    True
    

    隐藏窗口:

    win.Hide(0)
    

    显示窗口:

    win.Show(0)
    

    窗口最小化:

    win.Minimize()
    

    窗口最大化:

    win.Maximize()
    

    判断窗口是否已经被最小化:

    auto.IsIconic(win.NativeWindowHandle)
    

    IsIconic进支持传入本地窗口句柄。

    将最小化的窗口的恢复显示:

    修改窗口的位置和大小,例如将某个窗口调整到最后一个屏幕的一半:

    rects = auto.GetMonitorsRect()
    rect = rects[-1]
    win.MoveWindow(rect.left, rect.top,
                   rect.width()//2, rect.height()-30)
    

    不过这种调整方法对于cmd这种命令行窗口无效,只能在获取其TransformPattern对象后,调用MoveResize方法来实现。上面的MoveWindow等价于:

    transform_win = win.GetTransformPattern()
    transform_win.Move(rect.left, rect.top)
    transform_win.Resize(rect.width()//2, rect.height()-30)
    

    移动窗口到屏幕中心位置:

    win.MoveToCenter()
    

    窗口置顶:

    window.SetTopmost(True)
    

    获取窗口标题并修改窗口标题:

    win.SetWindowText(win.GetWindowText()+"|小小明")
    

    获取运行当前python程序控制台窗口的标题:

    auto.GetConsoleTitle()
    # auto.GetConsoleOriginalTitle()
    

    设置运行当前python程序控制台窗口的标题:

    auto.SetConsoleTitle('自定义控制台标题')
    

    WalkTree遍历子控件

    除了auto.WalkTree遍历目标控件外,还有auto.WalkControl遍历控件,区别在于auto.WalkTree必须传入自定义函数指定遍历的行为。auto.WalkControl将会在后面涉及可折叠类型的控件遍历时进行演示,下面给出一个简单的通过WalkTree遍历桌面的示例:

    import uiautomation as auto
    
    
    def GetFirstChild(control):
        return control.GetFirstChildControl()
    
    
    def GetNextSibling(control):
        return control.GetNextSiblingControl()
    
    
    desktop = auto.GetRootControl()
    for control, depth in auto.WalkTree(desktop, getFirstChild=GetFirstChild, getNextSibling=GetNextSibling,
                                        includeTop=True, maxDepth=2):
        if not control.Name:
            continue
        print(' ' * depth * 4, control.Name)
    

    maxDepth指定了遍历深度,除了指定这两个方法以外还可以只转入getChildren方法:

    def GetChildren(control):
        return control.GetChildren()
    
    
    for control, depth, remain in auto.WalkTree(desktop,
                                                getChildren=GetChildren,
                                                includeTop=True,
                                                maxDepth=2):
        if not control.Name:
            continue
        print(' ' * depth * 4, control.Name)
    

    结果过滤的方逻辑我们还可以写到yieldCondition的传入函数中:

    def yieldCondition(control, depth):
        if control.Name:
            return True
    
    
    for control, depth, remain in auto.WalkTree(desktop,
                                                getChildren=GetChildren,
                                                yieldCondition=yieldCondition,
                                                includeTop=True,
                                                maxDepth=2):
        print(' ' * depth * 4, control.Name)
    

    在我电脑当前执行结果均为:

     桌面 1
         任务栏
             开始
             在这里输入你要搜索的内容
             开始
             在这里输入你要搜索的内容
             系统时钟, 23:02, ‎2021/‎11/‎15
         test - Jupyter Notebook - 360安全浏览器 13.1
             Chrome Legacy Window
         一文掌握uiautomation的经典案例.md• - Typora
             Typora
         UIAutomation_demos – clipboard_test.py PyCharm
         Program Manager
    

    WalkTree的规则是当设置getChildren函数时,忽略getFirstChild和getNextSibling,否则使用这两个函数。设置yieldCondition函数时则开启额外的过滤。

    甚至可以使用WalkTree方法计算全排列问题:

    def NextPermutations(aTuple):
        left, permutation = aTuple
        ret = []
        for i, item in enumerate(left):
            nextLeft = left[:]
            del nextLeft[i]
            nextPermutation = permutation + [item]
            ret.append((nextLeft, nextPermutation))
        return ret
    
    
    uniqueItems = list("abc")
    n = len(uniqueItems)
    count = 0
    for (left, permutation), depth, remain in auto.WalkTree((uniqueItems, []), NextPermutations,
                                                            yieldCondition=lambda c, d: d == n):
        count += 1
        print(count, permutation)
    
    1 ['a', 'b', 'c']
    2 ['a', 'c', 'b']
    3 ['b', 'a', 'c']
    4 ['b', 'c', 'a']
    5 ['c', 'a', 'b']
    6 ['c', 'b', 'a']
    

    可以看到已经顺利的计算出正确的结果。

    Bitmap位图对象的使用

    默认新建的图片为空白透明图片:

    width, height = 500, 500
    # 创建一张透明图片
    bitmap = auto.Bitmap(width, height)
    

    然后我们可以设置一点颜色,首先以逐像素遍历的方式操作:

    width, height = 500, 500
    # 创建一张透明图片
    bitmap = auto.Bitmap(width, height)
    start = auto.ProcessTime()
    for x in range(width):
        for y in range(height):
            color = ((x-width)**2+(y-height)**2)*255//(width**2+height**2)
            bitmap.SetPixelColor(x, y, 0xFF00FF | color << 24)
    cost = auto.ProcessTime() - start
    print(f'SetPixelColor 逐像素设置 {width}x{height} 图片的颜色耗时 {cost:.3f}s')
    bitmap.ToFile('tmp.png')
    
    SetPixelColor 逐像素设置 500x500 图片的颜色耗时 0.648s
    

    上述代码遍历每个像素点,通过SetPixelColor方法设置颜色。可以看到耗时达到0.6秒以上,能否快一点呢?

    SetPixelColorsOfRect方法可以直接设置整个区域的颜色:

    start = auto.ProcessTime()
    colors = []
    for x in range(width):
        for y in range(height):
            color = ((x-width)**2+(y-height)**2)*255//(width**2+height**2)
            colors.append(0xFF00FF | color << 24)
    bitmap.SetPixelColorsOfRect(0, 0, width, height, colors)
    cost = auto.ProcessTime() - start
    print(f'SetPixelColorsOfRect 设置 {width}x{height} 图片矩形区域的颜色,耗时 {cost:.3f}s')
    bitmap.ToFile('tmp.png')
    
    SetPixelColorsOfRect 设置 500x500 图片矩形区域的颜色,耗时 0.460s
    

    显然设置整个区域的颜色会更快一些。

    经测试使用GetPixelColor方法获取到的颜色值可能会因为负数补码导致获取到的值与当初设置的不一致。我们可以通过GetAllPixelColors方法获取原生数组后,然后计算偏移量获取颜色值:

    colors = bitmap.GetAllPixelColors()
    
    
    def getPixelColor(x, y):
        return colors[x*width+y]
        
    getPixelColor(10, 10)
    
    4110352639
    

    可以通过控件的ToBitmap方法对该控件截图获取Bitmap对象,传入参数可以决定截取的范围,例如我们截图桌面范围480*360范围内(不传参则获取整个控件)的图片:

    root = auto.GetRootControl()
    bitmap = root.ToBitmap(0, 0, 480, 360)
    bitmap.ToFile('tmp.png')
    Image("tmp.png")
    

    image-20211115214149137

    ToBitmap方法也可以使用Bitmap.FromControl方法替代:

    bitmap = auto.Bitmap.FromControl(control, x, y, width, height)
    

    裁切图片:

    with bitmap.Copy(150, 100, 80, 102) as subBitmap:
        subBitmap.ToFile('tmp.png')
        display(Image("tmp.png"))
    

    结果:成功裁切出 极速PDF阅读器 的图标。

    若需要同时裁切多个部分,可以使用GetPixelColorsOfRects方法:

    width, height = 75, 85
    rects = [((width*i, 0, width, height)) for i in range(3)]
    colors = bitmap.GetPixelColorsOfRects(rects)
    for nativeArray in colors:
        with auto.Bitmap(width, height) as subBitmap:
            subBitmap.SetPixelColorsOfRect(0, 0, width, height, nativeArray)
            subBitmap.ToFile('tmp.png')
            display(Image("tmp.png"))
    

    image-20211115215855714

    X轴翻转:

     with bitmap.RotateFlip(auto.RotateFlipType.RotateNoneFlipX) as bmp:
            bmp.ToFile('tmp.png')
            display(Image("tmp.png"))
    

    Y轴翻转:

    with bitmap.RotateFlip(auto.RotateFlipType.RotateNoneFlipY) as bmp:
        bmp.ToFile('tmp.png')
        display(Image("tmp.png"))
    

    90度旋转:

    with bitmap.Rotate(90) as bmp:
        bmp.ToFile('tmp.png')
        display(Image("tmp.png"))
    

    30度旋转(非90度整数倍 旋转)

    with bitmap.Rotate(30) as bmp:
        bmp.ToFile('tmp.png')
        display(Image("tmp.png"))
    

    image-20211115220338272

    对多个显示器分别截屏

    结合前面的方法,我们可以对桌面截屏,对鼠标下的控件截屏,对当前激活窗口截屏等等。

    基本都是在获取相应控件后调用如下方法:

    control.CaptureToImage(savePath)
    

    获取桌面控件前面已经演示,下面看看如何同时获取多个屏幕的截图:

    c = auto.GetRootControl()
    rects = auto.GetMonitorsRect()
    print(rects)
    for rect in rects:
        c.CaptureToImage('tmp.png', rect.left, rect.top,
                         rect.width(), rect.height())
        display(Image("tmp.png"))
    

    核心点就是通过GetMonitorsRect获取所有屏幕的坐标范围数组,截图时指定坐标范围即可。

    剪切板操作

    通常我们会使用pyperclip对文本进行复制粘贴,但实际上uiautomation所支持的剪切板操作会更加丰富,不仅支持纯文本,还支持富文本和图片。

    涉及文件的剪切板操作,个人已经在**《UI自动化控制微信发送文件》**一文中实现将文件设置到剪切板中。

    获取当前剪切板的内容格式:

    import uiautomation as auto
    
    formats = auto.GetClipboardFormats()
    print(formats)
    
    {49282: 'HTML Format', 49402: 'Rich Text Format', 13: 'CF_UNICODETEXT', 1: 'CF_TEXT', 49287: 'UniformResourceLocator', 50062: 'JAVA_DATAFLAVOR:application/x-java-jvm-local-objectref; class=c', 16: 'CF_LOCALE', 7: 'CF_OEMTEXT'}
    

    读取剪切板时,我们可以根据当前剪切板的格式分别作不同的处理:

    formats = auto.GetClipboardFormats()
    for k, v in formats.items():
        if k == auto.ClipboardFormat.CF_UNICODETEXT:
            print("文本格式:", auto.GetClipboardText())
        elif k == auto.ClipboardFormat.CF_HTML:
            htmlText = auto.GetClipboardHtml()
            print("富文本格式:", htmlText)
        elif k == auto.ClipboardFormat.CF_BITMAP:
            bmp = auto.GetClipboardBitmap()
            print("位图:", bmp)
    

    不过更关键的是设置数据到剪切板。

    设置文本到剪切板:

    auto.SetClipboardText('Hello World')
    

    设置富文本到剪切板:

    auto.SetClipboardHtml('<h1>Title</h1><br><h3>Hello</h3><br><p>test html</p><br>')
    

    设置图片到剪切板,只需要将Bitmap设置到剪切板即可,下面演示通过图片文件路径构造Bitmap并设置到剪切板:

    with auto.Bitmap.FromFile(path) as bmp:
        auto.SetClipboardBitmap(bmp)
    

    而根据文件路径设置文件到剪切板已通过win32clipboard实现,详见:**《UI自动化控制微信发送文件》**一文。

    自带的Logger日志输出类

    uiautomation自带了日志输出类,有时我们希望输出不仅到控制台,同时输出到文件时,可以直接使用uiautomation自带的方法。

    基础输出:

    auto.Logger.Write(
        log: Any,
        consoleColor: int = -1,
        writeToFile: bool = True,
        printToStdout: bool = True,
        logFile: str = None,
        printTruncateLen: int = 0,
    )
    
    • log: 要输出的日志内容
    • consoleColor: 文本在控制台输出的颜色
    • writeToFile: 是否输出到文件,默认为True
    • printToStdout: 是否输出到控制台,默认为True
    • logFile: 日志文件所在位置,默认为当前目录下的@AutomationLog.txt文件
    • printTruncateLen: 日志截断大小,每条输出超过长度限制时在控制台的输出会被截断。设置该值小于等于0时则不截断。

    对于第二个颜色参数,我们可以直接通过auto.ConsoleColor中的变量来获取对应常量,例如:

    auto.Logger.Write('测试输出', auto.ConsoleColor.Yellow)
    

    可以看一下支持的颜色列表:

    colors = [color for color in dir(
        auto.ConsoleColor) if not color.startswith("__")]
    print(colors)
    
    ['Black', 'Blue', 'Cyan', 'DarkBlue', 'DarkCyan', 'DarkGray', 'DarkGreen', 'DarkMagenta', 'DarkRed', 'DarkYellow', 'Default', 'Gray', 'Green', 'Magenta', 'Red', 'White', 'Yellow']
    

    auto.Logger.WriteLine函数与auto.Logger.Write几乎等价,只是少了printTruncateLen参数。

    可以使用auto.Logger.ColorfullyWrite方法对指定部分的文本修改颜色:

    auto.Logger.ColorfullyWrite('一段文本<Color=Red>红色</Color>,黑色<Color=Blue>蓝色</Color>结束')
    

    image-20211116230456997

    不过以上命令的颜色效果只在有标准控制台的窗口输出才有效。

    全局热键与多线程

    在常规的场景中,我们一般使用pynput实现全局热键的注册,但实际上pynput相对于uiautomation库的热键功能是比较难用。

    uiautomation注册全局热键非常简单,只需要调用auto.RunByHotKey传入快捷键和函数即可。下面演示一个简单的例子,按下快捷键分别打印文本:

    import threading
    from threading import Event
    
    import uiautomation as auto
    
    
    def demo1(stopEvent: Event):
        thread = threading.currentThread()
        print(thread.name, thread.ident, "demo1")
    
    
    def demo2(stopEvent: Event):
        thread = threading.currentThread()
        print(thread.name, thread.ident, "demo2")
    
    
    def demo3(stopEvent: Event):
        thread = threading.currentThread()
        print(thread.name, thread.ident, "demo3")
    
    
    if __name__ == '__main__':
        thread = threading.currentThread()
        print(thread.name, thread.ident, "main")
        auto.RunByHotKey({
            (0, auto.Keys.VK_F2): demo1,
            (auto.ModifierKey.Control, auto.Keys.VK_1): demo2,
            (auto.ModifierKey.Control | auto.ModifierKey.Shift, auto.Keys.VK_2): demo3,
        }, waitHotKeyReleased=False)
    

    以上代码分别注册了快捷键F2,Ctrl+1和Ctrl+Shift+2。下面测试运行该程序并分别按下这三个快捷键,最后按下Ctrl+D结束程序:

    MainThread 4404 main
    Register hotkey ('', 'VK_F2') successfully
    Register hotkey ('Control', 'VK_1') successfully
    Register hotkey ('Control|Shift', 'VK_2') successfully
    Register exit hotkey ('Control', 'VK_D') successfully
    ----------hotkey ('', 'VK_F2') pressed----------
    Thread-1 4608 demo1
    <Thread(Thread-1, started 4608)> for function demo1 exits, hotkey ('', 'VK_F2')
    ----------hotkey ('Control', 'VK_1') pressed----------
    Thread-2 12468 demo2
    <Thread(Thread-2, started 12468)> for function demo2 exits, hotkey ('Control', 'VK_1')
    ----------hotkey ('Control|Shift', 'VK_2') pressed----------
    Thread-3 5428 demo3
    <Thread(Thread-3, started 5428)> for function demo3 exits, hotkey ('Control|Shift', 'VK_2')
    Exit hotkey pressed. Exit
    

    由于每次按下热键都会启动独立的线程执行该函数,从打印日志可以看到每次按下执行函数的线程都不同。

    RunByHotKey的参数列表如下:

    RunByHotKey(keyFunctions: Dict[Tuple[int, int], (...) -> Any],
                    stopHotKey: Optional[Tuple[int, int]] = None,
                    exitHotKey: Tuple[int, int] = (ModifierKey.Control, Keys.VK_D),
                    waitHotKeyReleased: bool = True)
    

    其中exitHotKey表示程序退出的快捷键,默认为Ctrl+D。waitHotKeyReleased表示是否等待弹起后执行,经测试设置为False不等待更佳。

    stopHotKey表示产生退出事件的快捷键,当我们设置该参数并在运行中按下该快捷键,函数的参数stopEvent将会被设置,调用.is_set()将会返回True。

    我们可以在热键需要执行一个耗时较长的循环操作时,在循环中判断该事件是否被设置,设置就退出循环。

    演示一个简单的示例,按下Ctrl+S启动热键,每0.5秒打印一个数字直到10,按下Ctrl+E则中断热键执行。重新按下Ctrl+S还可以继续:

    import threading
    from threading import Event
    
    import uiautomation as auto
    
    
    def demo1(stopEvent: Event):
        for i in range(10):
            if stopEvent.is_set():
                print("退出循环")
                break
            print(i, end=" ")
            stopEvent.wait(0.5)
    
    
    if __name__ == '__main__':
        thread = threading.currentThread()
        print(thread.name, thread.ident, "main")
        auto.RunByHotKey({
            (0, auto.Keys.VK_S): demo1,
        }, stopHotKey=(0, auto.Keys.VK_E),
            waitHotKeyReleased=True)
    

    测试一下:

    Register hotkey ('Control', 'VK_S') successfully
    Register stop hotkey ('Control', 'VK_E') successfully
    Register exit hotkey ('Control', 'VK_D') successfully
    ----------hotkey ('Control', 'VK_S') pressed----------
    0 1 2 3 4 ----------stop hotkey pressed----------
    退出热键被按下,结束!
    <Thread(Thread-1, started 10016)> for function demo1 exits, hotkey ('Control', 'VK_S')
    Exit hotkey pressed. Exit
    

    需要注意在线程中需要使用控件对象相关方法时,要在新线程中进行相应的初始化:

    auto.InitializeUIAutomationInCurrentThread()
    ...
    auto.UninitializeUIAutomationInCurrentThread()
    

    也可以使用with语句简化代码:

    with auto.UIAutomationInitializerInThread():
    	pass
    

    ...pass替换为线程中实际操作的代码。

    ⚠️:否则部分场景下会报错。

    管理员提权

    模板代码为:

    if __name__ == '__main__':
        if auto.IsUserAnAdmin():
            main()
        else:
            print('RunScriptAsAdmin', sys.executable, sys.argv)
            auto.RunScriptAsAdmin(sys.argv)
    

    将原本存在于__main__代码块中的内容存放于main()方法中即可。

    通过实例学习UI自动化

    控制win10计算器自动计算

    自动控制计算器更简单的方法是复制粘贴需要计算的字符串到计算器,但本节的目的为了演示点击按钮计算的效果。

    首先启动计算器并设置窗口置顶:

    import uiautomation as auto
    import os
    import sys
    import time
    import subprocess
    
    # 显示搜索控件所遍历的控件数和搜索时间
    auto.uiautomation.DEBUG_SEARCH_TIME = True
    # 设置全局搜索超时时间为1秒
    auto.uiautomation.SetGlobalSearchTimeout(1)
    # 创建计算器窗口控件
    calcWindow = auto.WindowControl(
        searchDepth=1, ClassName='ApplicationFrameWindow', Compare = lambda c,d : c.Name == 'Calculator' or c.Name == '计算器', desc='计算器窗口')
    if not calcWindow.Exists(0, 0):
        subprocess.Popen('calc')
    # 设置窗口前置
    calcWindow.SetTopmost(True)
    
    2021-09-21 23:10:34.990 <ipython-input-5-ff74c0c97515>[17] <module> -> {ClassName: 'ApplicationFrameWindow', desc: '计算器窗口', ControlType: WindowControl} TraverseControls: 2, SearchTime: 1.004s[23:10:33.984967 - 23:10:34.990062]
    

    ⚠️ Compare参数可以传入自定义搜索函数,两个参数分别为控件对象和搜索深度。上述代码能够适配Name属性可能为中文或英文的情况。

    不过ClassName参数足够定位到计算器窗口,Compare参数可以直接删除。

    Desc是无效的属性,可以在debug日志中打印出来,当然任何无效属性都可以在debug日志中打印出来。

    calcWindow.Exists(0, 0)判断了计算器窗口是否已经存在,防止重复打开新的窗口。

    切换到科学计算器:

    # 调低waitTime可以加快点击速度
    calcWindow.ButtonControl(AutomationId='TogglePaneButton',
                             desc='打开导航').Click(waitTime=0.01)
    calcWindow.ListItemControl(AutomationId='Scientific',
                               desc='选择科学计算器').Click(waitTime=0.01)
    clearButton = calcWindow.ButtonControl(AutomationId='clearEntryButton',
                                           desc='点击CE清空所有输入')
    if clearButton.Exists(0, 0):
        clearButton.Click(waitTime=0)
    else:
        calcWindow.ButtonControl(AutomationId='clearButton',
                                 desc='点击C清空所有输入').Click(waitTime=0)
    
    2021-09-21 23:22:28.966 <ipython-input-10-a63d4ee11410>[3] <module> -> {AutomationId: 'TogglePaneButton', desc: '打开导航', ControlType: ButtonControl} TraverseControls: 121, SearchTime: 0.061s[23:22:28.906224 - 23:22:28.966385]
    2021-09-21 23:22:29.518 <ipython-input-10-a63d4ee11410>[5] <module> -> {AutomationId: 'Scientific', desc: '选择科学计算器', ControlType: ListItemControl} TraverseControls: 133, SearchTime: 0.077s[23:22:29.442885 - 23:22:29.518933]
    2021-09-21 23:22:29.876 <ipython-input-10-a63d4ee11410>[12] <module> -> {AutomationId: 'clearButton', desc: '点击C清空所有输入', ControlType: ButtonControl} TraverseControls: 51, SearchTime: 0.029s[23:22:29.846955 - 23:22:29.876364]
    

    ⚠️清空按钮在输入框有数和没有数时,按钮的标题和AutomationId不一样,所以先判断其中一种情况的按钮是否存在,不存在则切换到另一种形式的按钮进行点击。

    其中按钮的AutomationId属性可以通过inspect.exe工具或automation.py脚本获取。

    下面只演示四则运算,先通过inspect.exe工具获取每个按钮控件对应的AutomationId

    image-20210921235027029

    然后通过auto.WalkControl遍历所有控件,从而缓存每个字符对应的按钮控件:

    id2char = {
        'num0Button': '0',
        'num1Button': '1',
        'num2Button': '2',
        'num3Button': '3',
        'num4Button': '4',
        'num5Button': '5',
        'num6Button': '6',
        'num7Button': '7',
        'num8Button': '8',
        'num9Button': '9',
        'decimalSeparatorButton': '.',
        'plusButton': '+',
        'minusButton': '-',
        'multiplyButton': '*',
        'divideButton': '/',
        'equalButton': '=',
        'openParenthesisButton': '(',
        'closeParenthesisButton': ')'
    }
    char2Button = {}
    for c, d in auto.WalkControl(calcWindow, maxDepth=4):
        if c.AutomationId in id2char:
            char2Button[id2char[c.AutomationId]] = c
    

    然后封装一下对四则运算表达式计算的函数:

    def calc(expression):
        expression = ''.join(expression.split())
        if not expression.endswith('='):
            expression += '='
        for char in expression:
            char2Button[char].Click(waitTime=0)
        time.sleep(0.1)
        calcWindow.SendKeys('{Ctrl}c', waitTime=0.1)
        return auto.GetClipboardText()
    

    测试一个四则表达式:

    calc('1234 * (4 + 5 + 6) - 78 / 90.8')
    

    需要截屏的话可以调用如下命令:

    calcWindow.CaptureToImage('calc.png', x=7, y=0, width=-14, height=-7)
    

    ⚠️x和y表示起始坐标,由于win10的计算器存在7像素的阴影,所以可以去掉。

    width和height为负数时,表示原宽度减少指定值。垂直方向上只有下方有阴影,水平方向上左右均有阴影。

    关闭程序只需调用:

    calcWindow.GetWindowPattern().Close()
    

    完整代码:

    import uiautomation as auto
    import os
    import sys
    import time
    import subprocess
    
    # 显示搜索控件所遍历的控件数和搜索时间
    auto.uiautomation.DEBUG_SEARCH_TIME = True
    # 设置全局搜索超时时间为1秒
    auto.uiautomation.SetGlobalSearchTimeout(1)
    # 创建计算器窗口控件
    calcWindow = auto.WindowControl(
        searchDepth=1, ClassName='ApplicationFrameWindow', desc='计算器窗口')
    if not calcWindow.Exists(0, 0):
        subprocess.Popen('calc')
    # 设置窗口前置
    calcWindow.SetTopmost(True)
    # 调低waitTime可以加快点击速度
    calcWindow.ButtonControl(AutomationId='TogglePaneButton',
                             desc='打开导航').Click(waitTime=0.01)
    calcWindow.ListItemControl(AutomationId='Scientific',
                               desc='选择科学计算器').Click(waitTime=0.01)
    clearButton = calcWindow.ButtonControl(AutomationId='clearEntryButton',
                                           desc='点击CE清空所有输入')
    if clearButton.Exists(0, 0):
        clearButton.Click(waitTime=0)
    else:
        calcWindow.ButtonControl(AutomationId='clearButton',
                                 desc='点击C清空所有输入').Click(waitTime=0)
    id2char = {
        'num0Button': '0',
        'num1Button': '1',
        'num2Button': '2',
        'num3Button': '3',
        'num4Button': '4',
        'num5Button': '5',
        'num6Button': '6',
        'num7Button': '7',
        'num8Button': '8',
        'num9Button': '9',
        'decimalSeparatorButton': '.',
        'plusButton': '+',
        'minusButton': '-',
        'multiplyButton': '*',
        'divideButton': '/',
        'equalButton': '=',
        'openParenthesisButton': '(',
        'closeParenthesisButton': ')'
    }
    char2Button = {}
    for c, d in auto.WalkControl(calcWindow, maxDepth=4):
        if c.AutomationId in id2char:
            char2Button[id2char[c.AutomationId]] = c
    
    
    def calc(expression):
        expression = ''.join(expression.split())
        if not expression.endswith('='):
            expression += '='
        for char in expression:
            char2Button[char].Click(waitTime=0)
        time.sleep(0.1)
        calcWindow.SendKeys('{Ctrl}c', waitTime=0.1)
        return auto.GetClipboardText()
    
    
    result = calc('1234 * (4 + 5 + 6) - 78 / 90.8')
    print('1234 * (4 + 5 + 6) - 78 / 90.8 =', result)
    result = calc('3*3+4*4')
    print('3*3+4*4 =', result)
    result = calc('2*3.14159*10')
    print('2*3.14159*10 =', result)
    calcWindow.CaptureToImage('calc.png', x=7, y=0, width=-14, height=-7)
    calcWindow.GetWindowPattern().Close()
    

    结果:

    1234 * (4 + 5 + 6) - 78 / 90.8 = 18509.140969162995594713656387665
    3*3+4*4 = 25
    2*3.14159*10 = 62.8318
    

    产生的截图:

    image-20211111210023786

    运行过程中的原速动图:

    请添加图片描述

    窗口的拖拽与缩放

    下面我们使用一个记事本窗口作为前置窗口就行演示,首先打开记事本窗口:

    import sys
    import time
    import subprocess
    
    import uiautomation as auto
    # 设置全局搜索超时时间为1秒
    auto.uiautomation.SetGlobalSearchTimeout(1)
    
    note = auto.WindowControl(searchDepth=1, ClassName='Notepad')
    if not note.Exists(0, 0):
        subprocess.Popen('notepad.exe')
    note.SetActive()
    note.SetTopmost(waitTime=0)
    

    调整窗口位置和大小后,输入文本:

    # 移动窗口的位置,前两个参数表示左上角的位置,后两个参数编程窗口大小
    note.MoveWindow(300, 70, 480, 400)
    edit = note.EditControl()
    edit.SendKeys('{Ctrl}{End}{Enter 2}我是置顶窗口!!!\n我将遮住其他窗口.')
    

    打开计算器窗口并调整:

    # 打开计算器窗口并调整:
    calcWindow = auto.WindowControl(
        searchDepth=1, ClassName='ApplicationFrameWindow')
    if not calcWindow.Exists(0, 0):
        subprocess.Popen('calc')
    calcWindow.SetActive()
    calcWindow.MoveWindow(100, 100, 320, 500)
    

    image-20211112202543934

    再进行本例最精彩的部分,以拖拽方式移动窗口:

    # 鼠标从位置(240, 110)拖拽到(840, 110)
    auto.DragDrop(240, 110, 840, 110, waitTime=0.1)
    # 再拖拽回去
    auto.DragDrop(840, 110, 240, 110, waitTime=0.1)
    

    最后打印文本并关闭窗口:

    calcWindow.GetWindowPattern().Close()
    edit.SendKeys('{Ctrl}{End}{Enter 2}关闭计算器窗口')
    vp = edit.GetValuePattern()
    print('当前文本:', vp.Value)
    # 激活记事本窗口
    note.SetActive()
    note.GetWindowPattern().Close()
    # 确认不保存
    auto.SendKeys('{ALT}n')
    

    完整代码:

    import sys
    import time
    import subprocess
    
    import uiautomation as auto
    # 设置全局搜索超时时间为1秒
    auto.uiautomation.SetGlobalSearchTimeout(1)
    
    note = auto.WindowControl(searchDepth=1, ClassName='Notepad')
    if not note.Exists(0, 0):
        subprocess.Popen('notepad.exe')
    note.SetActive()
    note.SetTopmost(waitTime=0)
    
    # 移动窗口的位置,前两个参数表示左上角的位置,后两个参数编程窗口大小
    note.MoveWindow(300, 70, 480, 400)
    edit = note.EditControl()
    edit.SendKeys('{Ctrl}{End}{Enter 2}我是置顶窗口!!!\n我将遮住其他窗口.')
    # 打开计算器窗口并调整:
    calcWindow = auto.WindowControl(
        searchDepth=1, ClassName='ApplicationFrameWindow')
    if not calcWindow.Exists(0, 0):
        subprocess.Popen('calc')
    calcWindow.SetActive()
    calcWindow.MoveWindow(100, 100, 320, 500)
    # 鼠标从位置(240, 110)拖拽到(840, 110)
    auto.DragDrop(240, 110, 840, 110, waitTime=0.1)
    # 再拖拽回去
    auto.DragDrop(840, 110, 240, 110, waitTime=0.1)
    # 打印文本并关闭窗口:
    calcWindow.GetWindowPattern().Close()
    edit.SendKeys('{Ctrl}{End}{Enter 2}关闭计算器窗口')
    vp = edit.GetValuePattern()
    print('当前文本:', vp.Value)
    # 激活记事本窗口
    note.SetActive()
    note.GetWindowPattern().Close()
    # 确认不保存
    auto.SendKeys('{ALT}n')
    

    运行效果(图片使用了全局色键压缩,导致颜色失真):

    请添加图片描述

    管理员提权操作并读取设备管理器栏目数据

    由于必须要有管理员权限才能读取设备管理器,在没有管理员权限的控制台需要进行管理员身份提权。本案例涉及很多小知识点,下面先拆分来单独列出来。

    获取屏幕大小:

    auto.GetScreenSize()
    
    (1920, 1080)
    

    获取当前运行的程序的控制台并移动:

    sw, sh = auto.GetScreenSize()
    cmdWindow = auto.GetConsoleWindow()
    if cmdWindow:
        cmdTransformPattern = cmdWindow.GetTransformPattern()
        cmdTransformPattern.Move(sw // 2, 0)
        cmdTransformPattern.Resize(sw // 2, sh * 3 // 4)
    

    打开设置管理器并修改窗口大小和位置:

    subprocess.Popen('mmc.exe devmgmt.msc')
    mmcWindow = auto.WindowControl(searchDepth = 1, ClassName = 'MMCMainFrame')
    mmcTransformPattern = mmcWindow.GetTransformPattern()
    mmcTransformPattern.Move(0, 0)
    mmcTransformPattern.Resize(sw // 2, sh * 3 // 4)
    

    下面我们的目标是提取出设备管理器的每一项的信息:

    image-20211112221747877

    tree = mmcWindow.TreeControl()
    for item, depth in auto.WalkControl(tree, includeTop=False):
        if not isinstance(item, auto.TreeItemControl):
            continue
        item.GetSelectionItemPattern().Select(waitTime=0.01)
        pattern = item.GetExpandCollapsePattern()
        if pattern.ExpandCollapseState == auto.ExpandCollapseState.Collapsed:
            pattern.Expand(waitTime=0.01)
        print(' ' * (depth - 1) * 4, item.Name)
    

    部分结果:

    DESKTOP-IS8QJHF
         IDE ATA/ATAPI 控制器
             Intel(R) 300 Series Chipset Family SATA AHCI Controller
         便携设备
             Seagate1.81TB
    	 .....
         音频输入和输出
             24B1W1G5 (英特尔(R) 显示器音频)
             Realtek Digital Output (Realtek High Definition Audio)
             立体声混音 (Realtek High Definition Audio)
             扬声器 (Realtek High Definition Audio)
    

    上述代码执行后,滚动条已经到底底部,下面我们可以执行以下代码让滚动条回到顶部:

    treeScrollPattern = tree.GetScrollPattern()
    treeScrollPattern.SetScrollPercent(-1, 0)
    

    SetScrollPercent传入的两个参数表示将滚动条移动到指定百分比位置:

    • horizontalPercent: 横向位置百分比
    • verticalPercent: 纵向位置百分比

    传入-1表示不移动,由于没有横向滚动条,所以第一个参数传入了-1。

    当然也可以移动到底部:

    treeScrollPattern.SetScrollPercent(-1, 100)
    

    也可以使用鼠标滑轮实现:

    # 滚动条未到顶就使鼠标滑轮不停往上滑
    while treeScrollPattern.VerticalScrollPercent > 0:
        tree.WheelUp(waitTime=0.01)
    # 滚动条未到底就使鼠标滑轮不停往下滑
    while treeScrollPattern.VerticalScrollPercent < 100:
        tree.WheelDown(waitTime=0.01)
    

    上述两个操作也可以直接通过快捷键实现:

    tree.SendKeys('{Ctrl}{Home}', waitTime = 1)
    tree.SendKeys('{Ctrl}{End}', waitTime = 1)
    

    前面我们在获取tree控件时,设备管理器并没有滚动条,展开控件后产生的滚动条。此时必须更新控件信息:

    print(tree.NativeWindowHandle, tree.Element, len(tree.GetChildren()))
    tree.Refind()
    print(tree.NativeWindowHandle, tree.Element, len(tree.GetChildren()))
    
    329304 <POINTER(IUIAutomationElement) ptr=0x1df11091f00 at 1df1c7d14c8> 1
    329304 <POINTER(IUIAutomationElement) ptr=0x1df11092480 at 1df1d889148> 2
    

    可以看到再重新查找控件后,控件信息发生变化。

    下面我们直接拖拽滚动条来实现界面区域的变化,我们需要先知道windows的滚动条组件由三个按钮和一个缩略图组成,首先我们获取滚动条和其中的滑动块对象:

    vScrollBar = tree.ScrollBarControl(AutomationId='NonClientVerticalScrollBar')
    thumb = vScrollBar.ThumbControl()
    

    然后移动鼠标到滑动块中心位置并获取位置坐标:

    x, y = thumb.MoveCursorToMyCenter()
    print(x, y)
    
    941 663
    

    获取整个滚动条的坐标范围:

    vScrollBarRect = vScrollBar.BoundingRectangle
    print(vScrollBarRect, vScrollBarRect.top, vScrollBarRect.bottom)
    
    (933,86,950,778)[17x692] 86 778
    

    鼠标拖拽滑动条从底到顶:

    vScrollBar = tree.ScrollBarControl(AutomationId='NonClientVerticalScrollBar')
    thumb = vScrollBar.ThumbControl()
    x, y = thumb.MoveCursorToMyCenter()
    vScrollBarRect = vScrollBar.BoundingRectangle
    auto.DragDrop(x, y, x, vScrollBarRect.top, waitTime=1)
    

    从顶到底:

    x, y = thumb.MoveCursorToMyCenter()
    auto.DragDrop(x, y, x, vScrollBarRect.bottom, waitTime=1)
    

    下面我们将上述方法整理到一个脚本中,假如我们需要管理员提权可以使用如下命令:

    auto.RunScriptAsAdmin(sys.argv)
    

    实际使用过程中需要使用如下命令判断是否已经具备管理员避免无限循环:

    auto.IsUserAnAdmin()
    

    完整代码:

    """
    小小明的代码
    CSDN主页:https://blog.csdn.net/as604049322
    """
    __author__ = '小小明'
    __time__ = '2021/11/12'
    
    import subprocess
    import sys
    import time
    
    import uiautomation as auto
    
    
    def main():
        sw, sh = auto.GetScreenSize()
        cmdWindow = auto.GetConsoleWindow()
        if cmdWindow:
            cmdTransformPattern = cmdWindow.GetTransformPattern()
            cmdTransformPattern.Move(sw // 4, 0)
            cmdTransformPattern.Resize(sw // 4, sh * 3 // 5)
    
        subprocess.Popen('mmc.exe devmgmt.msc')
        mmcWindow = auto.WindowControl(searchDepth=1, ClassName='MMCMainFrame')
        mmcTransformPattern = mmcWindow.GetTransformPattern()
        mmcTransformPattern.Move(0, 0)
        mmcTransformPattern.Resize(sw // 4, sh * 3 // 5)
    
        tree = mmcWindow.TreeControl()
        for item, depth in auto.WalkControl(tree, includeTop=False):
            if not isinstance(item, auto.TreeItemControl):
                continue
            item.GetSelectionItemPattern().Select(waitTime=0.01)
            pattern = item.GetExpandCollapsePattern()
            if pattern.ExpandCollapseState == auto.ExpandCollapseState.Collapsed:
                pattern.Expand(waitTime=0.01)
            print(' ' * (depth - 1) * 4, item.Name)
    
        tree = mmcWindow.TreeControl()
        treeScrollPattern = tree.GetScrollPattern()
        print("ScrollPattern.SetScrollPercent(1, 0)")
        treeScrollPattern.SetScrollPercent(1, 0)
        print("ScrollPattern.SetScrollPercent(1, 100)")
        treeScrollPattern.SetScrollPercent(1, 100)
    
        treeScrollPattern = tree.GetScrollPattern()
        print("滚动条未到顶就使鼠标滑轮不停往上滑")
        # 滚动条未到顶就使鼠标滑轮不停往上滑
        while treeScrollPattern.VerticalScrollPercent > 0:
            tree.WheelUp(waitTime=0.01)
        print("滚动条未到底就使鼠标滑轮不停往下滑")
        # 滚动条未到底就使鼠标滑轮不停往下滑
        while treeScrollPattern.VerticalScrollPercent < 100:
            tree.WheelDown(waitTime=0.01)
        print("快捷键控制窗口滚动到顶")
        tree.SendKeys('{Ctrl}{Home}', waitTime=1)
        time.sleep(1)
        print("快捷键控制窗口滚动到底")
        tree.SendKeys('{Ctrl}{End}', waitTime=1)
    
        tree.Refind()
    
        vScrollBar = tree.ScrollBarControl(AutomationId='NonClientVerticalScrollBar')
        thumb = vScrollBar.ThumbControl()
        vScrollBarRect = vScrollBar.BoundingRectangle
        x, y = thumb.MoveCursorToMyCenter()
        print("拖拽滚动条向上移动...")
        auto.DragDrop(x, y, x, vScrollBarRect.top, waitTime=1)
        x, y = thumb.MoveCursorToMyCenter()
        print("拖拽滚动条向下移动...")
        auto.DragDrop(x, y, x, vScrollBarRect.bottom, waitTime=1)
        print("关闭设备管理器")
        mmcWindow.GetWindowPattern().Close()
        input("已经运行完毕,回车结束")
    
    
    if __name__ == '__main__':
        if auto.IsUserAnAdmin():
            main()
        else:
            print('RunScriptAsAdmin', sys.executable, sys.argv)
            auto.RunScriptAsAdmin(sys.argv)
    

    运行过程:

    请添加图片描述

    记事本文本输入与字体调整

    首先打开记事本窗口,调整大小激活并置顶:

    import uiautomation as auto
    import subprocess
    # 设置全局搜索超时时间为1秒
    auto.uiautomation.SetGlobalSearchTimeout(1)
    
    # 查找notepad
    note = auto.WindowControl(
        searchDepth=1, ClassName='Notepad', RegexName='.* - 记事本')
    if not auto.WaitForExist(note, 0.1):
        subprocess.Popen('notepad')
    sw, sh = auto.GetScreenSize()
    note.MoveWindow(sw // 10, sh // 10, sw // 2, sh // 2)
    note.SetActive()
    note.SetTopmost()
    

    下面我们测试一下文本输入:

    text = """Windows中GUI自动化的三种技术:Windows API, MSAA - Microsoft Active Accessibility, UIAutomation
    用脚本语言AutoIT实现自动化就是第一种技术Windows API, 查找窗口句柄实现的。
    用工具Spy++查看程序,如果Spy++能识别程序窗口中的控件就能用这种技术。
    python的UI自动化测试模块 pywinauto,也是用这种技术实现的(现在pywinauto也支持UIAutomation了)。 
    但Windows API实现的自动化不支持WPF程序、Windows 8中的Metro程序,因为它们的控件都是自绘出来的,没有句柄的概念。
    用UIAutomation实现的自动化支持微软提供的各种界面开发框架,如Win32, MFC, Windows Forms, WPF, Metro App, IE。
    Qt, Firefox, Chrome实现了UI Automation Provider,也支持UIAutomation.
    
    tip:
    Win10系统下应该使用64位Python运行本程序,32位进程无法正确获取一些控件的边界坐标。
    详见:https://github.com/microsoft/accessibility-insights-windows/issues/1122
    """
    edit = note.EditControl()
    edit.Click(waitTime=0)
    edit.GetValuePattern().SetValue('你好')
    edit.SendKeys('{Ctrl}{End}{Enter}下面开始演示{! 4}{ENTER}', 0.2, 0)
    edit.SendKeys(text)
    edit.SendKeys('{Enter 3}0123456789{Enter}', waitTime=0)
    edit.SendKeys('ABCDEFGHIJKLMNOPQRSTUVWXYZ{ENTER}', waitTime=0)
    edit.SendKeys('abcdefghijklmnopqrstuvwxyz{ENTER}', waitTime=0)
    edit.SendKeys('`~!@#$%^&*()-_=+{ENTER}', waitTime=0)
    edit.SendKeys('[]{{}{}}\\|;:\'\",<.>/?{ENTER}', waitTime=0)
    edit.SendKeys('™®①②③④⑤⑥⑦⑧⑨⑩§№☆★○●◎◇◆□℃‰€■△▲※→←↑↓〓¤°#&@\^_―♂♀')
    edit.SendKeys("{ENTER}{CTRL}a")
    note.CaptureToImage('Notepad.png')
    

    其中SendKeys的参数为:control.SendKeys(text: str, interval: float = 0.01,waitTime: float = 0.5, charMode: bool = True)

    interval参数决定了输入的间隔,修改这个参数为0.2减慢了输入的频率更像打字机的效果。

    最终产生的截图为:

    image-20211113143004093

    下面我们修改一下字体相关信息:

    # 查找菜单
    note.MenuItemControl(Name='格式(O)').Click()
    note.MenuItemControl(Name='字体(F)...').Click()
    windowFont = note.WindowControl(Name='字体')
    font_data = {"字体(F):": "微软雅黑", "字形(Y):": "常规", "大小(S):": "小四"}
    for k, v in font_data.items():
        listItem = windowFont.ListControl(
            searchDepth=2, AutomationId='1000', Name=k).ListItemControl(Name=v)
        if listItem.Exists(1):
            listItem.GetScrollItemPattern().ScrollIntoView()
            listItem.Click()
    

    其中ScrollIntoView()方法滚动目标到可见区域。

    可以通过以下方面打印字体列表中的所有可选字体:

    font_ListControl = windowFont.ListControl(
        searchDepth=2, AutomationId='1000', Name="字体(F):")
    for item, depth in auto.WalkControl(font_ListControl, includeTop=True, maxDepth=2):
        if depth != 1 or item.ControlType != auto.ControlType.ListItemControl:
            continue
        print(item.Name)
    
    Agency FB
    Algerian
    Arial
    ...
    微软雅黑
    新宋体
    幼圆
    

    对于需要展开的项目则需要先展开下拉列表,例如打印所有的脚本项:

    windowFont.SetActive()
    combo = windowFont.ComboBoxControl(AutomationId='1140')
    pattern = combo.GetExpandCollapsePattern()
    if pattern.ExpandCollapseState == auto.ExpandCollapseState.Collapsed:
        pattern.Expand(waitTime=0)
    for item, depth in auto.WalkControl(combo, includeTop=True, maxDepth=2):
        if depth != 2 or item.ControlType != auto.ControlType.ListItemControl:
            continue
        print(item.Name)
    
    中文 GB2312
    西欧语言
    希腊语
    土耳其语
    中欧字符
    西里尔文
    

    我们选择中文类型的脚本:

    combo.Select('中文 GB2312')
    

    最后点击确定按钮:

    windowFont.ButtonControl(Name='确定').Click()
    

    完整代码:

    import uiautomation as auto
    import subprocess
    # 设置全局搜索超时时间为1秒
    auto.uiautomation.SetGlobalSearchTimeout(1)
    
    # 查找notepad
    note = auto.WindowControl(
        searchDepth=1, ClassName='Notepad', RegexName='.* - 记事本')
    if not auto.WaitForExist(note, 0.1):
        subprocess.Popen('notepad')
    sw, sh = auto.GetScreenSize()
    note.MoveWindow(sw // 10, sh // 10, sw // 2, sh // 2)
    note.SetActive()
    note.SetTopmost()
    text = """Windows中GUI自动化的三种技术:Windows API, MSAA - Microsoft Active Accessibility, UIAutomation
    用脚本语言AutoIT实现自动化就是第一种技术Windows API, 查找窗口句柄实现的。
    用工具Spy++查看程序,如果Spy++能识别程序窗口中的控件就能用这种技术。
    python的UI自动化测试模块 pywinauto,也是用这种技术实现的(现在pywinauto也支持UIAutomation了)。 
    但Windows API实现的自动化不支持WPF程序、Windows 8中的Metro程序,因为它们的控件都是自绘出来的,没有句柄的概念。
    用UIAutomation实现的自动化支持微软提供的各种界面开发框架,如Win32, MFC, Windows Forms, WPF, Metro App, IE。
    Qt, Firefox, Chrome实现了UI Automation Provider,也支持UIAutomation.
    
    tip:
    Win10系统下应该使用64位Python运行本程序,32位进程无法正确获取一些控件的边界坐标。
    详见:https://github.com/microsoft/accessibility-insights-windows/issues/1122
    """
    edit = note.EditControl()
    edit.Click(waitTime=0)
    edit.GetValuePattern().SetValue('你好')
    edit.SendKeys('{Ctrl}{End}{Enter}下面开始演示{! 4}{ENTER}', 0.2, 0)
    edit.SendKeys(text)
    edit.SendKeys('{Enter 3}0123456789{Enter}', waitTime=0)
    edit.SendKeys('ABCDEFGHIJKLMNOPQRSTUVWXYZ{ENTER}', waitTime=0)
    edit.SendKeys('abcdefghijklmnopqrstuvwxyz{ENTER}', waitTime=0)
    edit.SendKeys('`~!@#$%^&*()-_=+{ENTER}', waitTime=0)
    edit.SendKeys('[]{{}{}}\\|;:\'\",<.>/?{ENTER}', waitTime=0)
    edit.SendKeys('™®①②③④⑤⑥⑦⑧⑨⑩§№☆★○●◎◇◆□℃‰€■△▲※→←↑↓〓¤°#&@\^_―♂♀')
    edit.SendKeys("{ENTER}{CTRL}a")
    note.CaptureToImage('Notepad.png')
    # 查找菜单
    note.MenuItemControl(Name='格式(O)').Click()
    note.MenuItemControl(Name='字体(F)...').Click()
    windowFont = note.WindowControl(Name='字体')
    font_data = {"字体(F):": "微软雅黑", "字形(Y):": "常规", "大小(S):": "小四"}
    for k, v in font_data.items():
        listItem = windowFont.ListControl(
            searchDepth=2, AutomationId='1000', Name=k).ListItemControl(Name=v)
        if listItem.Exists(1):
            listItem.GetScrollItemPattern().ScrollIntoView()
            listItem.Click()
    windowFont.SetActive()
    combo = windowFont.ComboBoxControl(AutomationId='1140')
    combo.Select('中文 GB2312')
    windowFont.ButtonControl(Name='确定').Click()
    note.SetTopmost(False)
    subprocess.Popen('Notepad.png', shell=True)
    

    执行结果:

    请添加图片描述

    wireshark抓包数据读取

    下面我们演示如何保存wireshark抓包到的数据。首先我们需要自行打开wireshark程序后进行抓包。抓包几秒钟后有了如下结果:

    image-20211113172346882

    下面我们的目标是保存这个表格的数据。

    首先我们需要获取wireshark的窗口对象,经过inspect工具的检查,ClassName和AutomationId均为MainWindow:

    image-20211113170212787

    这个对象的Name属性与窗口标题一致会根据状态不断变化,所以不能作为一个查找项,正常情况在能够保证开的窗口很少的情况下,我们可以通过如下简单的代码获取到wireshark的窗口对象:

    win = auto.WindowControl(
        searchDepth=1, ClassName='MainWindow', AutomationId='MainWindow')
    

    但是ClassName值为MainWindow的窗口实在太多了,我们无法确认时就需要其他方法来查找窗口,否则可能找到的窗口并不是wireshark。

    但是wireshark有个过滤选择器工具栏的AutomationId为"MainWindow.displayFilterToolBar":

    image-20211113171242405

    我们可以以此为唯一标识找出wireshark窗口对象。

    思路:从桌面根对象下面开始遍历每个窗口对象,判断它是否存在AutomationId为"MainWindow.displayFilterToolBar"的工具栏,找到即可返回,具体编码:

    import uiautomation as auto
    import time
    # 设置全局搜索超时时间为1秒
    auto.uiautomation.SetGlobalSearchTimeout(1)
    
    def find_wiresharkWindow():
        for win in auto.GetRootControl().GetChildren():
            if win.ClassName != 'MainWindow' or win.AutomationId != 'MainWindow':
                continue
            if win.ToolBarControl(AutomationId='MainWindow.displayFilterToolBar').Exists(0, 0):
                return win
    

    然后我们在获取到窗口后,调整一下窗口位置:

    wiresharkWindow = find_wiresharkWindow()
    sw, sh = auto.GetScreenSize()
    wiresharkWindow.MoveWindow(10, 10, sw // 2, sh * 3 // 5)
    wiresharkWindow.SetActive(waitTime=0.1)
    wiresharkWindow.SetTopmost()
    

    然后可以开始遍历树控件获取数据:

    print("按下F12可中止程序遍历")
    tree = wiresharkWindow.TreeControl(
        searchDepth=4, ClassName='PacketList', Name='Packet list')
    rect = tree.BoundingRectangle
    tree.Click(y=50, waitTime=0.1)
    auto.SendKeys('{Home}', waitTime=0.1)
    treeItemCount = 0
    columnCount = 0
    for item, depth in auto.WalkControl(tree):
        if isinstance(item, auto.HeaderControl):
            print(item.Name, end=" ")
            columnCount += 1
        elif isinstance(item, auto.TreeItemControl):
            if treeItemCount % columnCount == 0:
                print()
            treeItemCount += 1
            print(item.Name, end=" ")
            if item.BoundingRectangle.bottom >= rect.bottom:
                auto.SendKeys('{PageDown}', waitTime=0.1)
        if auto.IsKeyPressed(auto.Keys.VK_F12):
            print("F12已被按下,停止采集")
            break
    

    为了避免程序持续无休止的运行,增加快捷键判断,循环中发现某个快捷键被按下则退出循环。可以看到打印结果很完美:

    image-20211113173852600

    下面我们重新组织一下代码,使结果最终能保存为pandas的datafream对象,完整代码如下:

    import pandas as pd
    import uiautomation as auto
    
    # 设置全局搜索超时时间为1秒
    auto.uiautomation.SetGlobalSearchTimeout(1)
    
    
    def find_wiresharkWindow():
        for win in auto.GetRootControl().GetChildren():
            if win.ClassName != 'MainWindow' or win.AutomationId != 'MainWindow':
                continue
            if win.ToolBarControl(AutomationId='MainWindow.displayFilterToolBar').Exists(0, 0):
                return win
    
    
    wiresharkWindow = find_wiresharkWindow()
    sw, sh = auto.GetScreenSize()
    wiresharkWindow.MoveWindow(10, 10, sw // 2, sh * 3 // 5)
    wiresharkWindow.SetActive(waitTime=0.1)
    wiresharkWindow.SetTopmost()
    
    print("按下F12可中止程序遍历")
    tree = wiresharkWindow.TreeControl(
        searchDepth=4, ClassName='PacketList', Name='Packet list')
    rect = tree.BoundingRectangle
    tree.Click(y=50, waitTime=0.1)
    auto.SendKeys('{Home}', waitTime=0.1)
    treeItemCount = 0
    columnCount = 0
    header = []
    data, row = [], []
    for item, depth in auto.WalkControl(tree):
        if isinstance(item, auto.HeaderControl):
            header.append(item.Name)
            columnCount += 1
        elif isinstance(item, auto.TreeItemControl):
            if row and treeItemCount % columnCount == 0:
                data.append(row)
                row = []
            treeItemCount += 1
            row.append(item.Name)
            if item.BoundingRectangle.bottom >= rect.bottom:
                auto.SendKeys('{PageDown}', waitTime=0.1)
        if auto.IsKeyPressed(auto.Keys.VK_F12):
            print("F12已被按下,停止采集")
            break
    wiresharkWindow.SetTopmost(False)
    if row:
        data.append(row)
    df = pd.DataFrame(data, columns=header)
    df
    

    结果:

    image-20211113180144045

    PDF目录折叠展开提取器

    本例计划开发一个针对树形控件的折叠展开工具,计划开发三个热键功能:

    • 递归展开某树形控件
    • 递归折叠某树形控件
    • 递归提取整棵树的文本

    当我们按某个快捷键时,则会自动定位当前鼠标下的控件,是树形控件时就会进行对应的操作。针对递归提取整棵树的文本,鼠标放到一个树形控件的任意位置,都会自动定位当前树的祖先控件。

    下面针对这三个功能注册热键为Ctrl+1,Ctrl+2,Ctrl+3。最终完整代码为:

    import uiautomation as auto
    
    
    def ExpandTreeItem(stopEvent):
        with auto.UIAutomationInitializerInThread():
            tree = auto.ControlFromCursor()
            if tree.ControlType not in [auto.ControlType.TreeControl, auto.ControlType.TreeItemControl]:
                return
            for item, depth in auto.WalkControl(tree, includeTop=True):
                if not isinstance(item, auto.TreeItemControl):
                    continue
                ecpt = item.GetExpandCollapsePattern()
                if ecpt and ecpt.ExpandCollapseState == auto.ExpandCollapseState.Collapsed:
                    sipt = item.GetScrollItemPattern()
                    if sipt:
                        sipt.ScrollIntoView(waitTime=0.01)
                    ecpt.Expand(waitTime=0.01)
            sipt = tree.GetScrollItemPattern()
            if sipt:
                sipt.ScrollIntoView(waitTime=0.01)
    
    
    def copy_pdf_catalog(stopEvent):
        result = []
        with auto.UIAutomationInitializerInThread():
            tree = auto.ControlFromCursor()
            if tree.ControlType not in [auto.ControlType.TreeControl, auto.ControlType.TreeItemControl]:
                return
            tree = tree.GetAncestorControl(lambda c, d: isinstance(c, auto.TreeControl))
            for item, depth in auto.WalkControl(tree, includeTop=True):
                if not isinstance(item, auto.TreeItemControl):
                    continue
                name = item.Name
                if name:
                    print("\t" * (depth - 1) + name)
                    result.append("\t" * (depth - 1) + name)
                ecpt = item.GetExpandCollapsePattern()
                if ecpt and ecpt.ExpandCollapseState == auto.ExpandCollapseState.Collapsed:
                    sipt = item.GetScrollItemPattern()
                    if sipt:
                        sipt.ScrollIntoView(waitTime=0.01)
                    ecpt.Expand(waitTime=0.01)
        text = "\n".join(result)
        auto.SetClipboardText(text)
    
    
    def CollapseTreeItem_recursion(treeItem: auto.TreeItemControl):
        if not isinstance(treeItem, auto.TreeItemControl):
            return
        children = treeItem.GetChildren()
        if not children:
            return
        for it in children:
            CollapseTreeItem_recursion(it)
        ecpt = treeItem.GetExpandCollapsePattern()
        if ecpt and ecpt.ExpandCollapseState == auto.ExpandCollapseState.Expanded:
            ecpt.Collapse(waitTime=0.05)
    
    
    def collapseTreeItem(stopEvent):
        with auto.UIAutomationInitializerInThread():
            treeItem = auto.ControlFromCursor()
            CollapseTreeItem_recursion(treeItem)
    
    
    if __name__ == '__main__':
        auto.RunByHotKey({
            (auto.ModifierKey.Control, auto.Keys.VK_1): collapseTreeItem,
            (auto.ModifierKey.Control, auto.Keys.VK_2): ExpandTreeItem,
            (auto.ModifierKey.Control, auto.Keys.VK_3): copy_pdf_catalog,
        }, waitHotKeyReleased=False)
    

    经测试对极速PDF的目录完全有效,我们可以用来递归折叠或展开PDF的目录,或者直接从UI提取目录的内容。

    运行后程序显示:

    Register hotkey ('Control', 'VK_1') successfully
    Register hotkey ('Control', 'VK_2') successfully
    Register hotkey ('Control', 'VK_3') successfully
    Register exit hotkey ('Control', 'VK_D') successfully
    

    先按下Ctrl+1测试一下折叠功能(录屏软件挡住时无法获取到鼠标下的控件,所以用手机录):

    请添加图片描述

    按下Ctrl+2则非常顺利的进行了相应的反向操作,大家可以自己感受一下。

    按下Ctrl+3,执行完毕后,我将剪切板中的内容粘贴到记事本:

    image-20211118004639754

    可以看到已经完美提取出PDF的目录,该复制到剪切板中的目录还可以直接粘贴到思维导图中。

    展开全文
  • 大家想到使用Java这门编程语言大多是脑海中是浮现的B/S开发,高并发、云计算、大数据等等,今天给大家带来的是Java不一样的使用方式——使用Java做PC桌面程序开发。 不同的是,之前Java是使用swing或者awt开发,但是...
  • 用python进行桌面程序开发

    万次阅读 2019-05-30 15:02:31
    Python是一种面向对象、直译式计算机程序设计语言...Python适用在各种程序的开发中,目前最常用在Web程序,移动程序以及桌面程序开发中。 但是遗憾的是,python官方只提供了IDLE一个IDE,功能上却是是不敢恭维。市面...
  • 使用Python开发windows桌面程序

    万次阅读 2018-05-08 11:00:00
    使用Python开发windows桌面程序一、开发前期准备 1. boa-constructor-0.6.1.bin.setup.exe #一个wxWidges的集成开发环境,简单如Delphi,可以直接拖拽控件,并且和其他集成环境不一样, #它不与集成开发环境的...
  • C#跨平台开发桌面程序(Avalonia)

    千次阅读 热门讨论 2020-05-25 19:10:45
    文章目录 序 研究感受 开发准备 新建Avalonia工程 开发简单示例程序 MessageBox Windows运行效果演示 Linux运行效果 生成Ubuntu 18.04版本 运行效果 神奇的BUG 总结 序 这两天一直在研究跨平台桌面程序开发,我玩过...
  • 学生。 自学了c,c++基础知识以及数据结构,计算机组成原理。...想用vs创建一个完整的应用程序,就像平时在电脑上下载那些软件一样,当然了功能先不论,但完全不知道怎么入手。 现在还应该学习什么,需要有什么书籍?
  • VS2019打包C#桌面程序

    千次阅读 2020-09-14 17:02:58
    创建桌面快捷方式 右键自己的主输出程序,选择Create Shortcut to 主输出 from… 生成桌面快捷方式 把TestDemo拖入User’s Desktop中即可自动生成桌面快捷方式 5.生成Setup1 生成成功后打开文件所在位置,产生了两个...
  • HTML如何编写为桌面程序

    千次阅读 2019-09-23 16:47:23
    学过/用过HTML的人应该都知道HTML是标记语言,是在网页上执行/使用的,在这里小编告诉你HTML也可以用来做桌面程序,这种桌面程序一般是微客户端 工具/原料 html dreamweaver 方法...
  • python能写桌面程序吗?

    千次阅读 2020-03-10 17:12:56
    其实Python/Java/PHP都不适合用来做桌面开发,Java还是有几个比较成熟的产品...尽管Python本身不擅长做桌面开发,可是Python跟C++融合得非常好,而C++非常擅长做桌面开发,Python的强大正是因为Python和C++融合得非...
  • 如何设计一个远程桌面程序

    千次阅读 2020-06-16 14:30:09
    今天和大家聊一聊如何实现一个远程桌面程序。 关于远程桌面 对于远程桌面程序,相信大家一定有所了解。 常见的产品有TeamViewer,向日葵,微软远程桌面,QQ远程桌面等等。 远程桌面能够在定位用户问题,协助操作,...
  • Electron前端开发桌面程序--入门篇

    万次阅读 2018-06-15 13:31:24
    前言前端开发桌面程序这个概念已经出现有一段时间了,这项技术也已经走向成熟,Github上nw和光electron的star就差不多有10w颗星了,github也衍生出了很多开源的桌面项目俨然成了一个热门项目。既然这么热,那就一个...
  • python编写windows桌面程序(小Demo)

    万次阅读 2016-12-22 17:07:47
    前段时间,由于公司业务需求,需要导出腾讯云中的对象存储服务bucket下指定文件夹所有apk文件的下载地址和一些文件夹创建的操作,但因文件较多,人工去遍历肯定不可取,因此就想着撸个python桌面程序。功能比较简单...
  • centos7 桌面程序自动启动

    千次阅读 2019-04-27 12:50:08
    一、把自己开发的程序 (我这是Qt开发的桌面程序)增加桌面快捷菜单 直接上代码 # cat /usr/share/applications/dskclient.desktop [Desktop Entry] Version=1.0 Name=DesktopClient Icon=network-workgroup Exec=...
  • 使用Python开发windows桌面程序【超简单】

    万次阅读 多人点赞 2019-06-29 22:00:06
    一个wxWidges的集成开发环境,简单如Delphi,可以直接拖拽控件,并且和其他集成环境不一样, #它不与集成开发环境的MainLoop冲突,用pythonwin,pyScripter都会冲突,典型报错就是运行第二次 #程序的时候,直接导致...
  • SWT桌面程序开发实例图解教程

    热门讨论 2012-05-18 19:12:56
    SWT桌面程序开发实例图解教程 包括桌面程序开发所需的IDE工具
  • 如何在网页中打开一个本地桌面程序转载 2015年11月26日 20:30:141473通过网页链接打开本地本地程序,想到最多的方法就是通过activex控件,但这里介绍一个通过注册新协议来打开本地程序的方法。参考网上的对QQ的分析...
  • 用Java编写一个最简单的桌面程序

    万次阅读 多人点赞 2018-03-03 13:42:30
    使用Java的优势在于网络应用方面,但Java也提供了强大的用于开发桌面程序的API,它们包含在javax.swing包中.使用这个包可以编写简单的Java桌面应用程序.Java的javax.swing包提供了许多用来设计GUI的组件类,在学习GUI...
  • web调用windows桌面程序

    千次阅读 2018-08-28 11:43:59
    目的:使用electron创建一个应用程序编译之后,得到如下目录,包含一个可执行文件,现在我们要通过浏览器唤起这个可执行文件 在任意位置创建一个.reg结束的文件,内容如下 Windows Registry Editor Version 5.00...
  • 介绍 如果您正在为智能手机开发应用程序,可能您已经听说过Google的Flutter这...其实,Flutter不仅限于移动端的跨平台(Android、iOS),还可以为桌面环境(Windows,macOS和Linux)构建应用程序,这点在今年的Goole...
  • DELPHI开发LINUX桌面程序

    千次阅读 2019-01-08 06:47:54
    DELPHI开发LINUX桌面程序 DELPHI官方目前为止尚不能开发LINUX桌面程序。 但三方控件FmxLinux(商业控件)是可以的。网上有破解版本。
  • 本节课会介绍Python的GUI(图形用户界面)编程,用Python也可以写出漂亮的桌面程序,建议此次课程大家稍作了解不要浪费太多时间,因为没有哪家公司会招聘以为Python程序员开发桌面程序吧? 学完此次课程,我能做...
  • 简介我们在网络上总会遇到一些惊奇的网站今天的办法就是直接用这个网站生成一个可以在桌面运行的EXE文件以后如果想要访问,直接点击这个文件即可目前 Chrome 浏览器就支持把网站生成桌面应用...
  • 今天想用 .net core做个小程序。却意外的发现没有可视化编辑器!!!(重要的事情用三个感叹号) 虽然我搞了十多年的 .net 开发,但是我确实从来没有自己编辑过 Designer.cs 这个文件。微软不是不让我编辑么! /// ...
  • Ubuntu设置桌面程序自动启动方法前言起源方法聚体步骤 二选一命令行执行 (下面目录直接放到终端执行即可)图形界面操作至此完成 前言 一个优秀的程序员不然是需要一款良好的操作系统支持的,做码农的时间里厌烦了...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 458,537
精华内容 183,414
关键字:

桌面程序