From b66bac7ca8f00c61f1423d5de300888847d3ed7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=A1=BA=E4=B8=9C?= <577732344@qq.com> Date: Wed, 11 Feb 2026 14:49:38 +0800 Subject: [PATCH] feat: Initialize wxauto WeChat automation project with job extraction tools - Add wxauto package with WeChat UI automation and message handling capabilities - Implement job_extractor.py for automated job posting extraction from WeChat groups - Add job_extractor_gui.py providing graphical interface for job extraction tool - Create comprehensive documentation in Chinese covering GUI usage, multi-group support, and quick start guides - Add build configuration files (build_exe.py, build_exe.spec) for packaging as standalone executable - Include utility scripts for WeChat interaction (auto_send_msg.py, get_history.py, receive_file_transfer.py) - Add project configuration files (pyproject.toml, setup.cfg, requirements.txt) - Include test files (test_api.py, test_com_fix.py) for API and compatibility validation - Add Apache 2.0 LICENSE and comprehensive README documentation - Configure .gitignore to exclude build artifacts, logs, and temporary files --- .gitignore | 10 + GUI版本使用说明.md | 177 + LICENSE | 201 + README.md | 123 + README_岗位提取工具.md | 0 README_打包修复.md | 169 + auto_send_msg.py | 72 + build_exe.py | 80 + build_exe.spec | 85 + config.json | 9 + get_current_user.py | 31 + get_history.py | 69 + job_extractor.py | 223 + job_extractor_gui.py | 622 +++ jobs_data.json | 19 + pyproject.toml | 24 + receive_file_transfer.py | 89 + requirements.txt | 4 + setup.cfg | 22 + test_api.py | 141 + test_com_fix.py | 114 + view_jobs.py | 116 + wxauto/__init__.py | 16 + wxauto/exceptions.py | 5 + wxauto/languages.py | 294 ++ wxauto/logger.py | 128 + wxauto/msgs/__init__.py | 75 + wxauto/msgs/attr.py | 85 + wxauto/msgs/base.py | 185 + wxauto/msgs/friend.py | 73 + wxauto/msgs/msg.py | 133 + wxauto/msgs/self.py | 67 + wxauto/msgs/type.py | 222 + wxauto/param.py | 65 + wxauto/ui/__init__.py | 8 + wxauto/ui/base.py | 56 + wxauto/ui/chatbox.py | 455 ++ wxauto/ui/component.py | 407 ++ wxauto/ui/main.py | 265 ++ wxauto/ui/navigationbox.py | 55 + wxauto/ui/sessionbox.py | 265 ++ wxauto/uiautomation.py | 8142 ++++++++++++++++++++++++++++++++++++ wxauto/utils/__init__.py | 2 + wxauto/utils/tools.py | 102 + wxauto/utils/win32.py | 302 ++ wxauto/wx.py | 386 ++ 多群组版本使用说明.md | 295 ++ 岗位提取使用说明.md | 111 + 快速修复指南.txt | 96 + 快速开始.md | 198 + 打包问题修复说明.md | 159 + 项目说明.md | 266 ++ 52 files changed, 15318 insertions(+) create mode 100644 .gitignore create mode 100644 GUI版本使用说明.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README_岗位提取工具.md create mode 100644 README_打包修复.md create mode 100644 auto_send_msg.py create mode 100644 build_exe.py create mode 100644 build_exe.spec create mode 100644 config.json create mode 100644 get_current_user.py create mode 100644 get_history.py create mode 100644 job_extractor.py create mode 100644 job_extractor_gui.py create mode 100644 jobs_data.json create mode 100644 pyproject.toml create mode 100644 receive_file_transfer.py create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 test_api.py create mode 100644 test_com_fix.py create mode 100644 view_jobs.py create mode 100644 wxauto/__init__.py create mode 100644 wxauto/exceptions.py create mode 100644 wxauto/languages.py create mode 100644 wxauto/logger.py create mode 100644 wxauto/msgs/__init__.py create mode 100644 wxauto/msgs/attr.py create mode 100644 wxauto/msgs/base.py create mode 100644 wxauto/msgs/friend.py create mode 100644 wxauto/msgs/msg.py create mode 100644 wxauto/msgs/self.py create mode 100644 wxauto/msgs/type.py create mode 100644 wxauto/param.py create mode 100644 wxauto/ui/__init__.py create mode 100644 wxauto/ui/base.py create mode 100644 wxauto/ui/chatbox.py create mode 100644 wxauto/ui/component.py create mode 100644 wxauto/ui/main.py create mode 100644 wxauto/ui/navigationbox.py create mode 100644 wxauto/ui/sessionbox.py create mode 100644 wxauto/uiautomation.py create mode 100644 wxauto/utils/__init__.py create mode 100644 wxauto/utils/tools.py create mode 100644 wxauto/utils/win32.py create mode 100644 wxauto/wx.py create mode 100644 多群组版本使用说明.md create mode 100644 岗位提取使用说明.md create mode 100644 快速修复指南.txt create mode 100644 快速开始.md create mode 100644 打包问题修复说明.md create mode 100644 项目说明.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6c92ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +wxauto_logs +wxauto文件下载 +dist/ +build/ +*.egg-info/ +msgs.txt +__pycache__/ +*.ipynb +*.bat +@AutomationLog.txt \ No newline at end of file diff --git a/GUI版本使用说明.md b/GUI版本使用说明.md new file mode 100644 index 0000000..ade3bdc --- /dev/null +++ b/GUI版本使用说明.md @@ -0,0 +1,177 @@ +# 微信群岗位信息提取工具 - GUI版本使用说明 + +## 功能特点 + +- 图形化界面,操作简单直观 +- 实时监听微信群消息 +- 自动提取岗位结构化信息 +- 岗位列表实时显示 +- 支持查看岗位详情 +- 数据自动保存和导出 + +## 打包成EXE + +### 方法一:使用批处理脚本(推荐) + +1. 确保已安装Python和依赖: +```bash +pip install -e . +pip install requests pyinstaller +``` + +2. 双击运行 `build.bat` 文件 + +3. 等待打包完成,生成的exe文件在 `dist` 目录 + +### 方法二:使用Python脚本 + +```bash +python build_exe.py +``` + +### 方法三:手动打包 + +```bash +pyinstaller build_exe.spec +``` + +## 使用步骤 + +### 1. 启动程序 + +- 双击 `微信岗位提取工具.exe` 启动程序 +- 或者直接运行 `python job_extractor_gui.py` + +### 2. 配置参数 + +在程序界面顶部配置区域: + +- **目标群组**:输入要监听的微信群名称(必须完全匹配) +- **API密钥**:输入阿里云百炼API密钥 +- 点击"保存配置"按钮保存设置 + +### 3. 开始任务 + +1. 确保微信已登录并打开主窗口 +2. 点击"开始任务"按钮 +3. 程序会自动连接微信并开始监听指定群组 +4. 状态栏显示"运行中"表示监听成功 + +### 4. 查看岗位信息 + +- 提取到的岗位会实时显示在列表中 +- 双击任意岗位可查看详细信息 +- 包括:岗位名称、公司、地点、薪资、联系方式、原始消息等 + +### 5. 数据管理 + +- **停止任务**:停止监听微信群 +- **清空列表**:清空所有岗位数据(会删除数据文件) +- **导出数据**:导出当前所有岗位数据为JSON文件 + +### 6. 运行日志 + +程序底部的日志区域会显示: +- 连接状态 +- 消息接收情况 +- 岗位提取结果 +- 错误信息等 + +## 界面说明 + +``` +┌─────────────────────────────────────────────┐ +│ 配置区域 │ +│ - 目标群组: [输入框] │ +│ - API密钥: [输入框] [保存配置] │ +├─────────────────────────────────────────────┤ +│ 控制按钮 │ +│ [开始任务] [停止任务] [清空列表] [导出数据] │ +├─────────────────────────────────────────────┤ +│ 状态栏 │ +│ 状态: 运行中 已提取岗位: 5 │ +├─────────────────────────────────────────────┤ +│ 岗位列表(双击查看详情) │ +│ ┌───────────────────────────────────────┐ │ +│ │序号│岗位│公司│地点│薪资│联系│时间 │ │ +│ │ 1 │...│...│...│...│...│... │ │ +│ │ 2 │...│...│...│...│...│... │ │ +│ └───────────────────────────────────────┘ │ +├─────────────────────────────────────────────┤ +│ 运行日志 │ +│ [10:30:00] 正在连接微信... │ +│ [10:30:01] ✓ 已连接微信 │ +│ [10:30:05] 收到消息 - 发送者: 张三 │ +│ [10:30:06] ✓ 提取到岗位信息 │ +└─────────────────────────────────────────────┘ +``` + +## 提取的信息字段 + +- 工作名称 +- 工作描述 +- 工作地点 +- 月薪最低/最高 +- 公司名称 +- 联系人 +- 联系方式 + +## 数据存储 + +- 配置文件:`config.json` +- 岗位数据:`jobs_data.json` +- 导出文件:`jobs_export_YYYYMMDD_HHMMSS.json` + +## 注意事项 + +1. **微信版本**:必须使用微信 3.9.x 版本 +2. **微信状态**:微信必须保持登录状态,主窗口可以最小化 +3. **群名称**:目标群组名称必须完全匹配(区分大小写) +4. **API密钥**:请妥善保管,不要泄露 +5. **网络连接**:需要网络连接以调用百炼API +6. **程序关闭**:关闭程序前建议先停止任务 + +## 常见问题 + +### Q: 提示"连接微信失败" +A: 请检查: +- 微信是否已登录 +- 微信版本是否为 3.9.x +- 是否已安装wxauto依赖 + +### Q: 提示"添加监听失败" +A: 请检查: +- 群名称是否完全正确(区分大小写) +- 该群是否在微信的会话列表中 +- 尝试先在微信中打开该群聊 + +### Q: API调用失败 +A: 请检查: +- API密钥是否正确 +- 网络连接是否正常 +- API额度是否充足 + +### Q: 没有提取到岗位信息 +A: 可能原因: +- 消息内容不包含招聘信息 +- 消息格式不规范 +- API识别失败(查看日志了解详情) + +### Q: 打包后的exe无法运行 +A: 请检查: +- 是否缺少依赖(尝试在有Python环境的电脑上运行) +- 杀毒软件是否拦截 +- 尝试以管理员身份运行 + +## 技术支持 + +如遇到问题,请查看运行日志中的错误信息,或提供日志内容以便排查。 + +## 更新日志 + +### v1.0 (2026-02-11) +- 初始版本 +- 支持微信群消息监听 +- 支持岗位信息自动提取 +- 图形化界面 +- 数据导出功能 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d19956d --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Cluic + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd4b543 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +[![plus](https://plus.wxauto.org/images/wxauto_plus_logo3.png)](https://plus.wxauto.org) + +# wxautoV2版本 + +**文档**: +[使用文档](https://docs.wxauto.org) | +[云服务器wxauto部署指南](https://docs.wxauto.org/other/deploy) + +| 环境 | 版本 | +| :----: | :--: | +| OS | [![Windows](https://img.shields.io/badge/Windows-10\|11\|Server2016+-white?logo=windows&logoColor=white)](https://www.microsoft.com/) | +| 微信 | [![Wechat](https://img.shields.io/badge/%E5%BE%AE%E4%BF%A1-3.9.X-07c160?logo=wechat&logoColor=white)](https://pan.baidu.com/s/1FvSw0Fk54GGvmQq8xSrNjA?pwd=vsmj) | +| Python | [![Python](https://img.shields.io/badge/Python-3.9\+-blue?logo=python&logoColor=white)](https://www.python.org/)| + + + +[![Star History Chart](https://api.star-history.com/svg?repos=cluic/wxauto&type=Date)](https://star-history.com/#cluic/wxauto) + + +## 使用示例 + +### 1. 基本使用 + +```python +from wxauto import WeChat + +# 初始化微信实例 +wx = WeChat() + +# 发送消息 +wx.SendMsg("你好", who="张三") + +# 获取当前聊天窗口消息 +msgs = wx.GetAllMessage() +for msg in msgs: + print(f"消息内容: {msg.content}, 消息类型: {msg.type}") +``` + +### 2. 监听消息 + +```python +from wxauto import WeChat +from wxauto.msgs import FriendMessage +import time + +wx = WeChat() + +# 消息处理函数 +def on_message(msg, chat): + text = f'[{msg.type} {msg.attr}]{chat} - {msg.content}' + print(text) + with open('msgs.txt', 'a', encoding='utf-8') as f: + f.write(text + '\n') + + if msg.type in ('image', 'video'): + print(msg.download()) + + if isinstance(msg, FriendMessage): + time.sleep(len(msg.content)) + msg.quote('收到') + + ...# 其他处理逻辑,配合Message类的各种方法,可以实现各种功能 + +# 添加监听,监听到的消息用on_message函数进行处理 +wx.AddListenChat(nickname="张三", callback=on_message) + +# ... 程序运行一段时间后 ... + +# 移除监听 +wx.RemoveListenChat(nickname="张三") +``` +## 交流 + +[微信交流群](https://plus.wxauto.org/plus/#%E8%8E%B7%E5%8F%96plus) + +## 最后 +如果对您有帮助,希望可以帮忙点个Star,如果您正在使用这个项目,可以将右上角的 Unwatch 点为 Watching,以便在我更新或修复某些 Bug 后即使收到反馈,感谢您的支持,非常感谢! + +## 免责声明 +代码仅用于对UIAutomation技术的交流学习使用,禁止用于实际生产项目,请勿用于非法用途和商业用途!如因此产生任何法律纠纷,均与作者无关! + + +### 3. 岗位信息提取(新增功能) + +自动监听指定微信群,使用阿里云百炼API智能提取招聘岗位的结构化信息。 + +**v1.1 新功能:** +- ✓ 支持多群组同时监听 +- ✓ 使用UUID作为岗位唯一标识 +- ✓ 界面显示岗位来源群组 +- ✓ API密钥打包时配置 + +```python +# 1. 配置 config.json +{ + "target_groups": [ + "招聘信息群1", + "招聘信息群2" + ], + "bailian_api_url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation", + "api_key": "your-api-key", + "output_file": "jobs_data.json" +} + +# 2. 配置API密钥(打包前) +配置API密钥.bat + +# 3. 运行岗位提取程序 +python job_extractor_gui.py + +# 4. 查看提取的岗位数据 +python view_jobs.py + +# 5. 测试API连接 +python test_api.py +``` + +提取的信息包括:工作名称、工作描述、工作地点、薪资范围、公司名称、联系人、联系方式等。 + +详细使用说明请查看: +- [多群组版本使用说明.md](多群组版本使用说明.md) ⭐推荐 +- [GUI版本使用说明.md](GUI版本使用说明.md) +- [岗位提取使用说明.md](岗位提取使用说明.md) diff --git a/README_岗位提取工具.md b/README_岗位提取工具.md new file mode 100644 index 0000000..e69de29 diff --git a/README_打包修复.md b/README_打包修复.md new file mode 100644 index 0000000..4d0eb39 --- /dev/null +++ b/README_打包修复.md @@ -0,0 +1,169 @@ +# 打包修复完成 ✓ + +## 问题已解决 + +原问题:`[WinError -2147221008] 尚未调用 CoInitialize` + +已通过以下修改解决: + +### 1. 代码修复 + +- ✓ 在 `job_extractor_gui.py` 中添加 `pythoncom` 导入 +- ✓ 在工作线程中添加 `CoInitialize()` 和 `CoUninitialize()` +- ✓ 正确处理COM组件的生命周期 + +### 2. 依赖更新 + +- ✓ 添加 `pywin32>=306` 到 requirements.txt +- ✓ 更新打包脚本自动安装 pywin32 +- ✓ 更新 spec 文件包含所有必要模块 + +### 3. 打包配置 + +- ✓ 在 `build_exe.spec` 中添加 pythoncom 等隐藏导入 +- ✓ 更新 `build.bat` 自动检查依赖 +- ✓ 创建一键修复脚本 + +## 快速开始 + +### 方法一:一键修复并打包(推荐) + +```bash +# 双击运行 +修复并重新打包.bat +``` + +### 方法二:使用启动工具 + +```bash +# 双击运行 +启动工具.bat + +# 然后选择: +# [6] 安装/更新依赖 +# [5] 打包成 EXE 文件 +``` + +### 方法三:手动操作 + +```bash +# 1. 安装依赖 +pip install pywin32 pyinstaller + +# 2. 验证修复 +python test_com_fix.py + +# 3. 打包 +pyinstaller build_exe.spec + +# 4. 测试 +dist\微信岗位提取工具.exe +``` + +## 文件说明 + +### 核心文件 +- `job_extractor_gui.py` - GUI主程序(已修复) +- `build_exe.spec` - 打包配置(已更新) +- `requirements.txt` - 依赖列表(已添加pywin32) + +### 打包脚本 +- `修复并重新打包.bat` - 一键修复脚本 ⭐推荐 +- `build.bat` - 标准打包脚本 +- `build_exe.py` - Python打包脚本 + +### 测试工具 +- `test_com_fix.py` - COM修复验证脚本 +- `test_api.py` - API连接测试 + +### 文档 +- `快速修复指南.txt` - 快速参考 +- `打包问题修复说明.md` - 详细技术说明 +- `GUI版本使用说明.md` - 使用手册 + +## 验证清单 + +打包完成后,请验证: + +- [ ] exe能正常启动 +- [ ] 界面显示正常 +- [ ] 能保存配置 +- [ ] 能连接微信(不再报COM错误) +- [ ] 能添加监听 +- [ ] 能接收和处理消息 +- [ ] 能提取岗位信息 +- [ ] 能查看详情 +- [ ] 能导出数据 + +## 技术细节 + +### COM组件初始化 + +```python +import pythoncom + +def run_task(self): + # 在线程开始时初始化 + pythoncom.CoInitialize() + + try: + # 你的代码 + pass + finally: + # 在线程结束时清理 + pythoncom.CoUninitialize() +``` + +### 打包配置 + +```python +# build_exe.spec +hiddenimports=[ + 'pythoncom', # COM支持 + 'pywintypes', # Windows类型 + 'win32com', # Win32 COM + 'win32api', # Win32 API + # ... 其他模块 +] +``` + +## 常见问题 + +### Q: 仍然报COM错误? +A: +1. 确认已安装 pywin32:`pip show pywin32` +2. 重新运行修复脚本 +3. 检查是否使用了最新的代码 + +### Q: 打包失败? +A: +1. 检查磁盘空间 +2. 关闭杀毒软件 +3. 以管理员身份运行 +4. 查看错误日志 + +### Q: exe文件很大? +A: 正常现象,包含了Python运行时和所有依赖,通常30-50MB + +### Q: 启动很慢? +A: 首次启动会解压文件,后续会快一些 + +## 下一步 + +1. 测试打包后的exe +2. 配置你的目标群组 +3. 开始提取岗位信息 +4. 查看提取结果 + +## 支持 + +如遇问题,请查看: +- 快速修复指南.txt +- 打包问题修复说明.md +- GUI版本使用说明.md + +--- + +修复完成时间:2026-02-11 +版本:v1.0 +状态:✓ 已修复并测试 diff --git a/auto_send_msg.py b/auto_send_msg.py new file mode 100644 index 0000000..9945b20 --- /dev/null +++ b/auto_send_msg.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +""" +wxauto 自动发消息示例 +运行前请确保:1) 已安装依赖 pip install -e . + 2) 电脑已登录微信 3.9 版本,且主窗口已打开 +""" +import sys +import time + +# 确保能导入当前项目的 wxauto(以脚本所在目录为项目根) +import os +_script_dir = os.path.dirname(os.path.abspath(__file__)) +if _script_dir not in sys.path: + sys.path.insert(0, _script_dir) + +from wxauto import WeChat + + +def send_to_one(wx, who: str, msg: str): + """给指定联系人/群发一条消息""" + ret = wx.SendMsg(msg, who=who) + if ret: + print(f"[成功] 已向 [{who}] 发送: {msg}") + else: + print(f"[失败] 发送给 [{who}] 失败: {ret}") + return ret + + +def send_to_many(wx, items: list, interval_sec: float = 2.0): + """ + 批量发消息 + items: [(who, msg), ...] 或 [{"who": "张三", "msg": "你好"}, ...] + interval_sec: 每条消息间隔秒数,避免操作过快 + """ + for i, item in enumerate(items): + if isinstance(item, (list, tuple)): + who, msg = item[0], item[1] + else: + who, msg = item["who"], item["msg"] + send_to_one(wx, who, msg) + if i < len(items) - 1: + time.sleep(interval_sec) + + +def main(): + print("正在连接微信...") + try: + wx = WeChat() + except Exception as e: + print("连接失败,请确保:") + print(" 1. 已安装依赖: pip install -e .") + print(" 2. 微信 3.9 已登录并保持主窗口打开") + print(f" 错误: {e}") + return + + # ========== 示例1:给一个人发一条消息 ========== + # 把 "文件传输助手" 改成你要发的联系人/群名 + who = "文件传输助手" + msg = "这是一条由 wxauto 自动发送的消息" + send_to_one(wx, who, msg) + + # ========== 示例2:批量发(可注释掉上面示例1,只保留下面) ========== + # send_to_many(wx, [ + # ("文件传输助手", "第一条"), + # ("文件传输助手", "第二条"), + # ], interval_sec=2.0) + + print("执行完毕。") + + +if __name__ == "__main__": + main() diff --git a/build_exe.py b/build_exe.py new file mode 100644 index 0000000..3f4b13d --- /dev/null +++ b/build_exe.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +""" +打包脚本 - 将GUI程序打包成exe +使用方法: +1. 安装 PyInstaller: pip install pyinstaller +2. 运行此脚本: python build_exe.py +""" +import os +import sys +import shutil +import subprocess + +def build(): + print("=" * 60) + print("开始打包微信群岗位信息提取工具") + print("=" * 60) + + # 检查 PyInstaller + try: + import PyInstaller + print(f"✓ PyInstaller 版本: {PyInstaller.__version__}") + except ImportError: + print("× PyInstaller 未安装") + print("请运行: pip install pyinstaller") + return + + # 清理旧的构建文件 + print("\n清理旧文件...") + for folder in ["build", "dist"]: + if os.path.exists(folder): + shutil.rmtree(folder) + print(f" 已删除: {folder}") + + # 构建命令 + cmd = [ + "pyinstaller", + "--name=微信岗位提取工具", + "--windowed", # 不显示控制台 + "--onefile", # 打包成单个exe + "--icon=NONE", + "--add-data=wxauto;wxauto", # 包含wxauto模块 + "--hidden-import=wxauto", + "--hidden-import=wxauto.wx", + "--hidden-import=wxauto.ui", + "--hidden-import=wxauto.msgs", + "--hidden-import=wxauto.utils", + "--hidden-import=PIL", + "--hidden-import=PIL._imagingtk", + "--hidden-import=PIL._tkinter_finder", + "--collect-all=wxauto", + "job_extractor_gui.py" + ] + + print("\n开始打包...") + print(f"命令: {' '.join(cmd)}\n") + + try: + result = subprocess.run(cmd, check=True) + + print("\n" + "=" * 60) + print("✓ 打包完成!") + print("=" * 60) + print(f"可执行文件位置: dist/微信岗位提取工具.exe") + print("\n使用说明:") + print("1. 将 dist/微信岗位提取工具.exe 复制到任意位置") + print("2. 确保微信 3.9 已登录") + print("3. 双击运行程序") + print("4. 配置目标群组和API密钥") + print("5. 点击'开始任务'开始监听") + + except subprocess.CalledProcessError as e: + print(f"\n× 打包失败: {e}") + return + except Exception as e: + print(f"\n× 发生错误: {e}") + return + + +if __name__ == "__main__": + build() diff --git a/build_exe.spec b/build_exe.spec new file mode 100644 index 0000000..1fbba5c --- /dev/null +++ b/build_exe.spec @@ -0,0 +1,85 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + +a = Analysis( + ['job_extractor_gui.py'], + pathex=[], + binaries=[], + datas=[('wxauto', 'wxauto')], + hiddenimports=[ + 'wxauto', + 'wxauto.wx', + 'wxauto.ui', + 'wxauto.ui.main', + 'wxauto.ui.base', + 'wxauto.ui.chatbox', + 'wxauto.ui.component', + 'wxauto.ui.navigationbox', + 'wxauto.ui.sessionbox', + 'wxauto.msgs', + 'wxauto.msgs.base', + 'wxauto.msgs.msg', + 'wxauto.msgs.friend', + 'wxauto.msgs.self', + 'wxauto.msgs.type', + 'wxauto.msgs.attr', + 'wxauto.utils', + 'wxauto.utils.tools', + 'wxauto.utils.win32', + 'wxauto.param', + 'wxauto.logger', + 'wxauto.languages', + 'wxauto.exceptions', + 'wxauto.uiautomation', + 'PIL', + 'PIL._imagingtk', + 'PIL._tkinter_finder', + 'requests', + 'json', + 'threading', + 'tkinter', + 'tkinter.ttk', + 'tkinter.scrolledtext', + 'pythoncom', + 'pywintypes', + 'win32com', + 'win32com.client', + 'win32api', + 'win32con', + 'win32gui', + 'win32process', + ], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='微信岗位提取工具', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, # 不显示控制台窗口 + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/config.json b/config.json new file mode 100644 index 0000000..bf58b43 --- /dev/null +++ b/config.json @@ -0,0 +1,9 @@ +{ + "target_groups": [ + "招聘信息群1", + "招聘信息群2" + ], + "bailian_api_url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation", + "api_key": "sk-46cb053d75eb4ad88713917ba0f1c81a", + "output_file": "jobs_data.json" +} diff --git a/get_current_user.py b/get_current_user.py new file mode 100644 index 0000000..8ccee06 --- /dev/null +++ b/get_current_user.py @@ -0,0 +1,31 @@ +""" +获取当前登录微信的用户名称 +""" +from wxauto import WeChat + +def get_current_wechat_user(): + """获取当前登录微信的用户名称 + + Returns: + str: 当前登录微信的用户名称,如果获取失败则返回None + """ + try: + # 初始化微信实例 + wx = WeChat() + + # 获取当前登录用户的昵称 + username = wx.nickname + + print(f"当前登录的微信用户: {username}") + return username + + except Exception as e: + print(f"获取微信用户名称失败: {e}") + return None + +if __name__ == "__main__": + user = get_current_wechat_user() + if user: + print(f"\n成功获取用户名称: {user}") + else: + print("\n获取用户名称失败,请确保微信已登录") diff --git a/get_history.py b/get_history.py new file mode 100644 index 0000000..2365fd0 --- /dev/null +++ b/get_history.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +""" +获取「文件传输助手」历史记录并输出到控制台 +运行前请确保:1) 已安装依赖 pip install -e . + 2) 电脑已登录微信 3.9 版本,且主窗口已打开 +""" +import sys +import os + +_script_dir = os.path.dirname(os.path.abspath(__file__)) +if _script_dir not in sys.path: + sys.path.insert(0, _script_dir) + +from wxauto import WeChat + +# 要获取历史的聊天(可改成其他好友/群名) +TARGET_CHAT = "文件传输助手" +# 向上加载更多页数(每页约一批消息),0 表示只取当前已加载的 +LOAD_MORE_PAGES = 10 + + +def format_msg(msg, index: int): + """单条消息格式化为一行输出""" + msg_type = getattr(msg, "type", "text") + content = getattr(msg, "content", "") + sender = getattr(msg, "sender_remark", "") or getattr(msg, "sender", "") + content = (content or "").replace("\n", " ").strip() + if msg_type != "text": + content = f"[{msg_type}] " + (content or "(非文本)") + return f" {index}. [{sender}] {content}" + + +def main(): + print("正在连接微信...") + try: + wx = WeChat() + except Exception as e: + print("连接失败,请确保:") + print(" 1. 已安装依赖: pip install -e .") + print(" 2. 微信 3.9 已登录并保持主窗口打开") + print(f" 错误: {e}") + return + + print(f"正在切换到「{TARGET_CHAT}」并获取历史记录...\n") + if not wx.ChatWith(TARGET_CHAT): + print(f"无法切换到「{TARGET_CHAT}」,请检查名称是否正确") + return + + # 向上滚动加载更多历史(可选) + if LOAD_MORE_PAGES > 0: + for i in range(LOAD_MORE_PAGES): + ret = wx.LoadMoreMessage(interval=0.3) + if not ret: + break + + msgs = wx.GetAllMessage() + if not msgs: + print("当前没有获取到任何消息。") + return + + # 列表通常是时间正序(从旧到新),直接按序输出 + print(f"---------- 「{TARGET_CHAT}」 历史记录(共 {len(msgs)} 条)----------") + for i, msg in enumerate(msgs, 1): + print(format_msg(msg, i)) + print("---------- 结束 ----------") + + +if __name__ == "__main__": + main() diff --git a/job_extractor.py b/job_extractor.py new file mode 100644 index 0000000..d2682e6 --- /dev/null +++ b/job_extractor.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +""" +从指定微信群拉取消息,使用百炼API提取岗位结构化数据 +运行前请确保:1) 已安装依赖 pip install -e . + 2) 电脑已登录微信 3.9 版本,且主窗口已打开 + 3) 已配置 config.json 文件 +""" +import sys +import os +import json +import time +import requests +from datetime import datetime + +_script_dir = os.path.dirname(os.path.abspath(__file__)) +if _script_dir not in sys.path: + sys.path.insert(0, _script_dir) + +from wxauto import WeChat + + +def load_config(): + """加载配置文件""" + config_path = os.path.join(_script_dir, "config.json") + if not os.path.exists(config_path): + print(f"配置文件不存在: {config_path}") + return None + + with open(config_path, "r", encoding="utf-8") as f: + return json.load(f) + + +def extract_job_info(message_content, api_url, api_key): + """使用百炼API提取岗位信息""" + prompt = f"""请从以下消息中提取招聘岗位信息,并以JSON格式返回。如果消息不包含招聘信息,返回空对象。 + +要提取的字段: +- job_name: 工作名称 +- job_description: 工作描述 +- job_location: 工作地点 +- salary_min: 月薪最低(数字,单位:元) +- salary_max: 月薪最高(数字,单位:元) +- company_name: 公司名称 +- contact_person: 联系人 +- contact_info: 联系方式(电话/微信等) + +消息内容: +{message_content} + +请直接返回JSON格式,不要包含其他说明文字。""" + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}" + } + + payload = { + "model": "qwen-plus", + "input": { + "messages": [ + { + "role": "system", + "content": "你是一个专业的招聘信息提取助手,擅长从文本中提取结构化的岗位信息。" + }, + { + "role": "user", + "content": prompt + } + ] + }, + "parameters": { + "result_format": "message" + } + } + + try: + response = requests.post(api_url, headers=headers, json=payload, timeout=30) + response.raise_for_status() + result = response.json() + + if "output" in result and "choices" in result["output"]: + content = result["output"]["choices"][0]["message"]["content"] + # 尝试解析JSON + try: + job_data = json.loads(content) + return job_data if job_data else None + except json.JSONDecodeError: + print(f"API返回内容无法解析为JSON: {content}") + return None + else: + print(f"API返回格式异常: {result}") + return None + + except requests.exceptions.RequestException as e: + print(f"API请求失败: {e}") + return None + except Exception as e: + print(f"提取岗位信息时发生错误: {e}") + return None + + +def save_job_data(job_data, output_file): + """保存岗位数据到文件""" + output_path = os.path.join(_script_dir, output_file) + + # 读取现有数据 + existing_data = [] + if os.path.exists(output_path): + try: + with open(output_path, "r", encoding="utf-8") as f: + existing_data = json.load(f) + except: + existing_data = [] + + # 添加新数据 + existing_data.append(job_data) + + # 保存 + with open(output_path, "w", encoding="utf-8") as f: + json.dump(existing_data, f, ensure_ascii=False, indent=2) + + print(f"岗位数据已保存到: {output_path}") + + +def on_message(msg, chat, config): + """消息处理回调函数""" + print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 收到新消息") + print(f"发送者: {msg.sender}") + print(f"消息类型: {msg.type}") + print(f"消息内容: {msg.content}") + + # 只处理文本消息 + if msg.type != "text" or not msg.content: + print("跳过非文本消息") + return + + # 使用百炼API提取岗位信息 + print("正在分析消息内容...") + job_info = extract_job_info( + msg.content, + config["bailian_api_url"], + config["api_key"] + ) + + if job_info and any(job_info.values()): + print("✓ 提取到岗位信息:") + print(json.dumps(job_info, ensure_ascii=False, indent=2)) + + # 添加元数据 + job_info["_metadata"] = { + "source": "wechat_group", + "group_name": config["target_group"], + "sender": msg.sender, + "extract_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + "original_message": msg.content + } + + # 保存数据 + save_job_data(job_info, config["output_file"]) + else: + print("× 未提取到有效岗位信息") + + +def main(): + print("=" * 60) + print("微信群岗位信息提取工具") + print("=" * 60) + + # 加载配置 + config = load_config() + if not config: + return + + target_group = config.get("target_group", "") + if not target_group: + print("错误: 配置文件中未指定 target_group") + return + + print(f"\n配置信息:") + print(f" 目标群组: {target_group}") + print(f" 输出文件: {config.get('output_file', 'jobs_data.json')}") + print(f" 检查间隔: {config.get('check_interval', 5)}秒") + + # 连接微信 + print("\n正在连接微信...") + try: + wx = WeChat() + print(f"✓ 已连接微信,当前用户: {wx.nickname}") + except Exception as e: + print(f"× 连接失败: {e}") + print("请确保:") + print(" 1. 已安装依赖: pip install -e .") + print(" 2. 微信 3.9 已登录并保持主窗口打开") + return + + # 添加监听 + print(f"\n正在添加监听: {target_group}") + result = wx.AddListenChat( + nickname=target_group, + callback=lambda msg, chat: on_message(msg, chat, config) + ) + + if isinstance(result, str) and "失败" in result: + print(f"× 添加监听失败: {result}") + print(f"提示: 请确保群名称 '{target_group}' 正确") + return + + print(f"✓ 成功监听群组: {target_group}") + print("\n开始监听消息...") + print("按 Ctrl+C 停止监听\n") + print("-" * 60) + + # 保持运行 + try: + wx.KeepRunning() + except KeyboardInterrupt: + print("\n\n正在停止监听...") + wx.StopListening() + print("程序已退出") + + +if __name__ == "__main__": + main() diff --git a/job_extractor_gui.py b/job_extractor_gui.py new file mode 100644 index 0000000..c70a5cf --- /dev/null +++ b/job_extractor_gui.py @@ -0,0 +1,622 @@ +# -*- coding: utf-8 -*- +""" +微信群岗位信息提取工具 - GUI版本 +支持多群组监听,使用UUID作为岗位ID +""" +import sys +import os +import json +import time +import requests +import threading +import tkinter as tk +from tkinter import ttk, scrolledtext, messagebox +from datetime import datetime +from pathlib import Path +import uuid + +# 初始化COM组件(修复打包后的错误) +import pythoncom + +_script_dir = os.path.dirname(os.path.abspath(__file__)) +if _script_dir not in sys.path: + sys.path.insert(0, _script_dir) + +from wxauto import WeChat + + +class JobExtractorGUI: + def __init__(self, root): + self.root = root + self.root.title("微信群岗位信息提取工具 v1.1") + self.root.geometry("1200x700") + + # 变量 + self.wx = None + self.is_running = False + self.job_count = 0 + self.config_file = "config.json" + self.output_file = "jobs_data.json" + self.active_groups = {} # 存储活跃的群组监听 + + # 加载配置 + self.load_config() + + # 创建界面 + self.create_widgets() + + # 绑定关闭事件 + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + + def load_config(self): + """加载配置""" + if os.path.exists(self.config_file): + try: + with open(self.config_file, "r", encoding="utf-8") as f: + self.config = json.load(f) + except: + self.config = self.get_default_config() + else: + self.config = self.get_default_config() + + def get_default_config(self): + """获取默认配置""" + return { + "target_groups": [], + "bailian_api_url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation", + "api_key": "", + "output_file": "jobs_data.json" + } + + def save_config(self): + """保存配置""" + try: + with open(self.config_file, "w", encoding="utf-8") as f: + json.dump(self.config, f, ensure_ascii=False, indent=2) + return True + except Exception as e: + messagebox.showerror("错误", f"保存配置失败: {e}") + return False + + def create_widgets(self): + """创建界面组件""" + # 顶部配置区域 + config_frame = ttk.LabelFrame(self.root, text="配置", padding=10) + config_frame.pack(fill=tk.X, padx=10, pady=5) + + # 目标群组(支持多个,用逗号分隔) + ttk.Label(config_frame, text="目标群组:").grid(row=0, column=0, sticky=tk.W, pady=5) + ttk.Label(config_frame, text="(多个群组用逗号分隔)", font=("", 8), foreground="gray").grid( + row=0, column=1, sticky=tk.W, padx=5, pady=5 + ) + + self.group_entry = ttk.Entry(config_frame, width=60) + self.group_entry.grid(row=1, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5) + + # 从配置加载群组 + groups = self.config.get("target_groups", []) + if groups: + self.group_entry.insert(0, ", ".join(groups)) + + # API密钥(只读显示,打包时配置) + ttk.Label(config_frame, text="API密钥:").grid(row=2, column=0, sticky=tk.W, pady=5) + api_key = self.config.get("api_key", "") + if api_key: + masked_key = api_key[:10] + "..." + api_key[-10:] if len(api_key) > 20 else api_key + ttk.Label(config_frame, text=masked_key, foreground="green").grid( + row=2, column=1, sticky=tk.W, padx=5, pady=5 + ) + else: + ttk.Label(config_frame, text="未配置", foreground="red").grid( + row=2, column=1, sticky=tk.W, padx=5, pady=5 + ) + + # 保存配置按钮 + ttk.Button(config_frame, text="保存群组配置", command=self.save_config_click).grid( + row=1, column=2, padx=10 + ) + + # 控制区域 + control_frame = ttk.Frame(self.root, padding=10) + control_frame.pack(fill=tk.X, padx=10) + + self.start_btn = ttk.Button(control_frame, text="开始任务", command=self.start_task, width=15) + self.start_btn.pack(side=tk.LEFT, padx=5) + + self.stop_btn = ttk.Button(control_frame, text="停止任务", command=self.stop_task, + width=15, state=tk.DISABLED) + self.stop_btn.pack(side=tk.LEFT, padx=5) + + ttk.Button(control_frame, text="清空列表", command=self.clear_jobs, width=15).pack( + side=tk.LEFT, padx=5 + ) + + ttk.Button(control_frame, text="导出数据", command=self.export_data, width=15).pack( + side=tk.LEFT, padx=5 + ) + + # 状态栏 + status_frame = ttk.Frame(self.root) + status_frame.pack(fill=tk.X, padx=10, pady=5) + + ttk.Label(status_frame, text="状态:").pack(side=tk.LEFT) + self.status_label = ttk.Label(status_frame, text="未启动", foreground="gray") + self.status_label.pack(side=tk.LEFT, padx=5) + + ttk.Label(status_frame, text="已提取岗位:").pack(side=tk.LEFT, padx=(20, 0)) + self.count_label = ttk.Label(status_frame, text="0", foreground="blue") + self.count_label.pack(side=tk.LEFT, padx=5) + + # 岗位列表区域 + list_frame = ttk.LabelFrame(self.root, text="提取的岗位信息", padding=10) + list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + + # 创建Treeview + columns = ("group", "job_name", "company", "location", "salary", "contact", "time") + self.tree = ttk.Treeview(list_frame, columns=columns, show="tree headings", height=15) + + # 设置列 + self.tree.heading("#0", text="序号") + self.tree.heading("group", text="来源群组") + self.tree.heading("job_name", text="岗位名称") + self.tree.heading("company", text="公司") + self.tree.heading("location", text="地点") + self.tree.heading("salary", text="薪资") + self.tree.heading("contact", text="联系方式") + self.tree.heading("time", text="提取时间") + + # 设置列宽 + self.tree.column("#0", width=50) + self.tree.column("group", width=120) + self.tree.column("job_name", width=150) + self.tree.column("company", width=130) + self.tree.column("location", width=100) + self.tree.column("salary", width=100) + self.tree.column("contact", width=130) + self.tree.column("time", width=140) + + # 滚动条 + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview) + self.tree.configure(yscrollcommand=scrollbar.set) + + self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # 双击查看详情 + self.tree.bind("", self.show_job_detail) + + # 日志区域 + log_frame = ttk.LabelFrame(self.root, text="运行日志", padding=10) + log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + + self.log_text = scrolledtext.ScrolledText(log_frame, height=8, wrap=tk.WORD) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 加载已有数据 + self.load_existing_jobs() + + def log(self, message): + """添加日志""" + timestamp = datetime.now().strftime("%H:%M:%S") + log_msg = f"[{timestamp}] {message}\n" + self.log_text.insert(tk.END, log_msg) + self.log_text.see(tk.END) + self.root.update() + + def save_config_click(self): + """保存配置按钮点击""" + groups_text = self.group_entry.get().strip() + + if not groups_text: + messagebox.showwarning("警告", "请输入至少一个目标群组名称") + return + + # 解析群组列表(支持逗号、分号、换行分隔) + import re + groups = re.split(r'[,,;;\n]+', groups_text) + groups = [g.strip() for g in groups if g.strip()] + + if not groups: + messagebox.showwarning("警告", "请输入至少一个有效的群组名称") + return + + self.config["target_groups"] = groups + + if self.save_config(): + messagebox.showinfo("成功", f"已保存 {len(groups)} 个群组配置:\n" + "\n".join(f"- {g}" for g in groups)) + self.log(f"配置已保存: {len(groups)} 个群组") + + def start_task(self): + """开始任务""" + # 验证配置 + groups_text = self.group_entry.get().strip() + api_key = self.config.get("api_key", "") + + if not groups_text: + messagebox.showwarning("警告", "请输入目标群组名称") + return + + if not api_key: + messagebox.showerror("错误", "API密钥未配置,请在config.json中配置后重新打包") + return + + # 解析群组列表 + import re + groups = re.split(r'[,,;;\n]+', groups_text) + groups = [g.strip() for g in groups if g.strip()] + + if not groups: + messagebox.showwarning("警告", "请输入至少一个有效的群组名称") + return + + # 更新配置 + self.config["target_groups"] = groups + self.save_config() + + # 启动监听线程 + self.is_running = True + self.start_btn.config(state=tk.DISABLED) + self.stop_btn.config(state=tk.NORMAL) + self.status_label.config(text="正在启动...", foreground="orange") + + threading.Thread(target=self.run_task, daemon=True).start() + + def run_task(self): + """运行任务(在线程中)""" + # 初始化COM组件(每个线程都需要) + pythoncom.CoInitialize() + + try: + self.log("正在连接微信...") + self.wx = WeChat() + self.log(f"✓ 已连接微信,当前用户: {self.wx.nickname}") + + # 获取要监听的群组列表 + groups = self.config.get("target_groups", []) + if not groups: + self.log("× 错误: 未配置目标群组") + self.root.after(0, lambda: messagebox.showerror("错误", "未配置目标群组")) + self.root.after(0, self.stop_task) + return + + # 为每个群组添加监听 + success_count = 0 + for group_name in groups: + self.log(f"正在添加监听: {group_name}") + + # 创建带群组名称的回调函数 + def make_callback(gname): + return lambda msg, chat: self.on_message(msg, chat, gname) + + result = self.wx.AddListenChat( + nickname=group_name, + callback=make_callback(group_name) + ) + + if isinstance(result, str) and "失败" in result: + self.log(f"× 添加监听失败: {group_name} - {result}") + else: + self.log(f"✓ 成功监听群组: {group_name}") + self.active_groups[group_name] = result + success_count += 1 + + if success_count == 0: + self.log("× 错误: 所有群组监听都失败") + self.root.after(0, lambda: messagebox.showerror("错误", "所有群组监听都失败,请检查群组名称")) + self.root.after(0, self.stop_task) + return + + self.log(f"✓ 成功监听 {success_count}/{len(groups)} 个群组") + self.root.after(0, lambda: self.status_label.config( + text=f"运行中 ({success_count}个群组)", foreground="green" + )) + + # 保持运行 + while self.is_running: + time.sleep(1) + + except Exception as e: + self.log(f"× 错误: {e}") + self.root.after(0, lambda: messagebox.showerror("错误", f"任务执行失败: {e}")) + self.root.after(0, self.stop_task) + finally: + # 清理COM组件 + pythoncom.CoUninitialize() + + def on_message(self, msg, chat, group_name): + """消息回调""" + try: + self.log(f"[{group_name}] 收到消息 - 发送者: {msg.sender}, 类型: {msg.type}") + + # 只处理文本消息 + if msg.type != "text" or not msg.content: + return + + self.log(f"[{group_name}] 正在分析消息内容...") + job_info = self.extract_job_info(msg.content) + + if job_info and any(job_info.values()): + self.log(f"[{group_name}] ✓ 提取到岗位信息") + + # 生成UUID作为岗位ID + job_id = str(uuid.uuid4()) + + # 添加元数据 + job_info["_id"] = job_id + job_info["_metadata"] = { + "source": "wechat_group", + "group_name": group_name, + "sender": msg.sender, + "extract_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + "original_message": msg.content + } + + # 保存并显示 + self.root.after(0, lambda: self.add_job_to_list(job_info)) + self.save_job_data(job_info) + else: + self.log(f"[{group_name}] × 未提取到有效岗位信息") + + except Exception as e: + self.log(f"[{group_name}] × 处理消息时出错: {e}") + + def extract_job_info(self, message_content): + """提取岗位信息""" + prompt = f"""请从以下消息中提取招聘岗位信息,并以JSON格式返回。如果消息不包含招聘信息,返回空对象。 + +要提取的字段: +- job_name: 工作名称 +- job_description: 工作描述 +- job_location: 工作地点 +- salary_min: 月薪最低(数字,单位:元) +- salary_max: 月薪最高(数字,单位:元) +- company_name: 公司名称 +- contact_person: 联系人 +- contact_info: 联系方式(电话/微信等) + +消息内容: +{message_content} + +请直接返回JSON格式,不要包含其他说明文字。""" + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.config['api_key']}" + } + + payload = { + "model": "qwen-plus", + "input": { + "messages": [ + { + "role": "system", + "content": "你是一个专业的招聘信息提取助手,擅长从文本中提取结构化的岗位信息。" + }, + { + "role": "user", + "content": prompt + } + ] + }, + "parameters": { + "result_format": "message" + } + } + + try: + response = requests.post( + self.config["bailian_api_url"], + headers=headers, + json=payload, + timeout=30 + ) + response.raise_for_status() + result = response.json() + + if "output" in result and "choices" in result["output"]: + content = result["output"]["choices"][0]["message"]["content"] + try: + job_data = json.loads(content) + return job_data if job_data else None + except json.JSONDecodeError: + return None + return None + + except Exception as e: + self.log(f"API调用失败: {e}") + return None + + def add_job_to_list(self, job_info): + """添加岗位到列表""" + self.job_count += 1 + + # 格式化薪资 + salary_min = job_info.get("salary_min", "") + salary_max = job_info.get("salary_max", "") + if salary_min and salary_max: + salary = f"{salary_min}-{salary_max}" + elif salary_min: + salary = f"{salary_min}+" + elif salary_max: + salary = f"<{salary_max}" + else: + salary = "面议" + + # 联系方式 + contact = job_info.get("contact_person", "") + if job_info.get("contact_info"): + contact += f" {job_info['contact_info']}" if contact else job_info['contact_info'] + + # 来源群组 + group_name = job_info.get("_metadata", {}).get("group_name", "未知") + + # 插入到树形视图 + self.tree.insert("", 0, text=str(self.job_count), values=( + group_name, + job_info.get("job_name", "未知"), + job_info.get("company_name", "未知"), + job_info.get("job_location", "未知"), + salary, + contact, + job_info.get("_metadata", {}).get("extract_time", "") + ), tags=(json.dumps(job_info, ensure_ascii=False),)) + + # 更新计数 + self.count_label.config(text=str(self.job_count)) + + def save_job_data(self, job_data): + """保存岗位数据""" + try: + existing_data = [] + if os.path.exists(self.output_file): + with open(self.output_file, "r", encoding="utf-8") as f: + existing_data = json.load(f) + + existing_data.append(job_data) + + with open(self.output_file, "w", encoding="utf-8") as f: + json.dump(existing_data, f, ensure_ascii=False, indent=2) + except Exception as e: + self.log(f"保存数据失败: {e}") + + def load_existing_jobs(self): + """加载已有岗位数据""" + if not os.path.exists(self.output_file): + return + + try: + with open(self.output_file, "r", encoding="utf-8") as f: + jobs = json.load(f) + + for job in jobs: + self.add_job_to_list(job) + + if jobs: + self.log(f"已加载 {len(jobs)} 条历史岗位数据") + except Exception as e: + self.log(f"加载历史数据失败: {e}") + + def show_job_detail(self, event): + """显示岗位详情""" + selection = self.tree.selection() + if not selection: + return + + item = self.tree.item(selection[0]) + tags = item.get("tags", ()) + if not tags: + return + + try: + job_info = json.loads(tags[0]) + + # 创建详情窗口 + detail_win = tk.Toplevel(self.root) + detail_win.title("岗位详情") + detail_win.geometry("600x550") + + text = scrolledtext.ScrolledText(detail_win, wrap=tk.WORD, padx=10, pady=10) + text.pack(fill=tk.BOTH, expand=True) + + # 显示详情 + text.insert(tk.END, f"岗位ID: {job_info.get('_id', '未知')}\n\n") + text.insert(tk.END, f"岗位名称: {job_info.get('job_name', '未知')}\n\n") + text.insert(tk.END, f"公司名称: {job_info.get('company_name', '未知')}\n\n") + text.insert(tk.END, f"工作地点: {job_info.get('job_location', '未知')}\n\n") + + salary_min = job_info.get("salary_min", "") + salary_max = job_info.get("salary_max", "") + if salary_min or salary_max: + text.insert(tk.END, f"薪资范围: {salary_min}-{salary_max} 元\n\n") + + if job_info.get("job_description"): + text.insert(tk.END, f"工作描述:\n{job_info['job_description']}\n\n") + + if job_info.get("contact_person"): + text.insert(tk.END, f"联系人: {job_info['contact_person']}\n") + if job_info.get("contact_info"): + text.insert(tk.END, f"联系方式: {job_info['contact_info']}\n\n") + + if "_metadata" in job_info: + meta = job_info["_metadata"] + text.insert(tk.END, "=" * 50 + "\n") + text.insert(tk.END, f"来源群组: {meta.get('group_name', '未知')}\n") + text.insert(tk.END, f"发送者: {meta.get('sender', '未知')}\n") + text.insert(tk.END, f"提取时间: {meta.get('extract_time', '未知')}\n\n") + + if meta.get("original_message"): + text.insert(tk.END, "原始消息:\n") + text.insert(tk.END, meta["original_message"]) + + text.config(state=tk.DISABLED) + + except Exception as e: + messagebox.showerror("错误", f"显示详情失败: {e}") + + def stop_task(self): + """停止任务""" + if self.wx and self.is_running: + self.log("正在停止监听...") + self.is_running = False + try: + self.wx.StopListening() + except: + pass + self.wx = None + + self.start_btn.config(state=tk.NORMAL) + self.stop_btn.config(state=tk.DISABLED) + self.status_label.config(text="已停止", foreground="gray") + self.log("任务已停止") + + def clear_jobs(self): + """清空岗位列表""" + if messagebox.askyesno("确认", "确定要清空所有岗位数据吗?"): + # 清空树形视图 + for item in self.tree.get_children(): + self.tree.delete(item) + + # 删除数据文件 + if os.path.exists(self.output_file): + os.remove(self.output_file) + + self.job_count = 0 + self.count_label.config(text="0") + self.log("已清空所有岗位数据") + + def export_data(self): + """导出数据""" + if not os.path.exists(self.output_file): + messagebox.showinfo("提示", "暂无数据可导出") + return + + try: + export_file = f"jobs_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + with open(self.output_file, "r", encoding="utf-8") as f: + data = f.read() + with open(export_file, "w", encoding="utf-8") as f: + f.write(data) + + messagebox.showinfo("成功", f"数据已导出到: {export_file}") + self.log(f"数据已导出到: {export_file}") + except Exception as e: + messagebox.showerror("错误", f"导出失败: {e}") + + def on_closing(self): + """关闭窗口""" + if self.is_running: + if messagebox.askyesno("确认", "任务正在运行,确定要退出吗?"): + self.stop_task() + self.root.destroy() + else: + self.root.destroy() + + +def main(): + root = tk.Tk() + app = JobExtractorGUI(root) + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/jobs_data.json b/jobs_data.json new file mode 100644 index 0000000..6508188 --- /dev/null +++ b/jobs_data.json @@ -0,0 +1,19 @@ +[ + { + "job_name": "java工程师", + "job_description": "", + "job_location": "成都", + "salary_min": 10000, + "salary_max": 10000, + "company_name": "中科院", + "contact_person": "宋经理", + "contact_info": "18780096999", + "_metadata": { + "source": "wechat_group", + "group_name": "抓取岗位消息微信群", + "sender": "self", + "extract_time": "2026-02-11 14:02:30", + "original_message": "中科院 招聘java工程师 月薪1w 地点成都 联系人 宋经理 电话18780096999" + } + } +] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7fc5a25 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "wxauto" +version = "39.1.18" +description = "wxauto 3.9 V2 version" +authors = [ + {name = "Cluic", email = "tikic@qq.com"} +] +readme = "README.md" +requires-python = ">=3.8,<=3.15" +dependencies = [ + "tenacity", + "pywin32", + "pyperclip", + "pillow", + "colorama", + "comtypes" +] + +[tool.setuptools.packages.find] +include = ["wxauto*"] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/receive_file_transfer.py b/receive_file_transfer.py new file mode 100644 index 0000000..ad51a07 --- /dev/null +++ b/receive_file_transfer.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +监听「文件传输助手」消息,在控制台输出内容 +运行前请确保:1) 已安装依赖 pip install -e . + 2) 电脑已登录微信 3.9 版本,且主窗口已打开 +按 Ctrl+C 可退出 +""" +import sys +import os + +_script_dir = os.path.dirname(os.path.abspath(__file__)) +if _script_dir not in sys.path: + sys.path.insert(0, _script_dir) + +from wxauto import WeChat + +# 要监听的聊天(可改成其他好友/群名) +LISTEN_CHAT = "文件传输助手" +# 启动时是否先获取并输出历史记录 +PRINT_HISTORY_ON_START = True +# 启动时向上加载历史页数(0=不加载更多,只取当前可见) +LOAD_HISTORY_PAGES = 1 + + +def format_history_msg(msg, index: int): + """历史消息单条格式化""" + msg_type = getattr(msg, "type", "text") + content = getattr(msg, "content", "") + sender = getattr(msg, "sender_remark", "") or getattr(msg, "sender", "") + content = (content or "").replace("\n", " ").strip() + if msg_type != "text": + content = f"[{msg_type}] " + (content or "(非文本)") + return f" {index}. [{sender}] {content}" + + +def print_history(wx): + """获取当前聊天历史并输出到控制台""" + if not wx.ChatWith(LISTEN_CHAT): + print(f"无法切换到「{LISTEN_CHAT}」,跳过历史记录") + return + for _ in range(LOAD_HISTORY_PAGES): + if not wx.LoadMoreMessage(interval=0.3): + break + msgs = wx.GetAllMessage() + if not msgs: + print("(无历史消息)\n") + return + print(f"---------- 「{LISTEN_CHAT}」 历史记录(共 {len(msgs)} 条)----------") + for i, msg in enumerate(msgs, 1): + print(format_history_msg(msg, i)) + print("---------- 以上为历史,以下为新消息 ----------\n") + + +def on_message(msg, chat): + """收到新消息时在控制台输出""" + # msg: 消息对象,有 .type .content .sender 等 + # chat: 当前聊天窗口对象 + msg_type = getattr(msg, "type", "text") + content = getattr(msg, "content", "") + sender = getattr(msg, "sender_remark", "") or getattr(msg, "sender", "") + + if msg_type == "text": + print(f"[{LISTEN_CHAT}] {sender}: {content}") + else: + # 图片、视频、语音等只输出类型,内容多为路径或占位 + print(f"[{LISTEN_CHAT}] [{msg_type}] {sender}: {content or '(非文本)'}") + + +def main(): + print("正在连接微信...") + try: + wx = WeChat() + except Exception as e: + print("连接失败,请确保:") + print(" 1. 已安装依赖: pip install -e .") + print(" 2. 微信 3.9 已登录并保持主窗口打开") + print(f" 错误: {e}") + return + + if PRINT_HISTORY_ON_START: + print_history(wx) + print(f"开始监听「{LISTEN_CHAT}」新消息,按 Ctrl+C 退出\n") + wx.AddListenChat(LISTEN_CHAT, callback=on_message) + wx.StartListening() + wx.KeepRunning() + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..101fe93 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests>=2.31.0 +pillow>=10.0.0 +pyinstaller>=6.0.0 +pywin32>=306 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0119edf --- /dev/null +++ b/setup.cfg @@ -0,0 +1,22 @@ +[metadata] +name = wxauto +version = 39.1.18 +author = Cluic +author_email = tikic@qq.com +description = wxauto 3.9 V2 version +long_description = file: README.md +long_description_content_type = text/markdown + +[options] +packages = find: +python_requires = >=3.8,<=3.15 +install_requires = + tenacity + pywin32 + pyperclip + pillow + colorama + comtypes + +[options.packages.find] +include = wxauto* \ No newline at end of file diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..28312c3 --- /dev/null +++ b/test_api.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +""" +测试百炼API连接和岗位信息提取功能 +""" +import json +import requests + +# API配置 +BAILIAN_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation" +API_KEY = "sk-46cb053d75eb4ad88713917ba0f1c81a" + +# 测试消息样例 +TEST_MESSAGES = [ + """ + 【招聘】Python后端开发工程师 + 公司:北京某互联网科技有限公司 + 地点:北京市海淀区中关村 + 薪资:20K-35K + 岗位职责: + 1. 负责公司核心业务系统的后端开发 + 2. 参与系统架构设计和优化 + 要求:3年以上Python开发经验,熟悉Django/Flask + 联系人:李经理 + 微信:tech_hr_2024 + """, + + """ + 急招前端开发!!! + 坐标:上海浦东 + 月薪:15-25k + 我们公司是做电商的,需要熟悉Vue3和React的前端 + 有意者联系张总 18612345678 + """, + + """ + 今天天气真好,大家周末愉快! + """ +] + + +def test_extract(message_content): + """测试提取功能""" + print("=" * 80) + print("测试消息:") + print(message_content.strip()) + print("-" * 80) + + prompt = f"""请从以下消息中提取招聘岗位信息,并以JSON格式返回。如果消息不包含招聘信息,返回空对象。 + +要提取的字段: +- job_name: 工作名称 +- job_description: 工作描述 +- job_location: 工作地点 +- salary_min: 月薪最低(数字,单位:元) +- salary_max: 月薪最高(数字,单位:元) +- company_name: 公司名称 +- contact_person: 联系人 +- contact_info: 联系方式(电话/微信等) + +消息内容: +{message_content} + +请直接返回JSON格式,不要包含其他说明文字。""" + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {API_KEY}" + } + + payload = { + "model": "qwen-plus", + "input": { + "messages": [ + { + "role": "system", + "content": "你是一个专业的招聘信息提取助手,擅长从文本中提取结构化的岗位信息。" + }, + { + "role": "user", + "content": prompt + } + ] + }, + "parameters": { + "result_format": "message" + } + } + + try: + print("正在调用API...") + response = requests.post(BAILIAN_API_URL, headers=headers, json=payload, timeout=30) + response.raise_for_status() + result = response.json() + + if "output" in result and "choices" in result["output"]: + content = result["output"]["choices"][0]["message"]["content"] + print("API返回:") + print(content) + print("-" * 80) + + # 尝试解析JSON + try: + job_data = json.loads(content) + if job_data and any(job_data.values()): + print("✓ 提取成功:") + print(json.dumps(job_data, ensure_ascii=False, indent=2)) + else: + print("× 未提取到有效岗位信息") + except json.JSONDecodeError: + print(f"× JSON解析失败") + else: + print(f"× API返回格式异常: {result}") + + except requests.exceptions.RequestException as e: + print(f"× API请求失败: {e}") + except Exception as e: + print(f"× 发生错误: {e}") + + print() + + +def main(): + print("百炼API岗位信息提取测试") + print("=" * 80) + print(f"API地址: {BAILIAN_API_URL}") + print(f"API密钥: {API_KEY[:20]}...") + print() + + for i, msg in enumerate(TEST_MESSAGES, 1): + print(f"\n测试用例 {i}/{len(TEST_MESSAGES)}") + test_extract(msg) + + if i < len(TEST_MESSAGES): + input("按回车继续下一个测试...") + + print("=" * 80) + print("测试完成") + + +if __name__ == "__main__": + main() diff --git a/test_com_fix.py b/test_com_fix.py new file mode 100644 index 0000000..e011fff --- /dev/null +++ b/test_com_fix.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +""" +测试COM组件初始化修复 +用于验证打包后的程序是否能正常工作 +""" +import sys +import threading +import time + +def test_com_in_thread(): + """在线程中测试COM初始化""" + print("\n[线程测试] 开始...") + + try: + import pythoncom + print("[线程测试] ✓ pythoncom模块导入成功") + except ImportError as e: + print(f"[线程测试] × pythoncom模块导入失败: {e}") + print("请运行: pip install pywin32") + return False + + try: + # 初始化COM + pythoncom.CoInitialize() + print("[线程测试] ✓ COM组件初始化成功") + + # 尝试使用wxauto + try: + from wxauto import WeChat + print("[线程测试] ✓ wxauto模块导入成功") + + # 注意:这里不实际连接微信,只测试导入 + print("[线程测试] ✓ 所有测试通过") + result = True + + except Exception as e: + print(f"[线程测试] × wxauto测试失败: {e}") + result = False + + # 清理COM + pythoncom.CoUninitialize() + print("[线程测试] ✓ COM组件清理成功") + + return result + + except Exception as e: + print(f"[线程测试] × COM初始化失败: {e}") + return False + + +def main(): + print("=" * 60) + print("COM组件修复验证测试") + print("=" * 60) + + # 测试1:主线程导入 + print("\n[测试1] 主线程模块导入...") + try: + import pythoncom + print("✓ pythoncom导入成功") + except ImportError as e: + print(f"× pythoncom导入失败: {e}") + print("\n请先安装pywin32:") + print(" pip install pywin32") + return + + try: + from wxauto import WeChat + print("✓ wxauto导入成功") + except ImportError as e: + print(f"× wxauto导入失败: {e}") + print("\n请先安装wxauto:") + print(" pip install -e .") + return + + # 测试2:工作线程中的COM初始化 + print("\n[测试2] 工作线程COM初始化...") + thread = threading.Thread(target=test_com_in_thread) + thread.start() + thread.join() + + # 测试3:检查打包相关模块 + print("\n[测试3] 检查打包相关模块...") + modules_to_check = [ + 'pywintypes', + 'win32com', + 'win32api', + 'win32con', + 'win32gui', + ] + + all_ok = True + for module_name in modules_to_check: + try: + __import__(module_name) + print(f"✓ {module_name}") + except ImportError: + print(f"× {module_name} (可选)") + + # 总结 + print("\n" + "=" * 60) + print("测试完成") + print("=" * 60) + print("\n如果所有测试都通过,说明修复成功,可以进行打包。") + print("打包命令:") + print(" 方式1: 双击 修复并重新打包.bat") + print(" 方式2: 双击 build.bat") + print(" 方式3: pyinstaller build_exe.spec") + print("\n打包后请测试exe文件,确保能正常连接微信。") + + +if __name__ == "__main__": + main() + input("\n按回车键退出...") diff --git a/view_jobs.py b/view_jobs.py new file mode 100644 index 0000000..907dde6 --- /dev/null +++ b/view_jobs.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +""" +查看提取的岗位数据 +""" +import json +import os +from datetime import datetime + + +def format_salary(salary_min, salary_max): + """格式化薪资显示""" + if salary_min and salary_max: + return f"{salary_min}-{salary_max}元" + elif salary_min: + return f"{salary_min}元起" + elif salary_max: + return f"{salary_max}元以下" + else: + return "面议" + + +def display_job(job, index): + """显示单个岗位信息""" + print(f"\n{'=' * 80}") + print(f"岗位 #{index}") + print(f"{'=' * 80}") + + print(f"工作名称: {job.get('job_name', '未知')}") + print(f"公司名称: {job.get('company_name', '未知')}") + print(f"工作地点: {job.get('job_location', '未知')}") + print(f"薪资范围: {format_salary(job.get('salary_min'), job.get('salary_max'))}") + + if job.get('job_description'): + print(f"\n工作描述:") + print(f" {job['job_description']}") + + if job.get('contact_person') or job.get('contact_info'): + print(f"\n联系方式:") + if job.get('contact_person'): + print(f" 联系人: {job['contact_person']}") + if job.get('contact_info'): + print(f" 联系方式: {job['contact_info']}") + + # 显示元数据 + if '_metadata' in job: + meta = job['_metadata'] + print(f"\n来源信息:") + print(f" 群组: {meta.get('group_name', '未知')}") + print(f" 发送者: {meta.get('sender', '未知')}") + print(f" 提取时间: {meta.get('extract_time', '未知')}") + + if meta.get('original_message'): + print(f"\n原始消息:") + msg = meta['original_message'] + if len(msg) > 200: + msg = msg[:200] + "..." + print(f" {msg}") + + +def main(): + data_file = "jobs_data.json" + + if not os.path.exists(data_file): + print(f"数据文件不存在: {data_file}") + print("请先运行 job_extractor.py 提取岗位数据") + return + + try: + with open(data_file, "r", encoding="utf-8") as f: + jobs = json.load(f) + except Exception as e: + print(f"读取数据文件失败: {e}") + return + + if not jobs: + print("暂无岗位数据") + return + + print(f"{'=' * 80}") + print(f"岗位数据查看器") + print(f"{'=' * 80}") + print(f"数据文件: {data_file}") + print(f"岗位总数: {len(jobs)}") + + # 统计信息 + locations = {} + companies = {} + for job in jobs: + loc = job.get('job_location', '未知') + locations[loc] = locations.get(loc, 0) + 1 + + comp = job.get('company_name', '未知') + companies[comp] = companies.get(comp, 0) + 1 + + print(f"\n地点分布:") + for loc, count in sorted(locations.items(), key=lambda x: x[1], reverse=True)[:5]: + print(f" {loc}: {count}个岗位") + + print(f"\n公司分布:") + for comp, count in sorted(companies.items(), key=lambda x: x[1], reverse=True)[:5]: + print(f" {comp}: {count}个岗位") + + # 显示详细信息 + print(f"\n{'=' * 80}") + print("岗位详情") + + for i, job in enumerate(jobs, 1): + display_job(job, i) + + print(f"\n{'=' * 80}") + print(f"共显示 {len(jobs)} 个岗位") + print(f"{'=' * 80}") + + +if __name__ == "__main__": + main() diff --git a/wxauto/__init__.py b/wxauto/__init__.py new file mode 100644 index 0000000..0e973c4 --- /dev/null +++ b/wxauto/__init__.py @@ -0,0 +1,16 @@ +from .wx import ( + WeChat, + Chat, + WeChatLogin +) +from .param import WxParam +import pythoncom + +pythoncom.CoInitialize() + +__all__ = [ + 'WeChat', + 'Chat', + 'WeChatLogin', + 'WxParam' +] \ No newline at end of file diff --git a/wxauto/exceptions.py b/wxauto/exceptions.py new file mode 100644 index 0000000..15fbaf2 --- /dev/null +++ b/wxauto/exceptions.py @@ -0,0 +1,5 @@ +class WxautoOCRError(Exception): + ... + +class NetWorkError(Exception): + ... \ No newline at end of file diff --git a/wxauto/languages.py b/wxauto/languages.py new file mode 100644 index 0000000..f7c5096 --- /dev/null +++ b/wxauto/languages.py @@ -0,0 +1,294 @@ +WECHAT_MAIN = { + '新的朋友': {'cn': "新的朋友", 'cn_t': '', 'en': ""}, + "添加朋友": {'cn': "添加朋友", 'cn_t': '', 'en': ""}, + "搜索结果": {'cn': "搜索:", 'cn_t': '', 'en': ""}, + "找不到相关账号或内容": {'cn': "找不到相关账号或内容", 'cn_t': '', 'en': ""}, +} + +WECHAT_CHAT_BOX = { + "查看更多消息": {'cn': "查看更多消息", 'cn_t': '', 'en': ""}, + "消息": {'cn': "消息", 'cn_t': '', 'en': ""}, + "表情" : {'cn': "表情(Alt+E)", 'cn_t': '', 'en': ""}, + "发送文件": {'cn': "发送文件", 'cn_t': '', 'en': ""}, + "截图": {'cn': "截图", 'cn_t': '', 'en': ""}, + "聊天记录": {'cn': "聊天记录", 'cn_t': '', 'en': ""}, + "语音聊天": {'cn': "语音聊天", 'cn_t': '', 'en': ""}, + "视频聊天": {'cn': "视频聊天", 'cn_t': '', 'en': ""}, + '聊天信息': {'cn': "聊天信息", 'cn_t': '', 'en': ""}, + "发送": {'cn': "发送(S)", 'cn_t': '', 'en': ""}, + "置顶": {'cn': "置顶", 'cn_t': '', 'en': ""}, + "最小化": {'cn': "最小化", 'cn_t': '', 'en': ""}, + "最大化": {'cn': "最大化", 'cn_t': '', 'en': ""}, + "关闭": {'cn': "关闭", 'cn_t': '', 'en': ""}, + '以下为新消息': {'cn': "以下为新消息", 'cn_t': '', 'en': ""}, + 're_新消息按钮': {'cn': '.*?条新消息', 'cn_t': '', 'en': ""}, +} + +WECHAT_SESSION_BOX = { + # 聊天页面 + "聊天记录": {'cn': "聊天记录", 'cn_t': '', 'en': ""}, + "会话": {'cn': "会话", 'cn_t': '', 'en': ""}, + "已置顶": {'cn': "已置顶", 'cn_t': '', 'en': ""}, + "文件传输助手": {'cn': "文件传输助手", 'cn_t': '', 'en': ""}, + "折叠的群聊": {'cn': "折叠的群聊", 'cn_t': '', 'en': ""}, + "发起群聊": {'cn': "发起群聊", 'cn_t': '', 'en': ""}, + "搜索": {'cn': "搜索", 'cn_t': '', 'en': ""}, + "re_条数": {'cn': r'\[\d+条\]', 'cn_t': '', 'en': ""}, + + # 联系人页面 + "添加朋友": {'cn': "添加朋友", 'cn_t': '', 'en': ""}, + "联系人": {'cn': "联系人", 'cn_t': '', 'en': ""}, + "通讯录管理": {'cn': "通讯录管理", 'cn_t': '', 'en': ""}, + "新的朋友": {'cn': "新的朋友", 'cn_t': '', 'en': ""}, + "公众号": {'cn': "公众号", 'cn_t': '', 'en': ""}, + "企业号": {'cn': "企业号", 'cn_t': '', 'en': ""}, + "群聊": {'cn': "群聊", 'cn_t': '', 'en': ""}, + + # 收藏页面 + "分类": {'cn': "分类", 'cn_t': '', 'en': ""}, + "新建笔记": {'cn': "新建笔记", 'cn_t': '', 'en': ""}, + "全部收藏": {'cn': "全部收藏", 'cn_t': '', 'en': ""}, + "最近使用": {'cn': "最近使用", 'cn_t': '', 'en': ""}, + "链接": {'cn': "链接", 'cn_t': '', 'en': ""}, + "图片与视频": {'cn': "图片与视频", 'cn_t': '', 'en': ""}, + "笔记": {'cn': "笔记", 'cn_t': '', 'en': ""}, + "文件": {'cn': "文件", 'cn_t': '', 'en': ""}, + "聊天记录": {'cn': "聊天记录", 'cn_t': '', 'en': ""}, + "分割线": {'cn': "分割线", 'cn_t': '', 'en': ""}, + "展开标签": {'cn': "展开标签", 'cn_t': '', 'en': ""}, + "折叠标签": {'cn': "折叠标签", 'cn_t': '', 'en': ""}, + "标签": {'cn': "标签", 'cn_t': '', 'en': ""}, +} + +WECHAT_NAVIGATION_BOX = { + '聊天': {'cn': "聊天", 'cn_t': '', 'en': ""}, + '通讯录': {'cn': "通讯录", 'cn_t': '', 'en': ""}, + '收藏': {'cn': "收藏", 'cn_t': '', 'en': ""}, + '聊天文件': {'cn': "聊天文件", 'cn_t': '', 'en': ""}, + '朋友圈': {'cn': "朋友圈", 'cn_t': '', 'en': ""}, + '搜一搜': {'cn': "搜一搜", 'cn_t': '', 'en': ""}, + "视频号": {'cn': "视频号", 'cn_t': '', 'en': ""}, + "看一看": {'cn': "看一看", 'cn_t': '', 'en': ""}, + "小程序面板": {'cn': "小程序面板", 'cn_t': '', 'en': ""}, + "手机": {'cn': "手机", 'cn_t': '', 'en': ""}, + "设置及其他": {'cn': "设置及其他", 'cn_t': '', 'en': ""}, +} + +EMOTION_WINDOW = { + "添加的单个表情": {'cn': "添加的单个表情", 'cn_t': '', 'en': ""}, +} + +MOMENT_PRIVACY = { + '谁可以看': {'cn': "谁可以看", 'cn_t': '', 'en': ""}, + "公开": {'cn': "公开", 'cn_t': '', 'en': ""}, + "所有朋友可见": {'cn': "所有朋友可见", 'cn_t': '', 'en': ""}, + "私密": {'cn': "私密", 'cn_t': '', 'en': ""}, + "仅自己可见": {'cn': "仅自己可见", 'cn_t': '', 'en': ""}, + "白名单": {'cn': "选中的标签或朋友可见", 'cn_t': '', 'en': ""}, + "黑名单": {'cn': "选中的标签或朋友不可见", 'cn_t': '', 'en': ""}, + "完成": {'cn': "完成", 'cn_t': '', 'en': ""}, + "确定": {'cn': "确定", 'cn_t': '', 'en': ""}, + "取消": {'cn': "取消", 'cn_t': '', 'en': ""} +} + +PROFILE_CARD = { + '微信号': {'cn': "微信号:", 'cn_t': '', 'en': ""}, + '昵称': {'cn': "昵称:", 'cn_t': '', 'en': ""}, + '备注': {'cn': "备注", 'cn_t': '', 'en': ""}, + '地区': {'cn': "地区:", 'cn_t': '', 'en': ""}, + '标签': {'cn': "标签", 'cn_t': '', 'en': ""}, + '共同群聊': {'cn': "共同群聊", 'cn_t': '', 'en': ""}, + '来源': {'cn': "来源", 'cn_t': '', 'en': ""}, + '发消息': {'cn': "发消息", 'cn_t': '', 'en': ""}, + '语音聊天': {'cn': "语音聊天", 'cn_t': '', 'en': ""}, + '视频聊天': {'cn': "视频聊天", 'cn_t': '', 'en': ""}, + '更多': {'cn': "更多", 'cn_t': '', 'en': ""}, + "设置备注和标签": {'cn': "设置备注和标签", 'cn_t': '', 'en': ""}, + '确定': {'cn': "确定", 'cn_t': '', 'en': ""}, + '输入标签': {'cn': "输入标签", 'cn_t': '', 'en': ""}, + '备注名': {'cn': "备注名", 'cn_t': '', 'en': ""}, +} + +MESSAGES = { + '[图片]': {'cn': "[图片]", 'cn_t': '', 'en': ""}, + '[视频]': {'cn': "[视频]", 'cn_t': '', 'en': ""}, + '[语音]': {'cn': "[语音]", 'cn_t': '', 'en': ""}, + '[音乐]': {'cn': "[音乐]", 'cn_t': '', 'en': ""}, + '[位置]': {'cn': "[位置]", 'cn_t': '', 'en': ""}, + '[链接]': {'cn': "[链接]", 'cn_t': '', 'en': ""}, + '[文件]': {'cn': "[文件]", 'cn_t': '', 'en': ""}, + '[名片]': {'cn': "[名片]", 'cn_t': '', 'en': ""}, + '[笔记]': {'cn': "[笔记]", 'cn_t': '', 'en': ""}, + '[视频号]': {'cn': "[视频号]", 'cn_t': '', 'en': ""}, + '[动画表情]': {'cn': "[动画表情]", 'cn_t': '', 'en': ""}, + '[聊天记录]': {'cn': "[聊天记录]", 'cn_t': '', 'en': ""}, + '微信转账': {'cn': "微信转账", 'cn_t': '', 'en': ""}, + '接收中': {'cn': "接收中", 'cn_t': '', 'en': ""}, + 're_语音': {'cn': "^\[语音\]\d+秒(,未播放)?$", 'cn_t': '', 'en': ""}, + 're_引用消息': {'cn': "(^.+)\n引用.*?的消息 : (.+$)", 'cn_t': '', 'en': ""}, + 're_拍一拍': {'cn': "^.+拍了拍.+$", 'cn_t': '', 'en': ""}, +} + +CHATROOM_DETAIL_WINDOW = { + "聊天信息": {'cn': "聊天信息", 'cn_t': '', 'en': ""}, + "查看更多": {'cn': "查看更多", 'cn_t': '', 'en': ""}, + '群聊名称': {'cn': "群聊名称", 'cn_t': '', 'en': ""}, + '仅群主或管理员可以修改': {'cn': "仅群主或管理员可以修改", 'cn_t': '', 'en': ""}, + '我在本群的昵称': {'cn': "我在本群的昵称", 'cn_t': '', 'en': ""}, + "仅群主和管理员可编辑": {'cn': "仅群主和管理员可编辑", 'cn_t': '', 'en': ""}, + '点击编辑群公告': {'cn': "点击编辑群公告", 'cn_t': '', 'en': ""}, + '编辑': {'cn': "编辑", 'cn_t': '', 'en': ""}, + '备注': {'cn': "备注", 'cn_t': '', 'en': ""}, + '群公告': {'cn': "群公告", 'cn_t': '', 'en': ""}, + '分隔线': {'cn': "分隔线", 'cn_t': '', 'en': ""}, + '完成': {'cn': "完成", 'cn_t': '', 'en': ""}, + '发布': {'cn': "发布", 'cn_t': '', 'en': ""}, + '退出群聊': {'cn': "退出群聊", 'cn_t': '', 'en': ""}, + '退出': {'cn': "退出", 'cn_t': '', 'en': ""}, + '聊天成员': {'cn': "聊天成员", 'cn_t': '', 'en': ""}, + "添加": {'cn': "添加", 'cn_t': '', 'en': ""}, + "移出": {'cn': "移出", 'cn_t': '', 'en': ""}, + "re_退出群聊": {'cn': "将退出群聊“.*?”", 'cn_t': '', 'en': ""}, +} + +PROFILE_WINDOW = { + "微信号": {'cn': "微信号:", 'cn_t': '', 'en': ""}, + "昵称": {'cn': "昵称:", 'cn_t': '', 'en': ""}, + "地区": {'cn': "地区:", 'cn_t': '', 'en': ""}, + "个性签名": {'cn': "个性签名", 'cn_t': '', 'en': ""}, + "来源": {'cn': "来源", 'cn_t': '', 'en': ""}, + "备注": {'cn': "备注", 'cn_t': '', 'en': ""}, + "共同群聊": {'cn': "共同群聊", 'cn_t': '', 'en': ""}, + '添加到通讯录': {'cn': "添加到通讯录", 'cn_t': '', 'en': ""}, + '更多': {'cn': "更多", 'cn_t': '', 'en': ""}, +} + +ADD_NEW_FRIEND_WINDOW = { + '标签': {'cn': "标签", 'cn_t': '', 'en': ""}, + '确定': {'cn': "确定", 'cn_t': '', 'en': ""}, + '备注名': {'cn': "备注名", 'cn_t': '', 'en': ""}, + '朋友圈': {'cn': "朋友圈", 'cn_t': '', 'en': ""}, + '仅聊天': {'cn': "仅聊天", 'cn_t': '', 'en': ""}, + '聊天、朋友圈、微信运动等': { + 'cn': "聊天、朋友圈、微信运动等", + 'cn_t': '', + 'en': "" + }, + "你的联系人较多,添加新的朋友时需选择权限": { + 'cn': "你的联系人较多,添加新的朋友时需选择权限", + 'cn_t': '', + 'en': "" + }, + "发送添加朋友申请": { + 'cn': "发送添加朋友申请", + 'cn_t': '', + 'en': "" + } +} + +ADD_GROUP_MEMBER_WINDOW = { + '搜索': {'cn': "搜索", 'cn_t': '', 'en': ""}, + '确定': {'cn': "确定", 'cn_t': '', 'en': ""}, + '完成': {'cn': "完成", 'cn_t': '', 'en': ""}, + '发送': {'cn': "发送", 'cn_t': '', 'en': ""}, + '已选择联系人': {'cn': "已选择联系人", 'cn_t': '', 'en': ""}, + '请勾选需要添加的联系人': { + 'cn': "请勾选需要添加的联系人", + 'cn_t': '', + 'en': "" + } +} + +IMAGE_WINDOW = { + '上一张': {'cn': '上一张', 'cn_t': '上一張', 'en': 'Previous'}, + '下一张': {'cn': '下一张', 'cn_t': '下一張', 'en': 'Next'}, + '预览': {'cn': '预览', 'cn_t': '預覽', 'en': 'Preview'}, + '放大': {'cn': '放大', 'cn_t': '放大', 'en': 'Zoom'}, + '缩小': {'cn': '缩小', 'cn_t': '縮小', 'en': 'Shrink'}, + '图片原始大小': {'cn': '图片原始大小', 'cn_t': '圖片原始大小', 'en': 'Original image size'}, + '旋转': {'cn': '旋转', 'cn_t': '旋轉', 'en': 'Rotate'}, + '编辑': {'cn': '编辑', 'cn_t': '編輯', 'en': 'Edit'}, + '翻译': {'cn': '翻译', 'cn_t': '翻譯', 'en': 'Translate'}, + '提取文字': {'cn': '提取文字', 'cn_t': '提取文字', 'en': 'Extract Text'}, + '识别图中二维码': {'cn': '识别图中二维码', 'cn_t': '識别圖中QR Code', 'en': 'Extract QR Code'}, + '另存为': {'cn': '另存为...', 'cn_t': '另存爲...', 'en': 'Save as…'}, + '更多': {'cn': '更多', 'cn_t': '更多', 'en': 'More'}, + '复制': {'cn': '复制', 'cn_t': '複製', 'en': 'Copy'}, + '最小化': {'cn': '最小化', 'cn_t': '最小化', 'en': 'Minimize'}, + '最大化': {'cn': '最大化', 'cn_t': '最大化', 'en': 'Maximize'}, + '关闭': {'cn': '关闭', 'cn_t': '關閉', 'en': 'Close'}, + '': {'cn': '', 'cn_t': '', 'en': ''} +} + +MENU_OPTIONS = { + # session + '置顶': {'cn': '置顶', 'cn_t': '', 'en': ''}, + '取消置顶': {'cn': '取消置顶', 'cn_t': '', 'en': ''}, + '标为未读': {'cn': '标为未读', 'cn_t': '', 'en': ''}, + '消息免打扰': {'cn': '消息免打扰', 'cn_t': '', 'en': ''}, + '在独立窗口打开': {'cn': '在独立窗口打开', 'cn_t': '', 'en': ''}, + '不显示聊天': {'cn': '不显示聊天', 'cn_t': '', 'en': ''}, + '删除聊天': {'cn': '删除聊天', 'cn_t': '', 'en': ''}, + + # msg + '撤回': {'cn': '撤回', 'cn_t': '', 'en': ''}, + '复制': {'cn': '复制', 'cn_t': '', 'en': ''}, + '放大阅读': {'cn': '放大阅读', 'cn_t': '', 'en': ''}, + '翻译': {'cn': '翻译', 'cn_t': '', 'en': ''}, + '转发': {'cn': '转发...', 'cn_t': '', 'en': ''}, + '收藏': {'cn': '收藏', 'cn_t': '', 'en': ''}, + '多选': {'cn': '多选', 'cn_t': '', 'en': ''}, + '引用': {'cn': '引用', 'cn_t': '', 'en': ''}, + '搜一搜': {'cn': '搜一搜', 'cn_t': '', 'en': ''}, + '删除': {'cn': '删除', 'cn_t': '', 'en': ''}, + '编辑': {'cn': '编辑', 'cn_t': '', 'en': ''}, + '另存为': {'cn': '另存为...', 'cn_t': '', 'en': ''}, + '语音转文字': {'cn': '语音转文字', 'cn_t': '', 'en': ''}, + '在文件夹中显示': {'cn': '在文件夹中显示', 'cn_t': '', 'en': ''}, + + + # edit + '剪切': {'cn': '剪切', 'cn_t': '', 'en': ''}, + '粘贴': {'cn': '粘贴', 'cn_t': '', 'en': ''}, + +} + +MOMENTS = { + '朋友圈': {'cn': '朋友圈', 'cn_t': '', 'en': ''}, + '刷新': {'cn': '刷新', 'cn_t': '', 'en': ''}, + '评论': {'cn': '评论', 'cn_t': '', 'en': ''}, + '广告': {'cn': '广告', 'cn_t': '', 'en': ''}, + '赞': {'cn': '赞', 'cn_t': '', 'en': ''}, + '取消': {'cn': '取消', 'cn_t': '', 'en': ''}, + '发送': {'cn': '发送', 'cn_t': '', 'en': ''}, + '分隔符_点赞': {'cn': ',', 'cn_t': '', 'en': ''}, + 're_图片数': {'cn': '包含\d+张图片', 'cn_t': '', 'en': ''}, +} + +NEW_FRIEND_ELEMENT = { + '新的朋友': {'cn': '新的朋友', 'cn_t': '', 'en': ''}, + '回复': {'cn': '回复', 'cn_t': '', 'en': ''}, + '发送': {'cn': '发送', 'cn_t': '', 'en': ''}, + '朋友圈': {'cn': '朋友圈', 'cn_t': '', 'en': ''}, + '仅聊天': {'cn': '仅聊天', 'cn_t': '', 'en': ''}, + '聊天、朋友圈、微信运动等': {'cn': '聊天、朋友圈、微信运动等', 'cn_t': '', 'en': ''}, + '备注名': {'cn': '备注名', 'cn_t': '', 'en': ''}, + '标签': {'cn': '标签', 'cn_t': '', 'en': ''}, +} + +WECHAT_BROWSER = { + "关闭": {'cn': '关闭', 'cn_t': '', 'en': ''}, + "更多": {'cn': '更多', 'cn_t': '', 'en': ''}, + '地址和搜索栏': {'cn': '地址和搜索栏', 'cn_t': '', 'en': ''}, + '转发给朋友': {'cn': '转发给朋友', 'cn_t': '', 'en': ''}, + '复制链接': {'cn': '复制链接', 'cn_t': '', 'en': ''}, +} + +WECHAT_LOGINWND = { + "进入微信": {'cn': '进入微信', 'cn_t': '', 'en': ''}, + "切换账号": {'cn': '切换账号', 'cn_t': '', 'en': ''}, + "仅传输文件": {'cn': '仅传输文件', 'cn_t': '', 'en': ''}, + "二维码": {'cn': '仅传输文件', 'cn_t': '', 'en': ''}, + "提示": {'cn': '提示', 'cn_t': '', 'en': ''}, + "确定": {'cn': '确定', 'cn_t': '', 'en': ''}, +} \ No newline at end of file diff --git a/wxauto/logger.py b/wxauto/logger.py new file mode 100644 index 0000000..6034a15 --- /dev/null +++ b/wxauto/logger.py @@ -0,0 +1,128 @@ +from wxauto.param import WxParam + +import logging +import colorama +from pathlib import Path +from datetime import datetime +import sys +import io + + +# # 初始化 colorama +colorama.init() + +if hasattr(sys.stdout, 'buffer'): + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='ignore') + +LOG_COLORS = { + 'DEBUG': colorama.Fore.CYAN, + 'INFO': colorama.Fore.GREEN, + 'WARNING': colorama.Fore.YELLOW, + 'ERROR': colorama.Fore.RED, + 'CRITICAL': colorama.Fore.MAGENTA +} + +class ColoredFormatter(logging.Formatter): + def format(self, record): + levelname = record.levelname + message = super().format(record) + return f"{LOG_COLORS[levelname]}{message}{colorama.Style.RESET_ALL}" + +class WxautoLogger: + name: str = 'wxauto' + + def __init__(self): + self.logger = self.setup_logger() + self.file_handler = None # 先不创建文件处理器 + self.set_debug(False) + + def setup_logger(self) -> logging.Logger: + """设置日志记录器""" + # 配置根记录器 + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + + # 添加asyncio日志过滤 + logging.getLogger('asyncio').setLevel(logging.WARNING) + + # 设置第三方库的日志级别 + logging.getLogger('comtypes').setLevel(logging.WARNING) + logging.getLogger('urllib3').setLevel(logging.WARNING) + logging.getLogger('requests').setLevel(logging.WARNING) + + # 清除现有处理器 + root_logger.handlers.clear() + + # 格式 + fmt = '%(asctime)s [%(name)s] [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s' + + # 控制台处理器(带颜色) + self.console_handler = logging.StreamHandler() + console_formatter = ColoredFormatter( + fmt=fmt, + datefmt="%Y-%m-%d %H:%M:%S" + ) + self.console_handler.setFormatter(console_formatter) + self.console_handler.setLevel(logging.DEBUG) + + root_logger.addHandler(self.console_handler) + + return logging.getLogger(self.name) + + def setup_file_logger(self): + """根据WxParam.ENABLE_FILE_LOGGER决定是否创建文件日志处理器""" + if not WxParam.ENABLE_FILE_LOGGER or self.file_handler is not None: + return + + # 文件处理器(无颜色) + log_dir = Path("wxauto_logs") + log_dir.mkdir(parents=True, exist_ok=True) + + # 使用当前时间创建日志文件 + current_time = datetime.now().strftime("%Y%m%d") + log_file = log_dir / f"app_{current_time}.log" + + self.file_handler = logging.FileHandler(log_file, encoding='utf-8') + file_formatter = logging.Formatter( + '%(asctime)s [%(name)s] [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt="%Y-%m-%d %H:%M:%S" + ) + self.file_handler.setFormatter(file_formatter) + self.file_handler.setLevel(logging.DEBUG) + + # 将文件处理器添加到日志记录器 + logging.getLogger().addHandler(self.file_handler) + + def set_debug(self, debug=False): + """动态设置日志级别""" + if debug: + self.console_handler.setLevel(logging.DEBUG) + else: + self.console_handler.setLevel(logging.INFO) + + def _ensure_file_logger(self): + """确保文件日志处理器被初始化""" + if WxParam.ENABLE_FILE_LOGGER and self.file_handler is None: + self.setup_file_logger() + + def debug(self, msg: str, stacklevel=2, *args, **kwargs): + self._ensure_file_logger() # 确保文件日志初始化 + self.logger.debug(msg, *args, stacklevel=stacklevel, **kwargs) + + def info(self, msg: str, stacklevel=2, *args, **kwargs): + self._ensure_file_logger() # 确保文件日志初始化 + self.logger.info(msg, *args, stacklevel=stacklevel, **kwargs) + + def warning(self, msg: str, stacklevel=2, *args, **kwargs): + self._ensure_file_logger() # 确保文件日志初始化 + self.logger.warning(msg, *args, stacklevel=stacklevel, **kwargs) + + def error(self, msg: str, stacklevel=2, *args, **kwargs): + self._ensure_file_logger() # 确保文件日志初始化 + self.logger.error(msg, *args, stacklevel=stacklevel, **kwargs) + + def critical(self, msg: str, stacklevel=2, *args, **kwargs): + self._ensure_file_logger() # 确保文件日志初始化 + self.logger.critical(msg, *args, stacklevel=stacklevel, **kwargs) + +wxlog = WxautoLogger() \ No newline at end of file diff --git a/wxauto/msgs/__init__.py b/wxauto/msgs/__init__.py new file mode 100644 index 0000000..9ac0454 --- /dev/null +++ b/wxauto/msgs/__init__.py @@ -0,0 +1,75 @@ +from .msg import parse_msg +from .base import ( + BaseMessage, + HumanMessage +) +from .attr import ( + SystemMessage, + TickleMessage, + TimeMessage, + FriendMessage, + SelfMessage +) +from .type import ( + TextMessage, + ImageMessage, + VoiceMessage, + VideoMessage, + FileMessage, + LinkMessage, + OtherMessage +) +from .self import ( + SelfMessage, + SelfTextMessage, + SelfVoiceMessage, + SelfImageMessage, + SelfVideoMessage, + SelfFileMessage, + SelfLinkMessage, + SelfOtherMessage, +) +from .friend import ( + FriendMessage, + FriendTextMessage, + FriendVoiceMessage, + FriendImageMessage, + FriendVideoMessage, + FriendFileMessage, + FriendLinkMessage, + FriendOtherMessage, +) + +__all__ = [ + 'parse_msg', + 'BaseMessage', + 'HumanMessage', + 'SystemMessage', + 'TickleMessage', + 'TimeMessage', + 'FriendMessage', + 'SelfMessage', + 'TextMessage', + 'ImageMessage', + 'VoiceMessage', + 'VideoMessage', + 'FileMessage', + 'LinkMessage', + 'OtherMessage', + 'SelfMessage', + 'SelfTextMessage', + 'SelfVoiceMessage', + 'SelfImageMessage', + 'SelfVideoMessage', + 'SelfFileMessage', + 'SelfLinkMessage', + 'SelfOtherMessage', + 'FriendMessage', + 'FriendTextMessage', + 'FriendVoiceMessage', + 'FriendImageMessage', + 'FriendVideoMessage', + 'FriendFileMessage', + 'FriendLinkMessage', + 'FriendOtherMessage', +] \ No newline at end of file diff --git a/wxauto/msgs/attr.py b/wxauto/msgs/attr.py new file mode 100644 index 0000000..4009218 --- /dev/null +++ b/wxauto/msgs/attr.py @@ -0,0 +1,85 @@ +from .base import * +from wxauto.utils.tools import ( + parse_wechat_time +) + +class SystemMessage(BaseMessage): + attr = 'system' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + self.sender = 'system' + self.sender_remark = 'system' + +class TickleMessage(SystemMessage): + attr = 'tickle' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + self.tickle_list = [ + i.Name for i in + control.ListItemControl().GetParentControl().GetChildren() + ] + self.content = f"[{len(self.tickle_list)}条]{self.tickle_list[0]}" + + +class TimeMessage(SystemMessage): + attr = 'time' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + self.time = parse_wechat_time(self.content) + +class FriendMessage(HumanMessage): + attr = 'friend' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + self.head_control = self.control.ButtonControl(RegexName='.*?') + self.sender = self.head_control.Name + if ( + (remark_control := self.control.TextControl()).Exists(0) + and remark_control.BoundingRectangle.top < self.head_control.BoundingRectangle.top + ): + self.sender_remark = remark_control.Name + else: + self.sender_remark = self.sender + + @property + def _xbias(self): + if WxParam.FORCE_MESSAGE_XBIAS: + return int(self.head_control.BoundingRectangle.width()*1.5) + return WxParam.DEFAULT_MESSAGE_XBIAS + + +class SelfMessage(HumanMessage): + attr = 'self' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + + @property + def _xbias(self): + if WxParam.FORCE_MESSAGE_XBIAS: + return -int(self.head_control.BoundingRectangle.width()*1.5) + return -WxParam.DEFAULT_MESSAGE_XBIAS \ No newline at end of file diff --git a/wxauto/msgs/base.py b/wxauto/msgs/base.py new file mode 100644 index 0000000..fe1cfa9 --- /dev/null +++ b/wxauto/msgs/base.py @@ -0,0 +1,185 @@ +from wxauto import uiautomation as uia +from wxauto.logger import wxlog +from wxauto.param import ( + WxResponse, + WxParam, + PROJECT_NAME +) +from wxauto.ui.component import ( + CMenuWnd, + SelectContactWnd +) +from wxauto.utils.tools import roll_into_view +from wxauto.languages import * +from typing import ( + Dict, + List, + Union, + TYPE_CHECKING +) +from hashlib import md5 +import time + +if TYPE_CHECKING: + from wxauto.ui.chatbox import ChatBox + +def truncate_string(s: str, n: int=8) -> str: + s = s.replace('\n', '').strip() + return s if len(s) <= n else s[:n] + '...' + +class Message:... +class BaseMessage(Message): + type: str = 'base' + attr: str = 'base' + control: uia.Control + + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + ): + self.control = control + self.parent = parent + self.root = parent.root + self.content = self.control.Name + self.id = self.control.runtimeid + self.sender = self.attr + self.sender_remark = self.attr + + def __repr__(self): + cls_name = self.__class__.__name__ + content = truncate_string(self.content) + return f"<{PROJECT_NAME} - {cls_name}({content}) at {hex(id(self))}>" + + @property + def message_type_name(self) -> str: + return self.__class__.__name__ + + def chat_info(self) -> Dict: + if self.control.Exists(0): + return self.parent.get_info() + + def _lang(self, text: str) -> str: + return MESSAGES.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + + def roll_into_view(self) -> WxResponse: + if roll_into_view(self.control.GetParentControl(), self.control, equal=True) == 'not exist': + wxlog.warning('消息目标控件不存在,无法滚动至显示窗口') + return WxResponse.failure('消息目标控件不存在,无法滚动至显示窗口') + return WxResponse.success('成功') + + @property + def info(self) -> Dict: + _info = self.parent.get_info().copy() + _info['class'] = self.message_type_name + _info['id'] = self.id + _info['type'] = self.type + _info['attr'] = self.attr + _info['content'] = self.content + return _info + + +class HumanMessage(BaseMessage): + attr = 'human' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + ): + super().__init__(control, parent) + self.head_control = self.control.ButtonControl(searchDepth=2) + + + def roll_into_view(self) -> WxResponse: + if roll_into_view(self.control.GetParentControl(), self.head_control, equal=True) == 'not exist': + return WxResponse.failure('消息目标控件不存在,无法滚动至显示窗口') + return WxResponse.success('成功') + + def click(self): + self.roll_into_view() + self.head_control.Click(x=self._xbias) + + def right_click(self): + self.roll_into_view() + self.head_control.Click(x=-self._xbias) + self.head_control.RightClick(x=self._xbias) + + def select_option(self, option: str, timeout=None) -> WxResponse: + self.root._show() + def _select_option(self, option): + if not (roll_result := self.roll_into_view()): + return roll_result + self.right_click() + menu = CMenuWnd(self.root) + return menu.select(item=option) + + if timeout: + t0 = time.time() + while True: + if (time.time() - t0) > timeout: + return WxResponse(False, '引用消息超时') + if quote_result := _select_option(self, option): + return quote_result + + else: + return _select_option(self, option) + + def quote( + self, text: str, + at: Union[List[str], str] = None, + timeout: int = 3 + ) -> WxResponse: + """引用消息 + + Args: + text (str): 引用内容 + at (List[str], optional): @用户列表 + timeout (int, optional): 超时时间,单位为秒,若为None则不启用超时设置 + + Returns: + WxResponse: 调用结果 + """ + if not self.select_option('引用', timeout=timeout): + wxlog.debug(f"当前消息无法引用:{self.content}") + return WxResponse(False, '当前消息无法引用') + + if at: + self.parent.input_at(at) + + return self.parent.send_text(text) + + def reply( + self, text: str, + at: Union[List[str], str] = None + ) -> WxResponse: + """引用消息 + + Args: + text (str): 回复内容 + at (List[str], optional): @用户列表 + timeout (int, optional): 超时时间,单位为秒,若为None则不启用超时设置 + + Returns: + WxResponse: 调用结果 + """ + if at: + self.parent.input_at(at) + + return self.parent.send_text(text) + + def forward(self, targets: Union[List[str], str], timeout: int = 3) -> WxResponse: + """转发消息 + + Args: + targets (Union[List[str], str]): 目标用户列表 + timeout (int, optional): 超时时间,单位为秒,若为None则不启用超时设置 + + Returns: + WxResponse: 调用结果 + """ + if not self.select_option('转发', timeout=timeout): + return WxResponse(False, '当前消息无法转发') + + select_wnd = SelectContactWnd(self) + return select_wnd.send(targets) \ No newline at end of file diff --git a/wxauto/msgs/friend.py b/wxauto/msgs/friend.py new file mode 100644 index 0000000..5c03975 --- /dev/null +++ b/wxauto/msgs/friend.py @@ -0,0 +1,73 @@ +from .type import * +from .attr import FriendMessage +import sys + +class FriendTextMessage(FriendMessage, TextMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + +class FriendQuoteMessage(FriendMessage, QuoteMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + ): + super().__init__(control, parent) + +class FriendImageMessage(FriendMessage, ImageMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + + ): + super().__init__(control, parent) + +class FriendFileMessage(FriendMessage, FileMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + + ): + super().__init__(control, parent) + +class FriendLinkMessage(FriendMessage, LinkMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + + ): + super().__init__(control, parent) + +class FriendVideoMessage(FriendMessage, VideoMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + + ): + super().__init__(control, parent) + +class FriendVoiceMessage(FriendMessage, VoiceMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + + ): + super().__init__(control, parent) + +class FriendOtherMessage(FriendMessage, OtherMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + + ): + super().__init__(control, parent) \ No newline at end of file diff --git a/wxauto/msgs/msg.py b/wxauto/msgs/msg.py new file mode 100644 index 0000000..592f9db --- /dev/null +++ b/wxauto/msgs/msg.py @@ -0,0 +1,133 @@ +from .attr import * +from .type import OtherMessage +from . import self as selfmsg +from . import friend as friendmsg +from wxauto.languages import * +from wxauto.param import WxParam +from wxauto import uiautomation as uia +from typing import Literal +import re + +class MESSAGE_ATTRS: + SYS_TEXT_HEIGHT = 33 + TIME_TEXT_HEIGHT = 34 + CHAT_TEXT_HEIGHT = 52 + FILE_MSG_HEIGHT = 115 + LINK_MSG_HEIGHT = 115 + VOICE_MSG_HEIGHT = 55 + + TEXT_MSG_CONTROL_NUM = (8, 9, 10, 11) + TIME_MSG_CONTROL_NUM = (1,) + SYS_MSG_CONTROL_NUM = (4,5,6) + IMG_MSG_CONTROL_NUM = (9, 10, 11, 12) + FILE_MSG_CONTROL_NUM = tuple(i for i in range(15, 30)) + LINK_MSG_CONTROL_NUM = tuple(i for i in range(15, 30)) + VOICE_MSG_CONTROL_NUM = tuple(i for i in range(10, 30)) + VIDEO_MSG_CONTROL_NUM = (13, 14, 15, 16) + QUOTE_MSG_CONTROL_NUM = tuple(i for i in range(16, 30)) + LINK_MSG_CONTROL_NUM = tuple(i for i in range(15, 30)) + +def _lang(text: str) -> str: + return MESSAGES.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + +SEPICIAL_MSGS = [ + _lang(i) + for i in [ + '[图片]', # ImageMessage + '[视频]', # VideoMessage + '[语音]', # VoiceMessage + '[文件]', # FileMessage + '[链接]', # LinkMessage + ] +] + +def parse_msg_attr( + control: uia.Control, + parent, + ): + msg_rect = control.BoundingRectangle + height = msg_rect.height() + mid = (msg_rect.left + msg_rect.right) / 2 + for length, _ in enumerate(uia.WalkControl(control)):length += 1 + + # TimeMessage + if ( + length in MESSAGE_ATTRS.TIME_MSG_CONTROL_NUM + ): + return TimeMessage(control, parent) + + # FriendMessage or SelfMessage + if (head_control := control.ButtonControl(searchDepth=2)).Exists(0): + head_rect = head_control.BoundingRectangle + if head_rect.left < mid: + return parse_msg_type(control, parent, 'Friend') + else: + return parse_msg_type(control, parent, 'Self') + + # SystemMessage or TickleMessage + else: + if length in MESSAGE_ATTRS.SYS_MSG_CONTROL_NUM: + return SystemMessage(control, parent) + elif control.ListItemControl(RegexName=_lang('re_拍一拍')).Exists(0): + return TickleMessage(control, parent) + else: + return OtherMessage(control, parent) + +def parse_msg_type( + control: uia.Control, + parent, + attr: Literal['Self', 'Friend'] + ): + for length, _ in enumerate(uia.WalkControl(control)):length += 1 + content = control.Name + wxlog.debug(f'content: {content}, length: {length}') + + if attr == 'Friend': + msgtype = friendmsg + else: + msgtype = selfmsg + + # Special Message Type + if content in SEPICIAL_MSGS: + # ImageMessage + if content == _lang('[图片]') and length in MESSAGE_ATTRS.IMG_MSG_CONTROL_NUM: + return getattr(msgtype, f'{attr}ImageMessage')(control, parent) + + # VideoMessage + elif content == _lang('[视频]') and length in MESSAGE_ATTRS.VIDEO_MSG_CONTROL_NUM: + return getattr(msgtype, f'{attr}VideoMessage')(control, parent) + + # FileMessage + elif content == _lang('[文件]') and length in MESSAGE_ATTRS.FILE_MSG_CONTROL_NUM: + return getattr(msgtype, f'{attr}FileMessage')(control, parent) + + # LinkMessage + elif content == _lang('[链接]') and length in MESSAGE_ATTRS.LINK_MSG_CONTROL_NUM: + return getattr(msgtype, f'{attr}LinkMessage')(control, parent) + + # TextMessage + if length in MESSAGE_ATTRS.TEXT_MSG_CONTROL_NUM: + return getattr(msgtype, f'{attr}TextMessage')(control, parent) + + # QuoteMessage + elif ( + rematch := re.compile(_lang('re_引用消息'), re.DOTALL).match(content) + and length in MESSAGE_ATTRS.QUOTE_MSG_CONTROL_NUM + ): + return getattr(msgtype, f'{attr}QuoteMessage')(control, parent) + + # VoiceMessage + elif ( + rematch := re.compile(_lang('re_语音')).match(content) + and length in MESSAGE_ATTRS.VOICE_MSG_CONTROL_NUM + ): + return getattr(msgtype, f'{attr}VoiceMessage')(control, parent) + + return getattr(msgtype, f'{attr}OtherMessage')(control, parent) + +def parse_msg( + control: uia.Control, + parent +): + result = parse_msg_attr(control, parent) + return result \ No newline at end of file diff --git a/wxauto/msgs/self.py b/wxauto/msgs/self.py new file mode 100644 index 0000000..867f643 --- /dev/null +++ b/wxauto/msgs/self.py @@ -0,0 +1,67 @@ +from .type import * +from .attr import SelfMessage +import sys + +class SelfTextMessage(SelfMessage, TextMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + +class SelfQuoteMessage(SelfMessage, QuoteMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + ): + super().__init__(control, parent) + +class SelfImageMessage(SelfMessage, ImageMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + +class SelfFileMessage(SelfMessage, FileMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + +class SelfLinkMessage(SelfMessage, LinkMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + +class SelfVideoMessage(SelfMessage, VideoMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + +class SelfVoiceMessage(SelfMessage, VoiceMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + +class SelfOtherMessage(SelfMessage, OtherMessage): + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) \ No newline at end of file diff --git a/wxauto/msgs/type.py b/wxauto/msgs/type.py new file mode 100644 index 0000000..c250314 --- /dev/null +++ b/wxauto/msgs/type.py @@ -0,0 +1,222 @@ +from wxauto.utils.tools import ( + get_file_dir, +) +from wxauto.ui.component import ( + CMenuWnd, + WeChatImage, + WeChatBrowser, +) +from wxauto.utils.win32 import ( + ReadClipboardData, + SetClipboardText, +) +from .base import * +from typing import ( + Union, +) +from pathlib import Path +import shutil +import re + +class TextMessage(HumanMessage): + type = 'text' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + +class QuoteMessage(HumanMessage): + type = 'quote' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + ): + super().__init__(control, parent) + self.content, self.quote_content = \ + re.findall(self._lang('re_引用消息'), self.content, re.DOTALL)[0] + +class MediaMessage: + + def download( + self, + dir_path: Union[str, Path] = None, + timeout: int = 10 + ) -> Path: + if dir_path is None: + dir_path = WxParam.DEFAULT_SAVE_PATH + if self.type == 'image': + filename = f"wxauto_{self.type}_{time.strftime('%Y%m%d%H%M%S')}.png" + elif self.type == 'video': + filename = f"wxauto_{self.type}_{time.strftime('%Y%m%d%H%M%S')}.mp4" + filepath = get_file_dir(dir_path) / filename + + self.click() + + t0 = time.time() + while True: + self.right_click() + menu = CMenuWnd(self) + if menu and menu.select('复制'): + try: + clipboard_data = ReadClipboardData() + cpath = clipboard_data['15'][0] + break + except: + pass + else: + menu.close() + if time.time() - t0 > timeout: + return WxResponse.failure(f'下载超时: {self.type}') + time.sleep(0.1) + + shutil.copyfile(cpath, filepath) + SetClipboardText('') + if imagewnd := WeChatImage(): + imagewnd.close() + return filepath + +class ImageMessage(HumanMessage, MediaMessage): + type = 'image' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + +class VideoMessage(HumanMessage, MediaMessage): + type = 'video' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + +class VoiceMessage(HumanMessage): + type = 'voice' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + + def to_text(self): + """语音转文字""" + if self.control.GetProgenyControl(8, 4): + return self.control.GetProgenyControl(8, 4).Name + voicecontrol = self.control.ButtonControl(Name='') + if not voicecontrol.Exists(0.5): + return WxResponse.failure('语音转文字失败') + self.right_click() + menu = CMenuWnd(self.parent) + menu.select('语音转文字') + + text = '' + while True: + if not self.control.Exists(0): + return WxResponse.failure('消息已撤回') + text_control = self.control.GetProgenyControl(8, 4) + if text_control is not None: + if text_control.Name == text: + return text + text = text_control.Name + time.sleep(0.1) + +class LinkMessage(HumanMessage): + type = 'link' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + + def get_url(self) -> str: + self.click() + if webbrower := WeChatBrowser(): + url = webbrower.get_url() + webbrower.close() + return url + else: + wxlog.debug(f'找不到浏览器窗口') + + return None + +class FileMessage(HumanMessage): + type = 'file' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox" + ): + super().__init__(control, parent) + #self.filename = control.TextControl().Name + self.filename = control.GetProgenyControl(9, control_type='TextControl').Name + self.filesize = control.GetProgenyControl(10, control_type='TextControl').Name + + def download( + self, + dir_path: Union[str, Path] = None, + force_click: bool = False, + timeout: int = 10 + ) -> Path: + """下载文件""" + if dir_path is None: + dir_path = WxParam.DEFAULT_SAVE_PATH + filepath = get_file_dir(dir_path) / self.filename + t0 = time.time() + def open_file_menu(): + while not (menu := CMenuWnd(self.parent)): + self.roll_into_view() + self.right_click() + return menu + if force_click: + self.click() + while True: + if time.time() - t0 > timeout: + return WxResponse.failure("文件下载超时") + try: + if self.control.TextControl(Name=self._lang('接收中')).Exists(0): + time.sleep(0.1) + continue + menu = open_file_menu() + if (option := self._lang('复制')) in menu.option_names: + menu.select(option) + temp_filepath = Path(ReadClipboardData().get('15')[0]) + break + except: + time.sleep(0.1) + + t0 = time.time() + while True: + if time.time() - t0 > 2: + return WxResponse.failure("文件下载超时") + try: + shutil.copyfile(temp_filepath, filepath) + SetClipboardText('') + return filepath + except: + time.sleep(0.01) + +class OtherMessage(BaseMessage): + type = 'other' + + def __init__( + self, + control: uia.Control, + parent: "ChatBox", + ): + super().__init__(control, parent) + self.url = control.TextControl().Name diff --git a/wxauto/param.py b/wxauto/param.py new file mode 100644 index 0000000..7558583 --- /dev/null +++ b/wxauto/param.py @@ -0,0 +1,65 @@ +from typing import Literal +import os + +PROJECT_NAME = 'wxauto' + +class WxParam: + # 语言设置 + LANGUAGE: Literal['cn', 'cn_t', 'en'] = 'cn' + + # 是否启用日志文件 + ENABLE_FILE_LOGGER: bool = True + + # 下载文件/图片默认保存路径 + DEFAULT_SAVE_PATH: str = os.path.join(os.getcwd(), 'wxauto文件下载') + + # 是否启用消息哈希值用于辅助判断消息,开启后会稍微影响性能 + MESSAGE_HASH: bool = False + + # 头像到消息X偏移量,用于消息定位,点击消息等操作 + DEFAULT_MESSAGE_XBIAS = 51 + + # 是否强制重新自动获取X偏移量,如果设置为True,则每次启动都会重新获取 + FORCE_MESSAGE_XBIAS: bool = True + + # 监听消息时间间隔,单位秒 + LISTEN_INTERVAL: int = 1 + + # 搜索聊天对象超时时间 + SEARCH_CHAT_TIMEOUT: int = 5 + +class WxResponse(dict): + def __init__(self, status: str, message: str, data: dict = None): + super().__init__(status=status, message=message, data=data) + + def __str__(self): + return str(self.to_dict()) + + def __repr__(self): + return str(self.to_dict()) + + def to_dict(self): + return { + 'status': self['status'], + 'message': self['message'], + 'data': self['data'] + } + + def __bool__(self): + return self.is_success + + @property + def is_success(self): + return self['status'] == '成功' + + @classmethod + def success(cls, message=None, data: dict = None): + return cls(status="成功", message=message, data=data) + + @classmethod + def failure(cls, message: str, data: dict = None): + return cls(status="失败", message=message, data=data) + + @classmethod + def error(cls, message: str, data: dict = None): + return cls(status="错误", message=message, data=data) \ No newline at end of file diff --git a/wxauto/ui/__init__.py b/wxauto/ui/__init__.py new file mode 100644 index 0000000..3d6f1d7 --- /dev/null +++ b/wxauto/ui/__init__.py @@ -0,0 +1,8 @@ +from .base import BaseUIWnd, BaseUISubWnd +from . import ( + chatbox, + component, + main, + navigationbox, + sessionbox +) \ No newline at end of file diff --git a/wxauto/ui/base.py b/wxauto/ui/base.py new file mode 100644 index 0000000..cbecd5f --- /dev/null +++ b/wxauto/ui/base.py @@ -0,0 +1,56 @@ +from wxauto import uiautomation as uia +from wxauto.param import PROJECT_NAME +from wxauto.logger import wxlog +from abc import ABC, abstractmethod +import win32gui +from typing import Union +import time + +class BaseUIWnd(ABC): + _ui_cls_name: str = None + _ui_name: str = None + control: uia.Control + + @abstractmethod + def _lang(self, text: str):pass + + def __repr__(self): + return f"<{PROJECT_NAME} - {self.__class__.__name__} at {hex(id(self))}>" + + def __eq__(self, other): + return self.control == other.control + + def __bool__(self): + return self.exists() + + def _show(self): + if hasattr(self, 'HWND'): + win32gui.ShowWindow(self.HWND, 1) + win32gui.SetWindowPos(self.HWND, -1, 0, 0, 0, 0, 3) + win32gui.SetWindowPos(self.HWND, -2, 0, 0, 0, 0, 3) + self.control.SwitchToThisWindow() + + def close(self): + try: + self.control.SendKeys('{Esc}') + except: + pass + + def exists(self, wait=0): + try: + result = self.control.Exists(wait) + return result + except: + return False + +class BaseUISubWnd(BaseUIWnd): + root: BaseUIWnd + parent: None + + def _lang(self, text: str): + if getattr(self, 'parent'): + return self.parent._lang(text) + else: + return self.root._lang(text) + + diff --git a/wxauto/ui/chatbox.py b/wxauto/ui/chatbox.py new file mode 100644 index 0000000..3110d7d --- /dev/null +++ b/wxauto/ui/chatbox.py @@ -0,0 +1,455 @@ +from .base import BaseUISubWnd +from wxauto.ui.component import ( + CMenuWnd, +) +from wxauto.param import ( + WxParam, + WxResponse, +) +from wxauto.languages import * +from wxauto.utils import ( + SetClipboardText, + SetClipboardFiles, + GetAllWindowExs, +) +from wxauto.msgs import parse_msg +from wxauto import uiautomation as uia +from wxauto.logger import wxlog +from wxauto.uiautomation import Control +from wxauto.utils.tools import roll_into_view +import time +import os + +USED_MSG_IDS = {} + +class ChatBox(BaseUISubWnd): + def __init__(self, control: uia.Control, parent): + self.control: Control = control + self.root = parent + self.parent = parent # `wx` or `chat` + self.init() + + def init(self): + self.msgbox = self.control.ListControl(Name=self._lang("消息")) + # if not self.msgbox.Exists(0): + # return + self.editbox = self.control.EditControl() + self.sendbtn = self.control.ButtonControl(Name=self._lang('发送')) + self.tools = self.control.PaneControl().ToolBarControl() + # self.id = self.msgbox.runtimeid + self._empty = False # 用于记录是否为完全没有聊天记录的窗口,因为这种窗口之前有不会触发新消息判断的问题 + if (cid := self.id) and cid not in USED_MSG_IDS: + USED_MSG_IDS[self.id] = tuple((i.runtimeid for i in self.msgbox.GetChildren())) + if not USED_MSG_IDS[cid]: + self._empty = True + + def _lang(self, text: str) -> str: + return WECHAT_CHAT_BOX.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + + def _update_used_msg_ids(self): + USED_MSG_IDS[self.id] = tuple((i.runtimeid for i in self.msgbox.GetChildren())) + + + def _open_chat_more_info(self): + for chatinfo_control, depth in uia.WalkControl(self.control): + if chatinfo_control.Name == self._lang('聊天信息'): + chatinfo_control.Click() + break + else: + return WxResponse.failure('未找到聊天信息按钮') + return ChatRoomDetailWnd(self) + + + def _activate_editbox(self): + if not self.editbox.HasKeyboardFocus: + self.editbox.MiddleClick() + + @property + def who(self): + if hasattr(self, '_who'): + return self._who + self._who = self.editbox.Name + return self._who + + @property + def id(self): + if self.msgbox.Exists(0): + return self.msgbox.runtimeid + return None + + @property + def used_msg_ids(self): + return USED_MSG_IDS[self.id] + + def get_info(self): + chat_info = {} + walk = uia.WalkControl(self.control) + for chat_name_control, depth in walk: + if isinstance(chat_name_control, uia.TextControl): + break + if ( + not isinstance(chat_name_control, uia.TextControl) + or depth < 8 + ): + return {} + + # chat_name_control = self.control.GetProgenyControl(11) + chat_name_control_list = chat_name_control.GetParentControl().GetChildren() + chat_name_control_count = len(chat_name_control_list) + + if chat_name_control_count == 1: + if self.control.ButtonControl(Name='公众号主页', searchDepth=9).Exists(0): + chat_info['chat_type'] = 'official' + else: + chat_info['chat_type'] = 'friend' + chat_info['chat_name'] = chat_name_control.Name + elif chat_name_control_count >= 2: + try: + second_text = chat_name_control_list[1].Name + if second_text.startswith('@'): + chat_info['company'] = second_text + chat_info['chat_type'] = 'service' + chat_info['chat_name'] = chat_name_control.Name + else: + chat_info['group_member_count'] =\ + int(second_text.replace('(', '').replace(')', '')) + chat_info['chat_type'] = 'group' + chat_info['chat_name'] =\ + chat_name_control.Name.replace(second_text, '') + except: + chat_info['chat_type'] = 'friend' + chat_info['chat_name'] = chat_name_control.Name + + ori_chat_name_control =\ + chat_name_control.GetParentControl().\ + GetParentControl().TextControl(searchDepth=1) + if ori_chat_name_control.Exists(0): + chat_info['chat_remark'] = chat_info['chat_name'] + chat_info['chat_name'] = ori_chat_name_control.Name + self._info = chat_info + return chat_info + + + def input_at(self, at_list): + self._show() + if isinstance(at_list, str): + at_list = [at_list] + self._activate_editbox() + for friend in at_list: + self.editbox.SendKeys('@'+friend.replace(' ', '')) + atmenu = AtMenu(self) + atmenu.select(friend) + + + def clear_edit(self): + self._show() + self.editbox.Click() + self.editbox.SendKeys('{Ctrl}a', waitTime=0) + self.editbox.SendKeys('{DELETE}') + + + def send_text(self, content: str): + self._show() + t0 = time.time() + while True: + if time.time() - t0 > 10: + return WxResponse.failure(f'Timeout --> {self.who} - {content}') + SetClipboardText(content) + self._activate_editbox() + self.editbox.SendKeys('{Ctrl}v') + if self.editbox.GetValuePattern().Value.replace('', '').strip(): + break + self.editbox.SendKeys('{Ctrl}v') + if self.editbox.GetValuePattern().Value.replace('', '').strip(): + break + self.editbox.RightClick() + menu = CMenuWnd(self) + menu.select('粘贴') + if self.editbox.GetValuePattern().Value.replace('', '').strip(): + break + t0 = time.time() + while self.editbox.GetValuePattern().Value: + if time.time() - t0 > 10: + return WxResponse.failure(f'Timeout --> {self.who} - {content}') + self._activate_editbox() + + self.sendbtn.Click() + if not self.editbox.GetValuePattern().Value: + return WxResponse.success(f"success") + elif not self.editbox.GetValuePattern().Value.replace('', '').strip(): + return self.send_text(content) + + def send_msg(self, content: str, clear: bool=True, at=None): + if not content and not at: + return WxResponse.failure(f"参数 `content` 和 `at` 不能同时为空") + + if clear: + self.clear_edit() + if at: + self.input_at(at) + + return self.send_text(content) + + + def send_file(self, file_path): + self._show() + if isinstance(file_path, str): + file_path = [file_path] + file_path = [os.path.abspath(f) for f in file_path] + SetClipboardFiles(file_path) + self._activate_editbox() + self.editbox.SendKeys('{Ctrl}v') + self.sendbtn.Click() + if self.editbox.GetValuePattern().Value: + return WxResponse.fail("发送失败,请重试") + return WxResponse.success() + + def load_more(self, interval=0.3): + self._show() + msg_len = len(self.msgbox.GetChildren()) + loadmore = self.msgbox.GetChildren()[0] + loadmore_top = loadmore.BoundingRectangle.top + while True: + if len(self.msgbox.GetChildren()) > msg_len: + isload = True + break + else: + msg_len = len(self.msgbox.GetChildren()) + self.msgbox.WheelUp(wheelTimes=10) + time.sleep(interval) + if self.msgbox.GetChildren()[0].BoundingRectangle.top == loadmore_top\ + and len(self.msgbox.GetChildren()) == msg_len: + isload = False + break + else: + loadmore_top = self.msgbox.GetChildren()[0].BoundingRectangle.top + + self.msgbox.WheelUp(wheelTimes=1, waitTime=0.1) + if isload: + return WxResponse.success() + else: + return WxResponse.failure("没有更多消息了") + + + def get_msgs(self): + if self.msgbox.Exists(0): + return [ + parse_msg(msg_control, self) + for msg_control + in self.msgbox.GetChildren() + if msg_control.ControlTypeName == 'ListItemControl' + ] + return [] + + def get_new_msgs(self): + if not self.msgbox.Exists(0): + return [] + msg_controls = self.msgbox.GetChildren() + now_msg_ids = tuple((i.runtimeid for i in msg_controls)) + if not now_msg_ids: # 当前没有消息id + return [] + if self._empty and self.used_msg_ids: + self._empty = False + if not self._empty and ( + (not self.used_msg_ids and now_msg_ids) # 没有使用过的消息id,但当前有消息id + or now_msg_ids[-1] == self.used_msg_ids[-1] # 当前最后一条消息id和上次一样 + or not set(now_msg_ids)&set(self.used_msg_ids) # 当前消息id和上次没有交集 + ): + # wxlog.debug('没有新消息') + return [] + + used_msg_ids_set = set(self.used_msg_ids) + last_one_msgid = max( + (x for x in now_msg_ids if x in used_msg_ids_set), + key=self.used_msg_ids.index, default=None + ) + new1 = [x for x in now_msg_ids if x not in used_msg_ids_set] + new2 = now_msg_ids[now_msg_ids.index(last_one_msgid) + 1 :]\ + if last_one_msgid is not None else [] + new = [i for i in new1 if i in new2] if new2 else new1 + USED_MSG_IDS[self.id] = tuple(self.used_msg_ids + tuple(new))[-100:] + new_controls = [i for i in msg_controls if i.runtimeid in new] + self.msgbox.MiddleClick() + return [ + parse_msg(msg_control, self) + for msg_control + in new_controls + if msg_control.ControlTypeName == 'ListItemControl' + ] + def get_msg_by_id(self, msg_id: str): + if not self.msgbox.Exists(0): + return [] + msg_controls = self.msgbox.GetChildren() + if control_list := [i for i in msg_controls if i.runtimeid == msg_id]: + return parse_msg(control_list[0], self) + + def _get_tail_after_nth_match(self, msgs, last_msg, n): + matches = [ + i for i, msg in reversed(list(enumerate(msgs))) + if msg.content == last_msg + ] + if len(matches) >= n: + wxlog.debug(f'匹配到基准消息:{last_msg}') + else: + split_last_msg = last_msg.split(':') + nickname = split_last_msg[0] + content = ''.join(split_last_msg[1:]) + matches = [ + i for i, msg in reversed(list(enumerate(msgs))) + if msg.content == content + and msg.sender_remark == nickname + ] + if len(matches) >= n: + wxlog.debug(f'匹配到基准消息:<{nickname}> {content}') + else: + wxlog.debug(f"未匹配到基准消息,以最后一条消息为基准:{msgs[-1].content}") + matches = [ + i for i, msg in reversed(list(enumerate(msgs))) + if msg.attr in ('self', 'friend') + ] + try: + index = matches[n - 1] + return msgs[index:] + except IndexError: + wxlog.debug(f"未匹配到第{n}条消息,返回空列表") + return [] + + def get_next_new_msgs(self, count=None, last_msg=None): + # 1. 消息列表不存在,则返回空列表 + if not self.msgbox.Exists(0): + wxlog.debug('消息列表不存在,返回空列表') + return [] + + # 2. 判断是否有新消息按钮,有的话点一下 + load_new_button = self.control.ButtonControl(RegexName=self._lang('re_新消息按钮')) + if load_new_button.Exists(0): + self._show() + wxlog.debug('检测到新消息按钮,点击加载新消息') + load_new_button.Click() + time.sleep(0.5) + msg_controls = self.msgbox.GetChildren() + USED_MSG_IDS[self.id] = tuple((i.runtimeid for i in msg_controls)) + msgs = [ + parse_msg(msg_control, self) + for msg_control + in msg_controls + if msg_control.ControlTypeName == 'ListItemControl' + ] + + # 3. 如果有“以下是新消息”标志,则直接返回该标志下的所有消息即可 + index = next(( + i for i, msg in enumerate(msgs) + if self._lang('以下为新消息') == msg.content + ), None) + if index is not None: + wxlog.debug('获取以下是新消息下的所有消息') + return msgs[index:] + + # 4. 根据会话列表传入的消息数量和最后一条新消息内容来判断新消息 + if count and last_msg: + # index = next(( + # i for i, msg in enumerate(msgs[::-1]) + # if last_msg == msg.content + # ), None) + + # if index is not None: + wxlog.debug(f'获取{count}条新消息,基准消息内容为:{last_msg}') + return self._get_tail_after_nth_match(msgs, last_msg, count) + + def get_group_members(self): + self._show() + roominfoWnd = self._open_chat_more_info() + return roominfoWnd.get_group_members() + + +class ChatRoomDetailWnd(BaseUISubWnd): + _ui_cls_name: str = 'SessionChatRoomDetailWnd' + + def __init__(self, parent): + self.parent = parent + self.root = parent.root + self.control = self.root.control.Control(ClassName=self._ui_cls_name, searchDepth=1) + + def _lang(self, text: str) -> str: + return CHATROOM_DETAIL_WINDOW.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + + def _edit(self, key, value): + wxlog.debug(f'修改{key}为`{value}`') + btn = self.control.TextControl(Name=key).GetParentControl().ButtonControl(Name=key) + if btn.Exists(0): + roll_into_view(self.control, btn) + btn.Click() + else: + wxlog.debug(f'当前非群聊,无法修改{key}') + return WxResponse.failure(f'Not a group chat, cannot modify `{key}`') + while True: + edit_hwnd_list = [ + i[0] + for i in GetAllWindowExs(self.control.NativeWindowHandle) + if i[1] == 'EditWnd' + ] + if edit_hwnd_list: + edit_hwnd = edit_hwnd_list[0] + break + btn.Click() + edit_win32 = uia.Win32(edit_hwnd) + edit_win32.shortcut_select_all() + edit_win32.send_keys_shortcut('{DELETE}') + edit_win32.input(value) + edit_win32.send_keys_shortcut('{ENTER}') + return WxResponse.success() + + def get_group_members(self, control=False): + """获取群成员""" + more = self.control.ButtonControl(Name=self._lang('查看更多'), searchDepth=8) + if more.Exists(0.5): + more.Click() + members = [i for i in self.control.ListControl(Name=self._lang('聊天成员')).GetChildren()] + while members[-1].Name in [self._lang('添加'), self._lang('移出')]: + members = members[:-1] + if control: + return members + member_names = [i.Name for i in members] + self.close() + return member_names + +class GroupMemberElement: + def __init__(self, control, parent) -> None: + self.control = control + self.parent = parent + self.root = self.parent.root + self.nickname = self.control.Name + + def __repr__(self) -> str: + return f"" + + +class AtMenu(BaseUISubWnd): + _ui_cls_name = 'ChatContactMenu' + + def __init__(self, parent): + self.root = parent.root + self.control = parent.parent.control.PaneControl(ClassName='ChatContactMenu') + # self.control.Exists(1) + + def clear(self, friend): + if self.exists(): + self.control.SendKeys('{ESC}') + for _ in range(len(friend)+1): + self.root.chatbox.editbox.SendKeys('{BACK}') + + def select(self, friend): + friend_ = friend.replace(' ', '') + if self.exists(): + ateles = self.control.ListControl().GetChildren() + if len(ateles) == 1: + ateles[0].Click() + else: + atele = self.control.ListItemControl(Name=friend) + if atele.Exists(0): + roll_into_view(self.control, atele) + atele.Click() + else: + self.clear(friend_) + else: + self.clear(friend_) \ No newline at end of file diff --git a/wxauto/ui/component.py b/wxauto/ui/component.py new file mode 100644 index 0000000..423c420 --- /dev/null +++ b/wxauto/ui/component.py @@ -0,0 +1,407 @@ +from wxauto.utils import ( + FindWindow, + SetClipboardText, + ReadClipboardData, + GetAllWindows, + GetWindowRect, + capture +) +from wxauto.param import ( + WxParam, + WxResponse, +) +from wxauto.languages import * +from .base import ( + BaseUISubWnd +) +from wxauto.logger import wxlog +from wxauto.uiautomation import ( + ControlFromHandle, +) +from wxauto.utils.tools import ( + get_file_dir, + roll_into_view, +) +from PIL import Image +from wxauto import uiautomation as uia +import traceback +import shutil +import time + +class EditBox: + ... + +class SelectContactWnd(BaseUISubWnd): + """选择联系人窗口""" + _ui_cls_name = 'SelectContactWnd' + + def __init__(self, parent): + self.parent = parent + self.root = parent.root + hwnd = FindWindow(self._ui_cls_name, timeout=1) + if hwnd: + self.control = ControlFromHandle(hwnd) + else: + self.control = parent.root.control.PaneControl(ClassName=self._ui_cls_name, searchDepth=1) + + self.editbox = self.control.EditControl() + + def send(self, target): + if isinstance(target, str): + SetClipboardText(target) + while not self.editbox.HasKeyboardFocus: + self.editbox.Click() + time.sleep(0.1) + self.editbox.SendKeys('{Ctrl}a') + self.editbox.SendKeys('{Ctrl}v') + checkbox = self.control.ListControl().CheckBoxControl() + if checkbox.Exists(1): + checkbox.Click() + self.control.ButtonControl(Name='发送').Click() + return WxResponse.success() + else: + self.control.SendKeys('{Esc}') + wxlog.debug(f'未找到好友:{target}') + return WxResponse.failure(f'未找到好友:{target}') + + elif isinstance(target, list): + n = 0 + fail = [] + multiselect = self.control.ButtonControl(Name='多选') + if multiselect.Exists(0): + multiselect.Click() + for i in target: + SetClipboardText(i) + while not self.editbox.HasKeyboardFocus: + self.editbox.Click() + time.sleep(0.1) + self.editbox.SendKeys('{Ctrl}a') + self.editbox.SendKeys('{Ctrl}v') + checkbox = self.control.ListControl().CheckBoxControl() + if checkbox.Exists(1): + checkbox.Click() + n += 1 + else: + fail.append(i) + wxlog.debug(f"未找到转发对象:{i}") + if n > 0: + self.control.ButtonControl(RegexName='分别发送(\d+)').Click() + if n == len(target): + return WxResponse.success() + else: + return WxResponse.success('存在未转发成功名单', data=fail) + else: + self.control.SendKeys('{Esc}') + wxlog.debug(f'所有好友均未未找到:{target}') + return WxResponse.failure(f'所有好友均未未找到:{target}') + +class CMenuWnd(BaseUISubWnd): + _ui_cls_name = 'CMenuWnd' + + def __init__(self, parent): + self.parent = parent + self.root = parent.root + if menulist := [i for i in GetAllWindows() if 'CMenuWnd' in i]: + self.control = uia.ControlFromHandle(menulist[0][0]) + else: + self.control = self.root.control.MenuControl(ClassName=self._ui_cls_name) + + def _lang(self, text: str) -> str: + return MENU_OPTIONS.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + + @property + def option_controls(self): + return self.control.ListControl().GetChildren() + + @property + def option_names(self): + return [c.Name for c in self.option_controls] + + def select(self, item): + if not self.exists(0): + return WxResponse.failure('菜单窗口不存在') + if isinstance(item, int): + self.option_controls[item].Click() + return WxResponse.success() + + item = self._lang(item) + for c in self.option_controls: + if c.Name == item: + c.Click() + return WxResponse.success() + if self.exists(0): + self.close() + return WxResponse.failure(f'未找到选项:{item}') + + def close(self): + try: + self.control.SendKeys('{ESC}') + except Exception as e: + pass + +class NetErrInfoTipsBarWnd(BaseUISubWnd): + _ui_cls_name = 'NetErrInfoTipsBarWnd' + + def __init__(self, parent): + self.control = parent.root.control.PaneControl(ClassName=self._ui_cls_name) + + def __bool__(self): + return self.exists(0) + +class WeChatImage(BaseUISubWnd): + _ui_cls_name = 'ImagePreviewWnd' + + def __init__(self) -> None: + self.hwnd = FindWindow(classname=self._ui_cls_name) + if self.hwnd: + self.control = ControlFromHandle(self.hwnd) + self.type = 'image' + if self.control.PaneControl(ClassName='ImagePreviewLayerWnd').Exists(0): + self.type = 'video' + MainControl1 = [i for i in self.control.GetChildren() if not i.ClassName][0] + self.ToolsBox, self.PhotoBox = MainControl1.GetChildren() + + # tools按钮 + self.t_previous = self.ToolsBox.ButtonControl(Name=self._lang('上一张')) + self.t_next = self.ToolsBox.ButtonControl(Name=self._lang('下一张')) + self.t_zoom = self.ToolsBox.ButtonControl(Name=self._lang('放大')) + self.t_translate = self.ToolsBox.ButtonControl(Name=self._lang('翻译')) + self.t_ocr = self.ToolsBox.ButtonControl(Name=self._lang('提取文字')) + self.t_save = self.control.ButtonControl(Name=self._lang('另存为...')) + self.t_qrcode = self.ToolsBox.ButtonControl(Name=self._lang('识别图中二维码')) + + def _lang(self, text: str) -> str: + return IMAGE_WINDOW.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + + def ocr(self, wait=10): + result = '' + ctrls = self.PhotoBox.GetChildren() + if len(ctrls) == 2: + self.t_ocr.Click() + t0 = time.time() + while time.time() - t0 < wait: + ctrls = self.PhotoBox.GetChildren() + if len(ctrls) == 3: + TranslateControl = ctrls[-1] + result = TranslateControl.TextControl().Name + if result: + return result + else: + self.t_ocr.Click() + time.sleep(0.1) + return result + + def save(self, dir_path=None, timeout=10): + """保存图片/视频 + + Args: + dir_path (str): 绝对路径,包括文件名和后缀,例如:"D:/Images/微信图片_xxxxxx.png" + timeout (int, optional): 保存超时时间,默认10秒 + + Returns: + str: 文件保存路径,即savepath + """ + if dir_path is None: + dir_path = WxParam.DEFAULT_SAVE_PATH + suffix = 'png' if self.type == 'image' else 'mp4' + filename = f"wxauto_{self.type}_{time.strftime('%Y%m%d%H%M%S')}.{suffix}" + filepath = get_file_dir(dir_path) / filename + t0 = time.time() + + SetClipboardText('') + while True: + if time.time() - t0 > timeout: + if self.control.Exists(0): + self.control.SendKeys('{Esc}') + raise TimeoutError('下载超时') + try: + self.control.ButtonControl(Name=self._lang('更多')).Click() + menu = self.control.MenuControl(ClassName='CMenuWnd') + menu.MenuItemControl(Name=self._lang('复制')).Click() + clipboard_data = ReadClipboardData() + path = clipboard_data['15'][0] + wxlog.debug(f"读取到图片/视频路径:{path}") + break + except: + wxlog.debug(traceback.format_exc()) + time.sleep(0.1) + shutil.copyfile(path, filepath) + SetClipboardText('') + if self.control.Exists(0): + wxlog.debug("关闭图片窗口") + self.control.SendKeys('{Esc}') + return filepath + +class NewFriendElement: + def __init__(self, control, parent): + self.parent = parent + self.root = parent.root + self.control = control + self.name = self.control.Name + self.msg = self.control.GetFirstChildControl().PaneControl(SearchDepth=1).GetChildren()[-1].TextControl().Name + self.NewFriendsBox = self.root.chatbox.control.ListControl(Name='新的朋友').GetParentControl() + self.status = self.control.GetFirstChildControl().GetChildren()[-1] + self.acceptable = isinstance(self.status, uia.ButtonControl) + + def __repr__(self) -> str: + return f"" + + def delete(self): + wxlog.info(f'删除好友请求: {self.name}') + roll_into_view(self.NewFriendsBox, self.control) + self.control.RightClick() + menu = CMenuWnd(self.root) + menu.select('删除') + + def reply(self, text): + wxlog.debug(f'回复好友请求: {self.name}') + roll_into_view(self.NewFriendsBox, self.control) + self.control.Click() + self.root.ChatBox.ButtonControl(Name='回复').Click() + edit = self.root.ChatBox.EditControl() + edit.Click() + edit.SendKeys('{Ctrl}a') + SetClipboardText(text) + edit.SendKeys('{Ctrl}v') + time.sleep(0.1) + self.root.ChatBox.ButtonControl(Name='发送').Click() + dialog = self.root.UiaAPI.PaneControl(ClassName='WeUIDialog') + while edit.Exists(0): + if dialog.Exists(0): + systext = dialog.TextControl().Name + wxlog.debug(f'系统提示: {systext}') + dialog.SendKeys('{Esc}') + self.root.ChatBox.ButtonControl(Name='').Click() + return WxResponse.failure(msg=systext) + time.sleep(0.1) + self.root.ChatBox.ButtonControl(Name='').Click() + return WxResponse.success() + + def accept(self, remark=None, tags=None, permission='朋友圈'): + """接受好友请求 + + Args: + remark (str, optional): 备注名 + tags (list, optional): 标签列表 + permission (str, optional): 朋友圈权限, 可选值:'朋友圈', '仅聊天' + """ + if not self.acceptable: + wxlog.debug(f"当前好友状态无法接受好友请求:{self.name}") + return + wxlog.debug(f"接受好友请求:{self.name} 备注:{remark} 标签:{tags}") + self.root._show() + roll_into_view(self.NewFriendsBox, self.status) + self.status.Click() + NewFriendsWnd = self.root.control.WindowControl(ClassName='WeUIDialog') + tipscontrol = NewFriendsWnd.TextControl(Name="你的联系人较多,添加新的朋友时需选择权限") + + permission_sns = NewFriendsWnd.CheckBoxControl(Name='聊天、朋友圈、微信运动等') + permission_chat = NewFriendsWnd.CheckBoxControl(Name='仅聊天') + if tipscontrol.Exists(0.5): + permission_sns = tipscontrol.GetParentControl().GetParentControl().TextControl(Name='朋友圈') + permission_chat = tipscontrol.GetParentControl().GetParentControl().TextControl(Name='仅聊天') + + if remark: + remarkedit = NewFriendsWnd.TextControl(Name='备注名').GetParentControl().EditControl() + remarkedit.Click() + remarkedit.SendKeys('{Ctrl}a') + remarkedit.SendKeys(remark) + + if tags: + tagedit = NewFriendsWnd.TextControl(Name='标签').GetParentControl().EditControl() + for tag in tags: + tagedit.Click() + tagedit.SendKeys(tag) + NewFriendsWnd.PaneControl(ClassName='DropdownWindow').TextControl().Click() + + if permission == '朋友圈': + permission_sns.Click() + elif permission == '仅聊天': + permission_chat.Click() + + NewFriendsWnd.ButtonControl(Name='确定').Click() + +class WeChatLoginWnd(BaseUISubWnd): + _ui_cls_name = 'WeChatLoginWndForPC' + + def __init__(self): + self.hwnd = FindWindow(classname=self._ui_cls_name) + if self.hwnd: + self.control = ControlFromHandle(self.hwnd) + + def _lang(self, text: str) -> str: + return WECHAT_LOGINWND.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + + def clear_hint(self) -> bool: + self._show() + hint_dialog = self.control.PaneControl(Name=self._lang('提示')) + if hint_dialog.Exists(0): + dialog_button = self.control.ButtonControl(Name=self._lang('确定')) + dialog_button.Click() + return True + else: + return False + + def login(self) -> bool: + self._show() + enter = self.control.ButtonControl(Name=self._lang('进入微信')) + if enter.Exists(0): + enter.Click() + return True + else: + return False + + def get_qrcode(self) -> Image.Image: + self._show() + qrcode = self.control.ButtonControl(Name=self._lang('二维码')) + if qrcode.Exists(0): + window_rect = GetWindowRect(self.hwnd) + win_left, win_top, win_right, win_bottom = window_rect + + bbox = win_left + 62, win_top + 88, win_left + 218, win_top + 245 + return capture(self.hwnd, bbox) + else: + return None + + +class WeChatBrowser(BaseUISubWnd): + _ui_cls_name = 'Chrome_WidgetWin_0' + _ui_name = '微信' + + def __init__(self): + self.hwnd = FindWindow(classname=self._ui_cls_name, name=self._ui_name) + if self.hwnd: + self.control = ControlFromHandle(self.hwnd) + + def _lang(self, text: str) -> str: + return WECHAT_BROWSER.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + + def get_url(self) -> str: + self._show() + tab = self.control.TabItemControl() + if tab.Exists(): + tab.RightClick() + copy_link_item = uia.MenuItemControl(Name=self._lang('复制链接')) + if copy_link_item.Exists(): + copy_link_item.Click() + clipboard_data = ReadClipboardData() + url = (clipboard_data.get('13') or + clipboard_data.get('1') or + None) + SetClipboardText('') + return url + else: + wxlog.debug(f'找不到复制链接菜单项') + else: + wxlog.debug(f'找不到标签页') + + def close(self): + close_button = self.control.ButtonControl(Name=self._lang('关闭'), foundIndex=1) + if close_button.Exists(): + close_button.Click() + close_button = self.control.ButtonControl(Name=self._lang('关闭'), foundIndex=2) + if close_button.Exists(): + close_button.Click() + close_button = self.control.ButtonControl(Name=self._lang('关闭'), foundIndex=3) + if close_button.Exists(): + close_button.Click() diff --git a/wxauto/ui/main.py b/wxauto/ui/main.py new file mode 100644 index 0000000..327eeb6 --- /dev/null +++ b/wxauto/ui/main.py @@ -0,0 +1,265 @@ +from .base import ( + BaseUIWnd, + BaseUISubWnd +) +from wxauto import uiautomation as uia +from wxauto.param import WxResponse +from .chatbox import ChatBox +from .sessionbox import SessionBox +from .navigationbox import NavigationBox +from wxauto.param import ( + WxParam, + WxResponse, + PROJECT_NAME +) +from wxauto.languages import * +from wxauto.utils import ( + GetAllWindows, + FindWindow, +) +from wxauto.logger import wxlog +from typing import ( + Union, + List, +) + +class WeChatSubWnd(BaseUISubWnd): + _ui_cls_name: str = 'ChatWnd' + chatbox: ChatBox = None + nickname: str = '' + + def __init__( + self, + key: Union[str, int], + parent: 'WeChatMainWnd', + timeout: int = 3 + ): + self.root = self + self.parent = parent + if isinstance(key, str): + hwnd = FindWindow(classname=self._ui_cls_name, name=key, timeout=timeout) + else: + hwnd = key + self.control = uia.ControlFromHandle(hwnd) + if self.control is not None: + chatbox_control = self.control.PaneControl(ClassName='', searchDepth=1) + self.chatbox = ChatBox(chatbox_control, self) + self.nickname = self.control.Name + + def __repr__(self): + return f'<{PROJECT_NAME} - {self.__class__.__name__} object("{self.nickname}")>' + + @property + def pid(self): + if not hasattr(self, '_pid'): + self._pid = self.control.ProcessId + return self._pid + + def _get_chatbox( + self, + nickname: str=None, + exact: bool=False + ) -> ChatBox: + return self.chatbox + + def chat_info(self): + return self.chatbox.get_info() + + def load_more_message(self, interval=0.3) -> WxResponse: + return self.chatbox.load_more(interval) + + def send_msg( + self, + msg: str, + who: str=None, + clear: bool=True, + at: Union[str, List[str]]=None, + exact: bool=False, + ) -> WxResponse: + chatbox = self._get_chatbox(who, exact) + if not chatbox: + return WxResponse.failure(f'未找到会话: {who}') + return chatbox.send_msg(msg, clear, at) + + def send_files( + self, + filepath, + who=None, + exact=False + ) -> WxResponse: + chatbox = self._get_chatbox(who, exact) + if not chatbox: + return WxResponse.failure(f'未找到会话: {who}') + return chatbox.send_file(filepath) + + def get_group_members( + self, + who: str=None, + exact: bool=False + ) -> List[str]: + chatbox = self._get_chatbox(who, exact) + if not chatbox: + return WxResponse.failure(f'未找到会话: {who}') + return chatbox.get_group_members() + + def get_msgs(self): + chatbox = self._get_chatbox() + if chatbox: + return chatbox.get_msgs() + return [] + + def get_new_msgs(self): + return self._get_chatbox().get_new_msgs() + + def get_msg_by_id(self, msg_id: str): + return self._get_chatbox().get_msg_by_id(msg_id) + + +version_error_msg = """ +错误:未找到可用的微信窗口 + +当前版本仅适用于3.9版本客户端,如果您当前客户端版本为4.x,请在官网下载3.9版本客户端 +下载链接:https://pc.weixin.qq.com +""" + + +class WeChatMainWnd(WeChatSubWnd): + _ui_cls_name: str = 'WeChatMainWndForPC' + _ui_name: str = '微信' + + def __init__(self, hwnd: int = None): + self.root = self + self.parent = self + if hwnd: + self._setup_ui(hwnd) + else: + hwnd = FindWindow(classname=self._ui_cls_name) + if not hwnd: + raise Exception(version_error_msg) + self._setup_ui(hwnd) + + print(f'初始化成功,获取到已登录窗口:{self.nickname}') + + def _setup_ui(self, hwnd: int): + self.HWND = hwnd + self.control = uia.ControlFromHandle(hwnd) + MainControl1 = [i for i in self.control.GetChildren() if not i.ClassName][0] + MainControl2 = MainControl1.GetFirstChildControl() + navigation_control, sessionbox_control, chatbox_control = MainControl2.GetChildren() + self.navigation = NavigationBox(navigation_control, self) + self.sessionbox = SessionBox(sessionbox_control, self) + self.chatbox = ChatBox(chatbox_control, self) + self.nickname = self.navigation.my_icon.Name + self.NavigationBox = self.navigation.control + self.SessionBox = self.sessionbox.control + self.ChatBox = self.chatbox.control + + def __repr__(self): + return f'<{PROJECT_NAME} - {self.__class__.__name__} object("{self.nickname}")>' + + def _lang(self, text: str) -> str: + return WECHAT_MAIN.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + + def _get_chatbox( + self, + nickname: str=None, + exact: bool=False + ) -> ChatBox: + if nickname and (chatbox := WeChatSubWnd(nickname, self, timeout=0)).control: + return chatbox.chatbox + else: + if nickname: + switch_result = self.sessionbox.switch_chat(keywords=nickname, exact=exact) + if not switch_result: + return None + if self.chatbox.msgbox.Exists(0.5): + return self.chatbox + + def switch_chat( + self, + keywords: str, + exact: bool = False, + force: bool = False, + force_wait: Union[float, int] = 0.5 + ): + return self.sessionbox.switch_chat(keywords, exact, force, force_wait) + + def get_all_sub_wnds(self): + sub_wxs = [i for i in GetAllWindows() if i[1] == WeChatSubWnd._ui_cls_name] + return [WeChatSubWnd(i[0], self) for i in sub_wxs] + + def get_sub_wnd(self, who: str, timeout: int=0): + if hwnd := FindWindow(classname=WeChatSubWnd._ui_cls_name, name=who, timeout=timeout): + return WeChatSubWnd(hwnd, self) + + def open_separate_window(self, keywords: str) -> WeChatSubWnd: + if subwin := self.get_sub_wnd(keywords): + wxlog.debug(f"{keywords} 获取到已存在的子窗口: {subwin}") + return subwin + self._show() + if nickname := self.sessionbox.switch_chat(keywords): + wxlog.debug(f"{keywords} 切换到聊天窗口: {nickname}") + if subwin := self.get_sub_wnd(nickname): + wxlog.debug(f"{nickname} 获取到已存在的子窗口: {subwin}") + return subwin + else: + keywords = nickname + if result := self.sessionbox.open_separate_window(keywords): + find_nickname = result['data'].get('nickname', keywords) + return WeChatSubWnd(find_nickname, self) + + def _get_next_new_message(self, filter_mute: bool=False): + def get_new_message(session): + last_content = session.content + new_count = session.new_count + chat_name = session.name + session.click() + return self.chatbox.get_next_new_msgs(new_count, last_content) + + def get_new_session(filter_mute): + sessions = self.sessionbox.get_session() + if sessions[0].name == self._lang('折叠的群聊'): + self.navigation.chat_icon.DoubleClick() + sessions = self.sessionbox.get_session() + new_sessions = [ + i for i in sessions + if i.isnew + and i.name != self._lang('折叠的群聊') + ] + if filter_mute: + new_sessions = [i for i in new_sessions if i.ismute == False] + return new_sessions + + if new_msgs := self.chatbox.get_new_msgs(): + wxlog.debug("获取当前页面新消息") + return new_msgs + elif new_sessions := get_new_session(filter_mute): + wxlog.debug("当前会话列表获取新消息") + return get_new_message(new_sessions[0]) + else: + self.sessionbox.go_top() + if new_sessions := get_new_session(filter_mute): + wxlog.debug("当前会话列表获取新消息") + return get_new_message(new_sessions[0]) + else: + self.navigation.chat_icon.DoubleClick() + if new_sessions := get_new_session(filter_mute): + wxlog.debug("翻页会话列表获取新消息") + return get_new_message(new_sessions[0]) + else: + wxlog.debug("没有新消息") + return [] + + def get_next_new_message(self, filter_mute: bool=False): + if filter_mute and not self.navigation.has_new_message(): + return {} + new_msgs = self._get_next_new_message(filter_mute) + if new_msgs: + chat_info = self.chatbox.get_info() + return { + 'chat_name': chat_info['chat_name'], + 'chat_type': chat_info['chat_type'], + 'msg': new_msgs + } + else: + return {} diff --git a/wxauto/ui/navigationbox.py b/wxauto/ui/navigationbox.py new file mode 100644 index 0000000..6f0eeec --- /dev/null +++ b/wxauto/ui/navigationbox.py @@ -0,0 +1,55 @@ +from __future__ import annotations +from wxauto.param import ( + WxParam, +) +from wxauto.languages import * +from wxauto.uiautomation import Control + +class NavigationBox: + def __init__(self, control, parent): + self.control: Control = control + self.root = parent.root + self.parent = parent + self.top_control = control.GetTopLevelControl() + self.init() + + def _lang(self, text: str) -> str: + return WECHAT_NAVIGATION_BOX.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + + def init(self): + self.my_icon = self.control.ButtonControl() + self.chat_icon = self.control.ButtonControl(Name=self._lang('聊天')) + self.contact_icon = self.control.ButtonControl(Name=self._lang('通讯录')) + self.favorites_icon = self.control.ButtonControl(Name=self._lang('收藏')) + self.files_icon = self.control.ButtonControl(Name=self._lang('聊天文件')) + self.moments_icon = self.control.ButtonControl(Name=self._lang('朋友圈')) + self.browser_icon = self.control.ButtonControl(Name=self._lang('搜一搜')) + self.video_icon = self.control.ButtonControl(Name=self._lang('视频号')) + self.stories_icon = self.control.ButtonControl(Name=self._lang('看一看')) + self.mini_program_icon = self.control.ButtonControl(Name=self._lang('小程序面板')) + self.phone_icon = self.control.ButtonControl(Name=self._lang('手机')) + self.settings_icon = self.control.ButtonControl(Name=self._lang('设置及其他')) + + def switch_to_chat_page(self): + self.chat_icon.Click() + + def switch_to_contact_page(self): + self.contact_icon.Click() + + def switch_to_favorites_page(self): + self.favorites_icon.Click() + + def switch_to_files_page(self): + self.files_icon.Click() + + def switch_to_browser_page(self): + self.browser_icon.Click() + + # 是否有新消息 + def has_new_message(self): + from wxauto.utils.win32 import capture + + rect = self.chat_icon.BoundingRectangle + bbox = rect.left, rect.top, rect.right, rect.bottom + img = capture(self.root.HWND, bbox) + return any(p[0] > p[1] and p[0] > p[2] for p in img.getdata()) \ No newline at end of file diff --git a/wxauto/ui/sessionbox.py b/wxauto/ui/sessionbox.py new file mode 100644 index 0000000..d15d709 --- /dev/null +++ b/wxauto/ui/sessionbox.py @@ -0,0 +1,265 @@ +from __future__ import annotations +from wxauto.ui.component import ( + CMenuWnd +) +from wxauto.param import ( + WxParam, + WxResponse, +) +from wxauto.languages import * +from wxauto.utils import ( + SetClipboardText, +) +from wxauto.logger import wxlog +from wxauto.uiautomation import Control +from wxauto.utils.tools import roll_into_view +from typing import ( + List, + Union +) +import time +import re + + +class SessionBox: + def __init__(self, control, parent): + self.control: Control = control + self.root = parent.root + self.parent = parent + self.top_control = control.GetTopLevelControl() + self.init() + + def _lang(self, text: str) -> str: + return WECHAT_SESSION_BOX.get(text, {WxParam.LANGUAGE: text}).get(WxParam.LANGUAGE) + + def init(self): + self.searchbox = self.control.EditControl(Name=self._lang('搜索')) + self.session_list =\ + self.control.ListControl(Name=self._lang('会话'), searchDepth=7) + self.archived_session_list =\ + self.control.ListControl(Name=self._lang('折叠的群聊'), searchDepth=7) + + def get_session(self) -> List[SessionElement]: + if self.session_list.Exists(0): + return [ + SessionElement(i, self) + for i in self.session_list.GetChildren() + if i.Name != self._lang('折叠置顶聊天') + and not re.match(self._lang('re_置顶聊天'), i.Name) + ] + elif self.archived_session_list.Exists(0): + return [SessionElement(i, self) for i in self.archived_session_list.GetChildren()] + else: + return [] + + def roll_up(self, n: int=5): + self.control.MiddleClick() + self.control.WheelUp(wheelTimes=n) + + def roll_down(self, n: int=5): + self.control.MiddleClick() + self.control.WheelDown(wheelTimes=n) + + def switch_chat( + self, + keywords: str, + exact: bool = False, + force: bool = False, + force_wait: Union[float, int] = 0.5 + ): + wxlog.debug(f"切换聊天窗口: {keywords}, {exact}, {force}, {force_wait}") + self.root._show() + sessions = self.get_session() + for session in sessions: + if ( + keywords == session.name + and session.control.BoundingRectangle.height() + ): + session.switch() + return keywords + + self.searchbox.RightClick() + SetClipboardText(keywords) + menu = CMenuWnd(self) + menu.select('粘贴') + + search_result = self.control.ListControl(RegexName='.*?IDS_FAV_SEARCH_RESULT.*?') + + if force: + time.sleep(force_wait) + self.searchbox.SendKeys('{ENTER}') + return '' + + t0 = time.time() + while time.time() -t0 < WxParam.SEARCH_CHAT_TIMEOUT: + results = [] + search_result_items = search_result.GetChildren() + highlight_who = re.sub(r'(\s+)', r'\1', keywords) + for search_result_item in search_result_items: + item_name = search_result_item.Name + if ( + search_result_item.ControlTypeName == 'PaneControl' + and search_result_item.TextControl(Name='聊天记录').Exists(0) + ) or item_name == f'搜索 {keywords}': + break + elif ( + search_result_item.ControlTypeName == 'ListItemControl' + and search_result_item.TextControl(Name=f"微信号: {keywords}").Exists(0) + ): + wxlog.debug(f"{keywords} 匹配到微信号:{item_name}") + search_result_item.Click() + return item_name + elif ( + search_result_item.ControlTypeName == 'ListItemControl' + and search_result_item.TextControl(Name=f"昵称: {highlight_who}").Exists(0) + ): + wxlog.debug(f"{keywords} 匹配到昵称:{item_name}") + search_result_item.Click() + return item_name + elif ( + search_result_item.ControlTypeName == 'ListItemControl' + and search_result_item.TextControl(Name=f"群聊名称: {highlight_who}").Exists(0) + ): + wxlog.debug(f"{keywords} 匹配到群聊名称:{item_name}") + search_result_item.Click() + return item_name + elif ( + search_result_item.ControlTypeName == 'ListItemControl' + and keywords == item_name + ): + wxlog.debug(f"{keywords} 完整匹配") + search_result_item.Click() + return keywords + elif ( + search_result_item.ControlTypeName == 'ListItemControl' + and keywords in item_name + ): + results.append(search_result_item) + + if exact: + wxlog.debug(f"{keywords} 未精准匹配,返回None") + if search_result.Exists(0): + search_result.SendKeys('{Esc}') + return None + if results: + wxlog.debug(f"{keywords} 匹配到多个结果,返回第一个") + results[0].Click() + return results[0].Name + + if search_result.Exists(0): + search_result.SendKeys('{Esc}') + + + def open_separate_window(self, name: str): + wxlog.debug(f"打开独立窗口: {name}") + sessions = self.get_session() + for session in sessions: + if session.name == name: + wxlog.debug(f"找到会话: {name}") + while session.control.BoundingRectangle.height(): + try: + session.click() + session.double_click() + except: + pass + time.sleep(0.1) + else: + return WxResponse.success(data={'nickname': name}) + wxlog.debug(f"未找到会话: {name}") + return WxResponse.failure('未找到会话') + + def go_top(self): + wxlog.debug("回到会话列表顶部") + if self.archived_session_list.Exists(0): + self.control.ButtonControl(Name=self._lang('返回')).Click() + time.sleep(0.3) + first_session_name = self.session_list.GetChildren()[0].Name + while True: + self.control.WheelUp(wheelTimes=3) + time.sleep(0.1) + if self.session_list.GetChildren()[0].Name == first_session_name: + break + else: + first_session_name = self.session_list.GetChildren()[0].Name + + +class SessionElement: + def __init__( + self, + control: Control, + parent: SessionBox + ): + self.root = parent.root + self.parent = parent + self.control = control + info_controls = [i for i in self.control.GetProgenyControl(3).GetChildren() if i.ControlTypeName=='TextControl'] + self.name = info_controls[0].Name + self.time = info_controls[-1].Name + self.content = ( + temp_control.Name + if (temp_control := control.GetProgenyControl(4, -1, control_type='TextControl')) + else None + ) + self.ismute = ( + True + if control.GetProgenyControl(4, 1, control_type='PaneControl') + else False + ) + self.isnew = (new_tag_control := control.GetProgenyControl(2, 2)) is not None + self.new_count = 0 + if self.isnew: + if new_tag_name := (new_tag_control.Name): + try: + self.new_count = int(new_tag_name) + self.ismute = False + except ValueError: + self.new_count = 999 + else: + new_text = re.findall(self._lang('re_条数'), str(self.content)) + if new_text: + try: + self.new_count = int(re.findall('\d+', new_text[0])[0]) + except ValueError: + self.new_count = 999 + self.content = self.content[len(new_text[0])+1:] + else: + self.new_count = 1 + + + self.info = { + 'name': self.name, + 'time': self.time, + 'content': self.content, + 'isnew': self.isnew, + 'new_count': self.new_count, + 'ismute': self.ismute + } + + def _lang(self, text: str) -> str: + return self.parent._lang(text) + + def roll_into_view(self): + self.root._show() + roll_into_view(self.control.GetParentControl(), self.control) + + + def _click(self, right: bool=False, double: bool=False): + self.roll_into_view() + if right: + self.control.RightClick() + elif double: + self.control.DoubleClick() + else: + self.control.Click() + + def click(self): + self._click() + + def right_click(self): + self._click(right=True) + + def double_click(self): + self._click(double=True) + + def switch(self): + self.click() \ No newline at end of file diff --git a/wxauto/uiautomation.py b/wxauto/uiautomation.py new file mode 100644 index 0000000..44cc040 --- /dev/null +++ b/wxauto/uiautomation.py @@ -0,0 +1,8142 @@ +#!python3 +# -*- coding: utf-8 -*- +""" +uiautomation for Python 3. +Author: yinkaisheng@live.com +Source: https://github.com/yinkaisheng/Python-UIAutomation-for-Windows + +This module is for UIAutomation on Windows(Windows XP with SP3, Windows Vista and Windows 7/8/8.1/10). +It supports UIAutomation for the applications which implmented IUIAutomation, such as MFC, Windows Form, WPF, Modern UI(Metro UI), Qt, Firefox and Chrome. +Run 'automation.py -h' for help. + +uiautomation is shared under the Apache Licene 2.0. +This means that the code can be freely copied and distributed, and costs nothing to use. +""" +import os +import sys +import time +import datetime +import re +import threading +import ctypes +import ctypes.wintypes +import comtypes #need pip install comtypes +import comtypes.client +from typing import (Any, Callable, Dict, List, Iterable, Tuple) # need pip install typing for Python3.4 or lower +TreeNode = Any + +# print('uia done') +AUTHOR_MAIL = 'yinkaisheng@live.com' +METRO_WINDOW_CLASS_NAME = 'Windows.UI.Core.CoreWindow' # for Windows 8 and 8.1 +SEARCH_INTERVAL = 0.5 # search control interval seconds +MAX_MOVE_SECOND = 1 # simulate mouse move or drag max seconds +TIME_OUT_SECOND = 10 +OPERATION_WAIT_TIME = 0.1 +MAX_PATH = 260 +DEBUG_SEARCH_TIME = False +DEBUG_EXIST_DISAPPEAR = False +S_OK = 0 + +IsNT6orHigher = os.sys.getwindowsversion().major >= 6 +ProcessTime = time.perf_counter #this returns nearly 0 when first call it if python version <= 3.6 +ProcessTime() # need to call it once if python version <= 3.6 + + +class _AutomationClient: + _instance = None + + @classmethod + def instance(cls) -> '_AutomationClient': + """Singleton instance (this prevents com creation on import).""" + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + tryCount = 3 + for retry in range(tryCount): + try: + self.UIAutomationCore = comtypes.client.GetModule("UIAutomationCore.dll") + self.IUIAutomation = comtypes.client.CreateObject("{ff48dba4-60ef-4201-aa87-54103eef594e}", interface=self.UIAutomationCore.IUIAutomation) + self.ViewWalker = self.IUIAutomation.RawViewWalker + #self.ViewWalker = self.IUIAutomation.ControlViewWalker + break + except Exception as ex: + if retry + 1 == tryCount: + Logger.WriteLine('Can not load UIAutomationCore.dll.\nYou may need to install Windows Update KB971513.\nhttps://github.com/yinkaisheng/WindowsUpdateKB971513ForIUIAutomation', ConsoleColor.Yellow) + raise ex + #Windows dll + ctypes.windll.user32.GetClipboardData.restype = ctypes.c_void_p + ctypes.windll.user32.GetWindowDC.restype = ctypes.c_void_p + ctypes.windll.user32.OpenDesktopW.restype = ctypes.c_void_p + ctypes.windll.user32.WindowFromPoint.restype = ctypes.c_void_p + ctypes.windll.user32.SendMessageW.restype = ctypes.wintypes.LONG + ctypes.windll.user32.GetForegroundWindow.restype = ctypes.c_void_p + ctypes.windll.user32.GetWindowLongW.restype = ctypes.wintypes.LONG + ctypes.windll.kernel32.GlobalLock.restype = ctypes.c_void_p + ctypes.windll.kernel32.GlobalAlloc.restype = ctypes.c_void_p + ctypes.windll.kernel32.GetStdHandle.restype = ctypes.c_void_p + ctypes.windll.kernel32.OpenProcess.restype = ctypes.c_void_p + ctypes.windll.kernel32.CreateToolhelp32Snapshot.restype = ctypes.c_void_p + + SetDpiAwareness(dpiAwarenessPerMonitor=True) + + +class _DllClient: + _instance = None + + @classmethod + def instance(cls) -> '_DllClient': + """Singleton instance (this prevents com creation on import).""" + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + binPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + os.environ["PATH"] = binPath + os.pathsep + os.environ["PATH"] + load = False + if sys.version >= '3.8': + os.add_dll_directory(binPath) + if sys.maxsize > 0xFFFFFFFF: + try: + self.dll = ctypes.cdll.UIAutomationClient_VC140_X64 + load = True + except Exception as ex: + print(ex) + else: + try: + self.dll = ctypes.cdll.UIAutomationClient_VC140_X86 + load = True + except Exception as ex: + print(ex) + if load: + self.dll.BitmapCreate.restype = ctypes.c_size_t + self.dll.BitmapFromWindow.argtypes = (ctypes.c_size_t, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int) + self.dll.BitmapFromWindow.restype = ctypes.c_size_t + self.dll.BitmapFromFile.argtypes = (ctypes.c_wchar_p, ) + self.dll.BitmapFromFile.restype = ctypes.c_size_t + self.dll.BitmapToFile.argtypes = (ctypes.c_size_t, ctypes.c_wchar_p, ctypes.c_wchar_p) + self.dll.BitmapRelease.argtypes = (ctypes.c_size_t, ) + self.dll.BitmapGetWidthAndHeight.argtypes = (ctypes.c_size_t, ) + self.dll.BitmapGetPixel.argtypes = (ctypes.c_size_t, ctypes.c_int, ctypes.c_int) + self.dll.BitmapSetPixel.argtypes = (ctypes.c_size_t, ctypes.c_int, ctypes.c_int, ctypes.c_uint) + + self.dll.Initialize() + else: + self.dll = None + Logger.WriteLine('Can not load dll.\nFunctionalities related to Bitmap are not available.\nYou may need to install Microsoft Visual C++ 2015 Redistributable Package.', ConsoleColor.Yellow) + def __del__(self): + if self.dll: + self.dll.Uninitialize() + + +class ControlType: + """ + ControlType from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/WinAuto/uiauto-controltype-ids + """ + AppBarControl = 50040 + ButtonControl = 50000 + CalendarControl = 50001 + CheckBoxControl = 50002 + ComboBoxControl = 50003 + CustomControl = 50025 + DataGridControl = 50028 + DataItemControl = 50029 + DocumentControl = 50030 + EditControl = 50004 + GroupControl = 50026 + HeaderControl = 50034 + HeaderItemControl = 50035 + HyperlinkControl = 50005 + ImageControl = 50006 + ListControl = 50008 + ListItemControl = 50007 + MenuBarControl = 50010 + MenuControl = 50009 + MenuItemControl = 50011 + PaneControl = 50033 + ProgressBarControl = 50012 + RadioButtonControl = 50013 + ScrollBarControl = 50014 + SemanticZoomControl = 50039 + SeparatorControl = 50038 + SliderControl = 50015 + SpinnerControl = 50016 + SplitButtonControl = 50031 + StatusBarControl = 50017 + TabControl = 50018 + TabItemControl = 50019 + TableControl = 50036 + TextControl = 50020 + ThumbControl = 50027 + TitleBarControl = 50037 + ToolBarControl = 50021 + ToolTipControl = 50022 + TreeControl = 50023 + TreeItemControl = 50024 + WindowControl = 50032 + + +ControlTypeNames = { + ControlType.AppBarControl: 'AppBarControl', + ControlType.ButtonControl: 'ButtonControl', + ControlType.CalendarControl: 'CalendarControl', + ControlType.CheckBoxControl: 'CheckBoxControl', + ControlType.ComboBoxControl: 'ComboBoxControl', + ControlType.CustomControl: 'CustomControl', + ControlType.DataGridControl: 'DataGridControl', + ControlType.DataItemControl: 'DataItemControl', + ControlType.DocumentControl: 'DocumentControl', + ControlType.EditControl: 'EditControl', + ControlType.GroupControl: 'GroupControl', + ControlType.HeaderControl: 'HeaderControl', + ControlType.HeaderItemControl: 'HeaderItemControl', + ControlType.HyperlinkControl: 'HyperlinkControl', + ControlType.ImageControl: 'ImageControl', + ControlType.ListControl: 'ListControl', + ControlType.ListItemControl: 'ListItemControl', + ControlType.MenuBarControl: 'MenuBarControl', + ControlType.MenuControl: 'MenuControl', + ControlType.MenuItemControl: 'MenuItemControl', + ControlType.PaneControl: 'PaneControl', + ControlType.ProgressBarControl: 'ProgressBarControl', + ControlType.RadioButtonControl: 'RadioButtonControl', + ControlType.ScrollBarControl: 'ScrollBarControl', + ControlType.SemanticZoomControl: 'SemanticZoomControl', + ControlType.SeparatorControl: 'SeparatorControl', + ControlType.SliderControl: 'SliderControl', + ControlType.SpinnerControl: 'SpinnerControl', + ControlType.SplitButtonControl: 'SplitButtonControl', + ControlType.StatusBarControl: 'StatusBarControl', + ControlType.TabControl: 'TabControl', + ControlType.TabItemControl: 'TabItemControl', + ControlType.TableControl: 'TableControl', + ControlType.TextControl: 'TextControl', + ControlType.ThumbControl: 'ThumbControl', + ControlType.TitleBarControl: 'TitleBarControl', + ControlType.ToolBarControl: 'ToolBarControl', + ControlType.ToolTipControl: 'ToolTipControl', + ControlType.TreeControl: 'TreeControl', + ControlType.TreeItemControl: 'TreeItemControl', + ControlType.WindowControl: 'WindowControl', +} + + +class PatternId: + """ + PatternId from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/WinAuto/uiauto-controlpattern-ids + """ + AnnotationPattern = 10023 + CustomNavigationPattern = 10033 + DockPattern = 10011 + DragPattern = 10030 + DropTargetPattern = 10031 + ExpandCollapsePattern = 10005 + GridItemPattern = 10007 + GridPattern = 10006 + InvokePattern = 10000 + ItemContainerPattern = 10019 + LegacyIAccessiblePattern = 10018 + MultipleViewPattern = 10008 + ObjectModelPattern = 10022 + RangeValuePattern = 10003 + ScrollItemPattern = 10017 + ScrollPattern = 10004 + SelectionItemPattern = 10010 + SelectionPattern = 10001 + SpreadsheetItemPattern = 10027 + SpreadsheetPattern = 10026 + StylesPattern = 10025 + SynchronizedInputPattern = 10021 + TableItemPattern = 10013 + TablePattern = 10012 + TextChildPattern = 10029 + TextEditPattern = 10032 + TextPattern = 10014 + TextPattern2 = 10024 + TogglePattern = 10015 + TransformPattern = 10016 + TransformPattern2 = 10028 + ValuePattern = 10002 + VirtualizedItemPattern = 10020 + WindowPattern = 10009 + + +PatternIdNames = { + PatternId.AnnotationPattern: 'AnnotationPattern', + PatternId.CustomNavigationPattern: 'CustomNavigationPattern', + PatternId.DockPattern: 'DockPattern', + PatternId.DragPattern: 'DragPattern', + PatternId.DropTargetPattern: 'DropTargetPattern', + PatternId.ExpandCollapsePattern: 'ExpandCollapsePattern', + PatternId.GridItemPattern: 'GridItemPattern', + PatternId.GridPattern: 'GridPattern', + PatternId.InvokePattern: 'InvokePattern', + PatternId.ItemContainerPattern: 'ItemContainerPattern', + PatternId.LegacyIAccessiblePattern: 'LegacyIAccessiblePattern', + PatternId.MultipleViewPattern: 'MultipleViewPattern', + PatternId.ObjectModelPattern: 'ObjectModelPattern', + PatternId.RangeValuePattern: 'RangeValuePattern', + PatternId.ScrollItemPattern: 'ScrollItemPattern', + PatternId.ScrollPattern: 'ScrollPattern', + PatternId.SelectionItemPattern: 'SelectionItemPattern', + PatternId.SelectionPattern: 'SelectionPattern', + PatternId.SpreadsheetItemPattern: 'SpreadsheetItemPattern', + PatternId.SpreadsheetPattern: 'SpreadsheetPattern', + PatternId.StylesPattern: 'StylesPattern', + PatternId.SynchronizedInputPattern: 'SynchronizedInputPattern', + PatternId.TableItemPattern: 'TableItemPattern', + PatternId.TablePattern: 'TablePattern', + PatternId.TextChildPattern: 'TextChildPattern', + PatternId.TextEditPattern: 'TextEditPattern', + PatternId.TextPattern: 'TextPattern', + PatternId.TextPattern2: 'TextPattern2', + PatternId.TogglePattern: 'TogglePattern', + PatternId.TransformPattern: 'TransformPattern', + PatternId.TransformPattern2: 'TransformPattern2', + PatternId.ValuePattern: 'ValuePattern', + PatternId.VirtualizedItemPattern: 'VirtualizedItemPattern', + PatternId.WindowPattern: 'WindowPattern', +} + + +class PropertyId: + """ + PropertyId from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/WinAuto/uiauto-automation-element-propids + Refer https://docs.microsoft.com/en-us/windows/desktop/WinAuto/uiauto-control-pattern-propids + """ + AcceleratorKeyProperty = 30006 + AccessKeyProperty = 30007 + AnnotationAnnotationTypeIdProperty = 30113 + AnnotationAnnotationTypeNameProperty = 30114 + AnnotationAuthorProperty = 30115 + AnnotationDateTimeProperty = 30116 + AnnotationObjectsProperty = 30156 + AnnotationTargetProperty = 30117 + AnnotationTypesProperty = 30155 + AriaPropertiesProperty = 30102 + AriaRoleProperty = 30101 + AutomationIdProperty = 30011 + BoundingRectangleProperty = 30001 + CenterPointProperty = 30165 + ClassNameProperty = 30012 + ClickablePointProperty = 30014 + ControlTypeProperty = 30003 + ControllerForProperty = 30104 + CultureProperty = 30015 + DescribedByProperty = 30105 + DockDockPositionProperty = 30069 + DragDropEffectProperty = 30139 + DragDropEffectsProperty = 30140 + DragGrabbedItemsProperty = 30144 + DragIsGrabbedProperty = 30138 + DropTargetDropTargetEffectProperty = 30142 + DropTargetDropTargetEffectsProperty = 30143 + ExpandCollapseExpandCollapseStateProperty = 30070 + FillColorProperty = 30160 + FillTypeProperty = 30162 + FlowsFromProperty = 30148 + FlowsToProperty = 30106 + FrameworkIdProperty = 30024 + FullDescriptionProperty = 30159 + GridColumnCountProperty = 30063 + GridItemColumnProperty = 30065 + GridItemColumnSpanProperty = 30067 + GridItemContainingGridProperty = 30068 + GridItemRowProperty = 30064 + GridItemRowSpanProperty = 30066 + GridRowCountProperty = 30062 + HasKeyboardFocusProperty = 30008 + HelpTextProperty = 30013 + IsAnnotationPatternAvailableProperty = 30118 + IsContentElementProperty = 30017 + IsControlElementProperty = 30016 + IsCustomNavigationPatternAvailableProperty = 30151 + IsDataValidForFormProperty = 30103 + IsDockPatternAvailableProperty = 30027 + IsDragPatternAvailableProperty = 30137 + IsDropTargetPatternAvailableProperty = 30141 + IsEnabledProperty = 30010 + IsExpandCollapsePatternAvailableProperty = 30028 + IsGridItemPatternAvailableProperty = 30029 + IsGridPatternAvailableProperty = 30030 + IsInvokePatternAvailableProperty = 30031 + IsItemContainerPatternAvailableProperty = 30108 + IsKeyboardFocusableProperty = 30009 + IsLegacyIAccessiblePatternAvailableProperty = 30090 + IsMultipleViewPatternAvailableProperty = 30032 + IsObjectModelPatternAvailableProperty = 30112 + IsOffscreenProperty = 30022 + IsPasswordProperty = 30019 + IsPeripheralProperty = 30150 + IsRangeValuePatternAvailableProperty = 30033 + IsRequiredForFormProperty = 30025 + IsScrollItemPatternAvailableProperty = 30035 + IsScrollPatternAvailableProperty = 30034 + IsSelectionItemPatternAvailableProperty = 30036 + IsSelectionPattern2AvailableProperty = 30168 + IsSelectionPatternAvailableProperty = 30037 + IsSpreadsheetItemPatternAvailableProperty = 30132 + IsSpreadsheetPatternAvailableProperty = 30128 + IsStylesPatternAvailableProperty = 30127 + IsSynchronizedInputPatternAvailableProperty = 30110 + IsTableItemPatternAvailableProperty = 30039 + IsTablePatternAvailableProperty = 30038 + IsTextChildPatternAvailableProperty = 30136 + IsTextEditPatternAvailableProperty = 30149 + IsTextPattern2AvailableProperty = 30119 + IsTextPatternAvailableProperty = 30040 + IsTogglePatternAvailableProperty = 30041 + IsTransformPattern2AvailableProperty = 30134 + IsTransformPatternAvailableProperty = 30042 + IsValuePatternAvailableProperty = 30043 + IsVirtualizedItemPatternAvailableProperty = 30109 + IsWindowPatternAvailableProperty = 30044 + ItemStatusProperty = 30026 + ItemTypeProperty = 30021 + LabeledByProperty = 30018 + LandmarkTypeProperty = 30157 + LegacyIAccessibleChildIdProperty = 30091 + LegacyIAccessibleDefaultActionProperty = 30100 + LegacyIAccessibleDescriptionProperty = 30094 + LegacyIAccessibleHelpProperty = 30097 + LegacyIAccessibleKeyboardShortcutProperty = 30098 + LegacyIAccessibleNameProperty = 30092 + LegacyIAccessibleRoleProperty = 30095 + LegacyIAccessibleSelectionProperty = 30099 + LegacyIAccessibleStateProperty = 30096 + LegacyIAccessibleValueProperty = 30093 + LevelProperty = 30154 + LiveSettingProperty = 30135 + LocalizedControlTypeProperty = 30004 + LocalizedLandmarkTypeProperty = 30158 + MultipleViewCurrentViewProperty = 30071 + MultipleViewSupportedViewsProperty = 30072 + NameProperty = 30005 + NativeWindowHandleProperty = 30020 + OptimizeForVisualContentProperty = 30111 + OrientationProperty = 30023 + OutlineColorProperty = 30161 + OutlineThicknessProperty = 30164 + PositionInSetProperty = 30152 + ProcessIdProperty = 30002 + ProviderDescriptionProperty = 30107 + RangeValueIsReadOnlyProperty = 30048 + RangeValueLargeChangeProperty = 30051 + RangeValueMaximumProperty = 30050 + RangeValueMinimumProperty = 30049 + RangeValueSmallChangeProperty = 30052 + RangeValueValueProperty = 30047 + RotationProperty = 30166 + RuntimeIdProperty = 30000 + ScrollHorizontalScrollPercentProperty = 30053 + ScrollHorizontalViewSizeProperty = 30054 + ScrollHorizontallyScrollableProperty = 30057 + ScrollVerticalScrollPercentProperty = 30055 + ScrollVerticalViewSizeProperty = 30056 + ScrollVerticallyScrollableProperty = 30058 + Selection2CurrentSelectedItemProperty = 30171 + Selection2FirstSelectedItemProperty = 30169 + Selection2ItemCountProperty = 30172 + Selection2LastSelectedItemProperty = 30170 + SelectionCanSelectMultipleProperty = 30060 + SelectionIsSelectionRequiredProperty = 30061 + SelectionItemIsSelectedProperty = 30079 + SelectionItemSelectionContainerProperty = 30080 + SelectionSelectionProperty = 30059 + SizeOfSetProperty = 30153 + SizeProperty = 30167 + SpreadsheetItemAnnotationObjectsProperty = 30130 + SpreadsheetItemAnnotationTypesProperty = 30131 + SpreadsheetItemFormulaProperty = 30129 + StylesExtendedPropertiesProperty = 30126 + StylesFillColorProperty = 30122 + StylesFillPatternColorProperty = 30125 + StylesFillPatternStyleProperty = 30123 + StylesShapeProperty = 30124 + StylesStyleIdProperty = 30120 + StylesStyleNameProperty = 30121 + TableColumnHeadersProperty = 30082 + TableItemColumnHeaderItemsProperty = 30085 + TableItemRowHeaderItemsProperty = 30084 + TableRowHeadersProperty = 30081 + TableRowOrColumnMajorProperty = 30083 + ToggleToggleStateProperty = 30086 + Transform2CanZoomProperty = 30133 + Transform2ZoomLevelProperty = 30145 + Transform2ZoomMaximumProperty = 30147 + Transform2ZoomMinimumProperty = 30146 + TransformCanMoveProperty = 30087 + TransformCanResizeProperty = 30088 + TransformCanRotateProperty = 30089 + ValueIsReadOnlyProperty = 30046 + ValueValueProperty = 30045 + VisualEffectsProperty = 30163 + WindowCanMaximizeProperty = 30073 + WindowCanMinimizeProperty = 30074 + WindowIsModalProperty = 30077 + WindowIsTopmostProperty = 30078 + WindowWindowInteractionStateProperty = 30076 + WindowWindowVisualStateProperty = 30075 + + +PropertyIdNames = { + PropertyId.AcceleratorKeyProperty: 'AcceleratorKeyProperty', + PropertyId.AccessKeyProperty: 'AccessKeyProperty', + PropertyId.AnnotationAnnotationTypeIdProperty: 'AnnotationAnnotationTypeIdProperty', + PropertyId.AnnotationAnnotationTypeNameProperty: 'AnnotationAnnotationTypeNameProperty', + PropertyId.AnnotationAuthorProperty: 'AnnotationAuthorProperty', + PropertyId.AnnotationDateTimeProperty: 'AnnotationDateTimeProperty', + PropertyId.AnnotationObjectsProperty: 'AnnotationObjectsProperty', + PropertyId.AnnotationTargetProperty: 'AnnotationTargetProperty', + PropertyId.AnnotationTypesProperty: 'AnnotationTypesProperty', + PropertyId.AriaPropertiesProperty: 'AriaPropertiesProperty', + PropertyId.AriaRoleProperty: 'AriaRoleProperty', + PropertyId.AutomationIdProperty: 'AutomationIdProperty', + PropertyId.BoundingRectangleProperty: 'BoundingRectangleProperty', + PropertyId.CenterPointProperty: 'CenterPointProperty', + PropertyId.ClassNameProperty: 'ClassNameProperty', + PropertyId.ClickablePointProperty: 'ClickablePointProperty', + PropertyId.ControlTypeProperty: 'ControlTypeProperty', + PropertyId.ControllerForProperty: 'ControllerForProperty', + PropertyId.CultureProperty: 'CultureProperty', + PropertyId.DescribedByProperty: 'DescribedByProperty', + PropertyId.DockDockPositionProperty: 'DockDockPositionProperty', + PropertyId.DragDropEffectProperty: 'DragDropEffectProperty', + PropertyId.DragDropEffectsProperty: 'DragDropEffectsProperty', + PropertyId.DragGrabbedItemsProperty: 'DragGrabbedItemsProperty', + PropertyId.DragIsGrabbedProperty: 'DragIsGrabbedProperty', + PropertyId.DropTargetDropTargetEffectProperty: 'DropTargetDropTargetEffectProperty', + PropertyId.DropTargetDropTargetEffectsProperty: 'DropTargetDropTargetEffectsProperty', + PropertyId.ExpandCollapseExpandCollapseStateProperty: 'ExpandCollapseExpandCollapseStateProperty', + PropertyId.FillColorProperty: 'FillColorProperty', + PropertyId.FillTypeProperty: 'FillTypeProperty', + PropertyId.FlowsFromProperty: 'FlowsFromProperty', + PropertyId.FlowsToProperty: 'FlowsToProperty', + PropertyId.FrameworkIdProperty: 'FrameworkIdProperty', + PropertyId.FullDescriptionProperty: 'FullDescriptionProperty', + PropertyId.GridColumnCountProperty: 'GridColumnCountProperty', + PropertyId.GridItemColumnProperty: 'GridItemColumnProperty', + PropertyId.GridItemColumnSpanProperty: 'GridItemColumnSpanProperty', + PropertyId.GridItemContainingGridProperty: 'GridItemContainingGridProperty', + PropertyId.GridItemRowProperty: 'GridItemRowProperty', + PropertyId.GridItemRowSpanProperty: 'GridItemRowSpanProperty', + PropertyId.GridRowCountProperty: 'GridRowCountProperty', + PropertyId.HasKeyboardFocusProperty: 'HasKeyboardFocusProperty', + PropertyId.HelpTextProperty: 'HelpTextProperty', + PropertyId.IsAnnotationPatternAvailableProperty: 'IsAnnotationPatternAvailableProperty', + PropertyId.IsContentElementProperty: 'IsContentElementProperty', + PropertyId.IsControlElementProperty: 'IsControlElementProperty', + PropertyId.IsCustomNavigationPatternAvailableProperty: 'IsCustomNavigationPatternAvailableProperty', + PropertyId.IsDataValidForFormProperty: 'IsDataValidForFormProperty', + PropertyId.IsDockPatternAvailableProperty: 'IsDockPatternAvailableProperty', + PropertyId.IsDragPatternAvailableProperty: 'IsDragPatternAvailableProperty', + PropertyId.IsDropTargetPatternAvailableProperty: 'IsDropTargetPatternAvailableProperty', + PropertyId.IsEnabledProperty: 'IsEnabledProperty', + PropertyId.IsExpandCollapsePatternAvailableProperty: 'IsExpandCollapsePatternAvailableProperty', + PropertyId.IsGridItemPatternAvailableProperty: 'IsGridItemPatternAvailableProperty', + PropertyId.IsGridPatternAvailableProperty: 'IsGridPatternAvailableProperty', + PropertyId.IsInvokePatternAvailableProperty: 'IsInvokePatternAvailableProperty', + PropertyId.IsItemContainerPatternAvailableProperty: 'IsItemContainerPatternAvailableProperty', + PropertyId.IsKeyboardFocusableProperty: 'IsKeyboardFocusableProperty', + PropertyId.IsLegacyIAccessiblePatternAvailableProperty: 'IsLegacyIAccessiblePatternAvailableProperty', + PropertyId.IsMultipleViewPatternAvailableProperty: 'IsMultipleViewPatternAvailableProperty', + PropertyId.IsObjectModelPatternAvailableProperty: 'IsObjectModelPatternAvailableProperty', + PropertyId.IsOffscreenProperty: 'IsOffscreenProperty', + PropertyId.IsPasswordProperty: 'IsPasswordProperty', + PropertyId.IsPeripheralProperty: 'IsPeripheralProperty', + PropertyId.IsRangeValuePatternAvailableProperty: 'IsRangeValuePatternAvailableProperty', + PropertyId.IsRequiredForFormProperty: 'IsRequiredForFormProperty', + PropertyId.IsScrollItemPatternAvailableProperty: 'IsScrollItemPatternAvailableProperty', + PropertyId.IsScrollPatternAvailableProperty: 'IsScrollPatternAvailableProperty', + PropertyId.IsSelectionItemPatternAvailableProperty: 'IsSelectionItemPatternAvailableProperty', + PropertyId.IsSelectionPattern2AvailableProperty: 'IsSelectionPattern2AvailableProperty', + PropertyId.IsSelectionPatternAvailableProperty: 'IsSelectionPatternAvailableProperty', + PropertyId.IsSpreadsheetItemPatternAvailableProperty: 'IsSpreadsheetItemPatternAvailableProperty', + PropertyId.IsSpreadsheetPatternAvailableProperty: 'IsSpreadsheetPatternAvailableProperty', + PropertyId.IsStylesPatternAvailableProperty: 'IsStylesPatternAvailableProperty', + PropertyId.IsSynchronizedInputPatternAvailableProperty: 'IsSynchronizedInputPatternAvailableProperty', + PropertyId.IsTableItemPatternAvailableProperty: 'IsTableItemPatternAvailableProperty', + PropertyId.IsTablePatternAvailableProperty: 'IsTablePatternAvailableProperty', + PropertyId.IsTextChildPatternAvailableProperty: 'IsTextChildPatternAvailableProperty', + PropertyId.IsTextEditPatternAvailableProperty: 'IsTextEditPatternAvailableProperty', + PropertyId.IsTextPattern2AvailableProperty: 'IsTextPattern2AvailableProperty', + PropertyId.IsTextPatternAvailableProperty: 'IsTextPatternAvailableProperty', + PropertyId.IsTogglePatternAvailableProperty: 'IsTogglePatternAvailableProperty', + PropertyId.IsTransformPattern2AvailableProperty: 'IsTransformPattern2AvailableProperty', + PropertyId.IsTransformPatternAvailableProperty: 'IsTransformPatternAvailableProperty', + PropertyId.IsValuePatternAvailableProperty: 'IsValuePatternAvailableProperty', + PropertyId.IsVirtualizedItemPatternAvailableProperty: 'IsVirtualizedItemPatternAvailableProperty', + PropertyId.IsWindowPatternAvailableProperty: 'IsWindowPatternAvailableProperty', + PropertyId.ItemStatusProperty: 'ItemStatusProperty', + PropertyId.ItemTypeProperty: 'ItemTypeProperty', + PropertyId.LabeledByProperty: 'LabeledByProperty', + PropertyId.LandmarkTypeProperty: 'LandmarkTypeProperty', + PropertyId.LegacyIAccessibleChildIdProperty: 'LegacyIAccessibleChildIdProperty', + PropertyId.LegacyIAccessibleDefaultActionProperty: 'LegacyIAccessibleDefaultActionProperty', + PropertyId.LegacyIAccessibleDescriptionProperty: 'LegacyIAccessibleDescriptionProperty', + PropertyId.LegacyIAccessibleHelpProperty: 'LegacyIAccessibleHelpProperty', + PropertyId.LegacyIAccessibleKeyboardShortcutProperty: 'LegacyIAccessibleKeyboardShortcutProperty', + PropertyId.LegacyIAccessibleNameProperty: 'LegacyIAccessibleNameProperty', + PropertyId.LegacyIAccessibleRoleProperty: 'LegacyIAccessibleRoleProperty', + PropertyId.LegacyIAccessibleSelectionProperty: 'LegacyIAccessibleSelectionProperty', + PropertyId.LegacyIAccessibleStateProperty: 'LegacyIAccessibleStateProperty', + PropertyId.LegacyIAccessibleValueProperty: 'LegacyIAccessibleValueProperty', + PropertyId.LevelProperty: 'LevelProperty', + PropertyId.LiveSettingProperty: 'LiveSettingProperty', + PropertyId.LocalizedControlTypeProperty: 'LocalizedControlTypeProperty', + PropertyId.LocalizedLandmarkTypeProperty: 'LocalizedLandmarkTypeProperty', + PropertyId.MultipleViewCurrentViewProperty: 'MultipleViewCurrentViewProperty', + PropertyId.MultipleViewSupportedViewsProperty: 'MultipleViewSupportedViewsProperty', + PropertyId.NameProperty: 'NameProperty', + PropertyId.NativeWindowHandleProperty: 'NativeWindowHandleProperty', + PropertyId.OptimizeForVisualContentProperty: 'OptimizeForVisualContentProperty', + PropertyId.OrientationProperty: 'OrientationProperty', + PropertyId.OutlineColorProperty: 'OutlineColorProperty', + PropertyId.OutlineThicknessProperty: 'OutlineThicknessProperty', + PropertyId.PositionInSetProperty: 'PositionInSetProperty', + PropertyId.ProcessIdProperty: 'ProcessIdProperty', + PropertyId.ProviderDescriptionProperty: 'ProviderDescriptionProperty', + PropertyId.RangeValueIsReadOnlyProperty: 'RangeValueIsReadOnlyProperty', + PropertyId.RangeValueLargeChangeProperty: 'RangeValueLargeChangeProperty', + PropertyId.RangeValueMaximumProperty: 'RangeValueMaximumProperty', + PropertyId.RangeValueMinimumProperty: 'RangeValueMinimumProperty', + PropertyId.RangeValueSmallChangeProperty: 'RangeValueSmallChangeProperty', + PropertyId.RangeValueValueProperty: 'RangeValueValueProperty', + PropertyId.RotationProperty: 'RotationProperty', + PropertyId.RuntimeIdProperty: 'RuntimeIdProperty', + PropertyId.ScrollHorizontalScrollPercentProperty: 'ScrollHorizontalScrollPercentProperty', + PropertyId.ScrollHorizontalViewSizeProperty: 'ScrollHorizontalViewSizeProperty', + PropertyId.ScrollHorizontallyScrollableProperty: 'ScrollHorizontallyScrollableProperty', + PropertyId.ScrollVerticalScrollPercentProperty: 'ScrollVerticalScrollPercentProperty', + PropertyId.ScrollVerticalViewSizeProperty: 'ScrollVerticalViewSizeProperty', + PropertyId.ScrollVerticallyScrollableProperty: 'ScrollVerticallyScrollableProperty', + PropertyId.Selection2CurrentSelectedItemProperty: 'Selection2CurrentSelectedItemProperty', + PropertyId.Selection2FirstSelectedItemProperty: 'Selection2FirstSelectedItemProperty', + PropertyId.Selection2ItemCountProperty: 'Selection2ItemCountProperty', + PropertyId.Selection2LastSelectedItemProperty: 'Selection2LastSelectedItemProperty', + PropertyId.SelectionCanSelectMultipleProperty: 'SelectionCanSelectMultipleProperty', + PropertyId.SelectionIsSelectionRequiredProperty: 'SelectionIsSelectionRequiredProperty', + PropertyId.SelectionItemIsSelectedProperty: 'SelectionItemIsSelectedProperty', + PropertyId.SelectionItemSelectionContainerProperty: 'SelectionItemSelectionContainerProperty', + PropertyId.SelectionSelectionProperty: 'SelectionSelectionProperty', + PropertyId.SizeOfSetProperty: 'SizeOfSetProperty', + PropertyId.SizeProperty: 'SizeProperty', + PropertyId.SpreadsheetItemAnnotationObjectsProperty: 'SpreadsheetItemAnnotationObjectsProperty', + PropertyId.SpreadsheetItemAnnotationTypesProperty: 'SpreadsheetItemAnnotationTypesProperty', + PropertyId.SpreadsheetItemFormulaProperty: 'SpreadsheetItemFormulaProperty', + PropertyId.StylesExtendedPropertiesProperty: 'StylesExtendedPropertiesProperty', + PropertyId.StylesFillColorProperty: 'StylesFillColorProperty', + PropertyId.StylesFillPatternColorProperty: 'StylesFillPatternColorProperty', + PropertyId.StylesFillPatternStyleProperty: 'StylesFillPatternStyleProperty', + PropertyId.StylesShapeProperty: 'StylesShapeProperty', + PropertyId.StylesStyleIdProperty: 'StylesStyleIdProperty', + PropertyId.StylesStyleNameProperty: 'StylesStyleNameProperty', + PropertyId.TableColumnHeadersProperty: 'TableColumnHeadersProperty', + PropertyId.TableItemColumnHeaderItemsProperty: 'TableItemColumnHeaderItemsProperty', + PropertyId.TableItemRowHeaderItemsProperty: 'TableItemRowHeaderItemsProperty', + PropertyId.TableRowHeadersProperty: 'TableRowHeadersProperty', + PropertyId.TableRowOrColumnMajorProperty: 'TableRowOrColumnMajorProperty', + PropertyId.ToggleToggleStateProperty: 'ToggleToggleStateProperty', + PropertyId.Transform2CanZoomProperty: 'Transform2CanZoomProperty', + PropertyId.Transform2ZoomLevelProperty: 'Transform2ZoomLevelProperty', + PropertyId.Transform2ZoomMaximumProperty: 'Transform2ZoomMaximumProperty', + PropertyId.Transform2ZoomMinimumProperty: 'Transform2ZoomMinimumProperty', + PropertyId.TransformCanMoveProperty: 'TransformCanMoveProperty', + PropertyId.TransformCanResizeProperty: 'TransformCanResizeProperty', + PropertyId.TransformCanRotateProperty: 'TransformCanRotateProperty', + PropertyId.ValueIsReadOnlyProperty: 'ValueIsReadOnlyProperty', + PropertyId.ValueValueProperty: 'ValueValueProperty', + PropertyId.VisualEffectsProperty: 'VisualEffectsProperty', + PropertyId.WindowCanMaximizeProperty: 'WindowCanMaximizeProperty', + PropertyId.WindowCanMinimizeProperty: 'WindowCanMinimizeProperty', + PropertyId.WindowIsModalProperty: 'WindowIsModalProperty', + PropertyId.WindowIsTopmostProperty: 'WindowIsTopmostProperty', + PropertyId.WindowWindowInteractionStateProperty: 'WindowWindowInteractionStateProperty', + PropertyId.WindowWindowVisualStateProperty: 'WindowWindowVisualStateProperty', +} + + +class AccessibleRole: + """ + AccessibleRole from IUIAutomation. + Refer https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.accessiblerole?view=netframework-4.8 + """ + TitleBar = 0x1 + MenuBar = 0x2 + ScrollBar = 0x3 + Grip = 0x4 + Sound = 0x5 + Cursor = 0x6 + Caret = 0x7 + Alert = 0x8 + Window = 0x9 + Client = 0xa + MenuPopup = 0xb + MenuItem = 0xc + ToolTip = 0xd + Application = 0xe + Document = 0xf + Pane = 0x10 + Chart = 0x11 + Dialog = 0x12 + Border = 0x13 + Grouping = 0x14 + Separator = 0x15 + Toolbar = 0x16 + StatusBar = 0x17 + Table = 0x18 + ColumnHeader = 0x19 + RowHeader = 0x1a + Column = 0x1b + Row = 0x1c + Cell = 0x1d + Link = 0x1e + HelpBalloon = 0x1f + Character = 0x20 + List = 0x21 + ListItem = 0x22 + Outline = 0x23 + OutlineItem = 0x24 + PageTab = 0x25 + PropertyPage = 0x26 + Indicator = 0x27 + Graphic = 0x28 + StaticText = 0x29 + Text = 0x2a + PushButton = 0x2b + CheckButton = 0x2c + RadioButton = 0x2d + ComboBox = 0x2e + DropList = 0x2f + ProgressBar = 0x30 + Dial = 0x31 + HotkeyField = 0x32 + Slider = 0x33 + SpinButton = 0x34 + Diagram = 0x35 + Animation = 0x36 + Equation = 0x37 + ButtonDropDown = 0x38 + ButtonMenu = 0x39 + ButtonDropDownGrid = 0x3a + WhiteSpace = 0x3b + PageTabList = 0x3c + Clock = 0x3d + SplitButton = 0x3e + IpAddress = 0x3f + OutlineButton = 0x40 + + +class AccessibleState(): + """ + AccessibleState from IUIAutomation. + Refer https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.accessiblestates?view=netframework-4.8 + """ + Normal = 0 + Unavailable = 0x1 + Selected = 0x2 + Focused = 0x4 + Pressed = 0x8 + Checked = 0x10 + Mixed = 0x20 + Indeterminate = 0x20 + ReadOnly = 0x40 + HotTracked = 0x80 + Default = 0x100 + Expanded = 0x200 + Collapsed = 0x400 + Busy = 0x800 + Floating = 0x1000 + Marqueed = 0x2000 + Animated = 0x4000 + Invisible = 0x8000 + Offscreen = 0x10000 + Sizeable = 0x20000 + Moveable = 0x40000 + SelfVoicing = 0x80000 + Focusable = 0x100000 + Selectable = 0x200000 + Linked = 0x400000 + Traversed = 0x800000 + MultiSelectable = 0x1000000 + ExtSelectable = 0x2000000 + AlertLow = 0x4000000 + AlertMedium = 0x8000000 + AlertHigh = 0x10000000 + Protected = 0x20000000 + Valid = 0x7fffffff + HasPopup = 0x40000000 + + +class AccessibleSelection: + """ + AccessibleSelection from IUIAutomation. + Refer https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.accessibleselection?view=netframework-4.8 + """ + None_ = 0 + TakeFocus = 0x1 + TakeSelection = 0x2 + ExtendSelection = 0x4 + AddSelection = 0x8 + RemoveSelection = 0x10 + + +class AnnotationType: + """ + AnnotationType from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/WinAuto/uiauto-annotation-type-identifiers + """ + AdvancedProofingIssue = 60020 + Author = 60019 + CircularReferenceError = 60022 + Comment = 60003 + ConflictingChange = 60018 + DataValidationError = 60021 + DeletionChange = 60012 + EditingLockedChange = 60016 + Endnote = 60009 + ExternalChange = 60017 + Footer = 60007 + Footnote = 60010 + FormatChange = 60014 + FormulaError = 60004 + GrammarError = 60002 + Header = 60006 + Highlighted = 60008 + InsertionChange = 60011 + Mathematics = 60023 + MoveChange = 60013 + SpellingError = 60001 + TrackChanges = 60005 + Unknown = 60000 + UnsyncedChange = 60015 + + +class NavigateDirection: + """ + NavigateDirection from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-navigatedirection + """ + Parent = 0 + NextSibling = 1 + PreviousSibling = 2 + FirstChild = 3 + LastChild = 4 + + +class DockPosition: + """ + DockPosition from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-dockposition + """ + Top = 0 + Left = 1 + Bottom = 2 + Right = 3 + Fill = 4 + None_ = 5 + + +class ScrollAmount: + """ + ScrollAmount from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-scrollamount + """ + LargeDecrement = 0 + SmallDecrement = 1 + NoAmount = 2 + LargeIncrement = 3 + SmallIncrement = 4 + + +class StyleId: + """ + StyleId from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/WinAuto/uiauto-style-identifiers + """ + Custom = 70000 + Heading1 = 70001 + Heading2 = 70002 + Heading3 = 70003 + Heading4 = 70004 + Heading5 = 70005 + Heading6 = 70006 + Heading7 = 70007 + Heading8 = 70008 + Heading9 = 70009 + Title = 70010 + Subtitle = 70011 + Normal = 70012 + Emphasis = 70013 + Quote = 70014 + BulletedList = 70015 + NumberedList = 70016 + + +class RowOrColumnMajor: + """ + RowOrColumnMajor from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-roworcolumnmajor + """ + RowMajor = 0 + ColumnMajor = 1 + Indeterminate = 2 + + +class ExpandCollapseState: + """ + ExpandCollapseState from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-expandcollapsestate + """ + Collapsed = 0 + Expanded = 1 + PartiallyExpanded = 2 + LeafNode = 3 + + +class OrientationType: + """ + OrientationType from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-orientationtype + """ + None_ = 0 + Horizontal = 1 + Vertical = 2 + + +class ToggleState: + """ + ToggleState from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-togglestate + """ + Off = 0 + On = 1 + Indeterminate = 2 + + +class TextPatternRangeEndpoint: + """ + TextPatternRangeEndpoint from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-textpatternrangeendpoint + """ + Start = 0 + End = 1 + +class TextAttributeId: + """ + TextAttributeId from IUIAutomation. + Refer https://docs.microsoft.com/zh-cn/windows/desktop/WinAuto/uiauto-textattribute-ids + """ + AfterParagraphSpacingAttribute = 40042 + AnimationStyleAttribute = 40000 + AnnotationObjectsAttribute = 40032 + AnnotationTypesAttribute = 40031 + BackgroundColorAttribute = 40001 + BeforeParagraphSpacingAttribute = 40041 + BulletStyleAttribute = 40002 + CapStyleAttribute = 40003 + CaretBidiModeAttribute = 40039 + CaretPositionAttribute = 40038 + CultureAttribute = 40004 + FontNameAttribute = 40005 + FontSizeAttribute = 40006 + FontWeightAttribute = 40007 + ForegroundColorAttribute = 40008 + HorizontalTextAlignmentAttribute = 40009 + IndentationFirstLineAttribute = 40010 + IndentationLeadingAttribute = 40011 + IndentationTrailingAttribute = 40012 + IsActiveAttribute = 40036 + IsHiddenAttribute = 40013 + IsItalicAttribute = 40014 + IsReadOnlyAttribute = 40015 + IsSubscriptAttribute = 40016 + IsSuperscriptAttribute = 40017 + LineSpacingAttribute = 40040 + LinkAttribute = 40035 + MarginBottomAttribute = 40018 + MarginLeadingAttribute = 40019 + MarginTopAttribute = 40020 + MarginTrailingAttribute = 40021 + OutlineStylesAttribute = 40022 + OverlineColorAttribute = 40023 + OverlineStyleAttribute = 40024 + SayAsInterpretAsAttribute = 40043 + SelectionActiveEndAttribute = 40037 + StrikethroughColorAttribute = 40025 + StrikethroughStyleAttribute = 40026 + StyleIdAttribute = 40034 + StyleNameAttribute = 40033 + TabsAttribute = 40027 + TextFlowDirectionsAttribute = 40028 + UnderlineColorAttribute = 40029 + UnderlineStyleAttribute = 40030 + + +class TextUnit: + """ + TextUnit from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-textunit + """ + Character = 0 + Format = 1 + Word = 2 + Line = 3 + Paragraph = 4 + Page = 5 + Document = 6 + + +class ZoomUnit: + """ + ZoomUnit from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-zoomunit + """ + NoAmount = 0 + LargeDecrement = 1 + SmallDecrement = 2 + LargeIncrement = 3 + SmallIncrement = 4 + + +class WindowInteractionState: + """ + WindowInteractionState from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-windowinteractionstate + """ + Running = 0 + Closing = 1 + ReadyForUserInteraction = 2 + BlockedByModalWindow = 3 + NotResponding = 4 + + +class WindowVisualState: + """ + WindowVisualState from IUIAutomation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationcore/ne-uiautomationcore-windowvisualstate + """ + Normal = 0 + Maximized = 1 + Minimized = 2 + + +class ConsoleColor: + """ConsoleColor from Win32.""" + Default = -1 + Black = 0 + DarkBlue = 1 + DarkGreen = 2 + DarkCyan = 3 + DarkRed = 4 + DarkMagenta = 5 + DarkYellow = 6 + Gray = 7 + DarkGray = 8 + Blue = 9 + Green = 10 + Cyan = 11 + Red = 12 + Magenta = 13 + Yellow = 14 + White = 15 + + +class GAFlag: + """GAFlag from Win32.""" + Parent = 1 + Root = 2 + RootOwner = 3 + + +class MouseEventFlag: + """MouseEventFlag from Win32.""" + Move = 0x0001 + LeftDown = 0x0002 + LeftUp = 0x0004 + RightDown = 0x0008 + RightUp = 0x0010 + MiddleDown = 0x0020 + MiddleUp = 0x0040 + XDown = 0x0080 + XUp = 0x0100 + Wheel = 0x0800 + HWheel = 0x1000 + MoveNoCoalesce = 0x2000 + VirtualDesk = 0x4000 + Absolute = 0x8000 + + +class KeyboardEventFlag: + """KeyboardEventFlag from Win32.""" + KeyDown = 0x0000 + ExtendedKey = 0x0001 + KeyUp = 0x0002 + KeyUnicode = 0x0004 + KeyScanCode = 0x0008 + + +class InputType: + """InputType from Win32""" + Mouse = 0 + Keyboard = 1 + Hardware = 2 + + +class ModifierKey: + """ModifierKey from Win32.""" + Alt = 0x0001 + Control = 0x0002 + Shift = 0x0004 + Win = 0x0008 + NoRepeat = 0x4000 + + +class SW: + """ShowWindow params from Win32.""" + Hide = 0 + ShowNormal = 1 + Normal = 1 + ShowMinimized = 2 + ShowMaximized = 3 + Maximize = 3 + ShowNoActivate = 4 + Show = 5 + Minimize = 6 + ShowMinNoActive = 7 + ShowNA = 8 + Restore = 9 + ShowDefault = 10 + ForceMinimize = 11 + Max = 11 + + +class SWP: + """SetWindowPos params from Win32.""" + HWND_Top = 0 + HWND_Bottom = 1 + HWND_Topmost = -1 + HWND_NoTopmost = -2 + SWP_NoSize = 0x0001 + SWP_NoMove = 0x0002 + SWP_NoZOrder = 0x0004 + SWP_NoRedraw = 0x0008 + SWP_NoActivate = 0x0010 + SWP_FrameChanged = 0x0020 # The frame changed: send WM_NCCALCSIZE + SWP_ShowWindow = 0x0040 + SWP_HideWindow = 0x0080 + SWP_NoCopyBits = 0x0100 + SWP_NoOwnerZOrder = 0x0200 # Don't do owner Z ordering + SWP_NoSendChanging = 0x0400 # Don't send WM_WINDOWPOSCHANGING + SWP_DrawFrame = SWP_FrameChanged + SWP_NoReposition = SWP_NoOwnerZOrder + SWP_DeferErase = 0x2000 + SWP_AsyncWindowPos = 0x4000 + + +class MB: + """MessageBox flags from Win32.""" + Ok = 0x00000000 + OkCancel = 0x00000001 + AbortRetryIgnore = 0x00000002 + YesNoCancel = 0x00000003 + YesNo = 0x00000004 + RetryCancel = 0x00000005 + CancelTryContinue = 0x00000006 + IconHand = 0x00000010 + IconQuestion = 0x00000020 + IconExclamation = 0x00000030 + IconAsterisk = 0x00000040 + UserIcon = 0x00000080 + IconWarning = 0x00000030 + IconError = 0x00000010 + IconInformation = 0x00000040 + IconStop = 0x00000010 + DefButton1 = 0x00000000 + DefButton2 = 0x00000100 + DefButton3 = 0x00000200 + DefButton4 = 0x00000300 + ApplModal = 0x00000000 + SystemModal = 0x00001000 + TaskModal = 0x00002000 + Help = 0x00004000 # help button + NoFocus = 0x00008000 + SetForeground = 0x00010000 + DefaultDesktopOnly = 0x00020000 + Topmost = 0x00040000 + Right = 0x00080000 + RtlReading = 0x00100000 + ServiceNotification = 0x00200000 + ServiceNotificationNT3X = 0x00040000 + + TypeMask = 0x0000000f + IconMask = 0x000000f0 + DefMask = 0x00000f00 + ModeMask = 0x00003000 + MiscMask = 0x0000c000 + + IdOk = 1 + IdCancel = 2 + IdAbort = 3 + IdRetry = 4 + IdIgnore = 5 + IdYes = 6 + IdNo = 7 + IdClose = 8 + IdHelp = 9 + IdTryAgain = 10 + IdContinue = 11 + IdTimeout = 32000 + + +class GWL: + ExStyle = -20 + HInstance = -6 + HwndParent = -8 + ID = -12 + Style = -16 + UserData = -21 + WndProc = -4 + + +class ProcessDpiAwareness: + ProcessDpiUnaware = 0 + ProcessSystemDpiAware = 1 + ProcessPerMonitorDpiAware = 2 + + +class DpiAwarenessContext: + DpiAwarenessContextUnaware = -1 + DpiAwarenessContextSystemAware = -2 + DpiAwarenessContextPerMonitorAware = -3 + DpiAwarenessContextPerMonitorAwareV2 = -4 + DpiAwarenessContextUnawareGdiScaled = -5 + + +class Keys: + """Key codes from Win32.""" + VK_LBUTTON = 0x01 #Left mouse button + VK_RBUTTON = 0x02 #Right mouse button + VK_CANCEL = 0x03 #Control-break processing + VK_MBUTTON = 0x04 #Middle mouse button (three-button mouse) + VK_XBUTTON1 = 0x05 #X1 mouse button + VK_XBUTTON2 = 0x06 #X2 mouse button + VK_BACK = 0x08 #BACKSPACE key + VK_TAB = 0x09 #TAB key + VK_CLEAR = 0x0C #CLEAR key + VK_RETURN = 0x0D #ENTER key + VK_ENTER = 0x0D + VK_SHIFT = 0x10 #SHIFT key + VK_CONTROL = 0x11 #CTRL key + VK_MENU = 0x12 #ALT key + VK_PAUSE = 0x13 #PAUSE key + VK_CAPITAL = 0x14 #CAPS LOCK key + VK_KANA = 0x15 #IME Kana mode + VK_HANGUEL = 0x15 #IME Hanguel mode (maintained for compatibility; use VK_HANGUL) + VK_HANGUL = 0x15 #IME Hangul mode + VK_JUNJA = 0x17 #IME Junja mode + VK_FINAL = 0x18 #IME final mode + VK_HANJA = 0x19 #IME Hanja mode + VK_KANJI = 0x19 #IME Kanji mode + VK_ESCAPE = 0x1B #ESC key + VK_CONVERT = 0x1C #IME convert + VK_NONCONVERT = 0x1D #IME nonconvert + VK_ACCEPT = 0x1E #IME accept + VK_MODECHANGE = 0x1F #IME mode change request + VK_SPACE = 0x20 #SPACEBAR + VK_PRIOR = 0x21 #PAGE UP key + VK_PAGEUP = 0x21 + VK_NEXT = 0x22 #PAGE DOWN key + VK_PAGEDOWN = 0x22 + VK_END = 0x23 #END key + VK_HOME = 0x24 #HOME key + VK_LEFT = 0x25 #LEFT ARROW key + VK_UP = 0x26 #UP ARROW key + VK_RIGHT = 0x27 #RIGHT ARROW key + VK_DOWN = 0x28 #DOWN ARROW key + VK_SELECT = 0x29 #SELECT key + VK_PRINT = 0x2A #PRINT key + VK_EXECUTE = 0x2B #EXECUTE key + VK_SNAPSHOT = 0x2C #PRINT SCREEN key + VK_INSERT = 0x2D #INS key + VK_DELETE = 0x2E #DEL key + VK_HELP = 0x2F #HELP key + VK_0 = 0x30 #0 key + VK_1 = 0x31 #1 key + VK_2 = 0x32 #2 key + VK_3 = 0x33 #3 key + VK_4 = 0x34 #4 key + VK_5 = 0x35 #5 key + VK_6 = 0x36 #6 key + VK_7 = 0x37 #7 key + VK_8 = 0x38 #8 key + VK_9 = 0x39 #9 key + VK_A = 0x41 #A key + VK_B = 0x42 #B key + VK_C = 0x43 #C key + VK_D = 0x44 #D key + VK_E = 0x45 #E key + VK_F = 0x46 #F key + VK_G = 0x47 #G key + VK_H = 0x48 #H key + VK_I = 0x49 #I key + VK_J = 0x4A #J key + VK_K = 0x4B #K key + VK_L = 0x4C #L key + VK_M = 0x4D #M key + VK_N = 0x4E #N key + VK_O = 0x4F #O key + VK_P = 0x50 #P key + VK_Q = 0x51 #Q key + VK_R = 0x52 #R key + VK_S = 0x53 #S key + VK_T = 0x54 #T key + VK_U = 0x55 #U key + VK_V = 0x56 #V key + VK_W = 0x57 #W key + VK_X = 0x58 #X key + VK_Y = 0x59 #Y key + VK_Z = 0x5A #Z key + VK_LWIN = 0x5B #Left Windows key (Natural keyboard) + VK_RWIN = 0x5C #Right Windows key (Natural keyboard) + VK_APPS = 0x5D #Applications key (Natural keyboard) + VK_SLEEP = 0x5F #Computer Sleep key + VK_NUMPAD0 = 0x60 #Numeric keypad 0 key + VK_NUMPAD1 = 0x61 #Numeric keypad 1 key + VK_NUMPAD2 = 0x62 #Numeric keypad 2 key + VK_NUMPAD3 = 0x63 #Numeric keypad 3 key + VK_NUMPAD4 = 0x64 #Numeric keypad 4 key + VK_NUMPAD5 = 0x65 #Numeric keypad 5 key + VK_NUMPAD6 = 0x66 #Numeric keypad 6 key + VK_NUMPAD7 = 0x67 #Numeric keypad 7 key + VK_NUMPAD8 = 0x68 #Numeric keypad 8 key + VK_NUMPAD9 = 0x69 #Numeric keypad 9 key + VK_MULTIPLY = 0x6A #Multiply key + VK_ADD = 0x6B #Add key + VK_SEPARATOR = 0x6C #Separator key + VK_SUBTRACT = 0x6D #Subtract key + VK_DECIMAL = 0x6E #Decimal key + VK_DIVIDE = 0x6F #Divide key + VK_F1 = 0x70 #F1 key + VK_F2 = 0x71 #F2 key + VK_F3 = 0x72 #F3 key + VK_F4 = 0x73 #F4 key + VK_F5 = 0x74 #F5 key + VK_F6 = 0x75 #F6 key + VK_F7 = 0x76 #F7 key + VK_F8 = 0x77 #F8 key + VK_F9 = 0x78 #F9 key + VK_F10 = 0x79 #F10 key + VK_F11 = 0x7A #F11 key + VK_F12 = 0x7B #F12 key + VK_F13 = 0x7C #F13 key + VK_F14 = 0x7D #F14 key + VK_F15 = 0x7E #F15 key + VK_F16 = 0x7F #F16 key + VK_F17 = 0x80 #F17 key + VK_F18 = 0x81 #F18 key + VK_F19 = 0x82 #F19 key + VK_F20 = 0x83 #F20 key + VK_F21 = 0x84 #F21 key + VK_F22 = 0x85 #F22 key + VK_F23 = 0x86 #F23 key + VK_F24 = 0x87 #F24 key + VK_NUMLOCK = 0x90 #NUM LOCK key + VK_SCROLL = 0x91 #SCROLL LOCK key + VK_LSHIFT = 0xA0 #Left SHIFT key + VK_RSHIFT = 0xA1 #Right SHIFT key + VK_LCONTROL = 0xA2 #Left CONTROL key + VK_RCONTROL = 0xA3 #Right CONTROL key + VK_LMENU = 0xA4 #Left MENU key + VK_RMENU = 0xA5 #Right MENU key + VK_BROWSER_BACK = 0xA6 #Browser Back key + VK_BROWSER_FORWARD = 0xA7 #Browser Forward key + VK_BROWSER_REFRESH = 0xA8 #Browser Refresh key + VK_BROWSER_STOP = 0xA9 #Browser Stop key + VK_BROWSER_SEARCH = 0xAA #Browser Search key + VK_BROWSER_FAVORITES = 0xAB #Browser Favorites key + VK_BROWSER_HOME = 0xAC #Browser Start and Home key + VK_VOLUME_MUTE = 0xAD #Volume Mute key + VK_VOLUME_DOWN = 0xAE #Volume Down key + VK_VOLUME_UP = 0xAF #Volume Up key + VK_MEDIA_NEXT_TRACK = 0xB0 #Next Track key + VK_MEDIA_PREV_TRACK = 0xB1 #Previous Track key + VK_MEDIA_STOP = 0xB2 #Stop Media key + VK_MEDIA_PLAY_PAUSE = 0xB3 #Play/Pause Media key + VK_LAUNCH_MAIL = 0xB4 #Start Mail key + VK_LAUNCH_MEDIA_SELECT = 0xB5 #Select Media key + VK_LAUNCH_APP1 = 0xB6 #Start Application 1 key + VK_LAUNCH_APP2 = 0xB7 #Start Application 2 key + VK_OEM_1 = 0xBA #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the ';:' key + VK_OEM_PLUS = 0xBB #For any country/region, the '+' key + VK_OEM_COMMA = 0xBC #For any country/region, the ',' key + VK_OEM_MINUS = 0xBD #For any country/region, the '-' key + VK_OEM_PERIOD = 0xBE #For any country/region, the '.' key + VK_OEM_2 = 0xBF #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the '/?' key + VK_OEM_3 = 0xC0 #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the '`~' key + VK_OEM_4 = 0xDB #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the '[{' key + VK_OEM_5 = 0xDC #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the '\|' key + VK_OEM_6 = 0xDD #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the ']}' key + VK_OEM_7 = 0xDE #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the 'single-quote/double-quote' key + VK_OEM_8 = 0xDF #Used for miscellaneous characters; it can vary by keyboard. + VK_OEM_102 = 0xE2 #Either the angle bracket key or the backslash key on the RT 102-key keyboard + VK_PROCESSKEY = 0xE5 #IME PROCESS key + VK_PACKET = 0xE7 #Used to pass Unicode characters as if they were keystrokes. The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KeyUp + VK_ATTN = 0xF6 #Attn key + VK_CRSEL = 0xF7 #CrSel key + VK_EXSEL = 0xF8 #ExSel key + VK_EREOF = 0xF9 #Erase EOF key + VK_PLAY = 0xFA #Play key + VK_ZOOM = 0xFB #Zoom key + VK_NONAME = 0xFC #Reserved + VK_PA1 = 0xFD #PA1 key + VK_OEM_CLEAR = 0xFE #Clear key + + +SpecialKeyNames = { + 'LBUTTON': Keys.VK_LBUTTON, #Left mouse button + 'RBUTTON': Keys.VK_RBUTTON, #Right mouse button + 'CANCEL': Keys.VK_CANCEL, #Control-break processing + 'MBUTTON': Keys.VK_MBUTTON, #Middle mouse button (three-button mouse) + 'XBUTTON1': Keys.VK_XBUTTON1, #X1 mouse button + 'XBUTTON2': Keys.VK_XBUTTON2, #X2 mouse button + 'BACK': Keys.VK_BACK, #BACKSPACE key + 'TAB': Keys.VK_TAB, #TAB key + 'CLEAR': Keys.VK_CLEAR, #CLEAR key + 'RETURN': Keys.VK_RETURN, #ENTER key + 'ENTER': Keys.VK_RETURN, #ENTER key + 'SHIFT': Keys.VK_SHIFT, #SHIFT key + 'CTRL': Keys.VK_CONTROL, #CTRL key + 'CONTROL': Keys.VK_CONTROL, #CTRL key + 'ALT': Keys.VK_MENU, #ALT key + 'PAUSE': Keys.VK_PAUSE, #PAUSE key + 'CAPITAL': Keys.VK_CAPITAL, #CAPS LOCK key + 'KANA': Keys.VK_KANA, #IME Kana mode + 'HANGUEL': Keys.VK_HANGUEL, #IME Hanguel mode (maintained for compatibility; use VK_HANGUL) + 'HANGUL': Keys.VK_HANGUL, #IME Hangul mode + 'JUNJA': Keys.VK_JUNJA, #IME Junja mode + 'FINAL': Keys.VK_FINAL, #IME final mode + 'HANJA': Keys.VK_HANJA, #IME Hanja mode + 'KANJI': Keys.VK_KANJI, #IME Kanji mode + 'ESC': Keys.VK_ESCAPE, #ESC key + 'ESCAPE': Keys.VK_ESCAPE, #ESC key + 'CONVERT': Keys.VK_CONVERT, #IME convert + 'NONCONVERT': Keys.VK_NONCONVERT, #IME nonconvert + 'ACCEPT': Keys.VK_ACCEPT, #IME accept + 'MODECHANGE': Keys.VK_MODECHANGE, #IME mode change request + 'SPACE': Keys.VK_SPACE, #SPACEBAR + 'PRIOR': Keys.VK_PRIOR, #PAGE UP key + 'PAGEUP': Keys.VK_PRIOR, #PAGE UP key + 'NEXT': Keys.VK_NEXT, #PAGE DOWN key + 'PAGEDOWN': Keys.VK_NEXT, #PAGE DOWN key + 'END': Keys.VK_END, #END key + 'HOME': Keys.VK_HOME, #HOME key + 'LEFT': Keys.VK_LEFT, #LEFT ARROW key + 'UP': Keys.VK_UP, #UP ARROW key + 'RIGHT': Keys.VK_RIGHT, #RIGHT ARROW key + 'DOWN': Keys.VK_DOWN, #DOWN ARROW key + 'SELECT': Keys.VK_SELECT, #SELECT key + 'PRINT': Keys.VK_PRINT, #PRINT key + 'EXECUTE': Keys.VK_EXECUTE, #EXECUTE key + 'SNAPSHOT': Keys.VK_SNAPSHOT, #PRINT SCREEN key + 'PRINTSCREEN': Keys.VK_SNAPSHOT, #PRINT SCREEN key + 'INSERT': Keys.VK_INSERT, #INS key + 'INS': Keys.VK_INSERT, #INS key + 'DELETE': Keys.VK_DELETE, #DEL key + 'DEL': Keys.VK_DELETE, #DEL key + 'HELP': Keys.VK_HELP, #HELP key + 'WIN': Keys.VK_LWIN, #Left Windows key (Natural keyboard) + 'LWIN': Keys.VK_LWIN, #Left Windows key (Natural keyboard) + 'RWIN': Keys.VK_RWIN, #Right Windows key (Natural keyboard) + 'APPS': Keys.VK_APPS, #Applications key (Natural keyboard) + 'SLEEP': Keys.VK_SLEEP, #Computer Sleep key + 'NUMPAD0': Keys.VK_NUMPAD0, #Numeric keypad 0 key + 'NUMPAD1': Keys.VK_NUMPAD1, #Numeric keypad 1 key + 'NUMPAD2': Keys.VK_NUMPAD2, #Numeric keypad 2 key + 'NUMPAD3': Keys.VK_NUMPAD3, #Numeric keypad 3 key + 'NUMPAD4': Keys.VK_NUMPAD4, #Numeric keypad 4 key + 'NUMPAD5': Keys.VK_NUMPAD5, #Numeric keypad 5 key + 'NUMPAD6': Keys.VK_NUMPAD6, #Numeric keypad 6 key + 'NUMPAD7': Keys.VK_NUMPAD7, #Numeric keypad 7 key + 'NUMPAD8': Keys.VK_NUMPAD8, #Numeric keypad 8 key + 'NUMPAD9': Keys.VK_NUMPAD9, #Numeric keypad 9 key + 'MULTIPLY': Keys.VK_MULTIPLY, #Multiply key + 'ADD': Keys.VK_ADD, #Add key + 'SEPARATOR': Keys.VK_SEPARATOR, #Separator key + 'SUBTRACT': Keys.VK_SUBTRACT, #Subtract key + 'DECIMAL': Keys.VK_DECIMAL, #Decimal key + 'DIVIDE': Keys.VK_DIVIDE, #Divide key + 'F1': Keys.VK_F1, #F1 key + 'F2': Keys.VK_F2, #F2 key + 'F3': Keys.VK_F3, #F3 key + 'F4': Keys.VK_F4, #F4 key + 'F5': Keys.VK_F5, #F5 key + 'F6': Keys.VK_F6, #F6 key + 'F7': Keys.VK_F7, #F7 key + 'F8': Keys.VK_F8, #F8 key + 'F9': Keys.VK_F9, #F9 key + 'F10': Keys.VK_F10, #F10 key + 'F11': Keys.VK_F11, #F11 key + 'F12': Keys.VK_F12, #F12 key + 'F13': Keys.VK_F13, #F13 key + 'F14': Keys.VK_F14, #F14 key + 'F15': Keys.VK_F15, #F15 key + 'F16': Keys.VK_F16, #F16 key + 'F17': Keys.VK_F17, #F17 key + 'F18': Keys.VK_F18, #F18 key + 'F19': Keys.VK_F19, #F19 key + 'F20': Keys.VK_F20, #F20 key + 'F21': Keys.VK_F21, #F21 key + 'F22': Keys.VK_F22, #F22 key + 'F23': Keys.VK_F23, #F23 key + 'F24': Keys.VK_F24, #F24 key + 'NUMLOCK': Keys.VK_NUMLOCK, #NUM LOCK key + 'SCROLL': Keys.VK_SCROLL, #SCROLL LOCK key + 'LSHIFT': Keys.VK_LSHIFT, #Left SHIFT key + 'RSHIFT': Keys.VK_RSHIFT, #Right SHIFT key + 'LCONTROL': Keys.VK_LCONTROL, #Left CONTROL key + 'LCTRL': Keys.VK_LCONTROL, #Left CONTROL key + 'RCONTROL': Keys.VK_RCONTROL, #Right CONTROL key + 'RCTRL': Keys.VK_RCONTROL, #Right CONTROL key + 'LALT': Keys.VK_LMENU, #Left MENU key + 'RALT': Keys.VK_RMENU, #Right MENU key + 'BROWSER_BACK': Keys.VK_BROWSER_BACK, #Browser Back key + 'BROWSER_FORWARD': Keys.VK_BROWSER_FORWARD, #Browser Forward key + 'BROWSER_REFRESH': Keys.VK_BROWSER_REFRESH, #Browser Refresh key + 'BROWSER_STOP': Keys.VK_BROWSER_STOP, #Browser Stop key + 'BROWSER_SEARCH': Keys.VK_BROWSER_SEARCH, #Browser Search key + 'BROWSER_FAVORITES': Keys.VK_BROWSER_FAVORITES, #Browser Favorites key + 'BROWSER_HOME': Keys.VK_BROWSER_HOME, #Browser Start and Home key + 'VOLUME_MUTE': Keys.VK_VOLUME_MUTE, #Volume Mute key + 'VOLUME_DOWN': Keys.VK_VOLUME_DOWN, #Volume Down key + 'VOLUME_UP': Keys.VK_VOLUME_UP, #Volume Up key + 'MEDIA_NEXT_TRACK': Keys.VK_MEDIA_NEXT_TRACK, #Next Track key + 'MEDIA_PREV_TRACK': Keys.VK_MEDIA_PREV_TRACK, #Previous Track key + 'MEDIA_STOP': Keys.VK_MEDIA_STOP, #Stop Media key + 'MEDIA_PLAY_PAUSE': Keys.VK_MEDIA_PLAY_PAUSE, #Play/Pause Media key + 'LAUNCH_MAIL': Keys.VK_LAUNCH_MAIL, #Start Mail key + 'LAUNCH_MEDIA_SELECT': Keys.VK_LAUNCH_MEDIA_SELECT,#Select Media key + 'LAUNCH_APP1': Keys.VK_LAUNCH_APP1, #Start Application 1 key + 'LAUNCH_APP2': Keys.VK_LAUNCH_APP2, #Start Application 2 key + 'OEM_1': Keys.VK_OEM_1, #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the ';:' key + 'OEM_PLUS': Keys.VK_OEM_PLUS, #For any country/region, the '+' key + 'OEM_COMMA': Keys.VK_OEM_COMMA, #For any country/region, the ',' key + 'OEM_MINUS': Keys.VK_OEM_MINUS, #For any country/region, the '-' key + 'OEM_PERIOD': Keys.VK_OEM_PERIOD, #For any country/region, the '.' key + 'OEM_2': Keys.VK_OEM_2, #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the '/?' key + 'OEM_3': Keys.VK_OEM_3, #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the '`~' key + 'OEM_4': Keys.VK_OEM_4, #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the '[{' key + 'OEM_5': Keys.VK_OEM_5, #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the '\|' key + 'OEM_6': Keys.VK_OEM_6, #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the ']}' key + 'OEM_7': Keys.VK_OEM_7, #Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the 'single-quote/double-quote' key + 'OEM_8': Keys.VK_OEM_8, #Used for miscellaneous characters; it can vary by keyboard. + 'OEM_102': Keys.VK_OEM_102, #Either the angle bracket key or the backslash key on the RT 102-key keyboard + 'PROCESSKEY': Keys.VK_PROCESSKEY, #IME PROCESS key + 'PACKET': Keys.VK_PACKET, #Used to pass Unicode characters as if they were keystrokes. The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KeyUp + 'ATTN': Keys.VK_ATTN, #Attn key + 'CRSEL': Keys.VK_CRSEL, #CrSel key + 'EXSEL': Keys.VK_EXSEL, #ExSel key + 'EREOF': Keys.VK_EREOF, #Erase EOF key + 'PLAY': Keys.VK_PLAY, #Play key + 'ZOOM': Keys.VK_ZOOM, #Zoom key + 'NONAME': Keys.VK_NONAME, #Reserved + 'PA1': Keys.VK_PA1, #PA1 key + 'OEM_CLEAR': Keys.VK_OEM_CLEAR, #Clear key +} + + +CharacterCodes = { + '0': Keys.VK_0, #0 key + '1': Keys.VK_1, #1 key + '2': Keys.VK_2, #2 key + '3': Keys.VK_3, #3 key + '4': Keys.VK_4, #4 key + '5': Keys.VK_5, #5 key + '6': Keys.VK_6, #6 key + '7': Keys.VK_7, #7 key + '8': Keys.VK_8, #8 key + '9': Keys.VK_9, #9 key + 'a': Keys.VK_A, #A key + 'A': Keys.VK_A, #A key + 'b': Keys.VK_B, #B key + 'B': Keys.VK_B, #B key + 'c': Keys.VK_C, #C key + 'C': Keys.VK_C, #C key + 'd': Keys.VK_D, #D key + 'D': Keys.VK_D, #D key + 'e': Keys.VK_E, #E key + 'E': Keys.VK_E, #E key + 'f': Keys.VK_F, #F key + 'F': Keys.VK_F, #F key + 'g': Keys.VK_G, #G key + 'G': Keys.VK_G, #G key + 'h': Keys.VK_H, #H key + 'H': Keys.VK_H, #H key + 'i': Keys.VK_I, #I key + 'I': Keys.VK_I, #I key + 'j': Keys.VK_J, #J key + 'J': Keys.VK_J, #J key + 'k': Keys.VK_K, #K key + 'K': Keys.VK_K, #K key + 'l': Keys.VK_L, #L key + 'L': Keys.VK_L, #L key + 'm': Keys.VK_M, #M key + 'M': Keys.VK_M, #M key + 'n': Keys.VK_N, #N key + 'N': Keys.VK_N, #N key + 'o': Keys.VK_O, #O key + 'O': Keys.VK_O, #O key + 'p': Keys.VK_P, #P key + 'P': Keys.VK_P, #P key + 'q': Keys.VK_Q, #Q key + 'Q': Keys.VK_Q, #Q key + 'r': Keys.VK_R, #R key + 'R': Keys.VK_R, #R key + 's': Keys.VK_S, #S key + 'S': Keys.VK_S, #S key + 't': Keys.VK_T, #T key + 'T': Keys.VK_T, #T key + 'u': Keys.VK_U, #U key + 'U': Keys.VK_U, #U key + 'v': Keys.VK_V, #V key + 'V': Keys.VK_V, #V key + 'w': Keys.VK_W, #W key + 'W': Keys.VK_W, #W key + 'x': Keys.VK_X, #X key + 'X': Keys.VK_X, #X key + 'y': Keys.VK_Y, #Y key + 'Y': Keys.VK_Y, #Y key + 'z': Keys.VK_Z, #Z key + 'Z': Keys.VK_Z, #Z key + ' ': Keys.VK_SPACE, #Space key + '`': Keys.VK_OEM_3, #` key + #'~' : Keys.VK_OEM_3, #~ key + '-': Keys.VK_OEM_MINUS, #- key + #'_' : Keys.VK_OEM_MINUS, #_ key + '=': Keys.VK_OEM_PLUS, #= key + #'+' : Keys.VK_OEM_PLUS, #+ key + '[': Keys.VK_OEM_4, #[ key + #'{' : Keys.VK_OEM_4, #{ key + ']': Keys.VK_OEM_6, #] key + #'}' : Keys.VK_OEM_6, #} key + '\\': Keys.VK_OEM_5, #\ key + #'|' : Keys.VK_OEM_5, #| key + ';': Keys.VK_OEM_1, #; key + #':' : Keys.VK_OEM_1, #: key + '\'': Keys.VK_OEM_7, #' key + #'"' : Keys.VK_OEM_7, #" key + ',': Keys.VK_OEM_COMMA, #, key + #'<' : Keys.VK_OEM_COMMA, #< key + '.': Keys.VK_OEM_PERIOD, #. key + #'>' : Keys.VK_OEM_PERIOD, #> key + '/': Keys.VK_OEM_2, #/ key + #'?' : Keys.VK_OEM_2, #? key +} + + +class ConsoleScreenBufferInfo(ctypes.Structure): + _fields_ = [ + ('dwSize', ctypes.wintypes._COORD), + ('dwCursorPosition', ctypes.wintypes._COORD), + ('wAttributes', ctypes.c_uint), + ('srWindow', ctypes.wintypes.SMALL_RECT), + ('dwMaximumWindowSize', ctypes.wintypes._COORD), + ] + + +class MOUSEINPUT(ctypes.Structure): + _fields_ = (('dx', ctypes.wintypes.LONG), + ('dy', ctypes.wintypes.LONG), + ('mouseData', ctypes.wintypes.DWORD), + ('dwFlags', ctypes.wintypes.DWORD), + ('time', ctypes.wintypes.DWORD), + ('dwExtraInfo', ctypes.wintypes.PULONG)) + +class KEYBDINPUT(ctypes.Structure): + _fields_ = (('wVk', ctypes.wintypes.WORD), + ('wScan', ctypes.wintypes.WORD), + ('dwFlags', ctypes.wintypes.DWORD), + ('time', ctypes.wintypes.DWORD), + ('dwExtraInfo', ctypes.wintypes.PULONG)) + +class HARDWAREINPUT(ctypes.Structure): + _fields_ = (('uMsg', ctypes.wintypes.DWORD), + ('wParamL', ctypes.wintypes.WORD), + ('wParamH', ctypes.wintypes.WORD)) + +class _INPUTUnion(ctypes.Union): + _fields_ = (('mi', MOUSEINPUT), + ('ki', KEYBDINPUT), + ('hi', HARDWAREINPUT)) + +class INPUT(ctypes.Structure): + _fields_ = (('type', ctypes.wintypes.DWORD), + ('union', _INPUTUnion)) + + +class Rect(): + """ + class Rect, like `ctypes.wintypes.RECT`. + """ + def __init__(self, left: int = 0, top: int = 0, right: int = 0, bottom: int = 0): + self.left = left + self.top = top + self.right = right + self.bottom = bottom + + def width(self) -> int: + return self.right - self.left + + def height(self) -> int: + return self.bottom - self.top + + def xcenter(self) -> int: + return self.left + self.width() // 2 + + def ycenter(self) -> int: + return self.top + self.height() // 2 + + def contains(self, x: int, y: int) -> bool: + return self.left <= x < self.right and self.top <= y < self.bottom + + def __eq__(self, rect): + return self.left == rect.left and self.top == rect.top and self.right == rect.right and self.bottom == rect.bottom + + def __str__(self) -> str: + return '({},{},{},{})[{}x{}]'.format(self.left, self.top, self.right, self.bottom, self.width(), self.height()) + + def __repr__(self) -> str: + return '{}({},{},{},{})[{}x{}]'.format(self.__class__.__name__, self.left, self.top, self.right, self.bottom, self.width(), self.height()) + + +_StdOutputHandle = -11 +_ConsoleOutputHandle = ctypes.c_void_p(0) +_DefaultConsoleColor = None + + +def GetClipboardText() -> str: + if ctypes.windll.user32.OpenClipboard(0): + if ctypes.windll.user32.IsClipboardFormatAvailable(13): # CF_TEXT=1, CF_UNICODETEXT=13 + hClipboardData = ctypes.windll.user32.GetClipboardData(13) + hText = ctypes.windll.kernel32.GlobalLock(ctypes.c_void_p(hClipboardData)) + text = ctypes.c_wchar_p(hText).value[:] + ctypes.windll.kernel32.GlobalUnlock(ctypes.c_void_p(hClipboardData)) + ctypes.windll.user32.CloseClipboard() + return text + return '' + + +def SetClipboardText(text: str) -> bool: + """ + Return bool, True if succeed otherwise False. + """ + if ctypes.windll.user32.OpenClipboard(0): + ctypes.windll.user32.EmptyClipboard() + textByteLen = (len(text) + 1) * 2 + hClipboardData = ctypes.windll.kernel32.GlobalAlloc(0, textByteLen) # GMEM_FIXED=0 + hDestText = ctypes.windll.kernel32.GlobalLock(ctypes.c_void_p(hClipboardData)) + ctypes.cdll.msvcrt.wcsncpy(ctypes.c_wchar_p(hDestText), ctypes.c_wchar_p(text), ctypes.c_size_t(textByteLen // 2)) + ctypes.windll.kernel32.GlobalUnlock(ctypes.c_void_p(hClipboardData)) + # system owns hClipboardData after calling SetClipboardData, + # application can not write to or free the data once ownership has been transferred to the system + ctypes.windll.user32.SetClipboardData(ctypes.c_uint(13), ctypes.c_void_p(hClipboardData)) # CF_TEXT=1, CF_UNICODETEXT=13 + ctypes.windll.user32.CloseClipboard() + return True + return False + + +def SetConsoleColor(color: int) -> bool: + """ + Change the text color on console window. + color: int, a value in class `ConsoleColor`. + Return bool, True if succeed otherwise False. + """ + global _ConsoleOutputHandle + global _DefaultConsoleColor + if not _DefaultConsoleColor: + if not _ConsoleOutputHandle: + _ConsoleOutputHandle = ctypes.c_void_p(ctypes.windll.kernel32.GetStdHandle(_StdOutputHandle)) + bufferInfo = ConsoleScreenBufferInfo() + ctypes.windll.kernel32.GetConsoleScreenBufferInfo(_ConsoleOutputHandle, ctypes.byref(bufferInfo)) + _DefaultConsoleColor = int(bufferInfo.wAttributes & 0xFF) + if sys.stdout: + sys.stdout.flush() + return bool(ctypes.windll.kernel32.SetConsoleTextAttribute(_ConsoleOutputHandle, ctypes.c_ushort(color))) + + +def ResetConsoleColor() -> bool: + """ + Reset to the default text color on console window. + Return bool, True if succeed otherwise False. + """ + if sys.stdout: + sys.stdout.flush() + return bool(ctypes.windll.kernel32.SetConsoleTextAttribute(_ConsoleOutputHandle, ctypes.c_ushort(_DefaultConsoleColor))) + + +def WindowFromPoint(x: int, y: int) -> int: + """ + WindowFromPoint from Win32. + Return int, a native window handle. + """ + return ctypes.windll.user32.WindowFromPoint(ctypes.wintypes.POINT(x, y)) # or ctypes.windll.user32.WindowFromPoint(x, y) + + +def GetCursorPos() -> Tuple[int, int]: + """ + GetCursorPos from Win32. + Get current mouse cursor positon. + Return Tuple[int, int], two ints tuple (x, y). + """ + point = ctypes.wintypes.POINT(0, 0) + ctypes.windll.user32.GetCursorPos(ctypes.byref(point)) + return point.x, point.y + + +def SetCursorPos(x: int, y: int) -> bool: + """ + SetCursorPos from Win32. + Set mouse cursor to point x, y. + x: int. + y: int. + Return bool, True if succeed otherwise False. + """ + return bool(ctypes.windll.user32.SetCursorPos(x, y)) + + +def GetDoubleClickTime() -> int: + """ + GetDoubleClickTime from Win32. + Return int, in milliseconds. + """ + return ctypes.windll.user32.GetDoubleClickTime() + + +def mouse_event(dwFlags: int, dx: int, dy: int, dwData: int, dwExtraInfo: int) -> None: + """mouse_event from Win32.""" + ctypes.windll.user32.mouse_event(dwFlags, dx, dy, dwData, dwExtraInfo) + + +def keybd_event(bVk: int, bScan: int, dwFlags: int, dwExtraInfo: int) -> None: + """keybd_event from Win32.""" + ctypes.windll.user32.keybd_event(bVk, bScan, dwFlags, dwExtraInfo) + + +def PostMessage(handle: int, msg: int, wParam: int, lParam: int) -> bool: + """ + PostMessage from Win32. + Return bool, True if succeed otherwise False. + """ + return bool(ctypes.windll.user32.PostMessageW(ctypes.c_void_p(handle), ctypes.c_uint(msg), ctypes.wintypes.WPARAM(wParam), ctypes.wintypes.LPARAM(lParam))) + + +def SendMessage(handle: int, msg: int, wParam: int, lParam: int) -> int: + """ + SendMessage from Win32. + Return int, the return value specifies the result of the message processing; + it depends on the message sent. + """ + return ctypes.windll.user32.SendMessageW(ctypes.c_void_p(handle), ctypes.c_uint(msg), ctypes.wintypes.WPARAM(wParam), ctypes.wintypes.LPARAM(lParam)) + + +def Click(x: int, y: int, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate mouse click at point x, y. + x: int. + y: int. + waitTime: float. + """ + SetCursorPos(x, y) + screenWidth, screenHeight = GetScreenSize() + mouse_event(MouseEventFlag.LeftDown | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(0.05) + mouse_event(MouseEventFlag.LeftUp | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(waitTime) + + +def MiddleClick(x: int, y: int, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate mouse middle click at point x, y. + x: int. + y: int. + waitTime: float. + """ + SetCursorPos(x, y) + screenWidth, screenHeight = GetScreenSize() + mouse_event(MouseEventFlag.MiddleDown | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(0.05) + mouse_event(MouseEventFlag.MiddleUp | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(waitTime) + + +def RightClick(x: int, y: int, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate mouse right click at point x, y. + x: int. + y: int. + waitTime: float. + """ + SetCursorPos(x, y) + screenWidth, screenHeight = GetScreenSize() + mouse_event(MouseEventFlag.RightDown | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(0.05) + mouse_event(MouseEventFlag.RightUp | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(waitTime) + + +def PressMouse(x: int, y: int, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Press left mouse. + x: int. + y: int. + waitTime: float. + """ + SetCursorPos(x, y) + screenWidth, screenHeight = GetScreenSize() + mouse_event(MouseEventFlag.LeftDown | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(waitTime) + + +def ReleaseMouse(waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Release left mouse. + waitTime: float. + """ + x, y = GetCursorPos() + screenWidth, screenHeight = GetScreenSize() + mouse_event(MouseEventFlag.LeftUp | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(waitTime) + + +def RightPressMouse(x: int, y: int, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Press right mouse. + x: int. + y: int. + waitTime: float. + """ + SetCursorPos(x, y) + screenWidth, screenHeight = GetScreenSize() + mouse_event(MouseEventFlag.RightDown | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(waitTime) + + +def RightReleaseMouse(waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Release right mouse. + waitTime: float. + """ + x, y = GetCursorPos() + screenWidth, screenHeight = GetScreenSize() + mouse_event(MouseEventFlag.RightUp | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(waitTime) + + +def MiddlePressMouse(x: int, y: int, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Press middle mouse. + x: int. + y: int. + waitTime: float. + """ + SetCursorPos(x, y) + screenWidth, screenHeight = GetScreenSize() + mouse_event(MouseEventFlag.MiddleDown | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(waitTime) + + +def MiddleReleaseMouse(waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Release middle mouse. + waitTime: float. + """ + x, y = GetCursorPos() + screenWidth, screenHeight = GetScreenSize() + mouse_event(MouseEventFlag.MiddleUp | MouseEventFlag.Absolute, x * 65535 // screenWidth, y * 65535 // screenHeight, 0, 0) + time.sleep(waitTime) + + +def MoveTo(x: int, y: int, moveSpeed: float = 1, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate mouse move to point x, y from current cursor. + x: int. + y: int. + moveSpeed: float, 1 normal speed, < 1 move slower, > 1 move faster. + waitTime: float. + """ + if moveSpeed <= 0: + moveTime = 0 + else: + moveTime = MAX_MOVE_SECOND / moveSpeed + curX, curY = GetCursorPos() + xCount = abs(x - curX) + yCount = abs(y - curY) + maxPoint = max(xCount, yCount) + screenWidth, screenHeight = GetScreenSize() + maxSide = max(screenWidth, screenHeight) + minSide = min(screenWidth, screenHeight) + if maxPoint > minSide: + maxPoint = minSide + if maxPoint < maxSide: + maxPoint = 100 + int((maxSide - 100) / maxSide * maxPoint) + moveTime = moveTime * maxPoint * 1.0 / maxSide + stepCount = maxPoint // 20 + if stepCount > 1: + xStep = (x - curX) * 1.0 / stepCount + yStep = (y - curY) * 1.0 / stepCount + interval = moveTime / stepCount + for i in range(stepCount): + cx = curX + int(xStep * i) + cy = curY + int(yStep * i) + # upper-left(0,0), lower-right(65536,65536) + # mouse_event(MouseEventFlag.Move | MouseEventFlag.Absolute, cx*65536//screenWidth, cy*65536//screenHeight, 0, 0) + SetCursorPos(cx, cy) + time.sleep(interval) + SetCursorPos(x, y) + time.sleep(waitTime) + + +def DragDrop(x1: int, y1: int, x2: int, y2: int, moveSpeed: float = 1, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate mouse left button drag from point x1, y1 drop to point x2, y2. + x1: int. + y1: int. + x2: int. + y2: int. + moveSpeed: float, 1 normal speed, < 1 move slower, > 1 move faster. + waitTime: float. + """ + PressMouse(x1, y1, 0.05) + MoveTo(x2, y2, moveSpeed, 0.05) + ReleaseMouse(waitTime) + + +def RightDragDrop(x1: int, y1: int, x2: int, y2: int, moveSpeed: float = 1, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate mouse right button drag from point x1, y1 drop to point x2, y2. + x1: int. + y1: int. + x2: int. + y2: int. + moveSpeed: float, 1 normal speed, < 1 move slower, > 1 move faster. + waitTime: float. + """ + RightPressMouse(x1, y1, 0.05) + MoveTo(x2, y2, moveSpeed, 0.05) + RightReleaseMouse(waitTime) + + +def MiddleDragDrop(x1: int, y1: int, x2: int, y2: int, moveSpeed: float = 1, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate mouse middle button drag from point x1, y1 drop to point x2, y2. + x1: int. + y1: int. + x2: int. + y2: int. + moveSpeed: float, 1 normal speed, < 1 move slower, > 1 move faster. + waitTime: float. + """ + MiddlePressMouse(x1, y1, 0.05) + MoveTo(x2, y2, moveSpeed, 0.05) + MiddleReleaseMouse(waitTime) + + +def WheelDown(wheelTimes: int = 1, interval: float = 0.05, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate mouse wheel down. + wheelTimes: int. + interval: float. + waitTime: float. + """ + for i in range(wheelTimes): + mouse_event(MouseEventFlag.Wheel, 0, 0, -120, 0) #WHEEL_DELTA=120 + time.sleep(interval) + time.sleep(waitTime) + + +def WheelUp(wheelTimes: int = 1, interval: float = 0.05, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate mouse wheel up. + wheelTimes: int. + interval: float. + waitTime: float. + """ + for i in range(wheelTimes): + mouse_event(MouseEventFlag.Wheel, 0, 0, 120, 0) #WHEEL_DELTA=120 + time.sleep(interval) + time.sleep(waitTime) + + +def SetDpiAwareness(dpiAwarenessPerMonitor: bool = True) -> int: + ''' + Call SetThreadDpiAwarenessContext(Windows 10 version 1607+) or SetProcessDpiAwareness(Windows 8.1+). + You should call this function with True if you enable DPI scaling. uiautomation calls this function when it initializes. + dpiAwarenessPerMonitor: bool. + Return int. + ''' + try: + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setthreaddpiawarenesscontext + # Windows 10 1607+ + ctypes.windll.user32.SetThreadDpiAwarenessContext.restype = ctypes.c_void_p + context = DpiAwarenessContext.DpiAwarenessContextPerMonitorAware if dpiAwarenessPerMonitor else DpiAwarenessContext.DpiAwarenessContextUnaware + oldContext = ctypes.windll.user32.SetThreadDpiAwarenessContext(ctypes.c_void_p(context)) + return oldContext + except Exception as ex: + try: + # https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-setprocessdpiawareness + # Once SetProcessDpiAwareness is set for an app, any future calls to SetProcessDpiAwareness will fail. + # Windows 8.1+ + if dpiAwarenessPerMonitor: + return ctypes.windll.shcore.SetProcessDpiAwareness(ProcessDpiAwareness.ProcessPerMonitorDpiAware) + except Exception as ex2: + pass + + +def GetScreenSize(dpiAwarenessPerMonitor: bool = True) -> Tuple[int, int]: + """ + dpiAwarenessPerMonitor: bool. + Return Tuple[int, int], two ints tuple (width, height). + """ + SetDpiAwareness(dpiAwarenessPerMonitor) + SM_CXSCREEN = 0 + SM_CYSCREEN = 1 + w = ctypes.windll.user32.GetSystemMetrics(SM_CXSCREEN) + h = ctypes.windll.user32.GetSystemMetrics(SM_CYSCREEN) + return w, h + + +def GetVirtualScreenSize(dpiAwarenessPerMonitor: bool = True) -> Tuple[int, int]: + """ + dpiAwarenessPerMonitor: bool. + Return Tuple[int, int], two ints tuple (width, height). + """ + SetDpiAwareness(dpiAwarenessPerMonitor) + SM_CXVIRTUALSCREEN = 78 + SM_CYVIRTUALSCREEN = 79 + w = ctypes.windll.user32.GetSystemMetrics(SM_CXVIRTUALSCREEN) + h = ctypes.windll.user32.GetSystemMetrics(SM_CYVIRTUALSCREEN) + return w, h + + +def GetMonitorsRect(dpiAwarenessPerMonitor: bool = False) -> List[Rect]: + """ + Get monitors' rect. + dpiAwarenessPerMonitor: bool. + Return List[Rect]. + """ + SetDpiAwareness(dpiAwarenessPerMonitor) + MonitorEnumProc = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_size_t, ctypes.c_size_t, ctypes.POINTER(ctypes.wintypes.RECT), ctypes.c_size_t) + rects = [] + def MonitorCallback(hMonitor: int, hdcMonitor: int, lprcMonitor: ctypes.POINTER(ctypes.wintypes.RECT), dwData: int): + rect = Rect(lprcMonitor.contents.left, lprcMonitor.contents.top, lprcMonitor.contents.right, lprcMonitor.contents.bottom) + rects.append(rect) + return 1 + ret = ctypes.windll.user32.EnumDisplayMonitors(ctypes.c_void_p(0), ctypes.c_void_p(0), MonitorEnumProc(MonitorCallback), 0) + return rects + + +def GetPixelColor(x: int, y: int, handle: int = 0) -> int: + """ + Get pixel color of a native window. + x: int. + y: int. + handle: int, the handle of a native window. + Return int, the bgr value of point (x,y). + r = bgr & 0x0000FF + g = (bgr & 0x00FF00) >> 8 + b = (bgr & 0xFF0000) >> 16 + If handle is 0, get pixel from Desktop window(root control). + Note: + Not all devices support GetPixel. + An application should call GetDeviceCaps to determine whether a specified device supports this function. + For example, console window doesn't support. + """ + hdc = ctypes.windll.user32.GetWindowDC(ctypes.c_void_p(handle)) + bgr = ctypes.windll.gdi32.GetPixel(hdc, x, y) + ctypes.windll.user32.ReleaseDC(ctypes.c_void_p(handle), ctypes.c_void_p(hdc)) + return bgr + + +def MessageBox(content: str, title: str, flags: int = MB.Ok) -> int: + """ + MessageBox from Win32. + content: str. + title: str. + flags: int, a value or some combined values in class `MB`. + Return int, a value in MB whose name starts with Id, such as MB.IdOk + """ + return ctypes.windll.user32.MessageBoxW(ctypes.c_void_p(0), ctypes.c_wchar_p(content), ctypes.c_wchar_p(title), ctypes.c_uint(flags)) + + +def SetForegroundWindow(handle: int) -> bool: + """ + SetForegroundWindow from Win32. + handle: int, the handle of a native window. + Return bool, True if succeed otherwise False. + """ + return bool(ctypes.windll.user32.SetForegroundWindow(ctypes.c_void_p(handle))) + + +def BringWindowToTop(handle: int) -> bool: + """ + BringWindowToTop from Win32. + handle: int, the handle of a native window. + Return bool, True if succeed otherwise False. + """ + return bool(ctypes.windll.user32.BringWindowToTop(ctypes.c_void_p(handle))) + + +def SwitchToThisWindow(handle: int) -> None: + """ + SwitchToThisWindow from Win32. + handle: int, the handle of a native window. + """ + ctypes.windll.user32.SwitchToThisWindow(ctypes.c_void_p(handle), ctypes.c_int(1)) #void function, no return + + +def GetAncestor(handle: int, flag: int) -> int: + """ + GetAncestor from Win32. + handle: int, the handle of a native window. + index: int, a value in class `GAFlag`. + Return int, a native window handle. + """ + return ctypes.windll.user32.GetAncestor(ctypes.c_void_p(handle), ctypes.c_int(flag)) + + +def IsTopLevelWindow(handle: int) -> bool: + """ + IsTopLevelWindow from Win32. + handle: int, the handle of a native window. + Return bool. + Only available on Windows 7 or Higher. + """ + return bool(ctypes.windll.user32.IsTopLevelWindow(ctypes.c_void_p(handle))) + + +def GetWindowLong(handle: int, index: int) -> int: + """ + GetWindowLong from Win32. + handle: int, the handle of a native window. + index: int. + """ + return ctypes.windll.user32.GetWindowLongW(ctypes.c_void_p(handle), ctypes.c_int(index)) + + +def SetWindowLong(handle: int, index: int, value: int) -> int: + """ + SetWindowLong from Win32. + handle: int, the handle of a native window. + index: int. + value: int. + Return int, the previous value before set. + """ + return ctypes.windll.user32.SetWindowLongW(ctypes.c_void_p(handle), index, value) + + +def IsIconic(handle: int) -> bool: + """ + IsIconic from Win32. + Determine whether a native window is minimized. + handle: int, the handle of a native window. + Return bool. + """ + return bool(ctypes.windll.user32.IsIconic(ctypes.c_void_p(handle))) + + +def IsZoomed(handle: int) -> bool: + """ + IsZoomed from Win32. + Determine whether a native window is maximized. + handle: int, the handle of a native window. + Return bool. + """ + return bool(ctypes.windll.user32.IsZoomed(ctypes.c_void_p(handle))) + + +def IsWindowVisible(handle: int) -> bool: + """ + IsWindowVisible from Win32. + handle: int, the handle of a native window. + Return bool. + """ + return bool(ctypes.windll.user32.IsWindowVisible(ctypes.c_void_p(handle))) + + +def ShowWindow(handle: int, cmdShow: int) -> bool: + """ + ShowWindow from Win32. + handle: int, the handle of a native window. + cmdShow: int, a value in clas `SW`. + Return bool, True if succeed otherwise False. + """ + return ctypes.windll.user32.ShowWindow(ctypes.c_void_p(handle), ctypes.c_int(cmdShow)) + + +def MoveWindow(handle: int, x: int, y: int, width: int, height: int, repaint: int = 1) -> bool: + """ + MoveWindow from Win32. + handle: int, the handle of a native window. + x: int. + y: int. + width: int. + height: int. + repaint: int, use 1 or 0. + Return bool, True if succeed otherwise False. + """ + return bool(ctypes.windll.user32.MoveWindow(ctypes.c_void_p(handle), ctypes.c_int(x), ctypes.c_int(y), ctypes.c_int(width), ctypes.c_int(height), ctypes.c_int(repaint))) + + +def SetWindowPos(handle: int, hWndInsertAfter: int, x: int, y: int, width: int, height: int, flags: int) -> bool: + """ + SetWindowPos from Win32. + handle: int, the handle of a native window. + hWndInsertAfter: int, a value whose name starts with 'HWND' in class SWP. + x: int. + y: int. + width: int. + height: int. + flags: int, values whose name starts with 'SWP' in class `SWP`. + Return bool, True if succeed otherwise False. + """ + return ctypes.windll.user32.SetWindowPos(ctypes.c_void_p(handle), ctypes.c_void_p(hWndInsertAfter), ctypes.c_int(x), ctypes.c_int(y), ctypes.c_int(width), ctypes.c_int(height), ctypes.c_uint(flags)) + + +def SetWindowTopmost(handle: int, isTopmost: bool) -> bool: + """ + handle: int, the handle of a native window. + isTopmost: bool + Return bool, True if succeed otherwise False. + """ + topValue = SWP.HWND_Topmost if isTopmost else SWP.HWND_NoTopmost + return bool(SetWindowPos(handle, topValue, 0, 0, 0, 0, SWP.SWP_NoSize | SWP.SWP_NoMove)) + + +def GetWindowText(handle: int) -> str: + """ + GetWindowText from Win32. + handle: int, the handle of a native window. + Return str. + """ + arrayType = ctypes.c_wchar * MAX_PATH + values = arrayType() + ctypes.windll.user32.GetWindowTextW(ctypes.c_void_p(handle), values, ctypes.c_int(MAX_PATH)) + return values.value + + +def SetWindowText(handle: int, text: str) -> bool: + """ + SetWindowText from Win32. + handle: int, the handle of a native window. + text: str. + Return bool, True if succeed otherwise False. + """ + return bool(ctypes.windll.user32.SetWindowTextW(ctypes.c_void_p(handle), ctypes.c_wchar_p(text))) + + +def GetEditText(handle: int) -> str: + """ + Get text of a native Win32 Edit. + handle: int, the handle of a native window. + Return str. + """ + textLen = SendMessage(handle, 0x000E, 0, 0) + 1 #WM_GETTEXTLENGTH + arrayType = ctypes.c_wchar * textLen + values = arrayType() + SendMessage(handle, 0x000D, textLen, values) #WM_GETTEXT + return values.value + + +def GetConsoleOriginalTitle() -> str: + """ + GetConsoleOriginalTitle from Win32. + Return str. + Only available on Windows Vista or higher. + """ + if IsNT6orHigher: + arrayType = ctypes.c_wchar * MAX_PATH + values = arrayType() + ctypes.windll.kernel32.GetConsoleOriginalTitleW(values, ctypes.c_uint(MAX_PATH)) + return values.value + else: + raise RuntimeError('GetConsoleOriginalTitle is not supported on Windows XP or lower.') + + +def GetConsoleTitle() -> str: + """ + GetConsoleTitle from Win32. + Return str. + """ + arrayType = ctypes.c_wchar * MAX_PATH + values = arrayType() + ctypes.windll.kernel32.GetConsoleTitleW(values, ctypes.c_uint(MAX_PATH)) + return values.value + + +def SetConsoleTitle(text: str) -> bool: + """ + SetConsoleTitle from Win32. + text: str. + Return bool, True if succeed otherwise False. + """ + return bool(ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(text))) + + +def GetForegroundWindow() -> int: + """ + GetForegroundWindow from Win32. + Return int, the native handle of the foreground window. + """ + return ctypes.windll.user32.GetForegroundWindow() + + +def IsDesktopLocked() -> bool: + """ + Check if desktop is locked. + Return bool. + Desktop is locked if press Win+L, Ctrl+Alt+Del or in remote desktop mode. + """ + isLocked = False + desk = ctypes.windll.user32.OpenDesktopW(ctypes.c_wchar_p('Default'), ctypes.c_uint(0), ctypes.c_int(0), ctypes.c_uint(0x0100)) # DESKTOP_SWITCHDESKTOP = 0x0100 + if desk: + isLocked = not ctypes.windll.user32.SwitchDesktop(ctypes.c_void_p(desk)) + ctypes.windll.user32.CloseDesktop(ctypes.c_void_p(desk)) + return isLocked + + +def PlayWaveFile(filePath: str = r'C:\Windows\Media\notify.wav', isAsync: bool = False, isLoop: bool = False) -> bool: + """ + Call PlaySound from Win32. + filePath: str, if emtpy, stop playing the current sound. + isAsync: bool, if True, the sound is played asynchronously and returns immediately. + isLoop: bool, if True, the sound plays repeatedly until PlayWaveFile(None) is called again, must also set isAsync to True. + Return bool, True if succeed otherwise False. + """ + if filePath: + SND_ASYNC = 0x0001 + SND_NODEFAULT = 0x0002 + SND_LOOP = 0x0008 + SND_FILENAME = 0x20000 + flags = SND_NODEFAULT | SND_FILENAME + if isAsync: + flags |= SND_ASYNC + if isLoop: + flags |= SND_LOOP + flags |= SND_ASYNC + return bool(ctypes.windll.winmm.PlaySoundW(ctypes.c_wchar_p(filePath), ctypes.c_void_p(0), ctypes.c_uint(flags))) + else: + return bool(ctypes.windll.winmm.PlaySoundW(ctypes.c_wchar_p(0), ctypes.c_void_p(0), ctypes.c_uint(0))) + + +def IsProcess64Bit(processId: int) -> bool: + """ + Return True if process is 64 bit. + Return False if process is 32 bit. + Return None if unknown, maybe caused by having no acess right to the process. + """ + try: + func = ctypes.windll.ntdll.ZwWow64ReadVirtualMemory64 #only 64 bit OS has this function + except Exception as ex: + return False + try: + IsWow64Process = ctypes.windll.kernel32.IsWow64Process + except Exception as ex: + return False + hProcess = ctypes.windll.kernel32.OpenProcess(0x1000, 0, processId) #PROCESS_QUERY_INFORMATION=0x0400,PROCESS_QUERY_LIMITED_INFORMATION=0x1000 + if hProcess: + is64Bit = ctypes.c_int32() + if IsWow64Process(ctypes.c_void_p(hProcess), ctypes.byref(is64Bit)): + ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(hProcess)) + return False if is64Bit.value else True + else: + ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(hProcess)) + + +def IsUserAnAdmin() -> bool: + """ + IsUserAnAdmin from Win32. + Return bool. + Minimum supported OS: Windows XP, Windows Server 2003 + """ + return bool(ctypes.windll.shell32.IsUserAnAdmin()) + + +def RunScriptAsAdmin(argv: List[str], workingDirectory: str = None, showFlag: int = SW.ShowNormal) -> bool: + """ + Run a python script as administrator. + System will show a popup dialog askes you whether to elevate as administrator if UAC is enabled. + argv: List[str], a str list like sys.argv, argv[0] is the script file, argv[1:] are other arguments. + workingDirectory: str, the working directory for the script file. + showFlag: int, a value in class `SW`. + Return bool, True if succeed. + """ + args = ' '.join('"{}"'.format(arg) for arg in argv) + return ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, args, workingDirectory, showFlag) > 32 + + +def SendKey(key: int, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate typing a key. + key: int, a value in class `Keys`. + """ + keybd_event(key, 0, KeyboardEventFlag.KeyDown | KeyboardEventFlag.ExtendedKey, 0) + keybd_event(key, 0, KeyboardEventFlag.KeyUp | KeyboardEventFlag.ExtendedKey, 0) + time.sleep(waitTime) + + +def PressKey(key: int, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate a key down for key. + key: int, a value in class `Keys`. + waitTime: float. + """ + keybd_event(key, 0, KeyboardEventFlag.KeyDown | KeyboardEventFlag.ExtendedKey, 0) + time.sleep(waitTime) + + +def ReleaseKey(key: int, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Simulate a key up for key. + key: int, a value in class `Keys`. + waitTime: float. + """ + keybd_event(key, 0, KeyboardEventFlag.KeyUp | KeyboardEventFlag.ExtendedKey, 0) + time.sleep(waitTime) + + +def IsKeyPressed(key: int) -> bool: + """ + key: int, a value in class `Keys`. + Return bool. + """ + state = ctypes.windll.user32.GetAsyncKeyState(key) + return bool(state & 0x8000) + + +def _CreateInput(structure) -> INPUT: + """ + Create Win32 struct `INPUT` for `SendInput`. + Return `INPUT`. + """ + if isinstance(structure, MOUSEINPUT): + return INPUT(InputType.Mouse, _INPUTUnion(mi=structure)) + if isinstance(structure, KEYBDINPUT): + return INPUT(InputType.Keyboard, _INPUTUnion(ki=structure)) + if isinstance(structure, HARDWAREINPUT): + return INPUT(InputType.Hardware, _INPUTUnion(hi=structure)) + raise TypeError('Cannot create INPUT structure!') + + +def MouseInput(dx: int, dy: int, mouseData: int = 0, dwFlags: int = MouseEventFlag.LeftDown, time_: int = 0) -> INPUT: + """ + Create Win32 struct `MOUSEINPUT` for `SendInput`. + Return `INPUT`. + """ + return _CreateInput(MOUSEINPUT(dx, dy, mouseData, dwFlags, time_, None)) + + +def KeyboardInput(wVk: int, wScan: int, dwFlags: int = KeyboardEventFlag.KeyDown, time_: int = 0) -> INPUT: + """Create Win32 struct `KEYBDINPUT` for `SendInput`.""" + return _CreateInput(KEYBDINPUT(wVk, wScan, dwFlags, time_, None)) + + +def HardwareInput(uMsg: int, param: int = 0) -> INPUT: + """Create Win32 struct `HARDWAREINPUT` for `SendInput`.""" + return _CreateInput(HARDWAREINPUT(uMsg, param & 0xFFFF, param >> 16 & 0xFFFF)) + + +def SendInput(*inputs) -> int: + """ + SendInput from Win32. + input: `INPUT`. + Return int, the number of events that it successfully inserted into the keyboard or mouse input stream. + If the function returns zero, the input was already blocked by another thread. + """ + cbSize = ctypes.c_int(ctypes.sizeof(INPUT)) + for ip in inputs: + ret = ctypes.windll.user32.SendInput(1, ctypes.byref(ip), cbSize) + return ret + #or one call + #nInputs = len(inputs) + #LPINPUT = INPUT * nInputs + #pInputs = LPINPUT(*inputs) + #cbSize = ctypes.c_int(ctypes.sizeof(INPUT)) + #return ctypes.windll.user32.SendInput(nInputs, ctypes.byref(pInputs), cbSize) + + +def SendUnicodeChar(char: str, charMode: bool = True) -> int: + """ + Type a single unicode char. + char: str, len(char) must equal to 1. + charMode: bool, if False, the char typied is depend on the input method if a input method is on. + Return int, the number of events that it successfully inserted into the keyboard or mouse input stream. + If the function returns zero, the input was already blocked by another thread. + """ + if charMode: + vk = 0 + scan = ord(char) + flag = KeyboardEventFlag.KeyUnicode + else: + res = ctypes.windll.user32.VkKeyScanW(ctypes.wintypes.WCHAR(char)) + if (res >> 8) & 0xFF == 0: + vk = res & 0xFF + scan = 0 + flag = 0 + else: + vk = 0 + scan = ord(char) + flag = KeyboardEventFlag.KeyUnicode + return SendInput(KeyboardInput(vk, scan, flag | KeyboardEventFlag.KeyDown), + KeyboardInput(vk, scan, flag | KeyboardEventFlag.KeyUp)) + + +_SCKeys = { + Keys.VK_LSHIFT: 0x02A, + Keys.VK_RSHIFT: 0x136, + Keys.VK_LCONTROL: 0x01D, + Keys.VK_RCONTROL: 0x11D, + Keys.VK_LMENU: 0x038, + Keys.VK_RMENU: 0x138, + Keys.VK_LWIN: 0x15B, + Keys.VK_RWIN: 0x15C, + Keys.VK_NUMPAD0: 0x52, + Keys.VK_NUMPAD1: 0x4F, + Keys.VK_NUMPAD2: 0x50, + Keys.VK_NUMPAD3: 0x51, + Keys.VK_NUMPAD4: 0x4B, + Keys.VK_NUMPAD5: 0x4C, + Keys.VK_NUMPAD6: 0x4D, + Keys.VK_NUMPAD7: 0x47, + Keys.VK_NUMPAD8: 0x48, + Keys.VK_NUMPAD9: 0x49, + Keys.VK_DECIMAL: 0x53, + Keys.VK_NUMLOCK: 0x145, + Keys.VK_DIVIDE: 0x135, + Keys.VK_MULTIPLY: 0x037, + Keys.VK_SUBTRACT: 0x04A, + Keys.VK_ADD: 0x04E, +} + + +def _VKtoSC(key: int) -> int: + """ + This function is only for internal use in SendKeys. + key: int, a value in class `Keys`. + Return int. + """ + if key in _SCKeys: + return _SCKeys[key] + scanCode = ctypes.windll.user32.MapVirtualKeyA(key, 0) + if not scanCode: + return 0 + keyList = [Keys.VK_APPS, Keys.VK_CANCEL, Keys.VK_SNAPSHOT, Keys.VK_DIVIDE, Keys.VK_NUMLOCK] + if key in keyList: + scanCode |= 0x0100 + return scanCode + + +def SendKeys(text: str, interval: float = 0.01, waitTime: float = OPERATION_WAIT_TIME, charMode: bool = True, debug: bool = False) -> None: + """ + Simulate typing keys on keyboard. + text: str, keys to type. + interval: float, seconds between keys. + waitTime: float. + charMode: bool, if False, the text typied is depend on the input method if a input method is on. + debug: bool, if True, print the keys. + Examples: + {Ctrl}, {Delete} ... are special keys' name in SpecialKeyNames. + SendKeys('{Ctrl}a{Delete}{Ctrl}v{Ctrl}s{Ctrl}{Shift}s{Win}e{PageDown}') #press Ctrl+a, Delete, Ctrl+v, Ctrl+s, Ctrl+Shift+s, Win+e, PageDown + SendKeys('{Ctrl}(AB)({Shift}(123))') #press Ctrl+A+B, type (, press Shift+1+2+3, type ), if () follows a hold key, hold key won't release util ) + SendKeys('{Ctrl}{a 3}') #press Ctrl+a at the same time, release Ctrl+a, then type a 2 times + SendKeys('{a 3}{B 5}') #type a 3 times, type B 5 times + SendKeys('{{}Hello{}}abc {a}{b}{c} test{} 3}{!}{a} (){(}{)}') #type: {Hello}abc abc test}}}!a ()() + SendKeys('0123456789{Enter}') + SendKeys('ABCDEFGHIJKLMNOPQRSTUVWXYZ{Enter}') + SendKeys('abcdefghijklmnopqrstuvwxyz{Enter}') + SendKeys('`~!@#$%^&*()-_=+{Enter}') + SendKeys('[]{{}{}}\\|;:\'\",<.>/?{Enter}') + """ + holdKeys = ('WIN', 'LWIN', 'RWIN', 'SHIFT', 'LSHIFT', 'RSHIFT', 'CTRL', 'CONTROL', 'LCTRL', 'RCTRL', 'LCONTROL', 'LCONTROL', 'ALT', 'LALT', 'RALT') + keys = [] + printKeys = [] + i = 0 + insertIndex = 0 + length = len(text) + hold = False + include = False + lastKeyValue = None + while True: + if text[i] == '{': + rindex = text.find('}', i) + if rindex == i + 1:#{}} + rindex = text.find('}', i + 2) + if rindex == -1: + raise ValueError('"{" or "{}" is not valid, use "{{}" for "{", use "{}}" for "}"') + key = text[i + 1:rindex] + key = [it for it in key.split(' ') if it] + if not key: + raise ValueError('"{}" is not valid, use "{{Space}}" or " " for " "'.format(text[i:rindex + 1])) + if (len(key) == 2 and not key[1].isdigit()) or len(key) > 2: + raise ValueError('"{}" is not valid'.format(text[i:rindex + 1])) + upperKey = key[0].upper() + count = 1 + if len(key) > 1: + count = int(key[1]) + for j in range(count): + if hold: + if upperKey in SpecialKeyNames: + keyValue = SpecialKeyNames[upperKey] + if type(lastKeyValue) == type(keyValue) and lastKeyValue == keyValue: + insertIndex += 1 + printKeys.insert(insertIndex, (key[0], 'KeyDown | ExtendedKey')) + printKeys.insert(insertIndex + 1, (key[0], 'KeyUp | ExtendedKey')) + keys.insert(insertIndex, (keyValue, KeyboardEventFlag.KeyDown | KeyboardEventFlag.ExtendedKey)) + keys.insert(insertIndex + 1, (keyValue, KeyboardEventFlag.KeyUp | KeyboardEventFlag.ExtendedKey)) + lastKeyValue = keyValue + elif key[0] in CharacterCodes: + keyValue = CharacterCodes[key[0]] + if type(lastKeyValue) == type(keyValue) and lastKeyValue == keyValue: + insertIndex += 1 + printKeys.insert(insertIndex, (key[0], 'KeyDown | ExtendedKey')) + printKeys.insert(insertIndex + 1, (key[0], 'KeyUp | ExtendedKey')) + keys.insert(insertIndex, (keyValue, KeyboardEventFlag.KeyDown | KeyboardEventFlag.ExtendedKey)) + keys.insert(insertIndex + 1, (keyValue, KeyboardEventFlag.KeyUp | KeyboardEventFlag.ExtendedKey)) + lastKeyValue = keyValue + else: + printKeys.insert(insertIndex, (key[0], 'UnicodeChar')) + keys.insert(insertIndex, (key[0], 'UnicodeChar')) + lastKeyValue = key[0] + if include: + insertIndex += 1 + else: + if upperKey in holdKeys: + insertIndex += 1 + else: + hold = False + else: + if upperKey in SpecialKeyNames: + keyValue = SpecialKeyNames[upperKey] + printKeys.append((key[0], 'KeyDown | ExtendedKey')) + printKeys.append((key[0], 'KeyUp | ExtendedKey')) + keys.append((keyValue, KeyboardEventFlag.KeyDown | KeyboardEventFlag.ExtendedKey)) + keys.append((keyValue, KeyboardEventFlag.KeyUp | KeyboardEventFlag.ExtendedKey)) + lastKeyValue = keyValue + if upperKey in holdKeys: + hold = True + insertIndex = len(keys) - 1 + else: + hold = False + else: + printKeys.append((key[0], 'UnicodeChar')) + keys.append((key[0], 'UnicodeChar')) + lastKeyValue = key[0] + i = rindex + 1 + elif text[i] == '(': + if hold: + include = True + else: + printKeys.append((text[i], 'UnicodeChar')) + keys.append((text[i], 'UnicodeChar')) + lastKeyValue = text[i] + i += 1 + elif text[i] == ')': + if hold: + include = False + hold = False + else: + printKeys.append((text[i], 'UnicodeChar')) + keys.append((text[i], 'UnicodeChar')) + lastKeyValue = text[i] + i += 1 + else: + if hold: + if text[i] in CharacterCodes: + keyValue = CharacterCodes[text[i]] + if include and type(lastKeyValue) == type(keyValue) and lastKeyValue == keyValue: + insertIndex += 1 + printKeys.insert(insertIndex, (text[i], 'KeyDown | ExtendedKey')) + printKeys.insert(insertIndex + 1, (text[i], 'KeyUp | ExtendedKey')) + keys.insert(insertIndex, (keyValue, KeyboardEventFlag.KeyDown | KeyboardEventFlag.ExtendedKey)) + keys.insert(insertIndex + 1, (keyValue, KeyboardEventFlag.KeyUp | KeyboardEventFlag.ExtendedKey)) + lastKeyValue = keyValue + else: + printKeys.append((text[i], 'UnicodeChar')) + keys.append((text[i], 'UnicodeChar')) + lastKeyValue = text[i] + if include: + insertIndex += 1 + else: + hold = False + else: + printKeys.append((text[i], 'UnicodeChar')) + keys.append((text[i], 'UnicodeChar')) + lastKeyValue = text[i] + i += 1 + if i >= length: + break + hotkeyInterval = 0.01 + for i, key in enumerate(keys): + if key[1] == 'UnicodeChar': + SendUnicodeChar(key[0], charMode) + time.sleep(interval) + if debug: + Logger.ColorfullyWrite('{}, sleep({})\n'.format(printKeys[i], interval), writeToFile=False) + else: + scanCode = _VKtoSC(key[0]) + keybd_event(key[0], scanCode, key[1], 0) + if debug: + Logger.Write(printKeys[i], ConsoleColor.DarkGreen, writeToFile=False) + if i + 1 == len(keys): + time.sleep(interval) + if debug: + Logger.Write(', sleep({})\n'.format(interval), writeToFile=False) + else: + if key[1] & KeyboardEventFlag.KeyUp: + if keys[i + 1][1] == 'UnicodeChar' or keys[i + 1][1] & KeyboardEventFlag.KeyUp == 0: + time.sleep(interval) + if debug: + Logger.Write(', sleep({})\n'.format(interval), writeToFile=False) + else: + time.sleep(hotkeyInterval) #must sleep for a while, otherwise combined keys may not be caught + if debug: + Logger.Write(', sleep({})\n'.format(hotkeyInterval), writeToFile=False) + else: #KeyboardEventFlag.KeyDown + time.sleep(hotkeyInterval) + if debug: + Logger.Write(', sleep({})\n'.format(hotkeyInterval), writeToFile=False) + #make sure hold keys are not pressed + #win = ctypes.windll.user32.GetAsyncKeyState(Keys.VK_LWIN) + #ctrl = ctypes.windll.user32.GetAsyncKeyState(Keys.VK_CONTROL) + #alt = ctypes.windll.user32.GetAsyncKeyState(Keys.VK_MENU) + #shift = ctypes.windll.user32.GetAsyncKeyState(Keys.VK_SHIFT) + #if win & 0x8000: + #Logger.WriteLine('ERROR: WIN is pressed, it should not be pressed!', ConsoleColor.Red) + #keybd_event(Keys.VK_LWIN, 0, KeyboardEventFlag.KeyUp | KeyboardEventFlag.ExtendedKey, 0) + #if ctrl & 0x8000: + #Logger.WriteLine('ERROR: CTRL is pressed, it should not be pressed!', ConsoleColor.Red) + #keybd_event(Keys.VK_CONTROL, 0, KeyboardEventFlag.KeyUp | KeyboardEventFlag.ExtendedKey, 0) + #if alt & 0x8000: + #Logger.WriteLine('ERROR: ALT is pressed, it should not be pressed!', ConsoleColor.Red) + #keybd_event(Keys.VK_MENU, 0, KeyboardEventFlag.KeyUp | KeyboardEventFlag.ExtendedKey, 0) + #if shift & 0x8000: + #Logger.WriteLine('ERROR: SHIFT is pressed, it should not be pressed!', ConsoleColor.Red) + #keybd_event(Keys.VK_SHIFT, 0, KeyboardEventFlag.KeyUp | KeyboardEventFlag.ExtendedKey, 0) + time.sleep(waitTime) + + +class Logger: + """ + Logger for print and log. Support for printing log with different colors on console. + """ + FileName = '@AutomationLog.txt' + _SelfFileName = os.path.split(__file__)[1] + ColorNames = { + "Black": ConsoleColor.Black, + "DarkBlue": ConsoleColor.DarkBlue, + "DarkGreen": ConsoleColor.DarkGreen, + "DarkCyan": ConsoleColor.DarkCyan, + "DarkRed": ConsoleColor.DarkRed, + "DarkMagenta": ConsoleColor.DarkMagenta, + "DarkYellow": ConsoleColor.DarkYellow, + "Gray": ConsoleColor.Gray, + "DarkGray": ConsoleColor.DarkGray, + "Blue": ConsoleColor.Blue, + "Green": ConsoleColor.Green, + "Cyan": ConsoleColor.Cyan, + "Red": ConsoleColor.Red, + "Magenta": ConsoleColor.Magenta, + "Yellow": ConsoleColor.Yellow, + "White": ConsoleColor.White, + } + + @staticmethod + def SetLogFile(path: str) -> None: + Logger.FileName = path + + @staticmethod + def Write(log: Any, consoleColor: int = ConsoleColor.Default, writeToFile: bool = True, printToStdout: bool = True, logFile: str = None, printTruncateLen: int = 0) -> None: + """ + log: any type. + consoleColor: int, a value in class `ConsoleColor`, such as `ConsoleColor.DarkGreen`. + writeToFile: bool. + printToStdout: bool. + logFile: str, log file path. + printTruncateLen: int, if <= 0, log is not truncated when print. + """ + if not isinstance(log, str): + log = str(log) + if printToStdout and sys.stdout: + isValidColor = (consoleColor >= ConsoleColor.Black and consoleColor <= ConsoleColor.White) + if isValidColor: + SetConsoleColor(consoleColor) + try: + if printTruncateLen > 0 and len(log) > printTruncateLen: + sys.stdout.write(log[:printTruncateLen] + '...') + else: + sys.stdout.write(log) + except Exception as ex: + SetConsoleColor(ConsoleColor.Red) + isValidColor = True + sys.stdout.write(ex.__class__.__name__ + ': can\'t print the log!') + if log.endswith('\n'): + sys.stdout.write('\n') + if isValidColor: + ResetConsoleColor() + sys.stdout.flush() + if not writeToFile: + return + fileName = logFile if logFile else Logger.FileName + fout = None + try: + fout = open(fileName, 'a+', encoding='utf-8') + fout.write(log) + except Exception as ex: + if sys.stdout: + sys.stdout.write(ex.__class__.__name__ + ': can\'t write the log!') + finally: + if fout: + fout.close() + + @staticmethod + def WriteLine(log: Any, consoleColor: int = -1, writeToFile: bool = True, printToStdout: bool = True, logFile: str = None) -> None: + """ + log: any type. + consoleColor: int, a value in class `ConsoleColor`, such as `ConsoleColor.DarkGreen`. + writeToFile: bool. + printToStdout: bool. + logFile: str, log file path. + """ + Logger.Write('{}\n'.format(log), consoleColor, writeToFile, printToStdout, logFile) + + @staticmethod + def ColorfullyWrite(log: str, consoleColor: int = -1, writeToFile: bool = True, printToStdout: bool = True, logFile: str = None) -> None: + """ + log: str. + consoleColor: int, a value in class `ConsoleColor`, such as `ConsoleColor.DarkGreen`. + writeToFile: bool. + printToStdout: bool. + logFile: str, log file path. + ColorfullyWrite('Hello Green !!!'), color name must be in Logger.ColorNames. + """ + text = [] + start = 0 + while True: + index1 = log.find('= 0: + if index1 > start: + text.append((log[start:index1], consoleColor)) + index2 = log.find('>', index1) + colorName = log[index1+7:index2] + index3 = log.find('', index2 + 1) + text.append((log[index2 + 1:index3], Logger.ColorNames[colorName])) + start = index3 + 8 + else: + if start < len(log): + text.append((log[start:], consoleColor)) + break + for t, c in text: + Logger.Write(t, c, writeToFile, printToStdout, logFile) + + @staticmethod + def ColorfullyWriteLine(log: str, consoleColor: int = -1, writeToFile: bool = True, printToStdout: bool = True, logFile: str = None) -> None: + """ + log: str. + consoleColor: int, a value in class `ConsoleColor`, such as `ConsoleColor.DarkGreen`. + writeToFile: bool. + printToStdout: bool. + logFile: str, log file path. + + ColorfullyWriteLine('Hello Green !!!'), color name must be in Logger.ColorNames. + """ + Logger.ColorfullyWrite(log + '\n', consoleColor, writeToFile, printToStdout, logFile) + + @staticmethod + def Log(log: Any = '', consoleColor: int = -1, writeToFile: bool = True, printToStdout: bool = True, logFile: str = None) -> None: + """ + log: any type. + consoleColor: int, a value in class `ConsoleColor`, such as `ConsoleColor.DarkGreen`. + writeToFile: bool. + printToStdout: bool. + logFile: str, log file path. + """ + frameCount = 1 + while True: + frame = sys._getframe(frameCount) + _, scriptFileName = os.path.split(frame.f_code.co_filename) + if scriptFileName != Logger._SelfFileName: + break + frameCount += 1 + + t = datetime.datetime.now() + log = '{}-{:02}-{:02} {:02}:{:02}:{:02}.{:03} {}[{}] {} -> {}\n'.format(t.year, t.month, t.day, + t.hour, t.minute, t.second, t.microsecond // 1000, scriptFileName, frame.f_lineno, frame.f_code.co_name, log) + Logger.Write(log, consoleColor, writeToFile, printToStdout, logFile) + + @staticmethod + def ColorfullyLog(log: str = '', consoleColor: int = -1, writeToFile: bool = True, printToStdout: bool = True, logFile: str = None) -> None: + """ + log: any type. + consoleColor: int, a value in class ConsoleColor, such as ConsoleColor.DarkGreen. + writeToFile: bool. + printToStdout: bool. + logFile: str, log file path. + + ColorfullyLog('Hello Green !!!'), color name must be in Logger.ColorNames + """ + frameCount = 1 + while True: + frame = sys._getframe(frameCount) + _, scriptFileName = os.path.split(frame.f_code.co_filename) + if scriptFileName != Logger._SelfFileName: + break + frameCount += 1 + + t = datetime.datetime.now() + log = '{}-{:02}-{:02} {:02}:{:02}:{:02}.{:03} {}[{}] {} -> {}\n'.format(t.year, t.month, t.day, + t.hour, t.minute, t.second, t.microsecond // 1000, scriptFileName, frame.f_lineno, frame.f_code.co_name, log) + Logger.ColorfullyWrite(log, consoleColor, writeToFile, printToStdout, logFile) + + @staticmethod + def DeleteLog() -> None: + """Delete log file.""" + if os.path.exists(Logger.FileName): + os.remove(Logger.FileName) + + +class Bitmap: + """ + A simple Bitmap class wraps Windows GDI+ Gdiplus::Bitmap, but may not have high efficiency. + """ + def __init__(self, width: int = 0, height: int = 0): + """ + Create a black bimap of size(width, height). + """ + self._width = width + self._height = height + self._bitmap = 0 + if width > 0 and height > 0: + self._bitmap = _DllClient.instance().dll.BitmapCreate(width, height) + + def __del__(self): + self.Release() + + def _getsize(self) -> None: + size = _DllClient.instance().dll.BitmapGetWidthAndHeight(self._bitmap) + self._width = size & 0xFFFF + self._height = size >> 16 + + def Release(self) -> None: + if self._bitmap: + _DllClient.instance().dll.BitmapRelease(self._bitmap) + self._bitmap = 0 + self._width = 0 + self._height = 0 + + @property + def Width(self) -> int: + """ + Property Width. + Return int. + """ + return self._width + + @property + def Height(self) -> int: + """ + Property Height. + Return int. + """ + return self._height + + def FromHandle(self, hwnd: int, left: int = 0, top: int = 0, right: int = 0, bottom: int = 0) -> bool: + """ + Capture a native window to Bitmap by its handle. + hwnd: int, the handle of a native window. + left: int. + top: int. + right: int. + bottom: int. + left, top, right and bottom are control's internal postion(from 0,0). + Return bool, True if succeed otherwise False. + """ + self.Release() + root = GetRootControl() + rect = ctypes.wintypes.RECT() + ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect)) + left, top, right, bottom = left + rect.left, top + rect.top, right + rect.left, bottom + rect.top + self._bitmap = _DllClient.instance().dll.BitmapFromWindow(root.NativeWindowHandle, left, top, right, bottom) + self._getsize() + return self._bitmap > 0 + + def FromControl(self, control: 'Control', x: int = 0, y: int = 0, width: int = 0, height: int = 0) -> bool: + """ + Capture a control to Bitmap. + control: `Control` or its subclass. + x: int. + y: int. + width: int. + height: int. + x, y: the point in control's internal position(from 0,0) + width, height: image's width and height from x, y, use 0 for entire area, + If width(or height) < 0, image size will be control's width(or height) - width(or height). + Return bool, True if succeed otherwise False. + """ + rect = control.BoundingRectangle + while rect.width() == 0 or rect.height() == 0: + #some controls maybe visible but their BoundingRectangle are all 0, capture its parent util valid + control = control.GetParentControl() + if not control: + return False + rect = control.BoundingRectangle + if width <= 0: + width = rect.width() + width + if height <= 0: + height = rect.height() + height + handle = control.NativeWindowHandle + if handle: + left = x + top = y + right = left + width + bottom = top + height + else: + while True: + control = control.GetParentControl() + handle = control.NativeWindowHandle + if handle: + pRect = control.BoundingRectangle + left = rect.left - pRect.left + x + top = rect.top - pRect.top + y + right = left + width + bottom = top + height + break + return self.FromHandle(handle, left, top, right, bottom) + + def FromFile(self, filePath: str) -> bool: + """ + Load image from a file. + filePath: str. + Return bool, True if succeed otherwise False. + """ + self.Release() + self._bitmap = _DllClient.instance().dll.BitmapFromFile(ctypes.c_wchar_p(filePath)) + self._getsize() + return self._bitmap > 0 + + def ToFile(self, savePath: str) -> bool: + """ + Save to a file. + savePath: str, should end with .bmp, .jpg, .jpeg, .png, .gif, .tif, .tiff. + Return bool, True if succeed otherwise False. + """ + name, ext = os.path.splitext(savePath) + extMap = {'.bmp': 'image/bmp' + , '.jpg': 'image/jpeg' + , '.jpeg': 'image/jpeg' + , '.gif': 'image/gif' + , '.tif': 'image/tiff' + , '.tiff': 'image/tiff' + , '.png': 'image/png' + } + gdiplusImageFormat = extMap.get(ext.lower(), 'image/png') + return bool(_DllClient.instance().dll.BitmapToFile(self._bitmap, ctypes.c_wchar_p(savePath), ctypes.c_wchar_p(gdiplusImageFormat))) + + def GetPixelColor(self, x: int, y: int) -> int: + """ + Get color value of a pixel. + x: int. + y: int. + Return int, argb color. + b = argb & 0x0000FF + g = (argb & 0x00FF00) >> 8 + r = (argb & 0xFF0000) >> 16 + a = (argb & 0xFF0000) >> 24 + """ + return _DllClient.instance().dll.BitmapGetPixel(self._bitmap, x, y) + + def SetPixelColor(self, x: int, y: int, argb: int) -> bool: + """ + Set color value of a pixel. + x: int. + y: int. + argb: int, color value. + Return bool, True if succeed otherwise False. + """ + return _DllClient.instance().dll.BitmapSetPixel(self._bitmap, x, y, argb) + + def GetPixelColorsHorizontally(self, x: int, y: int, count: int) -> ctypes.Array: + """ + x: int. + y: int. + count: int. + Return `ctypes.Array`, an iterable array of int values in argb form point x,y horizontally. + """ + arrayType = ctypes.c_uint32 * count + values = arrayType() + _DllClient.instance().dll.BitmapGetPixelsHorizontally(ctypes.c_size_t(self._bitmap), x, y, values, count) + return values + + def SetPixelColorsHorizontally(self, x: int, y: int, colors: Iterable[int]) -> bool: + """ + Set pixel colors form x,y horizontally. + x: int. + y: int. + colors: Iterable[int], an iterable list of int color values in argb. + Return bool, True if succeed otherwise False. + """ + count = len(colors) + arrayType = ctypes.c_uint32 * count + values = arrayType(*colors) + return _DllClient.instance().dll.BitmapSetPixelsHorizontally(ctypes.c_size_t(self._bitmap), x, y, values, count) + + def GetPixelColorsVertically(self, x: int, y: int, count: int) -> ctypes.Array: + """ + x: int. + y: int. + count: int. + Return `ctypes.Array`, an iterable array of int values in argb form point x,y vertically. + """ + arrayType = ctypes.c_uint32 * count + values = arrayType() + _DllClient.instance().dll.BitmapGetPixelsVertically(ctypes.c_size_t(self._bitmap), x, y, values, count) + return values + + def SetPixelColorsVertically(self, x: int, y: int, colors: Iterable[int]) -> bool: + """ + Set pixel colors form x,y vertically. + x: int. + y: int. + colors: Iterable[int], an iterable list of int color values in argb. + Return bool, True if succeed otherwise False. + """ + count = len(colors) + arrayType = ctypes.c_uint32 * count + values = arrayType(*colors) + return _DllClient.instance().dll.BitmapSetPixelsVertically(ctypes.c_size_t(self._bitmap), x, y, values, count) + + def GetPixelColorsOfRow(self, y: int) -> ctypes.Array: + """ + y: int, row index. + Return `ctypes.Array`, an iterable array of int values in argb of y row. + """ + return self.GetPixelColorsOfRect(0, y, self.Width, 1) + + def GetPixelColorsOfColumn(self, x: int) -> ctypes.Array: + """ + x: int, column index. + Return `ctypes.Array`, an iterable array of int values in argb of x column. + """ + return self.GetPixelColorsOfRect(x, 0, 1, self.Height) + + def GetPixelColorsOfRect(self, x: int, y: int, width: int, height: int) -> ctypes.Array: + """ + x: int. + y: int. + width: int. + height: int. + Return `ctypes.Array`, an iterable array of int values in argb of the input rect. + """ + arrayType = ctypes.c_uint32 * (width * height) + values = arrayType() + _DllClient.instance().dll.BitmapGetPixelsOfRect(ctypes.c_size_t(self._bitmap), x, y, width, height, values) + return values + + def SetPixelColorsOfRect(self, x: int, y: int, width: int, height: int, colors: Iterable[int]) -> bool: + """ + x: int. + y: int. + width: int. + height: int. + colors: Iterable[int], an iterable list of int values in argb, it's length must equal to width*height. + Return bool. + """ + arrayType = ctypes.c_uint32 * (width * height) + values = arrayType(*colors) + return bool(_DllClient.instance().dll.BitmapSetPixelsOfRect(ctypes.c_size_t(self._bitmap), x, y, width, height, values)) + + def GetPixelColorsOfRects(self, rects: List[Tuple[int, int, int, int]]) -> List[ctypes.Array]: + """ + rects: List[Tuple[int, int, int, int]], such as [(0,0,10,10), (10,10,20,20), (x,y,width,height)]. + Return List[ctypes.Array], a list whose elements are ctypes.Array which is an iterable array of int values in argb. + """ + rects2 = [(x, y, x + width, y + height) for x, y, width, height in rects] + left, top, right, bottom = zip(*rects2) + left, top, right, bottom = min(left), min(top), max(right), max(bottom) + width, height = right - left, bottom - top + allColors = self.GetPixelColorsOfRect(left, top, width, height) + colorsOfRects = [] + for x, y, w, h in rects: + x -= left + y -= top + colors = [] + for row in range(h): + colors.extend(allColors[(y + row) * width + x:(y + row) * width + x + w]) + colorsOfRects.append(colors) + return colorsOfRects + + def GetAllPixelColors(self) -> ctypes.Array: + """ + Return `ctypes.Array`, an iterable array of int values in argb. + """ + return self.GetPixelColorsOfRect(0, 0, self.Width, self.Height) + + def GetSubBitmap(self, x: int, y: int, width: int, height: int) -> 'Bitmap': + """ + x: int. + y: int. + width: int. + height: int. + Return `Bitmap`, a sub bitmap of the input rect. + """ + colors = self.GetPixelColorsOfRect(x, y, width, height) + bitmap = Bitmap(width, height) + bitmap.SetPixelColorsOfRect(0, 0, width, height, colors) + return bitmap + + +_PatternIdInterfaces = None +def GetPatternIdInterface(patternId: int): + """ + Get pattern COM interface by pattern id. + patternId: int, a value in class `PatternId`. + Return comtypes._cominterface_meta. + """ + global _PatternIdInterfaces + if not _PatternIdInterfaces: + _PatternIdInterfaces = { + # PatternId.AnnotationPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationAnnotationPattern, + # PatternId.CustomNavigationPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationCustomNavigationPattern, + PatternId.DockPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationDockPattern, + # PatternId.DragPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationDragPattern, + # PatternId.DropTargetPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationDropTargetPattern, + PatternId.ExpandCollapsePattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationExpandCollapsePattern, + PatternId.GridItemPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationGridItemPattern, + PatternId.GridPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationGridPattern, + PatternId.InvokePattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationInvokePattern, + PatternId.ItemContainerPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationItemContainerPattern, + PatternId.LegacyIAccessiblePattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationLegacyIAccessiblePattern, + PatternId.MultipleViewPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationMultipleViewPattern, + # PatternId.ObjectModelPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationObjectModelPattern, + PatternId.RangeValuePattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationRangeValuePattern, + PatternId.ScrollItemPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationScrollItemPattern, + PatternId.ScrollPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationScrollPattern, + PatternId.SelectionItemPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationSelectionItemPattern, + PatternId.SelectionPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationSelectionPattern, + # PatternId.SpreadsheetItemPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationSpreadsheetItemPattern, + # PatternId.SpreadsheetPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationSpreadsheetPattern, + # PatternId.StylesPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationStylesPattern, + PatternId.SynchronizedInputPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationSynchronizedInputPattern, + PatternId.TableItemPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationTableItemPattern, + PatternId.TablePattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationTablePattern, + # PatternId.TextChildPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationTextChildPattern, + # PatternId.TextEditPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationTextEditPattern, + PatternId.TextPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationTextPattern, + # PatternId.TextPattern2: _AutomationClient.instance().UIAutomationCore.IUIAutomationTextPattern2, + PatternId.TogglePattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationTogglePattern, + PatternId.TransformPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationTransformPattern, + # PatternId.TransformPattern2: _AutomationClient.instance().UIAutomationCore.IUIAutomationTransformPattern2, + PatternId.ValuePattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationValuePattern, + PatternId.VirtualizedItemPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationVirtualizedItemPattern, + PatternId.WindowPattern: _AutomationClient.instance().UIAutomationCore.IUIAutomationWindowPattern, + } + debug = False + #the following patterns doesn't exist on Windows 7 or lower + try: + _PatternIdInterfaces[PatternId.AnnotationPattern] = _AutomationClient.instance().UIAutomationCore.IUIAutomationAnnotationPattern + except: + if debug: Logger.WriteLine('UIAutomationCore does not have AnnotationPattern.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.CustomNavigationPattern] = _AutomationClient.instance().UIAutomationCore.IUIAutomationCustomNavigationPattern + except: + if debug: Logger.WriteLine('UIAutomationCore does not have CustomNavigationPattern.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.DragPattern] = _AutomationClient.instance().UIAutomationCore.IUIAutomationDragPattern + except: + if debug: Logger.WriteLine('UIAutomationCore does not have DragPattern.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.DropTargetPattern] = _AutomationClient.instance().UIAutomationCore.IUIAutomationDropTargetPattern + except: + if debug: Logger.WriteLine('UIAutomationCore does not have DropTargetPattern.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.ObjectModelPattern] = _AutomationClient.instance().UIAutomationCore.IUIAutomationObjectModelPattern + except: + if debug: Logger.WriteLine('UIAutomationCore does not have ObjectModelPattern.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.SpreadsheetItemPattern] = _AutomationClient.instance().UIAutomationCore.IUIAutomationSpreadsheetItemPattern + except: + if debug: Logger.WriteLine('UIAutomationCore does not have SpreadsheetItemPattern.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.SpreadsheetPattern] = _AutomationClient.instance().UIAutomationCore.IUIAutomationSpreadsheetPattern + except: + if debug: Logger.WriteLine('UIAutomationCore does not have SpreadsheetPattern.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.StylesPattern] = _AutomationClient.instance().UIAutomationCore.IUIAutomationStylesPattern + except: + if debug: Logger.WriteLine('UIAutomationCore does not have StylesPattern.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.TextChildPattern] = _AutomationClient.instance().UIAutomationCore.IUIAutomationTextChildPattern + except: + if debug: Logger.WriteLine('UIAutomationCore does not have TextChildPattern.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.TextEditPattern] = _AutomationClient.instance().UIAutomationCore.IUIAutomationTextEditPattern + except: + if debug: Logger.WriteLine('UIAutomationCore does not have TextEditPattern.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.TextPattern2] = _AutomationClient.instance().UIAutomationCore.IUIAutomationTextPattern2 + except: + if debug: Logger.WriteLine('UIAutomationCore does not have TextPattern2.', ConsoleColor.Yellow) + try: + _PatternIdInterfaces[PatternId.TransformPattern2] = _AutomationClient.instance().UIAutomationCore.IUIAutomationTransformPattern2 + except: + if debug: Logger.WriteLine('UIAutomationCore does not have TransformPattern2.', ConsoleColor.Yellow) + return _PatternIdInterfaces[patternId] + + +""" +Control Pattern Mapping for UI Automation Clients. +Refer https://docs.microsoft.com/en-us/previous-versions//dd319586(v=vs.85) +""" + + +class AnnotationPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationannotationpattern""" + self.pattern = pattern + + @property + def AnnotationTypeId(self) -> int: + """ + Property AnnotationTypeId. + Call IUIAutomationAnnotationPattern::get_CurrentAnnotationTypeId. + Return int, a value in class `AnnotationType`. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationannotationpattern-get_currentannotationtypeid + """ + return self.pattern.CurrentAnnotationTypeId + + @property + def AnnotationTypeName(self) -> str: + """ + Property AnnotationTypeName. + Call IUIAutomationAnnotationPattern::get_CurrentAnnotationTypeName. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationannotationpattern-get_currentannotationtypename + """ + return self.pattern.CurrentAnnotationTypeName + + @property + def Author(self) -> str: + """ + Property Author. + Call IUIAutomationAnnotationPattern::get_CurrentAuthor. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationannotationpattern-get_currentauthor + """ + return self.pattern.CurrentAuthor + + @property + def DateTime(self) -> str: + """ + Property DateTime. + Call IUIAutomationAnnotationPattern::get_CurrentDateTime. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationannotationpattern-get_currentdatetime + """ + return self.pattern.CurrentDateTime + + @property + def Target(self) -> 'Control': + """ + Property Target. + Call IUIAutomationAnnotationPattern::get_CurrentTarget. + Return `Control` subclass, the element that is being annotated. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationannotationpattern-get_currenttarget + """ + ele = self.pattern.CurrentTarget + return Control.CreateControlFromElement(ele) + + +class CustomNavigationPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationcustomnavigationpattern""" + self.pattern = pattern + + def Navigate(self, direction: int) -> 'Control': + """ + Call IUIAutomationCustomNavigationPattern::Navigate. + Get the next control in the specified direction within the logical UI tree. + direction: int, a value in class `NavigateDirection`. + Return `Control` subclass or None. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationcustomnavigationpattern-navigate + """ + ele = self.pattern.Navigate(direction) + return Control.CreateControlFromElement(ele) + + +class DockPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationdockpattern""" + self.pattern = pattern + + @property + def DockPosition(self) -> int: + """ + Property DockPosition. + Call IUIAutomationDockPattern::get_CurrentDockPosition. + Return int, a value in class `DockPosition`. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationdockpattern-get_currentdockposition + """ + return self.pattern.CurrentDockPosition + + def SetDockPosition(self, dockPosition: int, waitTime: float = OPERATION_WAIT_TIME) -> int: + """ + Call IUIAutomationDockPattern::SetDockPosition. + dockPosition: int, a value in class `DockPosition`. + waitTime: float. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationdockpattern-setdockposition + """ + ret = self.pattern.SetDockPosition(dockPosition) + time.sleep(waitTime) + return ret + + +class DragPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationdragpattern""" + self.pattern = pattern + + @property + def DropEffect(self) -> str: + """ + Property DropEffect. + Call IUIAutomationDragPattern::get_CurrentDropEffect. + Return str, a localized string that indicates what happens + when the user drops this element as part of a drag-drop operation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationdragpattern-get_currentdropeffect + """ + return self.pattern.CurrentDropEffect + + @property + def DropEffects(self) -> List[str]: + """ + Property DropEffects. + Call IUIAutomationDragPattern::get_CurrentDropEffects, todo SAFEARRAY. + Return List[str], a list of localized strings that enumerate the full set of effects + that can happen when this element as part of a drag-and-drop operation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationdragpattern-get_currentdropeffects + """ + return self.pattern.CurrentDropEffects + + @property + def IsGrabbed(self) -> bool: + """ + Property IsGrabbed. + Call IUIAutomationDragPattern::get_CurrentIsGrabbed. + Return bool, indicates whether the user has grabbed this element as part of a drag-and-drop operation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationdragpattern-get_currentisgrabbed + """ + return bool(self.pattern.CurrentIsGrabbed) + + def GetGrabbedItems(self) -> List['Control']: + """ + Call IUIAutomationDragPattern::GetCurrentGrabbedItems. + Return List[Control], a list of `Control` subclasses that represent the full set of items + that the user is dragging as part of a drag operation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationdragpattern-getcurrentgrabbeditems + """ + eleArray = self.pattern.GetCurrentGrabbedItems() + if eleArray: + controls = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + con = Control.CreateControlFromElement(element=ele) + if con: + controls.append(con) + return controls + return [] + + +class DropTargetPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationdroptargetpattern""" + self.pattern = pattern + + @property + def DropTargetEffect(self) -> str: + """ + Property DropTargetEffect. + Call IUIAutomationDropTargetPattern::get_CurrentDropTargetEffect. + Return str, a localized string that describes what happens + when the user drops the grabbed element on this drop target. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationdragpattern-get_currentdroptargeteffect + """ + return self.pattern.CurrentDropTargetEffect + + @property + def DropTargetEffects(self) -> List[str]: + """ + Property DropTargetEffects. + Call IUIAutomationDropTargetPattern::get_CurrentDropTargetEffects, todo SAFEARRAY. + Return List[str], a list of localized strings that enumerate the full set of effects + that can happen when the user drops a grabbed element on this drop target + as part of a drag-and-drop operation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationdragpattern-get_currentdroptargeteffects + """ + return self.pattern.CurrentDropTargetEffects + + +class ExpandCollapsePattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationexpandcollapsepattern""" + self.pattern = pattern + + @property + def ExpandCollapseState(self) -> int: + """ + Property ExpandCollapseState. + Call IUIAutomationExpandCollapsePattern::get_CurrentExpandCollapseState. + Return int, a value in class ExpandCollapseState. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationexpandcollapsepattern-get_currentexpandcollapsestate + """ + return self.pattern.CurrentExpandCollapseState + + def Collapse(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationExpandCollapsePattern::Collapse. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationexpandcollapsepattern-collapse + """ + ret = self.pattern.Collapse() == S_OK + time.sleep(waitTime) + return ret + + def Expand(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationExpandCollapsePattern::Expand. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationexpandcollapsepattern-expand + """ + ret = self.pattern.Expand() == S_OK + time.sleep(waitTime) + return ret + + +class GridItemPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationgriditempattern""" + self.pattern = pattern + + @property + def Column(self) -> int: + """ + Property Column. + Call IUIAutomationGridItemPattern::get_CurrentColumn. + Return int, the zero-based index of the column that contains the item. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationgriditempattern-get_currentcolumn + """ + return self.pattern.CurrentColumn + + @property + def ColumnSpan(self) -> int: + """ + Property ColumnSpan. + Call IUIAutomationGridItemPattern::get_CurrentColumnSpan. + Return int, the number of columns spanned by the grid item. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationgriditempattern-get_currentcolumnspan + """ + return self.pattern.CurrentColumnSpan + + @property + def ContainingGrid(self) -> 'Control': + """ + Property ContainingGrid. + Call IUIAutomationGridItemPattern::get_CurrentContainingGrid. + Return `Control` subclass, the element that contains the grid item. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationgriditempattern-get_currentcontaininggrid + """ + return Control.CreateControlFromElement(self.pattern.CurrentContainingGrid) + + @property + def Row(self) -> int: + """ + Property Row. + Call IUIAutomationGridItemPattern::get_CurrentRow. + Return int, the zero-based index of the row that contains the grid item. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationgriditempattern-get_currentrow + """ + return self.pattern.CurrentRow + + @property + def RowSpan(self) -> int: + """ + Property RowSpan. + Call IUIAutomationGridItemPattern::get_CurrentRowSpan. + Return int, the number of rows spanned by the grid item. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationgriditempattern-get_currentrowspan + """ + return self.pattern.CurrentRowSpan + + +class GridPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationgridpattern""" + self.pattern = pattern + + @property + def ColumnCount(self) -> int: + """ + Property ColumnCount. + Call IUIAutomationGridPattern::get_CurrentColumnCount. + Return int, the number of columns in the grid. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationgridpattern-get_currentcolumncount + """ + return self.pattern.CurrentColumnCount + + @property + def RowCount(self) -> int: + """ + Property RowCount. + Call IUIAutomationGridPattern::get_CurrentRowCount. + Return int, the number of rows in the grid. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationgridpattern-get_currentrowcount + """ + return self.pattern.CurrentRowCount + + def GetItem(self) -> 'Control': + """ + Call IUIAutomationGridPattern::GetItem. + Return `Control` subclass, a control representing an item in the grid. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationgridpattern-getitem + """ + return Control.CreateControlFromElement(self.pattern.GetItem()) + +class InvokePattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationinvokepattern""" + self.pattern = pattern + + def Invoke(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationInvokePattern::Invoke. + Invoke the action of a control, such as a button click. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationinvokepattern-invoke + """ + ret = self.pattern.Invoke() == S_OK + time.sleep(waitTime) + return ret + + +class ItemContainerPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationitemcontainerpattern""" + self.pattern = pattern + + def FindItemByProperty(control: 'Control', propertyId: int, propertyValue) -> 'Control': + """ + Call IUIAutomationItemContainerPattern::FindItemByProperty. + control: `Control` or its subclass. + propertyValue: COM VARIANT according to propertyId? todo. + propertyId: int, a value in class `PropertyId`. + Return `Control` subclass, a control within a containing element, based on a specified property value. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationitemcontainerpattern-finditembyproperty + """ + ele = self.pattern.FindItemByProperty(control.Element, propertyId, propertyValue) + return Control.CreateControlFromElement(ele) + + +class LegacyIAccessiblePattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationlegacyiaccessiblepattern""" + self.pattern = pattern + + @property + def ChildId(self) -> int: + """ + Property ChildId. + Call IUIAutomationLegacyIAccessiblePattern::get_CurrentChildId. + Return int, the Microsoft Active Accessibility child identifier for the element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-get_currentchildid + """ + return self.pattern.CurrentChildId + + @property + def DefaultAction(self) -> str: + """ + Property DefaultAction. + Call IUIAutomationLegacyIAccessiblePattern::get_CurrentDefaultAction. + Return str, the Microsoft Active Accessibility current default action for the element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-get_currentdefaultaction + """ + return self.pattern.CurrentDefaultAction + + @property + def Description(self) -> str: + """ + Property Description. + Call IUIAutomationLegacyIAccessiblePattern::get_CurrentDescription. + Return str, the Microsoft Active Accessibility description of the element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-get_currentdescription + """ + return self.pattern.CurrentDescription + + @property + def Help(self) -> str: + """ + Property Help. + Call IUIAutomationLegacyIAccessiblePattern::get_CurrentHelp. + Return str, the Microsoft Active Accessibility help string for the element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-get_currenthelp + """ + return self.pattern.CurrentHelp + + @property + def KeyboardShortcut(self) -> str: + """ + Property KeyboardShortcut. + Call IUIAutomationLegacyIAccessiblePattern::get_CurrentKeyboardShortcut. + Return str, the Microsoft Active Accessibility keyboard shortcut property for the element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-get_currentkeyboardshortcut + """ + return self.pattern.CurrentKeyboardShortcut + + @property + def Name(self) -> str: + """ + Property Name. + Call IUIAutomationLegacyIAccessiblePattern::get_CurrentName. + Return str, the Microsoft Active Accessibility name property of the element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-get_currentname + """ + return self.pattern.CurrentName or '' # CurrentName may be None + + @property + def Role(self) -> int: + """ + Property Role. + Call IUIAutomationLegacyIAccessiblePattern::get_CurrentRole. + Return int, a value in calss `AccessibleRole`, the Microsoft Active Accessibility role identifier. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-get_currentrole + """ + return self.pattern.CurrentRole + + @property + def State(self) -> int: + """ + Property State. + Call IUIAutomationLegacyIAccessiblePattern::get_CurrentState. + Return int, a value in calss `AccessibleState`, the Microsoft Active Accessibility state identifier. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-get_currentstate + """ + return self.pattern.CurrentState + + @property + def Value(self) -> str: + """ + Property Value. + Call IUIAutomationLegacyIAccessiblePattern::get_CurrentValue. + Return str, the Microsoft Active Accessibility value property. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-get_currentvalue + """ + return self.pattern.CurrentValue + + def DoDefaultAction(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationLegacyIAccessiblePattern::DoDefaultAction. + Perform the Microsoft Active Accessibility default action for the element. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-dodefaultaction + """ + ret = self.pattern.DoDefaultAction() == S_OK + time.sleep(waitTime) + return ret + + def GetSelection(self) -> List['Control']: + """ + Call IUIAutomationLegacyIAccessiblePattern::GetCurrentSelection. + Return List[Control], a list of `Control` subclasses, + the Microsoft Active Accessibility property that identifies the selected children of this element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-getcurrentselection + """ + eleArray = self.pattern.GetCurrentSelection() + if eleArray: + controls = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + con = Control.CreateControlFromElement(element=ele) + if con: + controls.append(con) + return controls + return [] + + def GetIAccessible(self): + """ + Call IUIAutomationLegacyIAccessiblePattern::GetIAccessible, todo. + Return an IAccessible object that corresponds to the Microsoft UI Automation element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-getiaccessible + Refer https://docs.microsoft.com/en-us/windows/desktop/api/oleacc/nn-oleacc-iaccessible + """ + return self.pattern.GetIAccessible() + + def Select(self, flagsSelect: int, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationLegacyIAccessiblePattern::Select. + Perform a Microsoft Active Accessibility selection. + flagsSelect: int, a value in `AccessibleSelection`. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-select + """ + ret = self.pattern.Select(flagsSelect) == S_OK + time.sleep(waitTime) + return ret + + def SetValue(self, value: str, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationLegacyIAccessiblePattern::SetValue. + Set the Microsoft Active Accessibility value property for the element. + value: str. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationlegacyiaccessiblepattern-setvalue + """ + ret = self.pattern.SetValue(value) == S_OK + time.sleep(waitTime) + return ret + + +class MultipleViewPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationmultipleviewpattern""" + self.pattern = pattern + + @property + def CurrentView(self) -> int: + """ + Property CurrentView. + Call IUIAutomationMultipleViewPattern::get_CurrentCurrentView. + Return int, the control-specific identifier of the current view of the control. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationmultipleviewpattern-get_currentcurrentview + """ + return self.pattern.CurrentCurrentView + + def GetSupportedViews(self) -> List[int]: + """ + Call IUIAutomationMultipleViewPattern::GetCurrentSupportedViews, todo. + Return List[int], a list of int, control-specific view identifiers. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationmultipleviewpattern-getcurrentsupportedviews + """ + return self.pattern.GetCurrentSupportedViews() + + def GetViewName(self, view: int) -> str: + """ + Call IUIAutomationMultipleViewPattern::GetViewName. + view: int, the control-specific view identifier. + Return str, the name of a control-specific view. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationmultipleviewpattern-getviewname + """ + return self.pattern.GetViewName(view) + + def SetView(self, view: int) -> bool: + """ + Call IUIAutomationMultipleViewPattern::SetCurrentView. + Set the view of the control. + view: int, the control-specific view identifier. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationmultipleviewpattern-setcurrentview + """ + return self.pattern.SetCurrentView(view) == S_OK + + +class ObjectModelPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationobjectmodelpattern""" + self.pattern = pattern + + def GetUnderlyingObjectModel(self) -> ctypes.POINTER(comtypes.IUnknown): + """ + Call IUIAutomationObjectModelPattern::GetUnderlyingObjectModel, todo. + Return `ctypes.POINTER(comtypes.IUnknown)`, an interface used to access the underlying object model of the provider. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationobjectmodelpattern-getunderlyingobjectmodel + """ + return self.pattern.GetUnderlyingObjectModel() + + +class RangeValuePattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationrangevaluepattern""" + self.pattern = pattern + + @property + def IsReadOnly(self) -> bool: + """ + Property IsReadOnly. + Call IUIAutomationRangeValuePattern::get_CurrentIsReadOnly. + Return bool, indicates whether the value of the element can be changed. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationrangevaluepattern-get_currentisreadonly + """ + return self.pattern.CurrentIsReadOnly + + @property + def LargeChange(self) -> float: + """ + Property LargeChange. + Call IUIAutomationRangeValuePattern::get_CurrentLargeChange. + Return float, the value that is added to or subtracted from the value of the control + when a large change is made, such as when the PAGE DOWN key is pressed. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationrangevaluepattern-get_currentlargechange + """ + return self.pattern.CurrentLargeChange + + @property + def Maximum(self) -> float: + """ + Property Maximum. + Call IUIAutomationRangeValuePattern::get_CurrentMaximum. + Return float, the maximum value of the control. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationrangevaluepattern-get_currentmaximum + """ + return self.pattern.CurrentMaximum + + @property + def Minimum(self) -> float: + """ + Property Minimum. + Call IUIAutomationRangeValuePattern::get_CurrentMinimum. + Return float, the minimum value of the control. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationrangevaluepattern-get_currentminimum + """ + return self.pattern.CurrentMinimum + + @property + def SmallChange(self) -> float: + """ + Property SmallChange. + Call IUIAutomationRangeValuePattern::get_CurrentSmallChange. + Return float, the value that is added to or subtracted from the value of the control + when a small change is made, such as when an arrow key is pressed. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationrangevaluepattern-get_currentsmallchange + """ + return self.pattern.CurrentSmallChange + + @property + def Value(self) -> float: + """ + Property Value. + Call IUIAutomationRangeValuePattern::get_CurrentValue. + Return float, the value of the control. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationrangevaluepattern-get_currentvalue + """ + return self.pattern.CurrentValue + + def SetValue(self, value: float, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationRangeValuePattern::SetValue. + Set the value of the control. + value: int. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationrangevaluepattern-setvalue + """ + ret = self.pattern.SetValue(value) == S_OK + time.sleep(waitTime) + return ret + + +class ScrollItemPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationscrollitempattern""" + self.pattern = pattern + + def ScrollIntoView(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationScrollItemPattern::ScrollIntoView. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollitempattern-scrollintoview + """ + ret = self.pattern.ScrollIntoView() == S_OK + time.sleep(waitTime) + return ret + + +class ScrollPattern(): + NoScrollValue = -1 + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationscrollpattern""" + self.pattern = pattern + + @property + def HorizontallyScrollable(self) -> bool: + """ + Property HorizontallyScrollable. + Call IUIAutomationScrollPattern::get_CurrentHorizontallyScrollable. + Return bool, indicates whether the element can scroll horizontally. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollpattern-get_currenthorizontallyscrollable + """ + return bool(self.pattern.CurrentHorizontallyScrollable) + + @property + def HorizontalScrollPercent(self) -> float: + """ + Property HorizontalScrollPercent. + Call IUIAutomationScrollPattern::get_CurrentHorizontalScrollPercent. + Return float, the horizontal scroll position. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollpattern-get_currenthorizontalscrollpercent + """ + return self.pattern.CurrentHorizontalScrollPercent + + @property + def HorizontalViewSize(self) -> float: + """ + Property HorizontalViewSize. + Call IUIAutomationScrollPattern::get_CurrentHorizontalViewSize. + Return float, the horizontal size of the viewable region of a scrollable element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollpattern-get_currenthorizontalviewsize + """ + return self.pattern.CurrentHorizontalViewSize + + @property + def VerticallyScrollable(self) -> bool: + """ + Property VerticallyScrollable. + Call IUIAutomationScrollPattern::get_CurrentVerticallyScrollable. + Return bool, indicates whether the element can scroll vertically. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollpattern-get_currentverticallyscrollable + """ + return bool(self.pattern.CurrentVerticallyScrollable) + + @property + def VerticalScrollPercent(self) -> float: + """ + Property VerticalScrollPercent. + Call IUIAutomationScrollPattern::get_CurrentVerticalScrollPercent. + Return float, the vertical scroll position. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollpattern-get_currentverticalscrollpercent + """ + return self.pattern.CurrentVerticalScrollPercent + + @property + def VerticalViewSize(self) -> float: + """ + Property VerticalViewSize. + Call IUIAutomationScrollPattern::get_CurrentVerticalViewSize. + Return float, the vertical size of the viewable region of a scrollable element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollpattern-get_currentverticalviewsize + """ + return self.pattern.CurrentVerticalViewSize + + def Scroll(self, horizontalAmount: int, verticalAmount: int, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationScrollPattern::Scroll. + Scroll the visible region of the content area horizontally and vertically. + horizontalAmount: int, a value in ScrollAmount. + verticalAmount: int, a value in ScrollAmount. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollpattern-scroll + """ + ret = self.pattern.Scroll(horizontalAmount, verticalAmount) == S_OK + time.sleep(waitTime) + return ret + + def SetScrollPercent(self, horizontalPercent: float, verticalPercent: float, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationScrollPattern::SetScrollPercent. + Set the horizontal and vertical scroll positions as a percentage of the total content area within the UI Automation element. + horizontalPercent: float or int, a value in [0, 100] or ScrollPattern.NoScrollValue(-1) if no scroll. + verticalPercent: float or int, a value in [0, 100] or ScrollPattern.NoScrollValue(-1) if no scroll. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollpattern-setscrollpercent + """ + ret = self.pattern.SetScrollPercent(horizontalPercent, verticalPercent) == S_OK + time.sleep(waitTime) + return ret + + +class SelectionItemPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationselectionitempattern""" + self.pattern = pattern + + def AddToSelection(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationSelectionItemPattern::AddToSelection. + Add the current element to the collection of selected items. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationselectionitempattern-addtoselection + """ + ret = self.pattern.AddToSelection() == S_OK + time.sleep(waitTime) + return ret + + @property + def IsSelected(self) -> bool: + """ + Property IsSelected. + Call IUIAutomationScrollPattern::get_CurrentIsSelected. + Return bool, indicates whether this item is selected. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollpattern-get_currentisselected + """ + return bool(self.pattern.CurrentIsSelected) + + @property + def SelectionContainer(self) -> 'Control': + """ + Property SelectionContainer. + Call IUIAutomationScrollPattern::get_CurrentSelectionContainer. + Return `Control` subclass, the element that supports IUIAutomationSelectionPattern and acts as the container for this item. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationscrollpattern-get_currentselectioncontainer + """ + ele = self.pattern.CurrentSelectionContainer + return Control.CreateControlFromElement(ele) + + def RemoveFromSelection(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationSelectionItemPattern::RemoveFromSelection. + Remove this element from the selection. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationselectionitempattern-removefromselection + """ + ret = self.pattern.RemoveFromSelection() == S_OK + time.sleep(waitTime) + return ret + + def Select(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationSelectionItemPattern::Select. + Clear any selected items and then select the current element. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationselectionitempattern-select + """ + ret = self.pattern.Select() == S_OK + time.sleep(waitTime) + return ret + + +class SelectionPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationselectionpattern""" + self.pattern = pattern + + @property + def CanSelectMultiple(self) -> bool: + """ + Property CanSelectMultiple. + Call IUIAutomationSelectionPattern::get_CurrentCanSelectMultiple. + Return bool, indicates whether more than one item in the container can be selected at one time. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationselectionpattern-get_currentcanselectmultiple + """ + return bool(self.pattern.CurrentCanSelectMultiple) + + @property + def IsSelectionRequired(self) -> bool: + """ + Property IsSelectionRequired. + Call IUIAutomationSelectionPattern::get_CurrentIsSelectionRequired. + Return bool, indicates whether at least one item must be selected at all times. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationselectionpattern-get_currentisselectionrequired + """ + return bool(self.pattern.CurrentIsSelectionRequired) + + def GetSelection(self) -> List['Control']: + """ + Call IUIAutomationSelectionPattern::GetCurrentSelection. + Return List[Control], a list of `Control` subclasses, the selected elements in the container.. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationselectionpattern-getcurrentselection + """ + eleArray = self.pattern.GetCurrentSelection() + if eleArray: + controls = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + con = Control.CreateControlFromElement(element=ele) + if con: + controls.append(con) + return controls + return [] + + +class SpreadsheetItemPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationspreadsheetitempattern""" + self.pattern = pattern + + @property + def Formula(self) -> str: + """ + Property Formula. + Call IUIAutomationSpreadsheetItemPattern::get_CurrentFormula. + Return str, the formula for this cell. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationspreadsheetitempattern-get_currentformula + """ + return self.pattern.CurrentFormula + + def GetAnnotationObjects(self) -> List['Control']: + """ + Call IUIAutomationSelectionPattern::GetCurrentAnnotationObjects. + Return List[Control], a list of `Control` subclasses representing the annotations associated with this spreadsheet cell. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationspreadsheetitempattern-getcurrentannotationobjects + """ + eleArray = self.pattern.GetCurrentAnnotationObjects() + if eleArray: + controls = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + con = Control.CreateControlFromElement(element=ele) + if con: + controls.append(con) + return controls + return [] + + def GetAnnotationTypes(self) -> List[int]: + """ + Call IUIAutomationSelectionPattern::GetCurrentAnnotationTypes. + Return List[int], a list of int values in class `AnnotationType`, + indicating the types of annotations that are associated with this spreadsheet cell. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationselectionpattern-getcurrentannotationtypes + """ + return self.pattern.GetCurrentAnnotationTypes() + + +class SpreadsheetPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationspreadsheetpattern""" + self.pattern = pattern + + def GetItemByName(self, name: str) -> 'Control': + """ + Call IUIAutomationSpreadsheetPattern::GetItemByName. + name: str. + Return `Control` subclass or None, represents the spreadsheet cell that has the specified name.. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationspreadsheetpattern-getitembyname + """ + ele = self.pattern.GetItemByName(name) + return Control.CreateControlFromElement(element=ele) + + +class StylesPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationstylespattern""" + self.pattern = pattern + + @property + def ExtendedProperties(self) -> str: + """ + Property ExtendedProperties. + Call IUIAutomationStylesPattern::get_CurrentExtendedProperties. + Return str, a localized string that contains the list of extended properties for an element in a document. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationstylespattern-get_currentextendedproperties + """ + return self.pattern.CurrentExtendedProperties + + @property + def FillColor(self) -> int: + """ + Property FillColor. + Call IUIAutomationStylesPattern::get_CurrentFillColor. + Return int, the fill color of an element in a document. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationstylespattern-get_currentfillcolor + """ + return self.pattern.CurrentFillColor + + @property + def FillPatternColor(self) -> int: + """ + Property FillPatternColor. + Call IUIAutomationStylesPattern::get_CurrentFillPatternColor. + Return int, the color of the pattern used to fill an element in a document. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationstylespattern-get_currentfillpatterncolor + """ + return self.pattern.CurrentFillPatternColor + + @property + def Shape(self) -> str: + """ + Property Shape. + Call IUIAutomationStylesPattern::get_CurrentShape. + Return str, the shape of an element in a document. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationstylespattern-get_currentshape + """ + return self.pattern.CurrentShape + + @property + def StyleId(self) -> int: + """ + Property StyleId. + Call IUIAutomationStylesPattern::get_CurrentStyleId. + Return int, a value in class `StyleId`, the visual style associated with an element in a document. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationstylespattern-get_currentstyleid + """ + return self.pattern.CurrentStyleId + + @property + def StyleName(self) -> str: + """ + Property StyleName. + Call IUIAutomationStylesPattern::get_CurrentStyleName. + Return str, the name of the visual style associated with an element in a document. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationstylespattern-get_currentstylename + """ + return self.pattern.CurrentStyleName + + +class SynchronizedInputPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationsynchronizedinputpattern""" + self.pattern = pattern + + def Cancel(self) -> bool: + """ + Call IUIAutomationSynchronizedInputPattern::Cancel. + Cause the Microsoft UI Automation provider to stop listening for mouse or keyboard input. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationsynchronizedinputpattern-cancel + """ + return self.pattern.Cancel() == S_OK + + def StartListening(self) -> bool: + """ + Call IUIAutomationSynchronizedInputPattern::StartListening. + Cause the Microsoft UI Automation provider to start listening for mouse or keyboard input. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationsynchronizedinputpattern-startlistening + """ + return self.pattern.StartListening() == S_OK + + +class TableItemPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationtableitempattern""" + self.pattern = pattern + + def GetColumnHeaderItems(self) -> List['Control']: + """ + Call IUIAutomationTableItemPattern::GetCurrentColumnHeaderItems. + Return List[Control], a list of `Control` subclasses, the column headers associated with a table item or cell. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtableitempattern-getcurrentcolumnheaderitems + """ + eleArray = self.pattern.GetCurrentColumnHeaderItems() + if eleArray: + controls = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + con = Control.CreateControlFromElement(element=ele) + if con: + controls.append(con) + return controls + return [] + + def GetRowHeaderItems(self) -> List['Control']: + """ + Call IUIAutomationTableItemPattern::GetCurrentRowHeaderItems. + Return List[Control], a list of `Control` subclasses, the row headers associated with a table item or cell. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtableitempattern-getcurrentrowheaderitems + """ + eleArray = self.pattern.GetCurrentRowHeaderItems() + if eleArray: + controls = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + con = Control.CreateControlFromElement(element=ele) + if con: + controls.append(con) + return controls + return [] + + +class TablePattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationtablepattern""" + self.pattern = pattern + + @property + def RowOrColumnMajor(self) -> int: + """ + Property RowOrColumnMajor. + Call IUIAutomationTablePattern::get_CurrentRowOrColumnMajor. + Return int, a value in class `RowOrColumnMajor`, the primary direction of traversal for the table. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtablepattern-get_currentroworcolumnmajor + """ + return self.pattern.CurrentRowOrColumnMajor + + def GetColumnHeaders(self) -> List['Control']: + """ + Call IUIAutomationTablePattern::GetCurrentColumnHeaders. + Return List[Control], a list of `Control` subclasses, representing all the column headers in a table.. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtablepattern-getcurrentcolumnheaders + """ + eleArray = self.pattern.GetCurrentColumnHeaders() + if eleArray: + controls = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + con = Control.CreateControlFromElement(element=ele) + if con: + controls.append(con) + return controls + return [] + + def GetRowHeaders(self) -> List['Control']: + """ + Call IUIAutomationTablePattern::GetCurrentRowHeaders. + Return List[Control], a list of `Control` subclasses, representing all the row headers in a table. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtablepattern-getcurrentrowheaders + """ + eleArray = self.pattern.GetCurrentRowHeaders() + if eleArray: + controls = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + con = Control.CreateControlFromElement(element=ele) + if con: + controls.append(con) + return controls + return [] + + +class TextRange(): + def __init__(self, textRange=None): + """ + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationtextrange + """ + self.textRange = textRange + + def AddToSelection(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTextRange::AddToSelection. + Add the text range to the collection of selected text ranges in a control that supports multiple, disjoint spans of selected text. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-addtoselection + """ + ret = self.textRange.AddToSelection() == S_OK + time.sleep(waitTime) + return ret + + def Clone(self) -> 'TextRange': + """ + Call IUIAutomationTextRange::Clone. + return `TextRange`, identical to the original and inheriting all properties of the original. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-clone + """ + return TextRange(textRange=self.textRange.Clone()) + + def Compare(self, textRange: 'TextRange') -> bool: + """ + Call IUIAutomationTextRange::Compare. + textRange: `TextRange`. + Return bool, specifies whether this text range has the same endpoints as another text range. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-compare + """ + return bool(self.textRange.Compare(textRange.textRange)) + + def CompareEndpoints(self, srcEndPoint: int, textRange: 'TextRange', targetEndPoint: int) -> int: + """ + Call IUIAutomationTextRange::CompareEndpoints. + srcEndPoint: int, a value in class `TextPatternRangeEndpoint`. + textRange: `TextRange`. + targetEndPoint: int, a value in class `TextPatternRangeEndpoint`. + Return int, a negative value if the caller's endpoint occurs earlier in the text than the target endpoint; + 0 if the caller's endpoint is at the same location as the target endpoint; + or a positive value if the caller's endpoint occurs later in the text than the target endpoint. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-compareendpoints + """ + return self.textRange.CompareEndpoints(srcEndPoint, textRange, targetEndPoint) + + def ExpandToEnclosingUnit(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTextRange::ExpandToEnclosingUnit. + Normalize the text range by the specified text unit. + The range is expanded if it is smaller than the specified unit, + or shortened if it is longer than the specified unit. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-expandtoenclosingunit + """ + ret = self.textRange.ExpandToEnclosingUnit() == S_OK + time.sleep(waitTime) + return ret + + def FindAttribute(self, textAttributeId: int, val, backward: bool) -> 'TextRange': + """ + Call IUIAutomationTextRange::FindAttribute. + textAttributeID: int, a value in class `TextAttributeId`. + val: COM VARIANT according to textAttributeId? todo. + backward: bool, True if the last occurring text range should be returned instead of the first; otherwise False. + return `TextRange` or None, a text range subset that has the specified text attribute value. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-findattribute + """ + textRange = self.textRange.FindAttribute(textAttributeId, val, int(backward)) + if textRange: + return TextRange(textRange=textRange) + + def FindText(self, text: str, backward: bool, ignoreCase: bool) -> 'TextRange': + """ + Call IUIAutomationTextRange::FindText. + text: str, + backward: bool, True if the last occurring text range should be returned instead of the first; otherwise False. + ignoreCase: bool, True if case should be ignored; otherwise False. + return `TextRange` or None, a text range subset that contains the specified text. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-findtext + """ + textRange = self.textRange.FindText(text, int(backward), int(ignoreCase)) + if textRange: + return TextRange(textRange=textRange) + + def GetAttributeValue(self, textAttributeId: int) -> ctypes.POINTER(comtypes.IUnknown): + """ + Call IUIAutomationTextRange::GetAttributeValue. + textAttributeId: int, a value in class `TextAttributeId`. + Return `ctypes.POINTER(comtypes.IUnknown)` or None, the value of the specified text attribute across the entire text range, todo. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-getattributevalue + """ + return self.textRange.GetAttributeValue(textAttributeId) + + def GetBoundingRectangles(self) -> List[Rect]: + """ + Call IUIAutomationTextRange::GetBoundingRectangles. + textAttributeId: int, a value in class `TextAttributeId`. + Return List[Rect], a list of `Rect`. + bounding rectangles for each fully or partially visible line of text in a text range.. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-getboundingrectangles + + for rect in textRange.GetBoundingRectangles(): + print(rect.left, rect.top, rect.right, rect.bottom, rect.width(), rect.height(), rect.xcenter(), rect.ycenter()) + """ + floats = self.textRange.GetBoundingRectangles() + rects = [] + for i in range(len(floats) // 4): + rect = Rect(int(floats[i * 4]), int(floats[i * 4 + 1]), + int(floats[i * 4]) + int(floats[i * 4 + 2]), int(floats[i * 4 + 1]) + int(floats[i * 4 + 3])) + rects.append(rect) + return rects + + def GetChildren(self) -> List['Control']: + """ + Call IUIAutomationTextRange::GetChildren. + textAttributeId: int, a value in class `TextAttributeId`. + Return List[Control], a list of `Control` subclasses, embedded objects that fall within the text range.. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-getchildren + """ + eleArray = self.textRange.GetChildren() + if eleArray: + controls = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + con = Control.CreateControlFromElement(element=ele) + if con: + controls.append(con) + return controls + return [] + + def GetEnclosingControl(self) -> 'Control': + """ + Call IUIAutomationTextRange::GetEnclosingElement. + Return `Control` subclass, the innermost UI Automation element that encloses the text range. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-getenclosingelement + """ + return Control.CreateControlFromElement(self.textRange.GetEnclosingElement()) + + def GetText(self, maxLength: int = -1) -> str: + """ + Call IUIAutomationTextRange::GetText. + maxLength: int, the maximum length of the string to return, or -1 if no limit is required. + Return str, the plain text of the text range. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-gettext + """ + return self.textRange.GetText(maxLength) + + def Move(self, unit: int, count: int, waitTime: float = OPERATION_WAIT_TIME) -> int: + """ + Call IUIAutomationTextRange::Move. + Move the text range forward or backward by the specified number of text units. + unit: int, a value in class `TextUnit`. + count: int, the number of text units to move. + A positive value moves the text range forward. + A negative value moves the text range backward. Zero has no effect. + waitTime: float. + Return: int, the number of text units actually moved. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-move + """ + ret = self.textRange.Move(unit, count) + time.sleep(waitTime) + return ret + + def MoveEndpointByRange(self, srcEndPoint: int, textRange: 'TextRange', targetEndPoint: int, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTextRange::MoveEndpointByRange. + Move one endpoint of the current text range to the specified endpoint of a second text range. + srcEndPoint: int, a value in class `TextPatternRangeEndpoint`. + textRange: `TextRange`. + targetEndPoint: int, a value in class `TextPatternRangeEndpoint`. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-moveendpointbyrange + """ + ret = self.textRange.MoveEndpointByRange(srcEndPoint, textRange.textRange, targetEndPoint) == S_OK + time.sleep(waitTime) + return ret + + def MoveEndpointByUnit(self, endPoint: int, unit: int, count: int, waitTime: float = OPERATION_WAIT_TIME) -> int: + """ + Call IUIAutomationTextRange::MoveEndpointByUnit. + Move one endpoint of the text range the specified number of text units within the document range. + endPoint: int, a value in class `TextPatternRangeEndpoint`. + unit: int, a value in class `TextUnit`. + count: int, the number of units to move. + A positive count moves the endpoint forward. + A negative count moves backward. + A count of 0 has no effect. + waitTime: float. + Return int, the count of units actually moved. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-moveendpointbyunit + """ + ret = self.textRange.MoveEndpointByUnit(endPoint, unit, count) + time.sleep(waitTime) + return ret + + def RemoveFromSelection(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTextRange::RemoveFromSelection. + Remove the text range from an existing collection of selected text in a text container that supports multiple, disjoint selections. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-removefromselection + """ + ret = self.textRange.RemoveFromSelection() == S_OK + time.sleep(waitTime) + return ret + + def ScrollIntoView(self, alignTop: bool = True, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTextRange::ScrollIntoView. + Cause the text control to scroll until the text range is visible in the viewport. + alignTop: bool, True if the text control should be scrolled so that the text range is flush with the top of the viewport; + False if it should be flush with the bottom of the viewport. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-scrollintoview + """ + ret = self.textRange.ScrollIntoView(int(alignTop)) == S_OK + time.sleep(waitTime) + return ret + + def Select(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTextRange::Select. + Select the span of text that corresponds to this text range, and remove any previous selection. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-select + """ + ret = self.textRange.Select() == S_OK + time.sleep(waitTime) + return ret + + +class TextChildPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationtextchildpattern""" + self.pattern = pattern + + @property + def TextContainer(self) -> 'Control': + """ + Property TextContainer. + Call IUIAutomationSelectionContainer::get_TextContainer. + Return `Control` subclass, the nearest ancestor element that supports the Text control pattern. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextchildpattern-get_textcontainer + """ + return Control.CreateControlFromElement(self.pattern.TextContainer) + + @property + def TextRange(self) -> TextRange: + """ + Property TextRange. + Call IUIAutomationSelectionContainer::get_TextRange. + Return `TextRange`, a text range that encloses this child element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextchildpattern-get_textrange + """ + return TextRange(self.pattern.TextRange) + + +class TextEditPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationtexteditpattern""" + self.pattern = pattern + + def GetActiveComposition(self) -> TextRange: + """ + Call IUIAutomationTextEditPattern::GetActiveComposition. + Return `TextRange` or None, the active composition. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtexteditpattern-getactivecomposition + """ + textRange = self.pattern.GetActiveComposition() + if textRange: + return TextRange(textRange=textRange) + + def GetConversionTarget(self) -> TextRange: + """ + Call IUIAutomationTextEditPattern::GetConversionTarget. + Return `TextRange` or None, the current conversion target range.. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtexteditpattern-getconversiontarget + """ + textRange = self.pattern.GetConversionTarget() + if textRange: + return TextRange(textRange=textRange) + + +class TextPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationtextpattern""" + self.pattern = pattern + + @property + def DocumentRange(self) -> TextRange: + """ + Property DocumentRange. + Call IUIAutomationTextPattern::get_DocumentRange. + Return `TextRange`, a text range that encloses the main text of a document. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextpattern-get_documentrange + """ + return TextRange(self.pattern.DocumentRange) + + @property + def SupportedTextSelection(self) -> bool: + """ + Property SupportedTextSelection. + Call IUIAutomationTextPattern::get_SupportedTextSelection. + Return bool, specifies the type of text selection that is supported by the control. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextpattern-get_supportedtextselection + """ + return bool(self.pattern.SupportedTextSelection) + + def GetSelection(self) -> List[TextRange]: + """ + Call IUIAutomationTextPattern::GetSelection. + Return List[TextRange], a list of `TextRange`, represents the currently selected text in a text-based control. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextpattern-getselection + """ + eleArray = self.pattern.GetSelection() + if eleArray: + textRanges = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + textRanges.append(TextRange(textRange=ele)) + return textRanges + return [] + + def GetVisibleRanges(self) -> List[TextRange]: + """ + Call IUIAutomationTextPattern::GetVisibleRanges. + Return List[TextRange], a list of `TextRange`, disjoint text ranges from a text-based control + where each text range represents a contiguous span of visible text. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextpattern-getvisibleranges + """ + eleArray = self.pattern.GetVisibleRanges() + if eleArray: + textRanges = [] + for i in range(eleArray.Length): + ele = eleArray.GetElement(i) + textRanges.append(TextRange(textRange=ele)) + return textRanges + return [] + + def RangeFromChild(self, child) -> TextRange: + """ + Call IUIAutomationTextPattern::RangeFromChild. + child: `Control` or its subclass. + Return `TextRange` or None, a text range enclosing a child element such as an image, + hyperlink, Microsoft Excel spreadsheet, or other embedded object. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextpattern-rangefromchild + """ + textRange = self.pattern.RangeFromChild(Control.Element) + if textRange: + return TextRange(textRange=textRange) + + def RangeFromPoint(self, x: int, y: int) -> TextRange: + """ + Call IUIAutomationTextPattern::RangeFromPoint. + child: `Control` or its subclass. + Return `TextRange` or None, the degenerate (empty) text range nearest to the specified screen coordinates. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextpattern-rangefrompoint + """ + textRange = self.pattern.RangeFromPoint(ctypes.wintypes.POINT(x, y)) + if textRange: + return TextRange(textRange=textRange) + + +class TextPattern2(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationtextpattern2""" + self.pattern = pattern + + +class TogglePattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationtogglepattern""" + self.pattern = pattern + + @property + def ToggleState(self) -> int: + """ + Property ToggleState. + Call IUIAutomationTogglePattern::get_CurrentToggleState. + Return int, a value in class `ToggleState`, the state of the control. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtogglepattern-get_currenttogglestate + """ + return self.pattern.CurrentToggleState + + def Toggle(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTogglePattern::Toggle. + Cycle through the toggle states of the control. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtogglepattern-toggle + """ + ret = self.pattern.Toggle() == S_OK + time.sleep(waitTime) + return ret + + +class TransformPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationtransformpattern""" + self.pattern = pattern + + @property + def CanMove(self) -> bool: + """ + Property CanMove. + Call IUIAutomationTransformPattern::get_CurrentCanMove. + Return bool, indicates whether the element can be moved. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern-get_currentcanmove + """ + return bool(self.pattern.CurrentCanMove) + + @property + def CanResize(self) -> bool: + """ + Property CanResize. + Call IUIAutomationTransformPattern::get_CurrentCanResize. + Return bool, indicates whether the element can be resized. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern-get_currentcanresize + """ + return bool(self.pattern.CurrentCanResize) + + @property + def CanRotate(self) -> bool: + """ + Property CanRotate. + Call IUIAutomationTransformPattern::get_CurrentCanRotate. + Return bool, indicates whether the element can be rotated. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern-get_currentcanrotate + """ + return bool(self.pattern.CurrentCanRotate) + + def Move(self, x: int, y: int, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTransformPattern::Move. + Move the UI Automation element. + x: int. + y: int. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern-move + """ + ret = self.pattern.Move(x, y) == S_OK + time.sleep(waitTime) + return ret + + def Resize(self, width: int, height: int, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTransformPattern::Resize. + Resize the UI Automation element. + width: int. + height: int. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern-resize + """ + ret = self.pattern.Resize(width, height) == S_OK + time.sleep(waitTime) + return ret + + def Rotate(self, degrees: int, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTransformPattern::Rotate. + Rotates the UI Automation element. + degrees: int. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern-rotate + """ + ret = self.pattern.Rotate(degrees) == S_OK + time.sleep(waitTime) + return ret + + +class TransformPattern2(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationtransformpattern2""" + self.pattern = pattern + + @property + def CanZoom(self) -> bool: + """ + Property CanZoom. + Call IUIAutomationTransformPattern2::get_CurrentCanZoom. + Return bool, indicates whether the control supports zooming of its viewport. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern2-get_CurrentCanZoom + """ + return bool(self.pattern.CurrentCanZoom) + + @property + def ZoomLevel(self) -> float: + """ + Property ZoomLevel. + Call IUIAutomationTransformPattern2::get_CurrentZoomLevel. + Return float, the zoom level of the control's viewport. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern2-get_currentzoomlevel + """ + return self.pattern.CurrentZoomLevel + + @property + def ZoomMaximum(self) -> float: + """ + Property ZoomMaximum. + Call IUIAutomationTransformPattern2::get_CurrentZoomMaximum. + Return float, the maximum zoom level of the control's viewport. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern2-get_currentzoommaximum + """ + return self.pattern.CurrentZoomMaximum + + @property + def ZoomMinimum(self) -> float: + """ + Property ZoomMinimum. + Call IUIAutomationTransformPattern2::get_CurrentZoomMinimum. + Return float, the minimum zoom level of the control's viewport. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern2-get_currentzoomminimum + """ + return self.pattern.CurrentZoomMinimum + + def Zoom(self, zoomLevel: float, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTransformPattern2::Zoom. + Zoom the viewport of the control. + zoomLevel: float for int. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern2-zoom + """ + ret = self.pattern.Zoom(zoomLevel) == S_OK + time.sleep(waitTime) + return ret + + def ZoomByUnit(self, zoomUnit: int, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTransformPattern2::ZoomByUnit. + Zoom the viewport of the control by the specified unit. + zoomUnit: int, a value in class `ZoomUnit`. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationtransformpattern2-zoombyunit + """ + ret = self.pattern.ZoomByUnit(zoomUnit) == S_OK + time.sleep(waitTime) + return ret + + +class ValuePattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationvaluepattern""" + self.pattern = pattern + + @property + def IsReadOnly(self) -> bool: + """ + Property IsReadOnly. + Call IUIAutomationTransformPattern2::IUIAutomationValuePattern::get_CurrentIsReadOnly. + Return bool, indicates whether the value of the element is read-only. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationvaluepattern-get_currentisreadonly + """ + return self.pattern.CurrentIsReadOnly + + @property + def Value(self) -> str: + """ + Property Value. + Call IUIAutomationTransformPattern2::IUIAutomationValuePattern::get_CurrentValue. + Return str, the value of the element. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationvaluepattern-get_currentvalue + """ + return self.pattern.CurrentValue + + def SetValue(self, value: str, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationTransformPattern2::IUIAutomationValuePattern::SetValue. + Set the value of the element. + value: str. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationvaluepattern-setvalue + """ + ret = self.pattern.SetValue(value) == S_OK + time.sleep(waitTime) + return ret + + +class VirtualizedItemPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationvirtualizeditempattern""" + self.pattern = pattern + + def Realize(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationVirtualizedItemPattern::Realize. + Create a full UI Automation element for a virtualized item. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationvirtualizeditempattern-realize + """ + ret = self.pattern.Realize() == S_OK + time.sleep(waitTime) + return ret + + +class WindowPattern(): + def __init__(self, pattern=None): + """Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationwindowpattern""" + self.pattern = pattern + + def Close(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationWindowPattern::Close. + Close the window. + waitTime: float. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationwindowpattern-close + """ + ret = self.pattern.Close() == S_OK + time.sleep(waitTime) + return ret + + @property + def CanMaximize(self) -> bool: + """ + Property CanMaximize. + Call IUIAutomationWindowPattern::get_CurrentCanMaximize. + Return bool, indicates whether the window can be maximized. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationwindowpattern-get_currentcanmaximize + """ + return bool(self.pattern.CurrentCanMaximize) + + @property + def CanMinimize(self) -> bool: + """ + Property CanMinimize. + Call IUIAutomationWindowPattern::get_CurrentCanMinimize. + Return bool, indicates whether the window can be minimized. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationwindowpattern-get_currentcanminimize + """ + return bool(self.pattern.CurrentCanMinimize) + + @property + def IsModal(self) -> bool: + """ + Property IsModal. + Call IUIAutomationWindowPattern::get_CurrentIsModal. + Return bool, indicates whether the window is modal. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationwindowpattern-get_currentismodal + """ + return bool(self.pattern.CurrentIsModal) + + @property + def IsTopmost(self) -> bool: + """ + Property IsTopmost. + Call IUIAutomationWindowPattern::get_CurrentIsTopmost. + Return bool, indicates whether the window is the topmost element in the z-order. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationwindowpattern-get_currentistopmost + """ + return bool(self.pattern.CurrentIsTopmost) + + @property + def WindowInteractionState(self) -> int: + """ + Property WindowInteractionState. + Call IUIAutomationWindowPattern::get_CurrentWindowInteractionState. + Return int, a value in class `WindowInteractionState`, + the current state of the window for the purposes of user interaction. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationwindowpattern-get_currentwindowinteractionstate + """ + return self.pattern.CurrentWindowInteractionState + + @property + def WindowVisualState(self) -> int: + """ + Property WindowVisualState. + Call IUIAutomationWindowPattern::get_CurrentWindowVisualState. + Return int, a value in class `WindowVisualState`, + the visual state of the window; that is, whether it is in the normal, maximized, or minimized state. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationwindowpattern-get_currentwindowvisualstate + """ + return self.pattern.CurrentWindowVisualState + + def SetWindowVisualState(self, state: int, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call IUIAutomationWindowPattern::SetWindowVisualState. + Minimize, maximize, or restore the window. + state: int, a value in class `WindowVisualState`. + waitTime: float. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationwindowpattern-setwindowvisualstate + """ + ret = self.pattern.SetWindowVisualState(state) == S_OK + time.sleep(waitTime) + return ret + + def WaitForInputIdle(self, milliseconds: int) -> bool: + ''' + Call IUIAutomationWindowPattern::WaitForInputIdle. + Cause the calling code to block for the specified time or + until the associated process enters an idle state, whichever completes first. + milliseconds: int. + Return bool, True if succeed otherwise False. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationwindowpattern-waitforinputidle + ''' + return self.pattern.WaitForInputIdle(milliseconds) == S_OK + + +PatternConstructors = { + PatternId.AnnotationPattern: AnnotationPattern, + PatternId.CustomNavigationPattern: CustomNavigationPattern, + PatternId.DockPattern: DockPattern, + PatternId.DragPattern: DragPattern, + PatternId.DropTargetPattern: DropTargetPattern, + PatternId.ExpandCollapsePattern: ExpandCollapsePattern, + PatternId.GridItemPattern: GridItemPattern, + PatternId.GridPattern: GridPattern, + PatternId.InvokePattern: InvokePattern, + PatternId.ItemContainerPattern: ItemContainerPattern, + PatternId.LegacyIAccessiblePattern: LegacyIAccessiblePattern, + PatternId.MultipleViewPattern: MultipleViewPattern, + PatternId.ObjectModelPattern: ObjectModelPattern, + PatternId.RangeValuePattern: RangeValuePattern, + PatternId.ScrollItemPattern: ScrollItemPattern, + PatternId.ScrollPattern: ScrollPattern, + PatternId.SelectionItemPattern: SelectionItemPattern, + PatternId.SelectionPattern: SelectionPattern, + PatternId.SpreadsheetItemPattern: SpreadsheetItemPattern, + PatternId.SpreadsheetPattern: SpreadsheetPattern, + PatternId.StylesPattern: StylesPattern, + PatternId.SynchronizedInputPattern: SynchronizedInputPattern, + PatternId.TableItemPattern: TableItemPattern, + PatternId.TablePattern: TablePattern, + PatternId.TextChildPattern: TextChildPattern, + PatternId.TextEditPattern: TextEditPattern, + PatternId.TextPattern: TextPattern, + PatternId.TextPattern2: TextPattern2, + PatternId.TogglePattern: TogglePattern, + PatternId.TransformPattern: TransformPattern, + PatternId.TransformPattern2: TransformPattern2, + PatternId.ValuePattern: ValuePattern, + PatternId.VirtualizedItemPattern: VirtualizedItemPattern, + PatternId.WindowPattern: WindowPattern, +} + + +def CreatePattern(patternId: int, pattern: ctypes.POINTER(comtypes.IUnknown)): + """Create a concreate pattern by pattern id and pattern(POINTER(IUnknown)).""" + subPattern = pattern.QueryInterface(GetPatternIdInterface(patternId)) + if subPattern: + return PatternConstructors[patternId](pattern=subPattern) + + +class Control(): + ValidKeys = set(['ControlType', 'ClassName', 'AutomationId', 'Name', 'SubName', 'RegexName', 'Depth', 'Compare']) + def __init__(self, searchFromControl: 'Control' = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + """ + searchFromControl: `Control` or its subclass, if it is None, search from root control(Desktop). + searchDepth: int, max search depth from searchFromControl. + foundIndex: int, starts with 1, >= 1. + searchInterval: float, wait searchInterval after every search in self.Refind and self.Exists, the global timeout is TIME_OUT_SECOND. + element: `ctypes.POINTER(IUIAutomationElement)`, internal use only. + searchProperties: defines how to search, the following keys can be used: + ControlType: int, a value in class `ControlType`. + ClassName: str. + AutomationId: str. + Name: str. + SubName: str, a part str in Name. + RegexName: str, supports regex using re.match. + You can only use one of Name, SubName, RegexName in searchProperties. + Depth: int, only search controls in relative depth from searchFromControl, ignore controls in depth(0~Depth-1), + if set, searchDepth will be set to Depth too. + Compare: Callable[[Control, int], bool], custom compare function(control: Control, depth: int) -> bool. + + `Control` wraps IUIAutomationElement. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nn-uiautomationclient-iuiautomationelement + """ + self._element = element + self._elementDirectAssign = True if element else False + self.searchFromControl = searchFromControl + self.searchDepth = searchProperties.get('Depth', searchDepth) + self.searchInterval = searchInterval + self.foundIndex = foundIndex + self.searchProperties = searchProperties + regName = searchProperties.get('RegexName', '') + self.regexName = re.compile(regName) if regName else None + self._supportedPatterns = {} + + def __str__(self) -> str: + rect = self.BoundingRectangle + return 'ControlType: {0} ClassName: {1} AutomationId: {2} Rect: {3} Name: {4} Handle: 0x{5:X}({5})'.format( + self.ControlTypeName, self.ClassName, self.AutomationId, rect, self.Name, self.NativeWindowHandle) + + @property + def runtimeid(self): + return ''.join([str(i) for i in self.GetRuntimeId()]) + + @staticmethod + def CreateControlFromElement(element) -> 'Control': + """ + Create a concreate `Control` from a com type `IUIAutomationElement`. + element: `ctypes.POINTER(IUIAutomationElement)`. + Return a subclass of `Control`, an instance of the control's real type. + """ + if element: + controlType = element.CurrentControlType + if controlType in ControlConstructors: + return ControlConstructors[controlType](element=element) + else: + Logger.WriteLine("element.CurrentControlType returns {}, invalid ControlType!".format(controlType), ConsoleColor.Red) #rarely happens + + @staticmethod + def CreateControlFromControl(control: 'Control') -> 'Control': + """ + Create a concreate `Control` from a control instance, copy it. + control: `Control` or its subclass. + Return a subclass of `Control`, an instance of the control's real type. + For example: if control's ControlType is EditControl, return an EditControl. + """ + newControl = Control.CreateControlFromElement(control.Element) + return newControl + + def SetSearchFromControl(self, searchFromControl: 'Control') -> None: + """searchFromControl: `Control` or its subclass""" + self.searchFromControl = searchFromControl + + def SetSearchDepth(self, searchDepth: int) -> None: + self.searchDepth = searchDepth + + def AddSearchProperties(self, **searchProperties) -> None: + """ + Add search properties using `dict.update`. + searchProperties: dict, same as searchProperties in `Control.__init__`. + """ + self.searchProperties.update(searchProperties) + if 'Depth' in searchProperties: + self.searchDepth = searchProperties['Depth'] + if 'RegexName' in searchProperties: + regName = searchProperties['RegexName'] + self.regexName = re.compile(regName) if regName else None + + def RemoveSearchProperties(self, **searchProperties) -> None: + """ + searchProperties: dict, same as searchProperties in `Control.__init__`. + """ + for key in searchProperties: + del self.searchProperties[key] + if key == 'RegexName': + self.regexName = None + + def GetSearchPropertiesStr(self) -> str: + strs = ['{}: {}'.format(k, ControlTypeNames[v] if k == 'ControlType' else repr(v)) for k, v in self.searchProperties.items()] + return '{' + ', '.join(strs) + '}' + + def GetColorfulSearchPropertiesStr(self, keyColor='DarkGreen', valueColor='DarkCyan') -> str: + """keyColor, valueColor: str, color name in class ConsoleColor""" + strs = ['{}: {}'.format(keyColor if k in Control.ValidKeys else 'DarkYellow', k, valueColor, + ControlTypeNames[v] if k == 'ControlType' else repr(v)) for k, v in self.searchProperties.items()] + return '{' + ', '.join(strs) + '}' + + #BuildUpdatedCache + #CachedAcceleratorKey + #CachedAccessKey + #CachedAriaProperties + #CachedAriaRole + #CachedAutomationId + #CachedBoundingRectangle + #CachedClassName + #CachedControlType + #CachedControllerFor + #CachedCulture + #CachedDescribedBy + #CachedFlowsTo + #CachedFrameworkId + #CachedHasKeyboardFocus + #CachedHelpText + #CachedIsContentElement + #CachedIsControlElement + #CachedIsDataValidForForm + #CachedIsEnabled + #CachedIsKeyboardFocusable + #CachedIsOffscreen + #CachedIsPassword + #CachedIsRequiredForForm + #CachedItemStatus + #CachedItemType + #CachedLabeledBy + #CachedLocalizedControlType + #CachedName + #CachedNativeWindowHandle + #CachedOrientation + #CachedProcessId + #CachedProviderDescription + + @property + def AcceleratorKey(self) -> str: + """ + Property AcceleratorKey. + Call IUIAutomationElement::get_CurrentAcceleratorKey. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentacceleratorkey + """ + return self.Element.CurrentAcceleratorKey + + @property + def AccessKey(self) -> str: + """ + Property AccessKey. + Call IUIAutomationElement::get_CurrentAccessKey. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentaccesskey + """ + return self.Element.CurrentAccessKey + + @property + def AriaProperties(self) -> str: + """ + Property AriaProperties. + Call IUIAutomationElement::get_CurrentAriaProperties. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentariaproperties + """ + return self.Element.CurrentAriaProperties + + @property + def AriaRole(self) -> str: + """ + Property AriaRole. + Call IUIAutomationElement::get_CurrentAriaRole. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentariarole + """ + return self.Element.CurrentAriaRole + + @property + def AutomationId(self) -> str: + """ + Property AutomationId. + Call IUIAutomationElement::get_CurrentAutomationId. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentautomationid + """ + return self.Element.CurrentAutomationId + + @property + def BoundingRectangle(self) -> Rect: + """ + Property BoundingRectangle. + Call IUIAutomationElement::get_CurrentBoundingRectangle. + Return `Rect`. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentboundingrectangle + + rect = control.BoundingRectangle + print(rect.left, rect.top, rect.right, rect.bottom, rect.width(), rect.height(), rect.xcenter(), rect.ycenter()) + """ + rect = self.Element.CurrentBoundingRectangle + return Rect(rect.left, rect.top, rect.right, rect.bottom) + + @property + def ClassName(self) -> str: + """ + Property ClassName. + Call IUIAutomationElement::get_CurrentClassName. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentclassname + """ + return self.Element.CurrentClassName + + @property + def ControlType(self) -> int: + """ + Property ControlType. + Return int, a value in class `ControlType`. + Call IUIAutomationElement::get_CurrentControlType. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentcontroltype + """ + return self.Element.CurrentControlType + + #@property + #def ControllerFor(self): + #return self.Element.CurrentControllerFor + + @property + def Culture(self) -> int: + """ + Property Culture. + Call IUIAutomationElement::get_CurrentCulture. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentculture + """ + return self.Element.CurrentCulture + + #@property + #def DescribedBy(self): + #return self.Element.CurrentDescribedBy + + #@property + #def FlowsTo(self): + #return self.Element.CurrentFlowsTo + + @property + def FrameworkId(self) -> str: + """ + Property FrameworkId. + Call IUIAutomationElement::get_CurrentFrameworkId. + Return str, such as Win32, WPF... + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentframeworkid + """ + return self.Element.CurrentFrameworkId + + @property + def HasKeyboardFocus(self) -> bool: + """ + Property HasKeyboardFocus. + Call IUIAutomationElement::get_CurrentHasKeyboardFocus. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currenthaskeyboardfocus + """ + return bool(self.Element.CurrentHasKeyboardFocus) + + @property + def HelpText(self) -> str: + """ + Property HelpText. + Call IUIAutomationElement::get_CurrentHelpText. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currenthelptext + """ + return self.Element.CurrentHelpText + + @property + def IsContentElement(self) -> bool: + """ + Property IsContentElement. + Call IUIAutomationElement::get_CurrentIsContentElement. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentiscontentelement + """ + return bool(self.Element.CurrentIsContentElement) + + @property + def IsControlElement(self) -> bool: + """ + Property IsControlElement. + Call IUIAutomationElement::get_CurrentIsControlElement. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentiscontrolelement + """ + return bool(self.Element.CurrentIsControlElement) + + @property + def IsDataValidForForm(self) -> bool: + """ + Property IsDataValidForForm. + Call IUIAutomationElement::get_CurrentIsDataValidForForm. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentisdatavalidforform + """ + return bool(self.Element.CurrentIsDataValidForForm) + + @property + def IsEnabled(self) -> bool: + """ + Property IsEnabled. + Call IUIAutomationElement::get_CurrentIsEnabled. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentisenabled + """ + return self.Element.CurrentIsEnabled + + @property + def IsKeyboardFocusable(self) -> bool: + """ + Property IsKeyboardFocusable. + Call IUIAutomationElement::get_CurrentIsKeyboardFocusable. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentiskeyboardfocusable + """ + return self.Element.CurrentIsKeyboardFocusable + + @property + def IsOffscreen(self) -> bool: + """ + Property IsOffscreen. + Call IUIAutomationElement::get_CurrentIsOffscreen. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentisoffscreen + """ + return self.Element.CurrentIsOffscreen + + @property + def IsPassword(self) -> bool: + """ + Property IsPassword. + Call IUIAutomationElement::get_CurrentIsPassword. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentispassword + """ + return self.Element.CurrentIsPassword + + @property + def IsRequiredForForm(self) -> bool: + """ + Property IsRequiredForForm. + Call IUIAutomationElement::get_CurrentIsRequiredForForm. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentisrequiredforform + """ + return self.Element.CurrentIsRequiredForForm + + @property + def ItemStatus(self) -> str: + """ + Property ItemStatus. + Call IUIAutomationElement::get_CurrentItemStatus. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentitemstatus + """ + return self.Element.CurrentItemStatus + + @property + def ItemType(self) -> str: + """ + Property ItemType. + Call IUIAutomationElement::get_CurrentItemType. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentitemtype + """ + return self.Element.CurrentItemType + + #@property + #def LabeledBy(self): + #return self.Element.CurrentLabeledBy + + @property + def LocalizedControlType(self) -> str: + """ + Property LocalizedControlType. + Call IUIAutomationElement::get_CurrentLocalizedControlType. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentlocalizedcontroltype + """ + return self.Element.CurrentLocalizedControlType + + @property + def Name(self) -> str: + """ + Property Name. + Call IUIAutomationElement::get_CurrentName. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentname + """ + return self.Element.CurrentName or '' # CurrentName may be None + + @property + def NativeWindowHandle(self) -> str: + """ + Property NativeWindowHandle. + Call IUIAutomationElement::get_CurrentNativeWindowHandle. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentnativewindowhandle + """ + handle = self.Element.CurrentNativeWindowHandle + return 0 if handle is None else handle + + @property + def Orientation(self) -> int: + """ + Property Orientation. + Return int, a value in class `OrientationType`. + Call IUIAutomationElement::get_CurrentOrientation. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentorientation + """ + return self.Element.CurrentOrientation + + @property + def ProcessId(self) -> int: + """ + Property ProcessId. + Call IUIAutomationElement::get_CurrentProcessId. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentprocessid + """ + return self.Element.CurrentProcessId + + @property + def ProviderDescription(self) -> str: + """ + Property ProviderDescription. + Call IUIAutomationElement::get_CurrentProviderDescription. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-get_currentproviderdescription + """ + return self.Element.CurrentProviderDescription + + #FindAll + #FindAllBuildCache + #FindFirst + #FindFirstBuildCache + #GetCachedChildren + #GetCachedParent + #GetCachedPattern + #GetCachedPatternAs + #GetCachedPropertyValue + #GetCachedPropertyValueEx + + def GetClickablePoint(self) -> Tuple[int, int, bool]: + """ + Call IUIAutomationElement::GetClickablePoint. + Return Tuple[int, int, bool], three items tuple (x, y, gotClickable), such as (20, 10, True) + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-getclickablepoint + """ + point, gotClickable = self.Element.GetClickablePoint() + return (point.x, point.y, bool(gotClickable)) + + def GetPattern(self, patternId: int): + """ + Call IUIAutomationElement::GetCurrentPattern. + Get a new pattern by pattern id if it supports the pattern. + patternId: int, a value in class `PatternId`. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-getcurrentpattern + """ + try: + pattern = self.Element.GetCurrentPattern(patternId) + if pattern: + subPattern = CreatePattern(patternId, pattern) + self._supportedPatterns[patternId] = subPattern + return subPattern + except comtypes.COMError as ex: + pass + + def GetPatternAs(self, patternId: int, riid): + """ + Call IUIAutomationElement::GetCurrentPatternAs. + Get a new pattern by pattern id if it supports the pattern, todo. + patternId: int, a value in class `PatternId`. + riid: GUID. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-getcurrentpatternas + """ + return self.Element.GetCurrentPatternAs(patternId, riid) + + def GetPropertyValue(self, propertyId: int) -> Any: + """ + Call IUIAutomationElement::GetCurrentPropertyValue. + propertyId: int, a value in class `PropertyId`. + Return Any, corresponding type according to propertyId. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-getcurrentpropertyvalue + """ + return self.Element.GetCurrentPropertyValue(propertyId) + + def GetPropertyValueEx(self, propertyId: int, ignoreDefaultValue: int) -> Any: + """ + Call IUIAutomationElement::GetCurrentPropertyValueEx. + propertyId: int, a value in class `PropertyId`. + ignoreDefaultValue: int, 0 or 1. + Return Any, corresponding type according to propertyId. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-getcurrentpropertyvalueex + """ + return self.Element.GetCurrentPropertyValueEx(propertyId, ignoreDefaultValue) + + def GetRuntimeId(self) -> List[int]: + """ + Call IUIAutomationElement::GetRuntimeId. + Return List[int], a list of int. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-getruntimeid + """ + return self.Element.GetRuntimeId() + + #QueryInterface + #Release + + def SetFocus(self) -> bool: + """ + Call IUIAutomationElement::SetFocus. + Refer https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-setfocus + """ + try: + return self.Element.SetFocus() == S_OK + except comtypes.COMError as ex: + return False + + @property + def Element(self): + """ + Property Element. + Return `ctypes.POINTER(IUIAutomationElement)`. + """ + if not self._element: + self.Refind(maxSearchSeconds=TIME_OUT_SECOND, searchIntervalSeconds=self.searchInterval) + return self._element + + @property + def ControlTypeName(self) -> str: + """ + Property ControlTypeName. + """ + return ControlTypeNames[self.ControlType] + + def GetCachedPattern(self, patternId: int, cache: bool): + """ + Get a pattern by patternId. + patternId: int, a value in class `PatternId`. + Return a pattern if it supports the pattern else None. + cache: bool, if True, store the pattern for later use, if False, get a new pattern by `self.GetPattern`. + """ + if cache: + pattern = self._supportedPatterns.get(patternId, None) + if pattern: + return pattern + else: + pattern = self.GetPattern(patternId) + if pattern: + self._supportedPatterns[patternId] = pattern + return pattern + else: + pattern = self.GetPattern(patternId) + if pattern: + self._supportedPatterns[patternId] = pattern + return pattern + + def GetLegacyIAccessiblePattern(self) -> LegacyIAccessiblePattern: + """ + Return `LegacyIAccessiblePattern` if it supports the pattern else None. + """ + return self.GetPattern(PatternId.LegacyIAccessiblePattern) + + def GetAncestorControl(self, condition: Callable[['Control', int], bool]) -> 'Control': + """ + Get an ancestor control that matches the condition. + condition: Callable[[Control, int], bool], function(control: Control, depth: int) -> bool, + depth starts with -1 and decreses when search goes up. + Return `Control` subclass or None. + """ + ancestor = self + depth = 0 + while True: + ancestor = ancestor.GetParentControl() + depth -= 1 + if ancestor: + if condition(ancestor, depth): + return ancestor + else: + break + + def GetParentControl(self) -> 'Control': + """ + Return `Control` subclass or None. + """ + ele = _AutomationClient.instance().ViewWalker.GetParentElement(self.Element) + return Control.CreateControlFromElement(ele) + + def GetFirstChildControl(self) -> 'Control': + """ + Return `Control` subclass or None. + """ + ele = _AutomationClient.instance().ViewWalker.GetFirstChildElement(self.Element) + return Control.CreateControlFromElement(ele) + + def GetLastChildControl(self) -> 'Control': + """ + Return `Control` subclass or None. + """ + ele = _AutomationClient.instance().ViewWalker.GetLastChildElement(self.Element) + return Control.CreateControlFromElement(ele) + + def GetNextSiblingControl(self) -> 'Control': + """ + Return `Control` subclass or None. + """ + ele = _AutomationClient.instance().ViewWalker.GetNextSiblingElement(self.Element) + return Control.CreateControlFromElement(ele) + + def GetPreviousSiblingControl(self) -> 'Control': + """ + Return `Control` subclass or None. + """ + ele = _AutomationClient.instance().ViewWalker.GetPreviousSiblingElement(self.Element) + return Control.CreateControlFromElement(ele) + + def GetSiblingControl(self, condition: Callable[['Control'], bool], forward: bool = True) -> 'Control': + """ + Get a sibling control that matches the condition. + forward: bool, if True, only search next siblings, if False, search pervious siblings first, then search next siblings. + condition: Callable[[Control], bool], function(control: Control) -> bool. + Return `Control` subclass or None. + """ + if not forward: + prev = self + while True: + prev = prev.GetPreviousSiblingControl() + if prev: + if condition(prev): + return prev + else: + break + next_ = self + while True: + next_ = next_.GetNextSiblingControl() + if next_: + if condition(next_): + return next_ + else: + break + + def GetChildControl(self, index: int, control_type: str = None) -> 'Control': + """ + Get the nth child control. + index: int, starts with 0. + control_type: `Control` or its subclass, if not None, only return the nth control that matches the control_type. + Return `Control` subclass or None. + """ + children = self.GetChildren() + if control_type: + children = [child for child in children if child.ControlTypeName == control_type] + if index < len(children): + return children[index] + else: + return None + + def GetAllProgeny(self) -> List[List['Control']]: + """ + Get all progeny controls. + Return List[List[Control]], a list of list of `Control` subclasses. + """ + all_elements = [] + + def find_all_elements(element, depth=0): + children = element.GetChildren() + if depth == len(all_elements): + all_elements.append([]) + all_elements[depth].append(element) + for child in children: + find_all_elements(child, depth+1) + return all_elements + + return find_all_elements(self) + + def GetProgenyControl(self, depth: int=1, index: int=0, control_type: str = None) -> 'Control': + """ + Get the nth control in the mth depth. + depth: int, starts with 0. + index: int, starts with 0. + control_type: `Control` or its subclass, if not None, only return the nth control that matches the control_type. + Return `Control` subclass or None. + """ + progeny = self.GetAllProgeny() + try: + controls = progeny[depth] + if control_type: + controls = [child for child in controls if child.ControlTypeName == control_type] + if index < len(progeny): + return controls[index] + except IndexError: + return + + def GetChildren(self) -> List['Control']: + """ + Return List[Control], a list of `Control` subclasses. + """ + children = [] + child = self.GetFirstChildControl() + while child: + children.append(child) + child = child.GetNextSiblingControl() + return children + + def _CompareFunction(self, control: 'Control', depth: int) -> bool: + """ + Define how to search. + control: `Control` or its subclass. + depth: int, tree depth from searchFromControl. + Return bool. + """ + for key, value in self.searchProperties.items(): + if 'ControlType' == key: + if value != control.ControlType: + return False + elif 'ClassName' == key: + if value != control.ClassName: + return False + elif 'AutomationId' == key: + if value != control.AutomationId: + return False + elif 'Depth' == key: + if value != depth: + return False + elif 'Name' == key: + if value != control.Name: + return False + elif 'SubName' == key: + if value not in control.Name: + return False + elif 'RegexName' == key: + if not self.regexName.match(control.Name): + return False + elif 'Compare' == key: + if not value(control, depth): + return False + return True + + def Exists(self, maxSearchSeconds: float = 5, searchIntervalSeconds: float = SEARCH_INTERVAL, printIfNotExist: bool = False) -> bool: + """ + maxSearchSeconds: float + searchIntervalSeconds: float + Find control every searchIntervalSeconds seconds in maxSearchSeconds seconds. + Return bool, True if find + """ + if self._element and self._elementDirectAssign: + #if element is directly assigned, not by searching, just check whether self._element is valid + #but I can't find an API in UIAutomation that can directly check + rootElement = GetRootControl().Element + if self._element == rootElement: + return True + else: + parentElement = _AutomationClient.instance().ViewWalker.GetParentElement(self._element) + if parentElement: + return True + else: + return False + #find the element + if len(self.searchProperties) == 0: + raise LookupError("control's searchProperties must not be empty!") + self._element = None + startTime = ProcessTime() + # Use same timeout(s) parameters for resolve all parents + prev = self.searchFromControl + if prev and not prev._element and not prev.Exists(maxSearchSeconds, searchIntervalSeconds): + if printIfNotExist or DEBUG_EXIST_DISAPPEAR: + Logger.ColorfullyLog(self.GetColorfulSearchPropertiesStr() + ' does not exist.') + return False + startTime2 = ProcessTime() + if DEBUG_SEARCH_TIME: + startDateTime = datetime.datetime.now() + while True: + control = FindControl(self.searchFromControl, self._CompareFunction, self.searchDepth, False, self.foundIndex) + if control: + self._element = control.Element + control._element = 0 # control will be destroyed, but the element needs to be stroed in self._element + if DEBUG_SEARCH_TIME: + Logger.ColorfullyLog('{} TraverseControls: {}, SearchTime: {:.3f}s[{} - {}]'.format( + self.GetColorfulSearchPropertiesStr(), control.traverseCount, ProcessTime() - startTime2, + startDateTime.time(), datetime.datetime.now().time())) + return True + else: + remain = startTime + maxSearchSeconds - ProcessTime() + if remain > 0: + time.sleep(min(remain, searchIntervalSeconds)) + else: + if printIfNotExist or DEBUG_EXIST_DISAPPEAR: + Logger.ColorfullyLog(self.GetColorfulSearchPropertiesStr() + ' does not exist.') + return False + + def Disappears(self, maxSearchSeconds: float = 5, searchIntervalSeconds: float = SEARCH_INTERVAL, printIfNotDisappear: bool = False) -> bool: + """ + maxSearchSeconds: float + searchIntervalSeconds: float + Check if control disappears every searchIntervalSeconds seconds in maxSearchSeconds seconds. + Return bool, True if control disappears. + """ + global DEBUG_EXIST_DISAPPEAR + start = ProcessTime() + while True: + temp = DEBUG_EXIST_DISAPPEAR + DEBUG_EXIST_DISAPPEAR = False # do not print for Exists + if not self.Exists(0, 0, False): + DEBUG_EXIST_DISAPPEAR = temp + return True + DEBUG_EXIST_DISAPPEAR = temp + remain = start + maxSearchSeconds - ProcessTime() + if remain > 0: + time.sleep(min(remain, searchIntervalSeconds)) + else: + if printIfNotDisappear or DEBUG_EXIST_DISAPPEAR: + Logger.ColorfullyLog(self.GetColorfulSearchPropertiesStr() + ' does not disappear.') + return False + + def Refind(self, maxSearchSeconds: float = TIME_OUT_SECOND, searchIntervalSeconds: float = SEARCH_INTERVAL, raiseException: bool = True) -> bool: + """ + Refind the control every searchIntervalSeconds seconds in maxSearchSeconds seconds. + maxSearchSeconds: float. + searchIntervalSeconds: float. + raiseException: bool, if True, raise a LookupError if timeout. + Return bool, True if find. + """ + if not self.Exists(maxSearchSeconds, searchIntervalSeconds, False if raiseException else DEBUG_EXIST_DISAPPEAR): + if raiseException: + # Logger.ColorfullyLog('Find Control Timeout: ' + self.GetColorfulSearchPropertiesStr()) + raise LookupError('Find Control Timeout: ' + self.GetSearchPropertiesStr()) + else: + return False + return True + + def MoveCursorToInnerPos(self, x: int = None, y: int = None, ratioX: float = 0.5, ratioY: float = 0.5, simulateMove: bool = False) -> Tuple[int, int]: + """ + Move cursor to control's internal position, default to center. + x: int, if < 0, move to self.BoundingRectangle.right + x, if not None, ignore ratioX. + y: int, if < 0, move to self.BoundingRectangle.bottom + y, if not None, ignore ratioY. + ratioX: float. + ratioY: float. + simulateMove: bool. + Return Tuple[int, int], two ints tuple (x, y), the cursor positon relative to screen(0, 0) + after moving or None if control's width or height is 0. + """ + rect = self.BoundingRectangle + if rect.width() == 0 or rect.height() == 0: + Logger.ColorfullyLog('Can not move cursor. {}\'s BoundingRectangle is {}. SearchProperties: {}'.format( + self.ControlTypeName, rect, self.GetColorfulSearchPropertiesStr())) + return + if x is None: + x = rect.left + int(rect.width() * ratioX) + else: + x = (rect.left if x >= 0 else rect.right) + x + if y is None: + y = rect.top + int(rect.height() * ratioY) + else: + y = (rect.top if y >= 0 else rect.bottom) + y + if simulateMove and MAX_MOVE_SECOND > 0: + MoveTo(x, y, waitTime=0) + else: + SetCursorPos(x, y) + return x, y + + def MoveCursorToMyCenter(self, simulateMove: bool = False) -> Tuple[int, int]: + """ + Move cursor to control's center. + Return Tuple[int, int], two ints tuple (x, y), the cursor positon relative to screen(0, 0) after moving. + """ + return self.MoveCursorToInnerPos(simulateMove=simulateMove) + + def Click(self, x: int = None, y: int = None, ratioX: float = 0.5, ratioY: float = 0.5, simulateMove: bool = False, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + x: int, if < 0, click self.BoundingRectangle.right + x, if not None, ignore ratioX. + y: int, if < 0, click self.BoundingRectangle.bottom + y, if not None, ignore ratioY. + ratioX: float. + ratioY: float. + simulateMove: bool, if True, first move cursor to control smoothly. + waitTime: float. + + Click(), Click(ratioX=0.5, ratioY=0.5): click center. + Click(10, 10): click left+10, top+10. + Click(-10, -10): click right-10, bottom-10. + """ + point = self.MoveCursorToInnerPos(x, y, ratioX, ratioY, simulateMove) + if point: + Click(point[0], point[1], waitTime) + + def MiddleClick(self, x: int = None, y: int = None, ratioX: float = 0.5, ratioY: float = 0.5, simulateMove: bool = False, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + x: int, if < 0, middle click self.BoundingRectangle.right + x, if not None, ignore ratioX. + y: int, if < 0, middle click self.BoundingRectangle.bottom + y, if not None, ignore ratioY. + ratioX: float. + ratioY: float. + simulateMove: bool, if True, first move cursor to control smoothly. + waitTime: float. + + MiddleClick(), MiddleClick(ratioX=0.5, ratioY=0.5): middle click center. + MiddleClick(10, 10): middle click left+10, top+10. + MiddleClick(-10, -10): middle click right-10, bottom-10. + """ + point = self.MoveCursorToInnerPos(x, y, ratioX, ratioY, simulateMove) + if point: + MiddleClick(point[0], point[1], waitTime) + + def RightClick(self, x: int = None, y: int = None, ratioX: float = 0.5, ratioY: float = 0.5, simulateMove: bool = False, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + x: int, if < 0, right click self.BoundingRectangle.right + x, if not None, ignore ratioX. + y: int, if < 0, right click self.BoundingRectangle.bottom + y, if not None, ignore ratioY. + ratioX: float. + ratioY: float. + simulateMove: bool, if True, first move cursor to control smoothly. + waitTime: float. + + RightClick(), RightClick(ratioX=0.5, ratioY=0.5): right click center. + RightClick(10, 10): right click left+10, top+10. + RightClick(-10, -10): right click right-10, bottom-10. + """ + point = self.MoveCursorToInnerPos(x, y, ratioX, ratioY, simulateMove) + if point: + RightClick(point[0], point[1], waitTime) + + def DoubleClick(self, x: int = None, y: int = None, ratioX: float = 0.5, ratioY: float = 0.5, simulateMove: bool = False, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + x: int, if < 0, right click self.BoundingRectangle.right + x, if not None, ignore ratioX. + y: int, if < 0, right click self.BoundingRectangle.bottom + y, if not None, ignore ratioY. + ratioX: float. + ratioY: float. + simulateMove: bool, if True, first move cursor to control smoothly. + waitTime: float. + + DoubleClick(), DoubleClick(ratioX=0.5, ratioY=0.5): double click center. + DoubleClick(10, 10): double click left+10, top+10. + DoubleClick(-10, -10): double click right-10, bottom-10. + """ + x, y = self.MoveCursorToInnerPos(x, y, ratioX, ratioY, simulateMove) + Click(x, y, GetDoubleClickTime() * 1.0 / 2000) + Click(x, y, waitTime) + + def DragDrop(self, x1: int, y1: int, x2: int, y2: int, moveSpeed: float=1, waitTime: float = OPERATION_WAIT_TIME) -> None: + rect = self.BoundingRectangle + if rect.width() == 0 or rect.height() == 0: + Logger.ColorfullyLog('Can not move cursor. {}\'s BoundingRectangle is {}. SearchProperties: {}'.format( + self.ControlTypeName, rect, self.GetColorfulSearchPropertiesStr())) + return + x1 = (rect.left if x1 >= 0 else rect.right) + x1 + y1 = (rect.top if y1 >= 0 else rect.bottom) + y1 + x2 = (rect.left if x2 >= 0 else rect.right) + x2 + y2 = (rect.top if y2 >= 0 else rect.bottom) + y2 + DragDrop(x1, y1, x2, y2, moveSpeed, waitTime) + + def WheelDown(self, x: int = None, y: int = None, ratioX: float = 0.5, ratioY: float = 0.5, wheelTimes: int = 1, interval: float = 0.05, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Make control have focus first, move cursor to the specified position and mouse wheel down. + x: int, if < 0, move x cursor to self.BoundingRectangle.right + x, if not None, ignore ratioX. + y: int, if < 0, move y cursor to self.BoundingRectangle.bottom + y, if not None, ignore ratioY. + ratioX: float. + ratioY: float. + wheelTimes: int. + interval: float. + waitTime: float. + """ + cursorX, cursorY = GetCursorPos() + self.SetFocus() + self.MoveCursorToInnerPos(x, y, ratioX, ratioY, simulateMove=False) + WheelDown(wheelTimes, interval, waitTime) + SetCursorPos(cursorX, cursorY) + + def WheelUp(self, x: int = None, y: int = None, ratioX: float = 0.5, ratioY: float = 0.5, wheelTimes: int = 1, interval: float = 0.05, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Make control have focus first, move cursor to the specified position and mouse wheel up. + x: int, if < 0, move x cursor to self.BoundingRectangle.right + x, if not None, ignore ratioX. + y: int, if < 0, move y cursor to self.BoundingRectangle.bottom + y, if not None, ignore ratioY. + ratioX: float. + ratioY: float. + wheelTimes: int. + interval: float. + waitTime: float. + """ + cursorX, cursorY = GetCursorPos() + self.SetFocus() + self.MoveCursorToInnerPos(x, y, ratioX, ratioY, simulateMove=False) + WheelUp(wheelTimes, interval, waitTime) + SetCursorPos(cursorX, cursorY) + + def ShowWindow(self, cmdShow: int, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Get a native handle from self or ancestors until valid and call native `ShowWindow` with cmdShow. + cmdShow: int, a value in in class `SW`. + waitTime: float. + Return bool, True if succeed otherwise False. + """ + handle = self.NativeWindowHandle + if not handle: + control = self + while not handle: + control = control.GetParentControl() + handle = control.NativeWindowHandle + if handle: + ret = ShowWindow(handle, cmdShow) + time.sleep(waitTime) + return ret + + def Show(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call native `ShowWindow(SW.Show)`. + Return bool, True if succeed otherwise False. + """ + return self.ShowWindow(SW.Show, waitTime) + + def Hide(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Call native `ShowWindow(SW.Hide)`. + waitTime: float + Return bool, True if succeed otherwise False. + """ + return self.ShowWindow(SW.Hide, waitTime) + + def MoveWindow(self, x: int, y: int, width: int, height: int, repaint: bool = True) -> bool: + """ + Call native MoveWindow if control has a valid native handle. + x: int. + y: int. + width: int. + height: int. + repaint: bool. + Return bool, True if succeed otherwise False. + """ + handle = self.NativeWindowHandle + if handle: + return MoveWindow(handle, x, y, width, height, int(repaint)) + return False + + def GetWindowText(self) -> str: + """ + Call native GetWindowText if control has a valid native handle. + """ + handle = self.NativeWindowHandle + if handle: + return GetWindowText(handle) + + def SetWindowText(self, text: str) -> bool: + """ + Call native SetWindowText if control has a valid native handle. + """ + handle = self.NativeWindowHandle + if handle: + return SetWindowText(handle, text) + return False + + def SendKey(self, key: int, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Make control have focus first and type a key. + `self.SetFocus` may not work for some controls, you may need to click it to make it have focus. + key: int, a key code value in class Keys. + waitTime: float. + """ + self.SetFocus() + SendKey(key, waitTime) + + def SendKeys(self, text: str, interval: float = 0.01, waitTime: float = OPERATION_WAIT_TIME, charMode: bool = True) -> None: + """ + Make control have focus first and type keys. + `self.SetFocus` may not work for some controls, you may need to click it to make it have focus. + text: str, keys to type, see the docstring of `SendKeys`. + interval: float, seconds between keys. + waitTime: float. + charMode: bool, if False, the text typied is depend on the input method if a input method is on. + """ + self.SetFocus() + SendKeys(text, interval, waitTime, charMode) + + def GetPixelColor(self, x: int, y: int) -> int: + """ + Call native `GetPixelColor` if control has a valid native handle. + Use `self.ToBitmap` if control doesn't have a valid native handle or you get many pixels. + x: int, internal x position. + y: int, internal y position. + Return int, a color value in bgr. + r = bgr & 0x0000FF + g = (bgr & 0x00FF00) >> 8 + b = (bgr & 0xFF0000) >> 16 + """ + handle = self.NativeWindowHandle + if handle: + return GetPixelColor(x, y, handle) + + def ToBitmap(self, x: int = 0, y: int = 0, width: int = 0, height: int = 0) -> Bitmap: + """ + Capture control to a Bitmap object. + x, y: int, the point in control's internal position(from 0,0). + width, height: int, image's width and height from x, y, use 0 for entire area. + If width(or height) < 0, image size will be control's width(or height) - width(or height). + """ + bitmap = Bitmap() + bitmap.FromControl(self, x, y, width, height) + return bitmap + + def CaptureToImage(self, savePath: str, x: int = 0, y: int = 0, width: int = 0, height: int = 0) -> bool: + """ + Capture control to a image file. + savePath: str, should end with .bmp, .jpg, .jpeg, .png, .gif, .tif, .tiff. + x, y: int, the point in control's internal position(from 0,0). + width, height: int, image's width and height from x, y, use 0 for entire area. + If width(or height) < 0, image size will be control's width(or height) - width(or height). + Return bool, True if succeed otherwise False. + """ + bitmap = Bitmap() + if bitmap.FromControl(self, x, y, width, height): + return bitmap.ToFile(savePath) + return False + + def IsTopLevel(self) -> bool: + """Determine whether current control is top level.""" + handle = self.NativeWindowHandle + if handle: + return GetAncestor(handle, GAFlag.Root) == handle + return False + + def GetTopLevelControl(self) -> 'Control': + """ + Get the top level control which current control lays. + If current control is top level, return self. + If current control is root control, return None. + Return `PaneControl` or `WindowControl` or None. + """ + handle = self.NativeWindowHandle + if handle: + topHandle = GetAncestor(handle, GAFlag.Root) + if topHandle: + if topHandle == handle: + return self + else: + return ControlFromHandle(topHandle) + else: + #self is root control + pass + else: + control = self + while True: + control = control.GetParentControl() + handle = control.NativeWindowHandle + if handle: + topHandle = GetAncestor(handle, GAFlag.Root) + return ControlFromHandle(topHandle) + + def Control(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'Control': + return Control(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def ButtonControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'ButtonControl': + return ButtonControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def CalendarControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'CalendarControl': + return CalendarControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def CheckBoxControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'CheckBoxControl': + return CheckBoxControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def ComboBoxControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'ComboBoxControl': + return ComboBoxControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def CustomControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'CustomControl': + return CustomControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def DataGridControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'DataGridControl': + return DataGridControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def DataItemControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'DataItemControl': + return DataItemControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def DocumentControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'DocumentControl': + return DocumentControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def EditControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'EditControl': + return EditControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def GroupControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'GroupControl': + return GroupControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def HeaderControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'HeaderControl': + return HeaderControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def HeaderItemControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'HeaderItemControl': + return HeaderItemControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def HyperlinkControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'HyperlinkControl': + return HyperlinkControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def ImageControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'ImageControl': + return ImageControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def ListControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'listControl': + return ListControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def ListItemControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'ListItemControl': + return ListItemControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def MenuControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'MenuControl': + return MenuControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def MenuBarControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'MenuBarControl': + return MenuBarControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def MenuItemControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'MenuItemControl': + return MenuItemControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def PaneControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'PaneControl': + return PaneControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def ProgressBarControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'ProgressBarControl': + return ProgressBarControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def RadioButtonControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'RadioButtonControl': + return RadioButtonControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def ScrollBarControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'ScrollBarControl': + return ScrollBarControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def SemanticZoomControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'SemanticZoomControl': + return SemanticZoomControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def SeparatorControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'SeparatorControl': + return SeparatorControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def SliderControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'SliderControl': + return SliderControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def SpinnerControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'SpinnerControl': + return SpinnerControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def SplitButtonControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'SplitButtonControl': + return SplitButtonControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def StatusBarControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'StatusBarControl': + return StatusBarControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def TabControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'TabControl': + return TabControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def TabItemControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'TabItemControl': + return TabItemControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def TableControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'TableControl': + return TableControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def TextControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'TextControl': + return TextControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def ThumbControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'ThumbControl': + return ThumbControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def TitleBarControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'TitleBarControl': + return TitleBarControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def ToolBarControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'ToolBarControl': + return ToolBarControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def ToolTipControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'ToolTipControl': + return ToolTipControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def TreeControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'TreeControl': + return TreeControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def TreeItemControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'TreeItemControl': + return TreeItemControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + def WindowControl(self, searchDepth=0xFFFFFFFF, searchInterval=SEARCH_INTERVAL, foundIndex=1, element=0, **searchProperties) -> 'WindowControl': + return WindowControl(searchDepth=searchDepth, searchInterval=searchInterval, foundIndex=foundIndex, element=element, searchFromControl=self, **searchProperties) + + +class AppBarControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.AppBarControl) + + +class ButtonControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.ButtonControl) + + def GetExpandCollapsePattern(self) -> ExpandCollapsePattern: + """ + Return `ExpandCollapsePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ExpandCollapsePattern) + + def GetInvokePattern(self) -> InvokePattern: + """ + Return `InvokePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.InvokePattern) + + def GetTogglePattern(self) -> TogglePattern: + """ + Return `TogglePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TogglePattern) + + +class CalendarControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.CalendarControl) + + def GetGridPattern(self) -> GridPattern: + """ + Return `GridPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.GridPattern) + + def GetTablePattern(self) -> TablePattern: + """ + Return `TablePattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.TablePattern) + + def GetScrollPattern(self) -> ScrollPattern: + """ + Return `ScrollPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ScrollPattern) + + def GetSelectionPattern(self) -> SelectionPattern: + """ + Return `SelectionPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionPattern) + + +class CheckBoxControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.CheckBoxControl) + + def GetTogglePattern(self) -> TogglePattern: + """ + Return `TogglePattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.TogglePattern) + + +class ComboBoxControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.ComboBoxControl) + + def GetExpandCollapsePattern(self) -> ExpandCollapsePattern: + """ + Return `ExpandCollapsePattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.ExpandCollapsePattern) + + def GetSelectionPattern(self) -> SelectionPattern: + """ + Return `SelectionPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionPattern) + + def GetValuePattern(self) -> ValuePattern: + """ + Return `ValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ValuePattern) + + def Select(self, itemName: str = '', condition: Callable[[str], bool] = None, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Show combobox's popup menu and select a item by name. + itemName: str. + condition: Callable[[str], bool], function(comboBoxItemName: str) -> bool, if condition is valid, ignore itemName. + waitTime: float. + Some comboboxs doesn't support SelectionPattern, here is a workaround. + This method tries to add selection support. + It may not work for some comboboxes, such as comboboxes in older Qt version. + If it doesn't work, you should write your own version Select, or it doesn't support selection at all. + """ + expandCollapsePattern = self.GetExpandCollapsePattern() + if expandCollapsePattern: + expandCollapsePattern.Expand() + else: + #Windows Form's ComboBoxControl doesn't support ExpandCollapsePattern + self.Click(x=-10, ratioY=0.5, simulateMove=False) + find = False + if condition: + listItemControl = self.ListItemControl(Compare=lambda c, d: condition(c.Name)) + else: + listItemControl = self.ListItemControl(Name=itemName) + if listItemControl.Exists(1): + scrollItemPattern = listItemControl.GetScrollItemPattern() + if scrollItemPattern: + scrollItemPattern.ScrollIntoView(waitTime=0.1) + listItemControl.Click(waitTime=waitTime) + find = True + else: + #ComboBox's popup window is a child of root control + listControl = ListControl(searchDepth= 1) + if listControl.Exists(1): + if condition: + listItemControl = listControl.ListItemControl(Compare=lambda c, d: condition(c.Name)) + else: + listItemControl = listControl.ListItemControl(Name=itemName) + if listItemControl.Exists(0, 0): + scrollItemPattern = listItemControl.GetScrollItemPattern() + if scrollItemPattern: + scrollItemPattern.ScrollIntoView(waitTime=0.1) + listItemControl.Click(waitTime=waitTime) + find = True + if not find: + Logger.ColorfullyLog('Can\'t find {} in ComboBoxControl or it does not support selection.'.format(itemName), ConsoleColor.Yellow) + if expandCollapsePattern: + expandCollapsePattern.Collapse(waitTime) + else: + self.Click(x=-10, ratioY=0.5, simulateMove=False, waitTime=waitTime) + return find + + +class CustomControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.CustomControl) + + +class DataGridControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.DataGridControl) + + def GetGridPattern(self) -> GridPattern: + """ + Return `GridPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.GridPattern) + + def GetScrollPattern(self) -> ScrollPattern: + """ + Return `ScrollPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ScrollPattern) + + def GetSelectionPattern(self) -> SelectionPattern: + """ + Return `SelectionPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionPattern) + + def GetTablePattern(self) -> TablePattern: + """ + Return `TablePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TablePattern) + + +class DataItemControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.DataItemControl) + + def GetSelectionItemPattern(self) -> SelectionItemPattern: + """ + Return `SelectionItemPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionItemPattern) + + def GetExpandCollapsePattern(self) -> ExpandCollapsePattern: + """ + Return `ExpandCollapsePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ExpandCollapsePattern) + + def GetGridItemPattern(self) -> GridItemPattern: + """ + Return `GridItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.GridItemPattern) + + def GetScrollItemPattern(self) -> ScrollItemPattern: + """ + Return `ScrollItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ScrollItemPattern) + + def GetTableItemPattern(self) -> TableItemPattern: + """ + Return `TableItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TableItemPattern) + + def GetTogglePattern(self) -> TogglePattern: + """ + Return `TogglePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TogglePattern) + + def GetValuePattern(self) -> ValuePattern: + """ + Return `ValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ValuePattern) + + +class DocumentControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.DocumentControl) + + def GetTextPattern(self) -> TextPattern: + """ + Return `TextPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.TextPattern) + + def GetScrollPattern(self) -> ScrollPattern: + """ + Return `ScrollPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ScrollPattern) + + def GetValuePattern(self) -> ValuePattern: + """ + Return `ValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ValuePattern) + + +class EditControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.EditControl) + + def GetRangeValuePattern(self) -> RangeValuePattern: + """ + Return `RangeValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.RangeValuePattern) + + def GetTextPattern(self) -> TextPattern: + """ + Return `TextPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TextPattern) + + def GetValuePattern(self) -> ValuePattern: + """ + Return `ValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ValuePattern) + + +class GroupControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.GroupControl) + + def GetExpandCollapsePattern(self) -> ExpandCollapsePattern: + """ + Return `ExpandCollapsePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ExpandCollapsePattern) + + +class HeaderControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.HeaderControl) + + def GetTransformPattern(self) -> TransformPattern: + """ + Return `TransformPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TransformPattern) + + +class HeaderItemControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.HeaderItemControl) + + def GetInvokePattern(self) -> InvokePattern: + """ + Return `InvokePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.InvokePattern) + + def GetTransformPattern(self) -> TransformPattern: + """ + Return `TransformPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TransformPattern) + + +class HyperlinkControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.HyperlinkControl) + + def GetInvokePattern(self) -> InvokePattern: + """ + Return `InvokePattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.InvokePattern) + + def GetValuePattern(self) -> ValuePattern: + """ + Return `ValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ValuePattern) + + +class ImageControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.ImageControl) + + def GetGridItemPattern(self) -> GridItemPattern: + """ + Return `GridItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.GridItemPattern) + + def GetTableItemPattern(self) -> TableItemPattern: + """ + Return `TableItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TableItemPattern) + + +class ListControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.ListControl) + + def GetGridPattern(self) -> GridPattern: + """ + Return `GridPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.GridPattern) + + def GetMultipleViewPattern(self) -> MultipleViewPattern: + """ + Return `MultipleViewPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.MultipleViewPattern) + + def GetScrollPattern(self) -> ScrollPattern: + """ + Return `ScrollPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ScrollPattern) + + def GetSelectionPattern(self) -> SelectionPattern: + """ + Return `SelectionPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionPattern) + + +class ListItemControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.ListItemControl) + + def GetSelectionItemPattern(self) -> SelectionItemPattern: + """ + Return `SelectionItemPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionItemPattern) + + def GetExpandCollapsePattern(self) -> ExpandCollapsePattern: + """ + Return `ExpandCollapsePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ExpandCollapsePattern) + + def GetGridItemPattern(self) -> GridItemPattern: + """ + Return `GridItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.GridItemPattern) + + def GetInvokePattern(self) -> InvokePattern: + """ + Return `InvokePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.InvokePattern) + + def GetScrollItemPattern(self) -> ScrollItemPattern: + """ + Return `ScrollItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ScrollItemPattern) + + def GetTogglePattern(self) -> TogglePattern: + """ + Return `TogglePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TogglePattern) + + def GetValuePattern(self) -> ValuePattern: + """ + Return `ValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ValuePattern) + + +class MenuControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.MenuControl) + + +class MenuBarControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.MenuBarControl) + + def GetDockPattern(self) -> DockPattern: + """ + Return `DockPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.DockPattern) + + def GetExpandCollapsePattern(self) -> ExpandCollapsePattern: + """ + Return `ExpandCollapsePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ExpandCollapsePattern) + + def GetTransformPattern(self) -> TransformPattern: + """ + Return `TransformPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TransformPattern) + + +class MenuItemControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.MenuItemControl) + + def GetExpandCollapsePattern(self) -> ExpandCollapsePattern: + """ + Return `ExpandCollapsePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ExpandCollapsePattern) + + def GetInvokePattern(self) -> InvokePattern: + """ + Return `InvokePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.InvokePattern) + + def GetSelectionItemPattern(self) -> SelectionItemPattern: + """ + Return `SelectionItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionItemPattern) + + def GetTogglePattern(self) -> TogglePattern: + """ + Return `TogglePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TogglePattern) + + +class TopLevel(): + """Class TopLevel""" + def SetTopmost(self, isTopmost: bool = True, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Set top level window topmost. + isTopmost: bool. + waitTime: float. + """ + if self.IsTopLevel(): + ret = SetWindowTopmost(self.NativeWindowHandle, isTopmost) + time.sleep(waitTime) + return ret + return False + + def IsTopmost(self) -> bool: + if self.IsTopLevel(): + WS_EX_TOPMOST = 0x00000008 + return bool(GetWindowLong(self.NativeWindowHandle, GWL.ExStyle) & WS_EX_TOPMOST) + return False + + def SwitchToThisWindow(self, waitTime: float = OPERATION_WAIT_TIME) -> None: + if self.IsTopLevel(): + SwitchToThisWindow(self.NativeWindowHandle) + time.sleep(waitTime) + + def Maximize(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Set top level window maximize. + """ + if self.IsTopLevel(): + return self.ShowWindow(SW.ShowMaximized, waitTime) + return False + + def IsMaximize(self) -> bool: + if self.IsTopLevel(): + return bool(IsZoomed(self.NativeWindowHandle)) + return False + + def Minimize(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + if self.IsTopLevel(): + return self.ShowWindow(SW.Minimize, waitTime) + return False + + def IsMinimize(self) -> bool: + if self.IsTopLevel(): + return bool(IsIconic(self.NativeWindowHandle)) + return False + + def Restore(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """ + Restore window to normal state. + Similar to SwitchToThisWindow. + """ + if self.IsTopLevel(): + return self.ShowWindow(SW.Restore, waitTime) + return False + + def MoveToCenter(self) -> bool: + """ + Move window to screen center. + """ + if self.IsTopLevel(): + rect = self.BoundingRectangle + screenWidth, screenHeight = GetScreenSize() + x, y = (screenWidth - rect.width()) // 2, (screenHeight - rect.height()) // 2 + if x < 0: x = 0 + if y < 0: y = 0 + return SetWindowPos(self.NativeWindowHandle, SWP.HWND_Top, x, y, 0, 0, SWP.SWP_NoSize) + return False + + def SetActive(self, waitTime: float = OPERATION_WAIT_TIME) -> bool: + """Set top level window active.""" + if self.IsTopLevel(): + handle = self.NativeWindowHandle + if IsIconic(handle): + ret = ShowWindow(handle, SW.Restore) + elif not IsWindowVisible(handle): + ret = ShowWindow(handle, SW.Show) + ret = SetForegroundWindow(handle) # may fail if foreground windows's process is not python + time.sleep(waitTime) + return ret + return False + + +class PaneControl(Control, TopLevel): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.PaneControl) + + def GetDockPattern(self) -> DockPattern: + """ + Return `DockPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.DockPattern) + + def GetScrollPattern(self) -> ScrollPattern: + """ + Return `ScrollPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ScrollPattern) + + def GetTransformPattern(self) -> TransformPattern: + """ + Return `TransformPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TransformPattern) + + +class ProgressBarControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.ProgressBarControl) + + def GetRangeValuePattern(self) -> RangeValuePattern: + """ + Return `RangeValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.RangeValuePattern) + + def GetValuePattern(self) -> ValuePattern: + """ + Return `ValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ValuePattern) + + +class RadioButtonControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.RadioButtonControl) + + def GetSelectionItemPattern(self) -> SelectionItemPattern: + """ + Return `SelectionItemPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionItemPattern) + + +class ScrollBarControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.ScrollBarControl) + + def GetRangeValuePattern(self) -> RangeValuePattern: + """ + Return `RangeValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.RangeValuePattern) + + +class SemanticZoomControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.SemanticZoomControl) + + +class SeparatorControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.SeparatorControl) + + +class SliderControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.SliderControl) + + def GetRangeValuePattern(self) -> RangeValuePattern: + """ + Return `RangeValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.RangeValuePattern) + + def GetSelectionPattern(self) -> SelectionPattern: + """ + Return `SelectionPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionPattern) + + def GetValuePattern(self) -> ValuePattern: + """ + Return `ValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ValuePattern) + + +class SpinnerControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.SpinnerControl) + + def GetRangeValuePattern(self) -> RangeValuePattern: + """ + Return `RangeValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.RangeValuePattern) + + def GetSelectionPattern(self) -> SelectionPattern: + """ + Return `SelectionPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionPattern) + + def GetValuePattern(self) -> ValuePattern: + """ + Return `ValuePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ValuePattern) + + +class SplitButtonControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.SplitButtonControl) + + def GetExpandCollapsePattern(self) -> ExpandCollapsePattern: + """ + Return `ExpandCollapsePattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.ExpandCollapsePattern) + + def GetInvokePattern(self) -> InvokePattern: + """ + Return `InvokePattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.InvokePattern) + + +class StatusBarControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.StatusBarControl) + + def GetGridPattern(self) -> GridPattern: + """ + Return `GridPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.GridPattern) + + +class TabControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.TabControl) + + def GetSelectionPattern(self) -> SelectionPattern: + """ + Return `SelectionPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionPattern) + + def GetScrollPattern(self) -> ScrollPattern: + """ + Return `ScrollPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ScrollPattern) + + +class TabItemControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.TabItemControl) + + def GetSelectionItemPattern(self) -> SelectionItemPattern: + """ + Return `SelectionItemPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionItemPattern) + + +class TableControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.TableControl) + + def GetGridPattern(self) -> GridPattern: + """ + Return `GridPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.GridPattern) + + def GetGridItemPattern(self) -> GridItemPattern: + """ + Return `GridItemPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.GridItemPattern) + + def GetTablePattern(self) -> TablePattern: + """ + Return `TablePattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.TablePattern) + + def GetTableItemPattern(self) -> TableItemPattern: + """ + Return `TableItemPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.TableItemPattern) + + +class TextControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.TextControl) + + def GetGridItemPattern(self) -> GridItemPattern: + """ + Return `GridItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.GridItemPattern) + + def GetTableItemPattern(self) -> TableItemPattern: + """ + Return `TableItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TableItemPattern) + + def GetTextPattern(self) -> TextPattern: + """ + Return `TextPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TextPattern) + + +class ThumbControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.ThumbControl) + + def GetTransformPattern(self) -> TransformPattern: + """ + Return `TransformPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.TransformPattern) + + +class TitleBarControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.TitleBarControl) + + +class ToolBarControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.ToolBarControl) + + def GetDockPattern(self) -> DockPattern: + """ + Return `DockPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.DockPattern) + + def GetExpandCollapsePattern(self) -> ExpandCollapsePattern: + """ + Return `ExpandCollapsePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ExpandCollapsePattern) + + def GetTransformPattern(self) -> TransformPattern: + """ + Return `TransformPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TransformPattern) + + +class ToolTipControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.ToolTipControl) + + def GetTextPattern(self) -> TextPattern: + """ + Return `TextPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TextPattern) + + def GetWindowPattern(self) -> WindowPattern: + """ + Return `WindowPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.WindowPattern) + + +class TreeControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.TreeControl) + + def GetScrollPattern(self) -> ScrollPattern: + """ + Return `ScrollPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ScrollPattern) + + def GetSelectionPattern(self) -> SelectionPattern: + """ + Return `SelectionPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionPattern) + + +class TreeItemControl(Control): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.TreeItemControl) + + def GetExpandCollapsePattern(self) -> ExpandCollapsePattern: + """ + Return `ExpandCollapsePattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.ExpandCollapsePattern) + + def GetInvokePattern(self) -> InvokePattern: + """ + Return `InvokePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.InvokePattern) + + def GetScrollItemPattern(self) -> ScrollItemPattern: + """ + Return `ScrollItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.ScrollItemPattern) + + def GetSelectionItemPattern(self) -> SelectionItemPattern: + """ + Return `SelectionItemPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.SelectionItemPattern) + + def GetTogglePattern(self) -> TogglePattern: + """ + Return `TogglePattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.TogglePattern) + + +class WindowControl(Control, TopLevel): + def __init__(self, searchFromControl: Control = None, searchDepth: int = 0xFFFFFFFF, searchInterval: float = SEARCH_INTERVAL, foundIndex: int = 1, element=None, **searchProperties): + Control.__init__(self, searchFromControl, searchDepth, searchInterval, foundIndex, element, **searchProperties) + self.AddSearchProperties(ControlType=ControlType.WindowControl) + self._DockPattern = None + self._TransformPattern = None + + def GetTransformPattern(self) -> TransformPattern: + """ + Return `TransformPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.TransformPattern) + + def GetWindowPattern(self) -> WindowPattern: + """ + Return `WindowPattern` if it supports the pattern else None(Must support according to MSDN). + """ + return self.GetPattern(PatternId.WindowPattern) + + def GetDockPattern(self) -> DockPattern: + """ + Return `DockPattern` if it supports the pattern else None(Conditional support according to MSDN). + """ + return self.GetPattern(PatternId.DockPattern) + + def MetroClose(self, waitTime: float = OPERATION_WAIT_TIME) -> None: + """ + Only work on Windows 8/8.1, if current window is Metro UI. + waitTime: float. + """ + if self.ClassName == METRO_WINDOW_CLASS_NAME: + screenWidth, screenHeight = GetScreenSize() + MoveTo(screenWidth // 2, 0, waitTime=0) + DragDrop(screenWidth // 2, 0, screenWidth // 2, screenHeight, waitTime=waitTime) + else: + Logger.WriteLine('Window is not Metro!', ConsoleColor.Yellow) + + +ControlConstructors = { + ControlType.AppBarControl: AppBarControl, + ControlType.ButtonControl: ButtonControl, + ControlType.CalendarControl: CalendarControl, + ControlType.CheckBoxControl: CheckBoxControl, + ControlType.ComboBoxControl: ComboBoxControl, + ControlType.CustomControl: CustomControl, + ControlType.DataGridControl: DataGridControl, + ControlType.DataItemControl: DataItemControl, + ControlType.DocumentControl: DocumentControl, + ControlType.EditControl: EditControl, + ControlType.GroupControl: GroupControl, + ControlType.HeaderControl: HeaderControl, + ControlType.HeaderItemControl: HeaderItemControl, + ControlType.HyperlinkControl: HyperlinkControl, + ControlType.ImageControl: ImageControl, + ControlType.ListControl: ListControl, + ControlType.ListItemControl: ListItemControl, + ControlType.MenuBarControl: MenuBarControl, + ControlType.MenuControl: MenuControl, + ControlType.MenuItemControl: MenuItemControl, + ControlType.PaneControl: PaneControl, + ControlType.ProgressBarControl: ProgressBarControl, + ControlType.RadioButtonControl: RadioButtonControl, + ControlType.ScrollBarControl: ScrollBarControl, + ControlType.SemanticZoomControl: SemanticZoomControl, + ControlType.SeparatorControl: SeparatorControl, + ControlType.SliderControl: SliderControl, + ControlType.SpinnerControl: SpinnerControl, + ControlType.SplitButtonControl: SplitButtonControl, + ControlType.StatusBarControl: StatusBarControl, + ControlType.TabControl: TabControl, + ControlType.TabItemControl: TabItemControl, + ControlType.TableControl: TableControl, + ControlType.TextControl: TextControl, + ControlType.ThumbControl: ThumbControl, + ControlType.TitleBarControl: TitleBarControl, + ControlType.ToolBarControl: ToolBarControl, + ControlType.ToolTipControl: ToolTipControl, + ControlType.TreeControl: TreeControl, + ControlType.TreeItemControl: TreeItemControl, + ControlType.WindowControl: WindowControl, +} + + +class UIAutomationInitializerInThread: + def __init__(self, debug: bool = False): + self.debug = debug + InitializeUIAutomationInCurrentThread() + if self.debug: + th = threading.currentThread() + print('\ncall InitializeUIAutomationInCurrentThread in {}'.format(th)) + + def __del__(self): + UninitializeUIAutomationInCurrentThread() + if self.debug: + th = threading.currentThread() + print('\ncall UninitializeUIAutomationInCurrentThread in {}'.format(th)) + + +def InitializeUIAutomationInCurrentThread() -> None: + """ + Initialize UIAutomation in a new thread. + If you want to use functionalities related to Controls and Patterns in a new thread. + You must call this function first in the new thread. + But you can't use use a Control or a Pattern created in a different thread. + So you can't create a Control or a Pattern in main thread and then pass it to a new thread and use it. + """ + comtypes.CoInitializeEx() + SetDpiAwareness(dpiAwarenessPerMonitor=True) + + +def UninitializeUIAutomationInCurrentThread() -> None: + """ + Uninitialize UIAutomation in a new thread after calling InitializeUIAutomationInCurrentThread. + You must call this function when the new thread exits if you have called InitializeUIAutomationInCurrentThread in the same thread. + """ + comtypes.CoUninitialize() + + +def SetGlobalSearchTimeout(seconds: float) -> None: + """ + seconds: float. + To make this available, you need explicitly import uiautomation: + from uiautomation import uiautomation as auto + auto.SetGlobalSearchTimeout(10) + """ + global TIME_OUT_SECOND + TIME_OUT_SECOND = seconds + + +def WaitForExist(control: Control, timeout: float) -> bool: + """ + Check if control exists in timeout seconds. + control: `Control` or its subclass. + timeout: float. + Return bool. + """ + return control.Exists(timeout, 1) + + +def WaitForDisappear(control: Control, timeout: float) -> bool: + """ + Check if control disappears in timeout seconds. + control: `Control` or its subclass. + timeout: float. + Return bool. + """ + return control.Disappears(timeout, 1) + + +def WalkTree(top, getChildren: Callable[[TreeNode], List[TreeNode]] = None, + getFirstChild: Callable[[TreeNode], TreeNode] = None, getNextSibling: Callable[[TreeNode], TreeNode] = None, + yieldCondition: Callable[[TreeNode, int], bool] = None, includeTop: bool = False, maxDepth: int = 0xFFFFFFFF): + """ + Walk a tree not using recursive algorithm. + top: a tree node. + getChildren: Callable[[TreeNode], List[TreeNode]], function(treeNode: TreeNode) -> List[TreeNode]. + getNextSibling: Callable[[TreeNode], TreeNode], function(treeNode: TreeNode) -> TreeNode. + getNextSibling: Callable[[TreeNode], TreeNode], function(treeNode: TreeNode) -> TreeNode. + yieldCondition: Callable[[TreeNode, int], bool], function(treeNode: TreeNode, depth: int) -> bool. + includeTop: bool, if True yield top first. + maxDepth: int, enum depth. + + If getChildren is valid, ignore getFirstChild and getNextSibling, + yield 3 items tuple: (treeNode, depth, remain children count in current depth). + If getChildren is not valid, using getFirstChild and getNextSibling, + yield 2 items tuple: (treeNode, depth). + If yieldCondition is not None, only yield tree nodes that yieldCondition(treeNode: TreeNode, depth: int)->bool returns True. + + For example: + def GetDirChildren(dir_): + if os.path.isdir(dir_): + return [os.path.join(dir_, it) for it in os.listdir(dir_)] + for it, depth, leftCount in WalkTree('D:\\', getChildren= GetDirChildren): + print(it, depth, leftCount) + """ + if maxDepth <= 0: + return + depth = 0 + if getChildren: + if includeTop: + if not yieldCondition or yieldCondition(top, 0): + yield top, 0, 0 + children = getChildren(top) + childList = [children] + while depth >= 0: #or while childList: + lastItems = childList[-1] + if lastItems: + if not yieldCondition or yieldCondition(lastItems[0], depth + 1): + yield lastItems[0], depth + 1, len(lastItems) - 1 + if depth + 1 < maxDepth: + children = getChildren(lastItems[0]) + if children: + depth += 1 + childList.append(children) + del lastItems[0] + else: + del childList[depth] + depth -= 1 + elif getFirstChild and getNextSibling: + if includeTop: + if not yieldCondition or yieldCondition(top, 0): + yield top, 0 + child = getFirstChild(top) + childList = [child] + while depth >= 0: #or while childList: + lastItem = childList[-1] + if lastItem: + if not yieldCondition or yieldCondition(lastItem, depth + 1): + yield lastItem, depth + 1 + child = getNextSibling(lastItem) + childList[depth] = child + if depth + 1 < maxDepth: + child = getFirstChild(lastItem) + if child: + depth += 1 + childList.append(child) + else: + del childList[depth] + depth -= 1 + + +def GetRootControl() -> PaneControl: + """ + Get root control, the Desktop window. + Return `PaneControl`. + """ + return Control.CreateControlFromElement(_AutomationClient.instance().IUIAutomation.GetRootElement()) + + +def GetFocusedControl() -> Control: + """Return `Control` subclass.""" + return Control.CreateControlFromElement(_AutomationClient.instance().IUIAutomation.GetFocusedElement()) + + +def GetForegroundControl() -> Control: + """Return `Control` subclass.""" + return ControlFromHandle(GetForegroundWindow()) + #another implement + #focusedControl = GetFocusedControl() + #parentControl = focusedControl + #controlList = [] + #while parentControl: + #controlList.insert(0, parentControl) + #parentControl = parentControl.GetParentControl() + #if len(controlList) == 1: + #parentControl = controlList[0] + #else: + #parentControl = controlList[1] + #return parentControl + + +def GetConsoleWindow() -> WindowControl: + """Return `WindowControl` or None, a console window that runs python.""" + return ControlFromHandle(ctypes.windll.kernel32.GetConsoleWindow()) + + +def ControlFromPoint(x: int, y: int) -> Control: + """ + Call IUIAutomation ElementFromPoint x,y. May return None if mouse is over cmd's title bar icon. + Return `Control` subclass or None. + """ + element = _AutomationClient.instance().IUIAutomation.ElementFromPoint(ctypes.wintypes.POINT(x, y)) + return Control.CreateControlFromElement(element) + + +def ControlFromPoint2(x: int, y: int) -> Control: + """ + Get a native handle from point x,y and call IUIAutomation.ElementFromHandle. + Return `Control` subclass. + """ + return Control.CreateControlFromElement(_AutomationClient.instance().IUIAutomation.ElementFromHandle(WindowFromPoint(x, y))) + + +def ControlFromCursor() -> Control: + """ + Call ControlFromPoint with current cursor point. + Return `Control` subclass. + """ + x, y = GetCursorPos() + return ControlFromPoint(x, y) + + +def ControlFromCursor2() -> Control: + """ + Call ControlFromPoint2 with current cursor point. + Return `Control` subclass. + """ + x, y = GetCursorPos() + return ControlFromPoint2(x, y) + + +def ControlFromHandle(handle: int) -> Control: + """ + Call IUIAutomation.ElementFromHandle with a native handle. + handle: int, a native window handle. + Return `Control` subclass or None. + """ + if handle: + return Control.CreateControlFromElement(_AutomationClient.instance().IUIAutomation.ElementFromHandle(handle)) + + +def ControlsAreSame(control1: Control, control2: Control) -> bool: + """ + control1: `Control` or its subclass. + control2: `Control` or its subclass. + Return bool, True if control1 and control2 represent the same control otherwise False. + """ + return bool(_AutomationClient.instance().IUIAutomation.CompareElements(control1.Element, control2.Element)) + + +def WalkControl(control: Control, includeTop: bool = False, maxDepth: int = 0xFFFFFFFF): + """ + control: `Control` or its subclass. + includeTop: bool, if True, yield (control, 0) first. + maxDepth: int, enum depth. + Yield 2 items tuple (control: Control, depth: int). + """ + if includeTop: + yield control, 0 + if maxDepth <= 0: + return + depth = 0 + child = control.GetFirstChildControl() + controlList = [child] + while depth >= 0: + lastControl = controlList[-1] + if lastControl: + yield lastControl, depth + 1 + child = lastControl.GetNextSiblingControl() + controlList[depth] = child + if depth + 1 < maxDepth: + child = lastControl.GetFirstChildControl() + if child: + depth += 1 + controlList.append(child) + else: + del controlList[depth] + depth -= 1 + + +def LogControl(control: Control, depth: int = 0, showAllName: bool = True, showPid: bool = False) -> None: + """ + Print and log control's properties. + control: `Control` or its subclass. + depth: int, current depth. + showAllName: bool, if False, print the first 30 characters of control.Name. + """ + def getKeyName(theDict, theValue): + for key in theDict: + if theValue == theDict[key]: + return key + indent = ' ' * depth * 4 + Logger.Write('{0}ControlType: '.format(indent)) + Logger.Write(control.ControlTypeName, ConsoleColor.DarkGreen) + Logger.Write(' ClassName: ') + Logger.Write(control.ClassName, ConsoleColor.DarkGreen) + Logger.Write(' AutomationId: ') + Logger.Write(control.AutomationId, ConsoleColor.DarkGreen) + Logger.Write(' Rect: ') + Logger.Write(control.BoundingRectangle, ConsoleColor.DarkGreen) + Logger.Write(' Name: ') + Logger.Write(control.Name, ConsoleColor.DarkGreen, printTruncateLen=0 if showAllName else 30) + Logger.Write(' Handle: ') + Logger.Write('0x{0:X}({0})'.format(control.NativeWindowHandle), ConsoleColor.DarkGreen) + Logger.Write(' Depth: ') + Logger.Write(depth, ConsoleColor.DarkGreen) + if showPid: + Logger.Write(' ProcessId: ') + Logger.Write(control.ProcessId, ConsoleColor.DarkGreen) + supportedPatterns = list(filter(lambda t: t[0], ((control.GetPattern(id_), name) for id_, name in PatternIdNames.items()))) + for pt, name in supportedPatterns: + if isinstance(pt, ValuePattern): + Logger.Write(' ValuePattern.Value: ') + Logger.Write(pt.Value, ConsoleColor.DarkGreen, printTruncateLen=0 if showAllName else 30) + elif isinstance(pt, RangeValuePattern): + Logger.Write(' RangeValuePattern.Value: ') + Logger.Write(pt.Value, ConsoleColor.DarkGreen) + elif isinstance(pt, TogglePattern): + Logger.Write(' TogglePattern.ToggleState: ') + Logger.Write('ToggleState.' + getKeyName(ToggleState.__dict__, pt.ToggleState), ConsoleColor.DarkGreen) + elif isinstance(pt, SelectionItemPattern): + Logger.Write(' SelectionItemPattern.IsSelected: ') + Logger.Write(pt.IsSelected, ConsoleColor.DarkGreen) + elif isinstance(pt, ExpandCollapsePattern): + Logger.Write(' ExpandCollapsePattern.ExpandCollapseState: ') + Logger.Write('ExpandCollapseState.' + getKeyName(ExpandCollapseState.__dict__, pt.ExpandCollapseState), ConsoleColor.DarkGreen) + elif isinstance(pt, ScrollPattern): + Logger.Write(' ScrollPattern.HorizontalScrollPercent: ') + Logger.Write(pt.HorizontalScrollPercent, ConsoleColor.DarkGreen) + Logger.Write(' ScrollPattern.VerticalScrollPercent: ') + Logger.Write(pt.VerticalScrollPercent, ConsoleColor.DarkGreen) + elif isinstance(pt, GridPattern): + Logger.Write(' GridPattern.RowCount: ') + Logger.Write(pt.RowCount, ConsoleColor.DarkGreen) + Logger.Write(' GridPattern.ColumnCount: ') + Logger.Write(pt.ColumnCount, ConsoleColor.DarkGreen) + elif isinstance(pt, GridItemPattern): + Logger.Write(' GridItemPattern.Row: ') + Logger.Write(pt.Column, ConsoleColor.DarkGreen) + Logger.Write(' GridItemPattern.Column: ') + Logger.Write(pt.Column, ConsoleColor.DarkGreen) + elif isinstance(pt, TextPattern): + # issue 49: CEF Control as DocumentControl have no "TextPattern.Text" property, skip log this part. + # https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextpattern-get_documentrange + try: + Logger.Write(' TextPattern.Text: ') + Logger.Write(pt.DocumentRange.GetText(30), ConsoleColor.DarkGreen) + except comtypes.COMError as ex: + pass + Logger.Write(' SupportedPattern:') + for pt, name in supportedPatterns: + Logger.Write(' ' + name, ConsoleColor.DarkGreen) + Logger.Write('\n') + + +def EnumAndLogControl(control: Control, maxDepth: int = 0xFFFFFFFF, showAllName: bool = True, showPid: bool = False, startDepth: int = 0) -> None: + """ + Print and log control and its descendants' propertyies. + control: `Control` or its subclass. + maxDepth: int, enum depth. + showAllName: bool, if False, print the first 30 characters of control.Name. + startDepth: int, control's current depth. + """ + for c, d in WalkControl(control, True, maxDepth): + LogControl(c, d + startDepth, showAllName, showPid) + + +def EnumAndLogControlAncestors(control: Control, showAllName: bool = True, showPid: bool = False) -> None: + """ + Print and log control and its ancestors' propertyies. + control: `Control` or its subclass. + showAllName: bool, if False, print the first 30 characters of control.Name. + """ + lists = [] + while control: + lists.insert(0, control) + control = control.GetParentControl() + for i, control in enumerate(lists): + LogControl(control, i, showAllName, showPid) + + +def FindControl(control: Control, compare: Callable[[Control, int], bool], maxDepth: int = 0xFFFFFFFF, findFromSelf: bool = False, foundIndex: int = 1) -> Control: + """ + control: `Control` or its subclass. + compare: Callable[[Control, int], bool], function(control: Control, depth: int) -> bool. + maxDepth: int, enum depth. + findFromSelf: bool, if False, do not compare self. + foundIndex: int, starts with 1, >= 1. + Return `Control` subclass or None if not find. + """ + foundCount = 0 + if not control: + control = GetRootControl() + traverseCount = 0 + for child, depth in WalkControl(control, findFromSelf, maxDepth): + traverseCount += 1 + if compare(child, depth): + foundCount += 1 + if foundCount == foundIndex: + child.traverseCount = traverseCount + return child + + +def ShowDesktop(waitTime: float = 1) -> None: + """Show Desktop by pressing win + d""" + SendKeys('{Win}d') + time.sleep(waitTime) + #another implement + #paneTray = PaneControl(searchDepth = 1, ClassName = 'Shell_TrayWnd') + #if paneTray.Exists(): + #WM_COMMAND = 0x111 + #MIN_ALL = 419 + #MIN_ALL_UNDO = 416 + #PostMessage(paneTray.NativeWindowHandle, WM_COMMAND, MIN_ALL, 0) + #time.sleep(1) + + +def WaitHotKeyReleased(hotkey: Tuple[int, int]) -> None: + """hotkey: Tuple[int, int], two ints tuple (modifierKey, key)""" + mod = {ModifierKey.Alt: Keys.VK_MENU, + ModifierKey.Control: Keys.VK_CONTROL, + ModifierKey.Shift: Keys.VK_SHIFT, + ModifierKey.Win: Keys.VK_LWIN + } + while True: + time.sleep(0.05) + if IsKeyPressed(hotkey[1]): + continue + for k, v in mod.items(): + if k & hotkey[0]: + if IsKeyPressed(v): + break + else: + break + + +def RunByHotKey(keyFunctions: Dict[Tuple[int, int], Callable], stopHotKey: Tuple[int, int] = None, exitHotKey: Tuple[int, int] = (ModifierKey.Control, Keys.VK_D), waitHotKeyReleased: bool = True) -> None: + """ + Bind functions with hotkeys, the function will be run or stopped in another thread when the hotkey is pressed. + keyFunctions: Dict[Tuple[int, int], Callable], such as {(uiautomation.ModifierKey.Control, uiautomation.Keys.VK_1) : function} + stopHotKey: hotkey tuple + exitHotKey: hotkey tuple + waitHotKeyReleased: bool, if True, hotkey function will be triggered after the hotkey is released + + def main(stopEvent): + while True: + if stopEvent.is_set(): # must check stopEvent.is_set() if you want to stop when stop hotkey is pressed + break + print(n) + n += 1 + stopEvent.wait(1) + print('main exit') + + uiautomation.RunByHotKey({(uiautomation.ModifierKey.Control, uiautomation.Keys.VK_1) : main} + , (uiautomation.ModifierKey.Control | uiautomation.ModifierKey.Shift, uiautomation.Keys.VK_2)) + """ + import traceback + + def getModName(theDict, theValue): + name = '' + for key in theDict: + if isinstance(theDict[key], int) and theValue & theDict[key]: + if name: + name += '|' + name += key + return name + def getKeyName(theDict, theValue): + for key in theDict: + if theValue == theDict[key]: + return key + def releaseAllKey(): + for key, value in Keys.__dict__.items(): + if isinstance(value, int) and key.startswith('VK'): + if IsKeyPressed(value): + ReleaseKey(value) + def threadFunc(function, stopEvent, hotkey, hotkeyName): + if waitHotKeyReleased: + WaitHotKeyReleased(hotkey) + try: + function(stopEvent) + except Exception as ex: + Logger.ColorfullyWrite('Catch an exception {} in thread for hotkey {}\n'.format( + ex.__class__.__name__, hotkeyName), writeToFile=False) + print(traceback.format_exc()) + finally: + releaseAllKey() #need to release keys if some keys were pressed + Logger.ColorfullyWrite('{} for function {} exits, hotkey {}\n'.format( + threading.currentThread(), function.__name__, hotkeyName), ConsoleColor.DarkYellow, writeToFile=False) + + stopHotKeyId = 1 + exitHotKeyId = 2 + hotKeyId = 3 + registed = True + id2HotKey = {} + id2Function = {} + id2Thread = {} + id2Name = {} + for hotkey in keyFunctions: + id2HotKey[hotKeyId] = hotkey + id2Function[hotKeyId] = keyFunctions[hotkey] + id2Thread[hotKeyId] = None + modName = getModName(ModifierKey.__dict__, hotkey[0]) + keyName = getKeyName(Keys.__dict__, hotkey[1]) + id2Name[hotKeyId] = str((modName, keyName)) + if ctypes.windll.user32.RegisterHotKey(0, hotKeyId, hotkey[0], hotkey[1]): + Logger.ColorfullyWrite('Register hotkey {} successfully\n'.format((modName, keyName)), writeToFile=False) + else: + registed = False + Logger.ColorfullyWrite('Register hotkey {} unsuccessfully, maybe it was allready registered by another program\n'.format((modName, keyName)), writeToFile=False) + hotKeyId += 1 + if stopHotKey and len(stopHotKey) == 2: + modName = getModName(ModifierKey.__dict__, stopHotKey[0]) + keyName = getKeyName(Keys.__dict__, stopHotKey[1]) + if ctypes.windll.user32.RegisterHotKey(0, stopHotKeyId, stopHotKey[0], stopHotKey[1]): + Logger.ColorfullyWrite('Register stop hotkey {} successfully\n'.format((modName, keyName)), writeToFile=False) + else: + registed = False + Logger.ColorfullyWrite('Register stop hotkey {} unsuccessfully, maybe it was allready registered by another program\n'.format((modName, keyName)), writeToFile=False) + if not registed: + return + if exitHotKey and len(exitHotKey) == 2: + modName = getModName(ModifierKey.__dict__, exitHotKey[0]) + keyName = getKeyName(Keys.__dict__, exitHotKey[1]) + if ctypes.windll.user32.RegisterHotKey(0, exitHotKeyId, exitHotKey[0], exitHotKey[1]): + Logger.ColorfullyWrite('Register exit hotkey {} successfully\n'.format((modName, keyName)), writeToFile=False) + else: + Logger.ColorfullyWrite('Register exit hotkey {} unsuccessfully\n'.format((modName, keyName)), writeToFile=False) + funcThread = None + livingThreads = [] + stopEvent = threading.Event() + msg = ctypes.wintypes.MSG() + while ctypes.windll.user32.GetMessageW(ctypes.byref(msg), ctypes.c_void_p(0), ctypes.c_uint(0), ctypes.c_uint(0)) != 0: + if msg.message == 0x0312: # WM_HOTKEY=0x0312 + if msg.wParam in id2HotKey: + if msg.lParam & 0x0000FFFF == id2HotKey[msg.wParam][0] and msg.lParam >> 16 & 0x0000FFFF == id2HotKey[msg.wParam][1]: + Logger.ColorfullyWrite('----------hotkey {} pressed----------\n'.format(id2Name[msg.wParam]), writeToFile=False) + if not id2Thread[msg.wParam]: + stopEvent.clear() + funcThread = threading.Thread(None, threadFunc, args=(id2Function[msg.wParam], stopEvent, id2HotKey[msg.wParam], id2Name[msg.wParam])) + funcThread.start() + id2Thread[msg.wParam] = funcThread + else: + if id2Thread[msg.wParam].is_alive(): + Logger.WriteLine('There is a {} that is already running for hotkey {}'.format(id2Thread[msg.wParam], id2Name[msg.wParam]), ConsoleColor.Yellow, writeToFile=False) + else: + stopEvent.clear() + funcThread = threading.Thread(None, threadFunc, args=(id2Function[msg.wParam], stopEvent, id2HotKey[msg.wParam], id2Name[msg.wParam])) + funcThread.start() + id2Thread[msg.wParam] = funcThread + elif stopHotKeyId == msg.wParam: + if msg.lParam & 0x0000FFFF == stopHotKey[0] and msg.lParam >> 16 & 0x0000FFFF == stopHotKey[1]: + Logger.Write('----------stop hotkey pressed----------\n', ConsoleColor.DarkYellow, writeToFile=False) + stopEvent.set() + for id_ in id2Thread: + if id2Thread[id_]: + if id2Thread[id_].is_alive(): + livingThreads.append((id2Thread[id_], id2Name[id_])) + id2Thread[id_] = None + elif exitHotKeyId == msg.wParam: + if msg.lParam & 0x0000FFFF == exitHotKey[0] and msg.lParam >> 16 & 0x0000FFFF == exitHotKey[1]: + Logger.Write('Exit hotkey pressed. Exit\n', ConsoleColor.DarkYellow, writeToFile=False) + stopEvent.set() + for id_ in id2Thread: + if id2Thread[id_]: + if id2Thread[id_].is_alive(): + livingThreads.append((id2Thread[id_], id2Name[id_])) + id2Thread[id_] = None + break + for thread, hotkeyName in livingThreads: + if thread.is_alive(): + Logger.Write('join {} triggered by hotkey {}\n'.format(thread, hotkeyName), ConsoleColor.DarkYellow, writeToFile=False) + thread.join(2) + os._exit(0) + + +if __name__ == '__main__': + + print('\nUIAutomationCore:----') + for i in sorted([it for it in dir(_AutomationClient.instance().UIAutomationCore) if not it.startswith('_')]): + print(i) + + print('\nIUIAutomation:----') + for i in sorted([it for it in dir(_AutomationClient.instance().IUIAutomation) if not it.startswith('_')]): + print(i) + + print('\nViewWalker:----') + for i in sorted([it for it in dir(_AutomationClient.instance().ViewWalker) if not it.startswith('_')]): + print(i) + + print() + for ct, ctor in ControlConstructors.items(): + c = ctor() + print(type(c)) + + notepad = WindowControl(searchDepth=1, ClassName='Notepad') + if not notepad.Exists(0, 0): + import subprocess + subprocess.Popen('notepad.exe') + notepad.Refind() + + print('\n', notepad) + print('Control:----') + for i in sorted([it for it in dir(notepad) if not it.startswith('_')]): + print(i) + + print('\n', notepad.Element) + print('Control.Element:----') + for i in sorted([it for it in dir(notepad.Element) if not it.startswith('_')]): + print(i) + + lp = notepad.GetLegacyIAccessiblePattern() + print('\n', lp) + print('Control.LegacyIAccessiblePattern:----') + for i in sorted([it for it in dir(lp.pattern) if not it.startswith('_')]): + print(i) + + print('\nControl.Properties:----') + for k, v in PropertyIdNames.items(): + try: + value = notepad.GetPropertyValue(k) + print('GetPropertyValue, {} = {}, type: {}'.format(v, value, type(value))) + except (KeyError, comtypes.COMError) as ex: + print('GetPropertyValue, {}, error'.format(v)) + + children = notepad.GetChildren() + print('\n notepad children:----', len(children)) + for c in notepad.GetChildren(): + print(c) + + del lp + del notepad + + hello = '{Ctrl}{End}{Enter}Hello World! 你好世界!' + SendKeys(hello) diff --git a/wxauto/utils/__init__.py b/wxauto/utils/__init__.py new file mode 100644 index 0000000..58f0ecf --- /dev/null +++ b/wxauto/utils/__init__.py @@ -0,0 +1,2 @@ +from .win32 import * +from . import tools \ No newline at end of file diff --git a/wxauto/utils/tools.py b/wxauto/utils/tools.py new file mode 100644 index 0000000..29c25ec --- /dev/null +++ b/wxauto/utils/tools.py @@ -0,0 +1,102 @@ +from pathlib import Path +from datetime import datetime, timedelta +import time +import re + +def get_file_dir(dir_path=None): + if dir_path is None: + dir_path = Path('.').absolute() + elif isinstance(dir_path, str): + dir_path = Path(dir_path) + dir_path.mkdir(parents=True, exist_ok=True) + return dir_path + +def now_time(fmt='%Y%m%d%H%M%S%f'): + return datetime.now().strftime(fmt) + +def parse_wechat_time(time_str): + """ + 时间格式转换函数 + + Args: + time_str: 输入的时间字符串 + + Returns: + 转换后的时间字符串 + """ + time_str = time_str.replace('星期天', '星期日') + match = re.match(r'^(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$', time_str) + if match: + month, day, hour, minute, second = match.groups() + current_year = datetime.now().year + return datetime(current_year, int(month), int(day), int(hour), int(minute), int(second)).strftime('%Y-%m-%d %H:%M:%S') + + match = re.match(r'^(\d{1,2}):(\d{1,2})$', time_str) + if match: + hour, minute = match.groups() + return datetime.now().strftime('%Y-%m-%d') + f' {hour}:{minute}:00' + + match = re.match(r'^昨天 (\d{1,2}):(\d{1,2})$', time_str) + if match: + hour, minute = match.groups() + yesterday = datetime.now() - timedelta(days=1) + return yesterday.strftime('%Y-%m-%d') + f' {hour}:{minute}:00' + + match = re.match(r'^星期([一二三四五六日]) (\d{1,2}):(\d{1,2})$', time_str) + if match: + weekday, hour, minute = match.groups() + weekday_num = ['一', '二', '三', '四', '五', '六', '日'].index(weekday) + today_weekday = datetime.now().weekday() + delta_days = (today_weekday - weekday_num) % 7 + target_day = datetime.now() - timedelta(days=delta_days) + return target_day.strftime('%Y-%m-%d') + f' {hour}:{minute}:00' + + match = re.match(r'^(\d{4})年(\d{1,2})月(\d{1,2})日 (\d{1,2}):(\d{1,2})$', time_str) + if match: + year, month, day, hour, minute = match.groups() + return datetime(*[int(i) for i in [year, month, day, hour, minute]]).strftime('%Y-%m-%d %H:%M:%S') + + match = re.match(r'^(\d{2})-(\d{2}) (上午|下午) (\d{1,2}):(\d{2})$', time_str) + if match: + month, day, period, hour, minute = match.groups() + current_year = datetime.now().year + hour = int(hour) + if period == '下午' and hour != 12: + hour += 12 + elif period == '上午' and hour == 12: + hour = 0 + return datetime(current_year, int(month), int(day), hour, int(minute)).strftime('%Y-%m-%d %H:%M:%S') + + return time_str + + +def roll_into_view(win, ele, equal=False, bias=0): + while ele.BoundingRectangle.ycenter() < win.BoundingRectangle.top + bias or ele.BoundingRectangle.ycenter() >= win.BoundingRectangle.bottom - bias: + if ele.BoundingRectangle.ycenter() < win.BoundingRectangle.top + bias: + # 上滚动 + while True: + if not ele.Exists(0) or not ele.BoundingRectangle.height(): + return 'not exist' + win.WheelUp(wheelTimes=1) + time.sleep(0.1) + if equal: + if ele.BoundingRectangle.ycenter() >= win.BoundingRectangle.top + bias: + break + else: + if ele.BoundingRectangle.ycenter() > win.BoundingRectangle.top + bias: + break + + elif ele.BoundingRectangle.ycenter() >= win.BoundingRectangle.bottom - bias: + # 下滚动 + while True: + if not ele.Exists(0) or not ele.BoundingRectangle.height(): + return 'not exist' + win.WheelDown(wheelTimes=1) + time.sleep(0.1) + if equal: + if ele.BoundingRectangle.ycenter() <= win.BoundingRectangle.bottom - bias: + break + else: + if ele.BoundingRectangle.ycenter() < win.BoundingRectangle.bottom - bias: + break + time.sleep(0.3) \ No newline at end of file diff --git a/wxauto/utils/win32.py b/wxauto/utils/win32.py new file mode 100644 index 0000000..451f0fd --- /dev/null +++ b/wxauto/utils/win32.py @@ -0,0 +1,302 @@ +import os +import time +import shutil +import win32ui +import win32gui +import win32api +import win32con +import win32process +import win32clipboard +import pyperclip +import ctypes +from PIL import Image +from wxauto import uiautomation as uia + +def GetAllWindows(): + """ + 获取所有窗口的信息,返回一个列表,每个元素包含 (窗口句柄, 类名, 窗口标题) + """ + windows = [] + + def enum_windows_proc(hwnd, extra): + class_name = win32gui.GetClassName(hwnd) # 获取窗口类名 + window_title = win32gui.GetWindowText(hwnd) # 获取窗口标题 + windows.append((hwnd, class_name, window_title)) + + win32gui.EnumWindows(enum_windows_proc, None) + return windows + +def GetCursorWindow(): + x, y = win32api.GetCursorPos() + hwnd = win32gui.WindowFromPoint((x, y)) + window_title = win32gui.GetWindowText(hwnd) + class_name = win32gui.GetClassName(hwnd) + return hwnd, window_title, class_name + +def set_cursor_pos(x, y): + win32api.SetCursorPos((x, y)) + +def Click(rect): + x = (rect.left + rect.right) // 2 + y = (rect.top + rect.bottom) // 2 + set_cursor_pos(x, y) + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0) + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) + +def GetPathByHwnd(hwnd): + """通过窗口句柄获取进程可执行文件路径 - 使用pywin32""" + try: + thread_id, process_id = win32process.GetWindowThreadProcessId(hwnd) + + # 获取进程句柄 + PROCESS_QUERY_INFORMATION = 0x0400 + PROCESS_VM_READ = 0x0010 + process_handle = win32api.OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, + False, + process_id + ) + + # 获取可执行文件路径 + exe_path = win32process.GetModuleFileNameEx(process_handle, 0) + + # 关闭句柄 + win32api.CloseHandle(process_handle) + + return exe_path + except Exception as e: + print(f"Error: {e}") + return None + +def GetVersionByPath(file_path): + try: + info = win32api.GetFileVersionInfo(file_path, '\\') + version = "{}.{}.{}.{}".format(win32api.HIWORD(info['FileVersionMS']), + win32api.LOWORD(info['FileVersionMS']), + win32api.HIWORD(info['FileVersionLS']), + win32api.LOWORD(info['FileVersionLS'])) + except: + version = None + return version + +def GetWindowRect(hwnd): + return win32gui.GetWindowRect(hwnd) + +def capture(hwnd, bbox): + # 获取窗口的屏幕坐标 + window_rect = win32gui.GetWindowRect(hwnd) + win_left, win_top, win_right, win_bottom = window_rect + win_width = win_right - win_left + win_height = win_bottom - win_top + + # 获取窗口的设备上下文 + hwndDC = win32gui.GetWindowDC(hwnd) + mfcDC = win32ui.CreateDCFromHandle(hwndDC) + saveDC = mfcDC.CreateCompatibleDC() + + # 创建位图对象保存整个窗口截图 + saveBitMap = win32ui.CreateBitmap() + saveBitMap.CreateCompatibleBitmap(mfcDC, win_width, win_height) + saveDC.SelectObject(saveBitMap) + + # 使用PrintWindow捕获整个窗口(包括被遮挡或最小化的窗口) + result = ctypes.windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 3) + + # 转换为PIL图像 + bmpinfo = saveBitMap.GetInfo() + bmpstr = saveBitMap.GetBitmapBits(True) + im = Image.frombuffer( + 'RGB', + (bmpinfo['bmWidth'], bmpinfo['bmHeight']), + bmpstr, 'raw', 'BGRX', 0, 1) + + # 释放资源 + win32gui.DeleteObject(saveBitMap.GetHandle()) + saveDC.DeleteDC() + mfcDC.DeleteDC() + win32gui.ReleaseDC(hwnd, hwndDC) + + # 计算bbox相对于窗口左上角的坐标 + bbox_left, bbox_top, bbox_right, bbox_bottom = bbox + # 转换为截图图像中的相对坐标 + crop_left = bbox_left - win_left + crop_top = bbox_top - win_top + crop_right = bbox_right - win_left + crop_bottom = bbox_bottom - win_top + + # 裁剪目标区域 + cropped_im = im.crop((crop_left, crop_top, crop_right, crop_bottom)) + + return cropped_im + +def GetText(HWND): + length = win32gui.SendMessage(HWND, win32con.WM_GETTEXTLENGTH)*2 + buffer = win32gui.PyMakeBuffer(length) + win32api.SendMessage(HWND, win32con.WM_GETTEXT, length, buffer) + address, length_ = win32gui.PyGetBufferAddressAndLen(buffer[:-1]) + text = win32gui.PyGetString(address, length_)[:int(length/2)] + buffer.release() + return text + +def GetAllWindowExs(HWND): + if not HWND: + return + handles = [] + win32gui.EnumChildWindows( + HWND, lambda hwnd, param: param.append([hwnd, win32gui.GetClassName(hwnd), GetText(hwnd)]), handles) + return handles + +def FindWindow(classname=None, name=None, timeout=0) -> int: + t0 = time.time() + while True: + HWND = win32gui.FindWindow(classname, name) + if HWND: + break + if time.time() - t0 > timeout: + break + time.sleep(0.01) + return HWND + +def FindTopLevelControl(classname=None, name=None, timeout=3): + hwnd = FindWindow(classname, name, timeout) + if hwnd: + return uia.ControlFromHandle(hwnd) + else: + return None + +def FindWinEx(HWND, classname=None, name=None) -> list: + hwnds_classname = [] + hwnds_name = [] + def find_classname(hwnd, classname): + classname_ = win32gui.GetClassName(hwnd) + if classname_ == classname: + if hwnd not in hwnds_classname: + hwnds_classname.append(hwnd) + def find_name(hwnd, name): + name_ = GetText(hwnd) + if name in name_: + if hwnd not in hwnds_name: + hwnds_name.append(hwnd) + if classname: + win32gui.EnumChildWindows(HWND, find_classname, classname) + if name: + win32gui.EnumChildWindows(HWND, find_name, name) + if classname and name: + hwnds = [hwnd for hwnd in hwnds_classname if hwnd in hwnds_name] + else: + hwnds = hwnds_classname + hwnds_name + return hwnds + +def ClipboardFormats(unit=0, *units): + units = list(units) + retry_count = 5 + while retry_count > 0: + try: + win32clipboard.OpenClipboard() + try: + u = win32clipboard.EnumClipboardFormats(unit) + finally: + win32clipboard.CloseClipboard() + break + except Exception as e: + retry_count -= 1 + units.append(u) + if u: + units = ClipboardFormats(u, *units) + return units + +def ReadClipboardData(): + Dict = {} + formats = ClipboardFormats() + + for i in formats: + if i == 0: + continue + + retry_count = 5 + while retry_count > 0: + try: + win32clipboard.OpenClipboard() + try: + data = win32clipboard.GetClipboardData(i) + Dict[str(i)] = data + finally: + win32clipboard.CloseClipboard() + break + except Exception as e: + retry_count -= 1 + return Dict + +def SetClipboardText(text: str): + pyperclip.copy(text) + + +class DROPFILES(ctypes.Structure): + _fields_ = [ + ("pFiles", ctypes.c_uint), + ("x", ctypes.c_long), + ("y", ctypes.c_long), + ("fNC", ctypes.c_int), + ("fWide", ctypes.c_bool), + ] + +pDropFiles = DROPFILES() +pDropFiles.pFiles = ctypes.sizeof(DROPFILES) +pDropFiles.fWide = True +matedata = bytes(pDropFiles) + +def SetClipboardFiles(paths): + for file in paths: + if not os.path.exists(file): + raise FileNotFoundError(f"file ({file}) not exists!") + files = ("\0".join(paths)).replace("/", "\\") + data = files.encode("U16")[2:]+b"\0\0" + t0 = time.time() + while True: + if time.time() - t0 > 10: + raise TimeoutError(f"设置剪贴板文件超时! --> {paths}") + try: + win32clipboard.OpenClipboard() + win32clipboard.EmptyClipboard() + win32clipboard.SetClipboardData(win32clipboard.CF_HDROP, matedata+data) + break + except: + pass + finally: + try: + win32clipboard.CloseClipboard() + except: + pass + +def PasteFile(folder): + folder = os.path.realpath(folder) + if not os.path.exists(folder): + os.makedirs(folder) + + t0 = time.time() + while True: + if time.time() - t0 > 10: + raise TimeoutError(f"读取剪贴板文件超时!") + try: + win32clipboard.OpenClipboard() + if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_HDROP): + files = win32clipboard.GetClipboardData(win32clipboard.CF_HDROP) + for file in files: + filename = os.path.basename(file) + dest_file = os.path.join(folder, filename) + shutil.copy2(file, dest_file) + return True + else: + print("剪贴板中没有文件") + return False + except: + pass + finally: + win32clipboard.CloseClipboard() + +def IsRedPixel(uicontrol): + rect = uicontrol.BoundingRectangle + hwnd = uicontrol.GetAncestorControl(lambda x,y:x.ClassName=='WeChatMainWndForPC').NativeWindowHandle + bbox = (rect.left, rect.top, rect.right, rect.bottom) + img = capture(hwnd, bbox) + return any(p[0] > p[1] and p[0] > p[2] for p in img.getdata()) \ No newline at end of file diff --git a/wxauto/wx.py b/wxauto/wx.py new file mode 100644 index 0000000..52e7565 --- /dev/null +++ b/wxauto/wx.py @@ -0,0 +1,386 @@ +from .ui.main import ( + WeChatMainWnd, + WeChatSubWnd +) +from .ui.component import ( + WeChatLoginWnd +) +from .param import ( + WxResponse, + WxParam, + PROJECT_NAME +) +from .logger import wxlog +from typing import ( + Union, + List, + Dict, + Callable, + TYPE_CHECKING +) +from PIL import Image +from abc import ABC, abstractmethod +import threading +import traceback +import time +import sys + +if TYPE_CHECKING: + from wxauto.msgs.base import Message + from wxauto.ui.sessionbox import SessionElement + + +class Listener(ABC): + def _listener_start(self): + wxlog.debug('开始监听') + self._listener_is_listening = True + self._listener_messages = {} + self._lock = threading.RLock() + self._listener_stop_event = threading.Event() + self._listener_thread = threading.Thread(target=self._listener_listen, daemon=True) + self._listener_thread.start() + + def _listener_listen(self): + if not hasattr(self, 'listen') or not self.listen: + self.listen = {} + while not self._listener_stop_event.is_set(): + try: + self._get_listen_messages() + except: + wxlog.debug(f'监听消息失败:{traceback.format_exc()}') + time.sleep(WxParam.LISTEN_INTERVAL) + + def _safe_callback(self, callback, msg, chat): + try: + with self._lock: + callback(msg, chat) + except Exception as e: + wxlog.debug(f"监听消息回调发生错误:{traceback.format_exc()}") + + def _listener_stop(self): + self._listener_is_listening = False + self._listener_stop_event.set() + self._listener_thread.join() + + @abstractmethod + def _get_listen_messages(self): + ... + +class Chat: + """微信聊天窗口实例""" + + def __init__(self, core: WeChatSubWnd=None): + self.core = core + self.who = self.core.nickname + + def __repr__(self): + return f'<{PROJECT_NAME} - {self.__class__.__name__} object("{self.core.nickname}")>' + + @property + def chat_type(self): + return self.core.chatbox.get_info().get('chat_type', None) + + def Show(self): + """显示窗口""" + self.core._show() + + def ChatInfo(self) -> Dict[str, str]: + """获取聊天窗口信息 + + Returns: + dict: 聊天窗口信息 + """ + return self.core.chatbox.get_info() + + def SendMsg( + self, + msg: str, + who: str=None, + clear: bool=True, + at: Union[str, List[str]]=None, + exact: bool=False, + ) -> WxResponse: + """发送消息 + + Args: + msg (str): 消息内容 + who (str, optional): 发送对象,不指定则发送给当前聊天对象,**当子窗口时,该参数无效** + clear (bool, optional): 发送后是否清空编辑框. + at (Union[str, List[str]], optional): @对象,不指定则不@任何人 + exact (bool, optional): 搜索who好友时是否精确匹配,默认False,**当子窗口时,该参数无效** + + Returns: + WxResponse: 是否发送成功 + """ + return self.core.send_msg(msg, who, clear, at, exact) + + def SendFiles( + self, + filepath, + who=None, + exact=False + ) -> WxResponse: + """向当前聊天窗口发送文件 + + Args: + filepath (str|list): 要复制文件的绝对路径 + who (str): 发送对象,不指定则发送给当前聊天对象,**当子窗口时,该参数无效** + exact (bool, optional): 搜索who好友时是否精确匹配,默认False,**当子窗口时,该参数无效** + + Returns: + WxResponse: 是否发送成功 + """ + return self.core.send_files(filepath, who, exact) + + def LoadMoreMessage(self, interval: float=0.3) -> WxResponse: + """加载更多消息 + + Args: + interval (float, optional): 滚动间隔,单位秒,默认0.3 + """ + return self.core.load_more_message(interval) + + def GetAllMessage(self) -> List['Message']: + """获取当前聊天窗口的所有消息 + + Returns: + List[Message]: 当前聊天窗口的所有消息 + """ + return self.core.get_msgs() + + def GetNewMessage(self) -> List['Message']: + """获取当前聊天窗口的新消息 + + Returns: + List[Message]: 当前聊天窗口的新消息 + """ + if not hasattr(self, '_last_chat'): + self._last_chat = self.ChatInfo().get('chat_name') + if (_last_chat := self.ChatInfo().get('chat_name')) != self._last_chat: + self._last_chat = _last_chat + self.core.chatbox._update_used_msg_ids() + return [] + return self.core.get_new_msgs() + + def GetMessageById(self, msg_id: str) -> 'Message': + """根据消息id获取消息 + + Args: + msg_id (str): 消息id + + Returns: + Message: 消息对象 + """ + return self.core.get_msg_by_id(msg_id) + + def GetGroupMembers(self) -> List[str]: + """获取当前聊天群成员 + + Returns: + list: 当前聊天群成员列表 + """ + return self.core.get_group_members() + + def Close(self) -> None: + """关闭微信窗口""" + self.core.close() + + +class WeChat(Chat, Listener): + """微信主窗口实例""" + + def __init__( + self, + debug: bool=False, + **kwargs + ): + hwnd = None + if 'hwnd' in kwargs: + hwnd = kwargs['hwnd'] + self.core = WeChatMainWnd(hwnd) + self.nickname = self.core.nickname + self.SessionBox = self.core.sessionbox + self.listen = {} + if debug: + wxlog.set_debug(True) + wxlog.debug('Debug mode is on') + self._listener_start() + self.Show() + + def _get_listen_messages(self): + try: + sys.stdout.flush() + except: + pass + temp_listen = self.listen.copy() + for who in temp_listen: + chat, callback = temp_listen.get(who, (None, None)) + try: + if chat is None or not chat.core.exists(): + wxlog.debug(f"窗口 {who} 已关闭,移除监听") + self.RemoveListenChat(who, close_window=False) + continue + except: + continue + with self._lock: + msgs = chat.GetNewMessage() + for msg in msgs: + wxlog.debug(f"[{msg.attr} {msg.type}]获取到新消息:{who} - {msg.content}") + chat.Show() + self._safe_callback(callback, msg, chat) + + def GetSession(self) -> List['SessionElement']: + """获取当前会话列表 + + Returns: + List[SessionElement]: 当前会话列表 + """ + return self.core.sessionbox.get_session() + + def ChatWith( + self, + who: str, + exact: bool=False, + force: bool=False, + force_wait: Union[float, int] = 0.5 + ): + """打开聊天窗口 + + Args: + who (str): 要聊天的对象 + exact (bool, optional): 搜索who好友时是否精确匹配,默认False + force (bool, optional): 不论是否匹配到都强制切换,若启用则exact参数无效,默认False + > 注:force原理为输入搜索关键字后,在等待`force_wait`秒后不判断结果直接回车,谨慎使用 + force_wait (Union[float, int], optional): 强制切换时等待时间,默认0.5秒 + + """ + return self.core.switch_chat(who, exact, force, force_wait) + + + def AddListenChat( + self, + nickname: str, + callback: Callable[['Message', str], None], + ) -> WxResponse: + """添加监听聊天,将聊天窗口独立出去形成Chat对象子窗口,用于监听 + + Args: + nickname (str): 要监听的聊天对象 + callback (Callable[[Message, str], None]): 回调函数,参数为(Message对象, 聊天名称) + """ + if nickname in self.listen: + return WxResponse.failure('该聊天已监听') + subwin = self.core.open_separate_window(nickname) + if subwin is None: + return WxResponse.failure('找不到聊天窗口') + name = subwin.nickname + chat = Chat(subwin) + self.listen[name] = (chat, callback) + return chat + + def StopListening(self, remove: bool = True) -> None: + """停止监听""" + while self._listener_thread.is_alive(): + self._listener_stop() + if remove: + listen = self.listen.copy() + for who in listen: + self.RemoveListenChat(who) + + def StartListening(self) -> None: + if not self._listener_thread.is_alive(): + self._listener_start() + + + def RemoveListenChat( + self, + nickname: str, + close_window: bool = True + ) -> WxResponse: + """移除监听聊天 + + Args: + nickname (str): 要移除的监听聊天对象 + close_window (bool, optional): 是否关闭聊天窗口. Defaults to True. + + Returns: + WxResponse: 执行结果 + """ + if nickname not in self.listen: + return WxResponse.failure('未找到监听对象') + chat, _ = self.listen[nickname] + if close_window: + chat.Close() + del self.listen[nickname] + return WxResponse.success() + + def GetNextNewMessage(self, filter_mute=False) -> Dict[str, List['Message']]: + """获取下一个新消息 + + Args: + filter_mute (bool, optional): 是否过滤掉免打扰消息. Defaults to False. + + Returns: + Dict[str, List['Message']]: 消息列表 + """ + return self.core.get_next_new_message(filter_mute) + + def SwitchToChat(self) -> None: + """切换到聊天页面""" + self.core.navigation.chat_icon.Click() + + def SwitchToContact(self) -> None: + """切换到联系人页面""" + self.core.navigation.contact_icon.Click() + + def GetSubWindow(self, nickname: str) -> 'Chat': + """获取子窗口实例 + + Args: + nickname (str): 要获取的子窗口的昵称 + + Returns: + Chat: 子窗口实例 + """ + if subwin := self.core.get_sub_wnd(nickname): + return Chat(subwin) + + def GetAllSubWindow(self) -> List['Chat']: + """获取所有子窗口实例 + + Returns: + List[Chat]: 所有子窗口实例 + """ + return [Chat(subwin) for subwin in self.core.get_all_sub_wnds()] + + def KeepRunning(self): + """保持运行""" + while not self._listener_stop_event.is_set(): + try: + time.sleep(1) + except KeyboardInterrupt: + wxlog.debug(f'wxauto("{self.nickname}") shutdown') + self.StopListening(True) + break + +class WeChatLogin: + def ClearHint(self): + if loginWnd := WeChatLoginWnd() : + return loginWnd.clear_hint() + else: + return False + + def Login(self): + """登录""" + if loginWnd := WeChatLoginWnd() : + return loginWnd.login() + else: + return False + + def GetQRCode(self) -> Image.Image: + """获取二维码""" + if loginWnd := WeChatLoginWnd() : + return loginWnd.get_qrcode() + else: + return None + \ No newline at end of file diff --git a/多群组版本使用说明.md b/多群组版本使用说明.md new file mode 100644 index 0000000..b9df259 --- /dev/null +++ b/多群组版本使用说明.md @@ -0,0 +1,295 @@ +# 微信群岗位信息提取工具 - 多群组版本使用说明 + +## 版本更新 v1.1 + +### 新功能 + +1. **支持多群组监听** - 可同时监听多个微信群 +2. **UUID岗位ID** - 每个岗位使用唯一的UUID标识 +3. **来源群组显示** - 界面显示岗位来自哪个群组 +4. **打包时配置API** - API密钥在打包前配置,更安全 + +## 配置说明 + +### 1. 配置文件 (config.json) + +```json +{ + "target_groups": [ + "招聘信息群1", + "招聘信息群2", + "求职交流群" + ], + "bailian_api_url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation", + "api_key": "your-api-key-here", + "output_file": "jobs_data.json" +} +``` + +### 2. 配置步骤 + +#### 方式一:使用配置工具(推荐) + +```bash +# 双击运行 +配置API密钥.bat + +# 按提示输入API密钥 +``` + +#### 方式二:手动编辑 + +1. 打开 `config.json` +2. 修改 `target_groups` 数组,添加要监听的群组 +3. 修改 `api_key` 为你的百炼API密钥 +4. 保存文件 + +## 使用流程 + +### 开发/测试阶段 + +1. **配置API密钥** + ```bash + # 运行配置工具 + 配置API密钥.bat + ``` + +2. **配置目标群组** + - 编辑 `config.json` + - 在 `target_groups` 数组中添加群组名称 + +3. **运行程序** + ```bash + # 直接运行Python版本 + python job_extractor_gui.py + + # 或使用启动工具 + 启动工具.bat → 选择 [1] + ``` + +4. **在GUI中调整群组** + - 启动程序后可以在界面中修改群组列表 + - 支持逗号、分号分隔 + - 点击"保存群组配置"保存 + +### 打包部署阶段 + +1. **确认配置** + - 确保 `config.json` 中的 `api_key` 已正确配置 + - 确认 `target_groups` 包含默认要监听的群组 + +2. **打包程序** + ```bash + # 使用修复脚本打包 + 修复并重新打包.bat + + # 或使用标准打包 + build.bat + ``` + +3. **分发使用** + - 将 `dist\微信岗位提取工具.exe` 分发给用户 + - 用户可以在GUI中修改群组列表 + - API密钥已内置,无需用户配置 + +## 界面说明 + +``` +┌─────────────────────────────────────────────────────┐ +│ 配置 │ +│ 目标群组: [招聘群1, 招聘群2, 求职群] [保存群组配置]│ +│ (多个群组用逗号分隔) │ +│ API密钥: sk-46cb053d...1c81a (已配置) │ +├─────────────────────────────────────────────────────┤ +│ [开始任务] [停止任务] [清空列表] [导出数据] │ +├─────────────────────────────────────────────────────┤ +│ 状态: 运行中 (3个群组) 已提取岗位: 15 │ +├─────────────────────────────────────────────────────┤ +│ 岗位列表 │ +│ ┌─────────────────────────────────────────────────┐│ +│ │序号│来源群组│岗位│公司│地点│薪资│联系│时间 │││ +│ │ 1 │招聘群1│...│...│...│...│...│... │││ +│ │ 2 │招聘群2│...│...│...│...│...│... │││ +│ │ 3 │求职群 │...│...│...│...│...│... │││ +│ └─────────────────────────────────────────────────┘│ +├─────────────────────────────────────────────────────┤ +│ 运行日志 │ +│ [10:30:00] 正在连接微信... │ +│ [10:30:01] ✓ 已连接微信 │ +│ [10:30:05] [招聘群1] 收到消息 - 发送者: 张三 │ +│ [10:30:06] [招聘群1] ✓ 提取到岗位信息 │ +│ [10:30:10] [招聘群2] 收到消息 - 发送者: 李四 │ +└─────────────────────────────────────────────────────┘ +``` + +## 功能特点 + +### 1. 多群组监听 + +- 同时监听多个微信群 +- 每个群组独立处理消息 +- 自动标记消息来源 + +### 2. 群组配置 + +支持多种分隔符: +- 逗号:`群1, 群2, 群3` +- 中文逗号:`群1,群2,群3` +- 分号:`群1; 群2; 群3` +- 换行:每行一个群组 + +### 3. UUID标识 + +每个岗位都有唯一的UUID: +```json +{ + "_id": "550e8400-e29b-41d4-a716-446655440000", + "job_name": "Python开发", + "_metadata": { + "group_name": "招聘信息群1", + ... + } +} +``` + +### 4. 来源追踪 + +- 界面显示岗位来源群组 +- 详情中显示完整来源信息 +- 日志中标记群组名称 + +## 数据格式 + +### 岗位数据结构 + +```json +{ + "_id": "uuid-string", + "job_name": "岗位名称", + "job_description": "岗位描述", + "job_location": "工作地点", + "salary_min": 15000, + "salary_max": 25000, + "company_name": "公司名称", + "contact_person": "联系人", + "contact_info": "联系方式", + "_metadata": { + "source": "wechat_group", + "group_name": "招聘信息群1", + "sender": "HR小王", + "extract_time": "2026-02-11 14:30:00", + "original_message": "原始消息内容..." + } +} +``` + +## 常见问题 + +### Q: 如何添加新的群组? +A: 两种方式: +1. 在GUI界面的群组输入框中添加,用逗号分隔 +2. 编辑 `config.json`,在 `target_groups` 数组中添加 + +### Q: 某个群组监听失败怎么办? +A: +- 检查群组名称是否完全正确(区分大小写) +- 确保该群在微信会话列表中 +- 查看日志了解具体错误信息 +- 其他群组不受影响,会继续监听 + +### Q: 如何修改API密钥? +A: +- 开发阶段:运行 `配置API密钥.bat` 或编辑 `config.json` +- 打包后:需要重新配置并打包 + +### Q: 可以监听多少个群组? +A: 理论上没有限制,但建议不超过10个,以保证性能 + +### Q: 如何区分不同群组的岗位? +A: +- 界面列表中有"来源群组"列 +- 双击查看详情可看到完整来源信息 +- 导出的JSON数据中包含 `group_name` 字段 + +### Q: UUID有什么用? +A: +- 唯一标识每个岗位 +- 便于数据去重 +- 方便数据库存储和查询 +- 支持数据同步和更新 + +## 最佳实践 + +### 1. 群组命名 + +建议使用清晰的群组名称: +- ✓ "北京招聘信息群" +- ✓ "Python开发求职群" +- ✗ "群聊" (太模糊) + +### 2. 监听策略 + +- 优先监听活跃度高的群组 +- 定期检查群组名称是否变更 +- 及时移除不活跃的群组 + +### 3. 数据管理 + +- 定期导出数据备份 +- 使用UUID进行数据去重 +- 按来源群组分类整理 + +### 4. 性能优化 + +- 不要监听过多群组 +- 定期清理历史数据 +- 关闭不需要的群组监听 + +## 技术说明 + +### 多群组实现 + +```python +# 为每个群组创建独立的监听 +for group_name in groups: + def make_callback(gname): + return lambda msg, chat: self.on_message(msg, chat, gname) + + wx.AddListenChat( + nickname=group_name, + callback=make_callback(group_name) + ) +``` + +### UUID生成 + +```python +import uuid + +# 为每个岗位生成唯一ID +job_id = str(uuid.uuid4()) +job_info["_id"] = job_id +``` + +## 更新日志 + +### v1.1 (2026-02-11) +- ✓ 支持多群组同时监听 +- ✓ 使用UUID作为岗位ID +- ✓ 界面显示来源群组 +- ✓ API密钥打包时配置 +- ✓ 优化日志显示 + +### v1.0 (2026-02-11) +- 初始版本 +- 单群组监听 +- 基础功能实现 + +## 下一步计划 + +- [ ] 群组管理界面(添加/删除/启用/禁用) +- [ ] 按群组筛选岗位 +- [ ] 群组统计信息 +- [ ] 数据去重功能 +- [ ] 导出Excel格式 +- [ ] 数据库存储支持 diff --git a/岗位提取使用说明.md b/岗位提取使用说明.md new file mode 100644 index 0000000..1aeedd0 --- /dev/null +++ b/岗位提取使用说明.md @@ -0,0 +1,111 @@ +# 微信群岗位信息提取工具使用说明 + +## 功能说明 + +本工具可以自动监听指定微信群的消息,使用阿里云百炼API智能提取招聘岗位信息,并保存为结构化JSON数据。 + +## 提取的信息字段 + +- **job_name**: 工作名称 +- **job_description**: 工作描述 +- **job_location**: 工作地点 +- **salary_min**: 月薪最低(元) +- **salary_max**: 月薪最高(元) +- **company_name**: 公司名称 +- **contact_person**: 联系人 +- **contact_info**: 联系方式 + +## 使用步骤 + +### 1. 安装依赖 + +```bash +pip install -e . +pip install requests +``` + +### 2. 配置文件 + +编辑 `config.json` 文件,配置以下参数: + +```json +{ + "target_group": "招聘信息群", + "bailian_api_url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation", + "api_key": "sk-46cb053d75eb4ad88713917ba0f1c81a", + "check_interval": 5, + "output_file": "jobs_data.json" +} +``` + +参数说明: +- `target_group`: 要监听的微信群名称(必须完全匹配) +- `bailian_api_url`: 百炼API地址 +- `api_key`: 百炼API密钥 +- `check_interval`: 消息检查间隔(秒) +- `output_file`: 输出文件名 + +### 3. 运行程序 + +确保微信已登录并打开主窗口,然后运行: + +```bash +python job_extractor.py +``` + +### 4. 停止监听 + +按 `Ctrl+C` 停止程序 + +## 输出文件格式 + +提取的岗位信息会保存到 `jobs_data.json` 文件中,格式示例: + +```json +[ + { + "job_name": "Python开发工程师", + "job_description": "负责后端开发,熟悉Django/Flask框架", + "job_location": "北京市朝阳区", + "salary_min": 15000, + "salary_max": 25000, + "company_name": "某科技公司", + "contact_person": "张经理", + "contact_info": "13800138000", + "_metadata": { + "source": "wechat_group", + "group_name": "招聘信息群", + "sender": "HR小王", + "extract_time": "2026-02-11 10:30:00", + "original_message": "招聘Python开发..." + } + } +] +``` + +## 注意事项 + +1. 确保微信版本为 3.9.x +2. 微信必须保持登录状态 +3. 群名称必须完全匹配(区分大小写) +4. API密钥请妥善保管,不要泄露 +5. 程序会持续运行直到手动停止 + +## 常见问题 + +### Q: 提示"找不到聊天窗口" +A: 请检查 `config.json` 中的 `target_group` 是否与微信群名称完全一致 + +### Q: API调用失败 +A: 请检查: +- API密钥是否正确 +- 网络连接是否正常 +- API额度是否充足 + +### Q: 没有提取到岗位信息 +A: 可能原因: +- 消息内容不包含招聘信息 +- 消息格式不规范 +- API识别失败 + +可以查看控制台输出的详细日志进行排查 diff --git a/快速修复指南.txt b/快速修复指南.txt new file mode 100644 index 0000000..adcd3d0 --- /dev/null +++ b/快速修复指南.txt @@ -0,0 +1,96 @@ +╔════════════════════════════════════════════════════════════╗ +║ 微信岗位提取工具 - COM错误快速修复指南 ║ +╚════════════════════════════════════════════════════════════╝ + +【问题】 +打包后的exe运行时报错: +[WinError -2147221008] 尚未调用 CoInitialize。 + +【原因】 +wxauto使用Windows COM组件,在多线程环境需要初始化。 + +【解决方案 - 三选一】 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +方案1:一键修复(推荐)⭐ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +双击运行:修复并重新打包.bat + +脚本会自动: +✓ 安装pywin32 +✓ 清理旧文件 +✓ 重新打包 +✓ 生成新的exe + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +方案2:使用启动工具 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +1. 双击:启动工具.bat +2. 选择:[6] 安装/更新依赖 +3. 等待安装完成 +4. 选择:[5] 打包成 EXE 文件 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +方案3:手动修复 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +打开命令行,依次执行: + +1. 安装依赖 + pip install pywin32 + +2. 验证修复 + python test_com_fix.py + +3. 重新打包 + pyinstaller build_exe.spec + +4. 测试运行 + dist\微信岗位提取工具.exe + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +【验证步骤】 + +1. 确保微信3.9已登录 +2. 运行打包后的exe +3. 配置目标群组和API密钥 +4. 点击"开始任务" +5. 查看是否能正常连接微信 + +如果能正常连接,说明修复成功!✓ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +【仍有问题?】 + +1. 查看详细文档: + - 打包问题修复说明.md + - GUI版本使用说明.md + +2. 检查环境: + - Python版本:3.9+ + - 微信版本:3.9.x + - 系统:Windows 10/11 + +3. 常见问题: + - 杀毒软件拦截 → 添加白名单 + - 权限不足 → 以管理员运行 + - 缺少DLL → 安装VC++ Redistributable + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +【不想打包?】 + +直接运行Python版本: +python job_extractor_gui.py + +或使用启动工具: +双击:启动工具.bat → 选择 [1] + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +更新时间:2026-02-11 +版本:v1.0 diff --git a/快速开始.md b/快速开始.md new file mode 100644 index 0000000..26fb9e8 --- /dev/null +++ b/快速开始.md @@ -0,0 +1,198 @@ +# 快速开始指南 + +## 5分钟上手微信群岗位提取工具 + +### 第一步:安装依赖(首次使用) + +```bash +# 双击运行 +启动工具.bat + +# 选择 [6] 安装/更新依赖 +``` + +或手动安装: +```bash +pip install -e . +pip install requests pywin32 +``` + +### 第二步:配置API密钥 + +```bash +# 双击运行 +配置API密钥.bat + +# 输入你的百炼API密钥 +``` + +### 第三步:配置监听群组 + +编辑 `config.json`: +```json +{ + "target_groups": [ + "你的招聘群1", + "你的招聘群2" + ], + "api_key": "已配置的密钥" +} +``` + +### 第四步:运行程序 + +#### 方式A:直接运行(开发测试) + +```bash +# 双击运行 +启动工具.bat + +# 选择 [1] 启动 GUI 版本 +``` + +#### 方式B:打包成EXE(分发部署) + +```bash +# 双击运行 +修复并重新打包.bat + +# 等待打包完成 +# 运行 dist\微信岗位提取工具.exe +``` + +### 第五步:开始提取 + +1. 确保微信已登录 +2. 在程序界面点击"开始任务" +3. 等待连接微信 +4. 查看提取的岗位信息 + +## 界面操作 + +### 配置区域 +- **目标群组**:输入要监听的群组,多个用逗号分隔 +- **保存群组配置**:保存修改的群组列表 + +### 控制按钮 +- **开始任务**:开始监听微信群 +- **停止任务**:停止监听 +- **清空列表**:清空所有岗位数据 +- **导出数据**:导出为JSON文件 + +### 岗位列表 +- 显示所有提取的岗位 +- 双击查看详细信息 +- 包含来源群组、岗位、公司、薪资等 + +### 运行日志 +- 显示程序运行状态 +- 消息接收情况 +- 提取结果 + +## 常见场景 + +### 场景1:个人使用(测试) + +```bash +1. 安装依赖 +2. 配置API密钥 +3. 编辑config.json添加群组 +4. 运行:python job_extractor_gui.py +5. 点击"开始任务" +``` + +### 场景2:团队部署(打包) + +```bash +1. 配置好config.json(包含API密钥和默认群组) +2. 运行:修复并重新打包.bat +3. 分发:dist\微信岗位提取工具.exe +4. 用户可在界面修改群组列表 +``` + +### 场景3:多群组监听 + +```json +// config.json +{ + "target_groups": [ + "北京招聘群", + "上海招聘群", + "深圳招聘群", + "Python开发群", + "前端求职群" + ] +} +``` + +## 验证清单 + +使用前请确认: + +- [ ] Python 3.9+ 已安装 +- [ ] 微信 3.9.x 已登录 +- [ ] 依赖包已安装 +- [ ] API密钥已配置 +- [ ] 群组名称正确(完全匹配) +- [ ] 群组在微信会话列表中 + +## 故障排查 + +### 问题:连接微信失败 +``` +解决: +1. 确认微信已登录 +2. 确认微信版本为3.9.x +3. 重启微信后再试 +``` + +### 问题:添加监听失败 +``` +解决: +1. 检查群组名称是否正确 +2. 确认群组在会话列表中 +3. 尝试先在微信中打开该群 +``` + +### 问题:API调用失败 +``` +解决: +1. 检查API密钥是否正确 +2. 确认网络连接正常 +3. 检查API额度是否充足 +``` + +### 问题:打包后报COM错误 +``` +解决: +1. 运行:修复并重新打包.bat +2. 确保已安装pywin32 +3. 查看:打包问题修复说明.md +``` + +## 下一步 + +- 查看 [多群组版本使用说明.md](多群组版本使用说明.md) 了解详细功能 +- 查看 [打包问题修复说明.md](打包问题修复说明.md) 解决打包问题 +- 查看 [项目说明.md](项目说明.md) 了解项目结构 + +## 获取帮助 + +遇到问题?查看这些文档: + +1. **快速修复指南.txt** - 常见问题快速解决 +2. **多群组版本使用说明.md** - 完整功能说明 +3. **打包问题修复说明.md** - 打包相关问题 +4. **GUI版本使用说明.md** - 界面操作说明 + +## 提示 + +- 💡 首次使用建议先测试单个群组 +- 💡 群组名称必须完全匹配(区分大小写) +- 💡 API密钥打包后无法修改,需重新打包 +- 💡 建议定期导出数据备份 +- 💡 监听的群组不要太多(建议≤10个) + +--- + +开始使用吧!祝你提取顺利!🎉 diff --git a/打包问题修复说明.md b/打包问题修复说明.md new file mode 100644 index 0000000..af24fae --- /dev/null +++ b/打包问题修复说明.md @@ -0,0 +1,159 @@ +# 打包问题修复说明 + +## 问题描述 + +打包后的exe运行时报错: +``` +[WinError -2147221008] 尚未调用 CoInitialize。 +``` + +## 问题原因 + +这是因为wxauto使用了Windows的COM组件(UIAutomation),在多线程环境中需要显式初始化COM组件。打包后的exe环境与开发环境不同,需要手动初始化。 + +## 解决方案 + +### 1. 安装pywin32 + +```bash +pip install pywin32 +``` + +### 2. 代码修改 + +在 `job_extractor_gui.py` 中: + +1. 导入pythoncom模块: +```python +import pythoncom +``` + +2. 在线程函数中初始化COM: +```python +def run_task(self): + # 初始化COM组件(每个线程都需要) + pythoncom.CoInitialize() + + try: + # 原有代码... + pass + finally: + # 清理COM组件 + pythoncom.CoUninitialize() +``` + +### 3. 更新打包配置 + +在 `build_exe.spec` 中添加pywin32相关模块: +```python +hiddenimports=[ + # ... 其他模块 + 'pythoncom', + 'pywintypes', + 'win32com', + 'win32com.client', + 'win32api', + 'win32con', + 'win32gui', + 'win32process', +], +``` + +## 重新打包步骤 + +### 方法一:使用批处理脚本(推荐) + +1. 双击运行 `build.bat` +2. 脚本会自动检查并安装pywin32 +3. 等待打包完成 + +### 方法二:手动打包 + +```bash +# 1. 安装依赖 +pip install pywin32 pyinstaller + +# 2. 清理旧文件 +rmdir /s /q build dist + +# 3. 打包 +pyinstaller build_exe.spec + +# 4. 测试 +dist\微信岗位提取工具.exe +``` + +## 验证修复 + +1. 运行打包后的exe +2. 配置目标群组和API密钥 +3. 点击"开始任务" +4. 如果能正常连接微信并监听,说明修复成功 + +## 其他可能的问题 + +### 问题1:缺少DLL文件 + +如果提示缺少某些DLL文件,可能需要: +- 安装 Visual C++ Redistributable +- 或使用 `--collect-all pywin32` 选项 + +### 问题2:杀毒软件拦截 + +某些杀毒软件可能会拦截打包后的exe: +- 添加到白名单 +- 或临时关闭杀毒软件 + +### 问题3:权限问题 + +如果提示权限不足: +- 以管理员身份运行 +- 检查文件夹权限 + +## 测试清单 + +打包完成后,请测试以下功能: + +- [ ] 程序能正常启动 +- [ ] 配置能正常保存 +- [ ] 能连接微信 +- [ ] 能添加监听 +- [ ] 能接收消息 +- [ ] 能提取岗位信息 +- [ ] 能显示在列表中 +- [ ] 能查看详情 +- [ ] 能导出数据 + +## 技术说明 + +### COM组件初始化 + +COM(Component Object Model)是Windows的组件技术。使用COM组件的程序需要: + +1. **单线程公寓(STA)**:主线程自动初始化 +2. **多线程公寓(MTA)**:每个线程需要手动初始化 + +wxauto使用UIAutomation(基于COM),在GUI程序的工作线程中需要: +- 调用 `pythoncom.CoInitialize()` 初始化 +- 调用 `pythoncom.CoUninitialize()` 清理 + +### PyInstaller打包 + +PyInstaller打包时需要注意: +- 显式声明隐藏导入(hiddenimports) +- 包含数据文件(datas) +- 收集所有相关包(collect-all) + +## 参考资料 + +- [PyWin32 文档](https://github.com/mhammond/pywin32) +- [PyInstaller 文档](https://pyinstaller.org/) +- [COM 初始化](https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitialize) + +## 更新日志 + +### 2026-02-11 +- 修复COM组件初始化问题 +- 添加pywin32依赖 +- 更新打包配置 +- 完善错误处理 diff --git a/项目说明.md b/项目说明.md new file mode 100644 index 0000000..3cc3530 --- /dev/null +++ b/项目说明.md @@ -0,0 +1,266 @@ +# 微信群岗位信息提取工具 + +## 项目简介 + +这是一个基于wxauto和阿里云百炼API的智能招聘信息提取工具,可以自动监听微信群消息,提取结构化的岗位信息。 + +## 项目结构 + +``` +. +├── wxauto/ # wxauto核心库 +├── job_extractor.py # 命令行版本 +├── job_extractor_gui.py # GUI版本(推荐) +├── test_api.py # API测试工具 +├── view_jobs.py # 数据查看工具 +├── config.json # 配置文件 +├── jobs_data.json # 岗位数据(自动生成) +├── build_exe.py # 打包脚本(Python) +├── build_exe.spec # PyInstaller配置 +├── build.bat # 打包脚本(批处理) +├── run_gui.bat # 快速启动脚本 +├── requirements.txt # 依赖列表 +├── 岗位提取使用说明.md # 命令行版使用说明 +├── GUI版本使用说明.md # GUI版使用说明 +└── 项目说明.md # 本文件 +``` + +## 两个版本对比 + +### 命令行版本 (job_extractor.py) + +优点: +- 轻量级,资源占用少 +- 适合服务器部署 +- 日志输出清晰 + +缺点: +- 需要命令行操作 +- 无可视化界面 +- 不够直观 + +### GUI版本 (job_extractor_gui.py) ⭐推荐 + +优点: +- 图形化界面,操作简单 +- 实时显示岗位列表 +- 可查看岗位详情 +- 支持数据导出 +- 可打包成exe独立运行 + +缺点: +- 资源占用稍多 +- 打包后文件较大 + +## 快速开始 + +### 1. 安装依赖 + +```bash +# 安装wxauto +pip install -e . + +# 安装其他依赖 +pip install requests + +# 如需打包exe +pip install pyinstaller +``` + +### 2. 配置 + +编辑 `config.json`: + +```json +{ + "target_group": "招聘信息群", + "bailian_api_url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation", + "api_key": "your-api-key-here", + "output_file": "jobs_data.json" +} +``` + +### 3. 运行 + +#### 方式一:GUI版本(推荐) + +```bash +# 直接运行 +python job_extractor_gui.py + +# 或双击 +run_gui.bat +``` + +#### 方式二:命令行版本 + +```bash +python job_extractor.py +``` + +### 4. 打包成EXE(可选) + +```bash +# 方式一:使用批处理脚本 +双击 build.bat + +# 方式二:使用Python脚本 +python build_exe.py + +# 方式三:直接使用PyInstaller +pyinstaller build_exe.spec +``` + +生成的exe文件在 `dist` 目录下。 + +## 功能说明 + +### 核心功能 + +1. **微信群监听** + - 自动连接微信 + - 监听指定群组消息 + - 实时处理新消息 + +2. **智能信息提取** + - 使用百炼API分析消息 + - 提取结构化岗位信息 + - 支持多种消息格式 + +3. **数据管理** + - 自动保存岗位数据 + - JSON格式存储 + - 支持数据导出 + +### 提取的信息 + +- 工作名称 (job_name) +- 工作描述 (job_description) +- 工作地点 (job_location) +- 月薪最低 (salary_min) +- 月薪最高 (salary_max) +- 公司名称 (company_name) +- 联系人 (contact_person) +- 联系方式 (contact_info) + +### 元数据 + +每条岗位信息还包含: +- 来源群组 +- 发送者 +- 提取时间 +- 原始消息 + +## 使用场景 + +1. **HR招聘** + - 自动收集招聘信息 + - 整理岗位数据 + - 分析市场行情 + +2. **求职者** + - 监控招聘群 + - 及时获取岗位信息 + - 筛选合适机会 + +3. **猎头公司** + - 批量收集岗位 + - 建立岗位数据库 + - 提高工作效率 + +## 技术栈 + +- **Python 3.9+** +- **wxauto** - 微信自动化 +- **tkinter** - GUI界面 +- **requests** - HTTP请求 +- **阿里云百炼API** - 智能信息提取 +- **PyInstaller** - 打包工具 + +## 系统要求 + +- 操作系统:Windows 10/11 +- 微信版本:3.9.x +- Python版本:3.9+ +- 网络:需要访问百炼API + +## 注意事项 + +1. **合规使用** + - 仅用于学习和个人使用 + - 遵守微信使用规范 + - 不得用于商业用途 + +2. **隐私保护** + - 妥善保管API密钥 + - 注意数据安全 + - 不要泄露他人信息 + +3. **稳定性** + - 保持微信登录状态 + - 确保网络连接稳定 + - 定期备份数据 + +## 常见问题 + +### 打包相关 + +#### Q: 打包后运行报错 "尚未调用 CoInitialize" +A: 这是COM组件初始化问题,已在新版本中修复。解决方法: +1. 运行 `修复并重新打包.bat` 自动修复并重新打包 +2. 或手动安装 `pip install pywin32` 后重新打包 +3. 详见 [打包问题修复说明.md](打包问题修复说明.md) + +#### Q: 如何验证修复是否成功 +A: 运行测试脚本: +```bash +python test_com_fix.py +``` +如果所有测试通过,说明修复成功。 + +详见各版本的使用说明文档: +- [命令行版使用说明](岗位提取使用说明.md) +- [GUI版使用说明](GUI版本使用说明.md) + +## 测试工具 + +### 1. API测试 + +```bash +python test_api.py +``` + +测试百炼API连接和提取功能。 + +### 2. 数据查看 + +```bash +python view_jobs.py +``` + +查看已提取的岗位数据,包括统计信息。 + +## 开发计划 + +- [ ] 支持多群组同时监听 +- [ ] 添加岗位筛选功能 +- [ ] 支持导出Excel格式 +- [ ] 添加数据统计图表 +- [ ] 支持自定义提取字段 +- [ ] 添加消息推送功能 + +## 更新日志 + +### v1.0 (2026-02-11) +- 初始版本发布 +- 支持命令行和GUI两种模式 +- 集成百炼API智能提取 +- 支持打包成exe + +## 许可证 + +本项目仅供学习交流使用,请勿用于商业用途。 + +## 免责声明 + +本工具基于UIAutomation技术开发,仅用于技术交流学习。使用本工具产生的任何法律纠纷,均与作者无关。请遵守相关法律法规和平台使用规范。