├── version.py ├── res ├── uispy.conf ├── file.png ├── folder.png ├── startup.png ├── qt4i_osx.icns └── qt4i_win.ico ├── requirements.txt ├── .gitignore ├── .pydevproject ├── .project ├── util ├── __init__.py └── logger.py ├── ui ├── __init__.py ├── app.py ├── sandboxframe.py └── mainframe.py ├── rpc ├── __init__.py ├── driver.py └── client.py ├── settings.py ├── README.md ├── uispy.spec ├── LICENSE.TXT └── CONTRIBUTING.md /version.py: -------------------------------------------------------------------------------- 1 | VERSION = "2.1.11" 2 | -------------------------------------------------------------------------------- /res/uispy.conf: -------------------------------------------------------------------------------- 1 | [uispy] 2 | bundle_id = com.apple.Maps 3 | 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | wxPython==4.0.0b1 2 | PyInstaller==3.3.1 3 | qt4i -------------------------------------------------------------------------------- /res/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtacore/iOSUISpy/HEAD/res/file.png -------------------------------------------------------------------------------- /res/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtacore/iOSUISpy/HEAD/res/folder.png -------------------------------------------------------------------------------- /res/startup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtacore/iOSUISpy/HEAD/res/startup.png -------------------------------------------------------------------------------- /res/qt4i_osx.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtacore/iOSUISpy/HEAD/res/qt4i_osx.icns -------------------------------------------------------------------------------- /res/qt4i_win.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtacore/iOSUISpy/HEAD/res/qt4i_win.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # python编译文件 2 | *.pyc 3 | # eclipse生成的备份文件 4 | *.bak 5 | # 打包自动生成的目录 6 | build/ 7 | dist/ 8 | # 日志文件 9 | *.log 10 | # 调试目录 11 | debug/ 12 | # Mac系统自动生成的文件 13 | *.DS_Store 14 | # virtual python环境,用于安装pyinstaller工具 15 | venv/ 16 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /${PROJECT_DIR_NAME} 5 | 6 | python 2.7 7 | Default 8 | 9 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | UISpy 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /util/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*-# 2 | # Tencent is pleased to support the open source community by making QTA available. 3 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 4 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 5 | # file except in compliance with the License. You may obtain a copy of the License at 6 | # 7 | # https://opensource.org/licenses/BSD-3-Clause 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed 10 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 11 | # OF ANY KIND, either express or implied. See the License for the specific language 12 | # governing permissions and limitations under the License. 13 | # 14 | -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | '''UISpy 16 | ''' -------------------------------------------------------------------------------- /rpc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | '''QT4i rpc相关接口的封装 16 | ''' -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | '''配置参数 16 | ''' 17 | 18 | import sys 19 | 20 | if getattr(sys, 'frozen', False): 21 | # we are running in a bundle 22 | RESOURCE_PATH = sys._MEIPASS # @UndefinedVariable 23 | else: 24 | # we are running in a normal Python environment 25 | RESOURCE_PATH = '../res' 26 | 27 | -------------------------------------------------------------------------------- /ui/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | '''UISpy App启动入口 16 | ''' 17 | 18 | 19 | 20 | import wx 21 | 22 | from mainframe import MainFrame 23 | from util.logger import Log 24 | 25 | 26 | class UISpyApp(wx.App): 27 | 28 | def OnInit(self): 29 | self.main = MainFrame() 30 | self.main.Center() 31 | self.main.Show() 32 | self.SetTopWindow(self.main) 33 | return True 34 | 35 | 36 | if __name__ == '__main__': 37 | try: 38 | Log.i('main','UISpy started...') 39 | app = UISpyApp() 40 | app.MainLoop() 41 | except: 42 | import traceback 43 | message = traceback.format_exc() 44 | Log.e('main', message) 45 | dialog = wx.MessageDialog(None, message, u"错误", wx.OK|wx.ICON_ERROR) 46 | dialog.ShowModal() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOSUISpy 2 | 3 | iOSUISpy is a UI Tool for [QT4i](https://github.com/tencent/qt4i) to inspect QPath of iOS controls for iOS App. 4 | 5 | ## Features 6 | * Inspect QPath or id of controls for any app in iOS device 7 | * Support sandbox viewing of iOS app 8 | * Support iOS app installation and uninstallation 9 | * Supoort iOS device remote control 10 | 11 | 12 | ## Get Started 13 | 14 | ### How to install iOSUISpy 15 | 16 | - Download [iOSUISpy Release Version](https://github.com/qtacore/iOSUISpy/releases) with the suffix ".dmg". 17 | - Move UISpy app to " /Applications" directory. 18 | 19 | ### How to use iOSUISpy 20 | 21 | - inspect QPath of iOS controls for iOS App 22 | - Open iOSUISpy by click icon of iOSUISpy. 23 | - Click "连接" button and wait until device is connected. 24 | - Select ios device and select ios app by bundle id. 25 | - Click "启动App" button and wait until sceenshot and ui tree of app appear. 26 | - Operate app to specified page and click "获取控件" button, and repeat this step for inspecting QPath. 27 | 28 | 29 | ### How to debug iOSUISpy project 30 | 31 | #### Debug with iOSUISpy source 32 | 33 | - Make sure that you are in Python 2.7 environment on MacOS system 34 | 35 | - Enter iOSUISpy project directory and install requirements library in terminal: 36 | ```shell 37 | $ pip install -i requirements.txt --user 38 | ``` 39 | 40 | - Select 'ui/app.py' file and run 41 | 42 | #### Build iOSUISpy Release version 43 | 44 | Run command line in Terminal in iOSUISpy project root directory, and the executable app will appear in the directory of "dist". 45 | ```shell 46 | $ pyinstaller --windowed --clean --noconfirm --onedir uispy.spec 47 | ``` 48 | -------------------------------------------------------------------------------- /uispy.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | import os 4 | import sys 5 | 6 | 7 | block_cipher = None 8 | 9 | 10 | project_path = os.getcwd() 11 | pkgs = [project_path] 12 | for filename in os.listdir(project_path): 13 | filepath = os.path.join(project_path, filename) 14 | if os.path.isdir(filepath): 15 | if '__init__.py' in os.listdir(filepath): 16 | pkgs.append(filepath) 17 | 18 | 19 | a = Analysis(['ui/app.py'], 20 | pathex=pkgs, 21 | binaries=[], 22 | datas=[('res','.')], 23 | hiddenimports=[], 24 | hookspath=[], 25 | runtime_hooks=[], 26 | excludes=[], 27 | win_no_prefer_redirects=False, 28 | win_private_assemblies=False, 29 | cipher=block_cipher) 30 | pyz = PYZ(a.pure, a.zipped_data, 31 | cipher=block_cipher) 32 | 33 | import version 34 | VERSION = version.VERSION 35 | 36 | if sys.platform == 'win32': 37 | exe = EXE(pyz, 38 | a.scripts, 39 | a.binaries, 40 | a.zipfiles, 41 | a.datas, 42 | name='UISpy', 43 | icon='res\\qt4i_win.ico', 44 | debug=False, 45 | strip=False, 46 | upx=True, 47 | console=False ) 48 | else: 49 | exe = EXE(pyz, 50 | a.scripts, 51 | exclude_binaries=True, 52 | name='UISpy', 53 | debug=False, 54 | strip=False, 55 | upx=True, 56 | console=True ) 57 | 58 | coll = COLLECT(exe, 59 | a.binaries, 60 | a.zipfiles, 61 | a.datas, 62 | strip=False, 63 | upx=True, 64 | name='UISpy') 65 | 66 | app = BUNDLE(coll, 67 | name='UISpy.app', 68 | icon='res/qt4i_osx.icns', 69 | info_plist={ 70 | 'CFBundleName': 'UISpy', 71 | 'CFBundleDisplayName': 'UISpy', 72 | 'CFBundleGetInfoString': "QT4i UISpy", 73 | 'CFBundleIdentifier': "com.tencent.ios.uispy", 74 | 'CFBundleVersion': VERSION, 75 | 'CFBundleShortVersionString': VERSION, 76 | 'NSHumanReadableCopyright': u"Copyright(c)2010-2017 Tencent All Rights Reserved." 77 | }, 78 | ) 79 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | Tencent is pleased to support the open source community by making QTA available. 2 | Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 3 | If you have downloaded a copy of the QTA binary from Tencent, please note that the QTA binary is licensed under the BSD 3-Clause License. 4 | If you have downloaded a copy of the QTA source code from Tencent, please note that QTAsource code is licensed under the BSD 3-Clause License, except for the third-party components listed below which are subject to different license terms. Your integration ofQTA into your own projects may require compliance with the BSD 3-ClauseLicense, as well as the other licenses applicable to the third-party components included within QTA. 5 | A copy of theBSD 3-Clause License is included in this file. 6 | 7 | Terms of the BSD 3-Clause License: 8 | -------------------------------------------------------------------- 9 | 10 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 11 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | 3. Neither the name of [copyright holder] nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to iOSUISpy 2 | Welcome to [report Issues](https://github.com/qtacore/iOSUISpy/issues) or [pull requests](https://github.com/qtacore/iOSUISpy/pulls). It's recommended to read the following Contributing Guild first to make contributing earlier. 3 | 4 | ## issues 5 | We use Git Issues to track public bugs and feature requests. 6 | 7 | ### Search Known Issues First 8 | Please search the exist issues to see if any similar issue or feature request has already been filed. You shold try to make sure your issue doesn't already exist. 9 | 10 | ### Reporting New Issues 11 | If you open an issue, the more information the better. Such as detailed description, screenshot or video of your problem, logcat or code blocks for your crash. 12 | 13 | ## Pull Requests 14 | We strongly welcome your pull request to make iOSUISpy better. 15 | 16 | ### Branch Management 17 | There are three main branch here: 18 | 19 | 1. `master` branch. 20 | 1. It is the latest (pre-)release branch. We use `master` for tag, with version number `1.1.0`, `1.2.0`, `1.3.0`... 21 | 2. **Don't submit any PR on `master` branch.** 22 | 2. `dev` branch. 23 | 1. It is our stable developing branch. After full testing, `dev` will publish to `master` branch for the next release. 24 | 2. **You are recommended to submit bugfix or feature PR on `dev` branch.** 25 | 3. `hotfix` branch. 26 | 1. It is the latest tag version for hot fix. If we accept your pull request, we may just tag with version number `1.1.1`, `1.2.3`. 27 | 2. **Only submit urgent PR on `hotfix` branch for next specific release.** 28 | 29 | Normal bugfix or feature request should submit on `dev` branch. After full testing, we will merge them on `master` branch for the next release. 30 | 31 | If you have some urgent bugfix on a published version, but the `master` branch have already far away with the latest tag version, you can submit a PR on hotfix. And it will be cherry picked to `dev` branch if it is possible. 32 | 33 | ``` 34 | master 35 | ↑ 36 | dev <--- hotfix PR 37 | ↑ 38 | feature/bugfix PR 39 | ``` 40 | 41 | ### Make Pull Requests 42 | The code team will monitor all pull request, we run some code check and test on it. After all tests passing, we will accecpt this pr. But it won't merge to `master` branch at once, which have some delay. 43 | 44 | Before submitting a pull request, please make sure the following is done 45 | 46 | 1. Fork the repo and create your branch from `master` or `hotfix`. 47 | 2. Update code or documentation if you have changed APIs. 48 | 3. Add the copyright notice to the top of any new files you've added. 49 | 4. Make sure your code lints and checkstyles. 50 | 5. Test and test again your code. 51 | 6. Now, you can submit your pull request on `dev` or `hotfix` branch. 52 | 53 | ## Code Style Guide 54 | Use [Code Style](https://www.python.org/dev/peps/pep-0008/) for Python. 55 | 56 | * 4 spaces for indentation rather than tabs 57 | 58 | ## License 59 | By contributing to iOSUISpy, you agree that your contributions will be licensed 60 | under its [BSD LICENSE](https://github.com/qtacore/iOSUISpy/blob/master/LICENSE.TXT) -------------------------------------------------------------------------------- /util/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | '''日志模块 17 | ''' 18 | 19 | 20 | import os, sys 21 | import logging 22 | from logging.handlers import RotatingFileHandler 23 | 24 | class Log(object): 25 | ''' 26 | ''' 27 | logger = None 28 | log_name = 'UISpy' 29 | 30 | @staticmethod 31 | def gen_log_path(): 32 | '''生成log存放路径 33 | ''' 34 | if getattr(sys, 'frozen', False): 35 | dir_root = os.path.dirname(sys.executable) 36 | else: 37 | dir_root = os.path.dirname(os.path.abspath(__file__)) 38 | log_name = '%s.log' % (Log.log_name) 39 | return os.path.join(dir_root, log_name) 40 | 41 | @staticmethod 42 | def get_logger(): 43 | if Log.logger == None: 44 | Log.logger = logging.getLogger(Log.log_name) 45 | if len(Log.logger.handlers) == 0: 46 | Log.logger.setLevel(logging.DEBUG) 47 | Log.logger.addHandler(logging.StreamHandler(sys.stdout)) 48 | fmt = logging.Formatter('%(asctime)s %(message)s') # %(filename)s %(funcName)s 49 | Log.logger.handlers[0].setFormatter(fmt) 50 | 51 | logger_path = Log.gen_log_path() 52 | file_handler = RotatingFileHandler(logger_path, mode='a', maxBytes=10*1024*1024, 53 | backupCount=2, encoding='utf-8', delay=0) 54 | fmt = logging.Formatter('%(asctime)s %(levelname)s %(thread)d %(message)s') # %(filename)s %(funcName)s 55 | file_handler.setFormatter(fmt) 56 | Log.logger.addHandler(file_handler) 57 | return Log.logger 58 | 59 | @staticmethod 60 | def call(func, tag, *args): 61 | ''' 62 | ''' 63 | msg = '[%s] %s' % (tag, ' '.join([arg.encode('utf8') if isinstance(arg, unicode) else arg for arg in args])) 64 | func = getattr(Log.get_logger(), func) 65 | return func(msg) 66 | 67 | @staticmethod 68 | def d(tag, *args): 69 | return Log.call('debug', tag, *args) 70 | 71 | @staticmethod 72 | def i(tag, *args): 73 | return Log.call('info', tag, *args) 74 | 75 | @staticmethod 76 | def w(tag, *args): 77 | return Log.call('warn', tag, *args) 78 | 79 | @staticmethod 80 | def e(tag, *args): 81 | return Log.call('error', tag, *args) 82 | 83 | @staticmethod 84 | def ex(tag, *args): 85 | return Log.call('exception', tag, *args) 86 | 87 | -------------------------------------------------------------------------------- /ui/sandboxframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | ''' 16 | sandbox界面 17 | ''' 18 | import wx 19 | import os 20 | import sys 21 | import base64 22 | import traceback 23 | 24 | from util.logger import Log 25 | from settings import RESOURCE_PATH 26 | 27 | 28 | 29 | SANDBOX_FILE_READ_FORM = ('.log', '.txt', '.plist', 'json', '.Indexed', '.conf', '.array', '.data', 'config') 30 | 31 | class TreeFrame(wx.Frame): 32 | 33 | def __init__(self, main_frame, root_path, title, device_driver, bundle_id): 34 | self._root_path = root_path 35 | self._main_frame = main_frame 36 | self._device_driver = device_driver 37 | self._bundle_id = bundle_id 38 | self._init_frame(title) 39 | 40 | def _init_frame(self, title): 41 | wx.Frame.__init__(self, None, -1, title, size=(950, 620), style=wx.DEFAULT_FRAME_STYLE & ~wx.MAXIMIZE_BOX & ~wx.RESIZE_BORDER) 42 | self.Bind(wx.EVT_CLOSE, self.on_close) 43 | 44 | self.directory_tree = {} 45 | self.image_list = wx.ImageList(14, 14) 46 | self.image_list.Add(wx.Image(os.path.join(RESOURCE_PATH, 'folder.png'), wx.BITMAP_TYPE_PNG).Scale(14,14).ConvertToBitmap()) 47 | self.image_list.Add(wx.Image(os.path.join(RESOURCE_PATH, 'file.png'), wx.BITMAP_TYPE_PNG).Scale(14,14).ConvertToBitmap()) 48 | 49 | self.treectrl = wx.TreeCtrl(self, wx.ID_ANY, size=(220, 600)) 50 | self.treectrl.AssignImageList(self.image_list) 51 | self.treectrl.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_tree_node_click) 52 | self.update_directory_tree() 53 | 54 | self.lbl = wx.TextCtrl(self, -1, pos=(220, 0), size=(730, 600), style=wx.TE_READONLY | wx.TE_MULTILINE) 55 | 56 | def update_tree_frame(self, title, device_driver, bundle_id): 57 | self.SetTitle(title) 58 | self.directory_tree = {} 59 | self._device_driver = device_driver 60 | self._bundle_id = bundle_id 61 | print 'bundle_id:%s' % self._bundle_id 62 | self.update_directory_tree() 63 | 64 | def update_directory_tree(self, bundle_id=None): 65 | self.treectrl.DeleteAllItems() 66 | if bundle_id: 67 | self._bundle_id = bundle_id 68 | self.treeroot = self.treectrl.AddRoot(self._bundle_id) 69 | self.treectrl.SetItemImage(self.treeroot, 0, which=wx.TreeItemIcon_Normal) 70 | # 添加子目录 71 | self.directory_tree[self.treeroot] = {'path':self._root_path, 'is_dir':True, 'expanded':True} 72 | self.add_item(self.treeroot, self._root_path) 73 | 74 | def on_tree_node_click(self, event): 75 | item_id = event.GetItem() 76 | path = self.directory_tree[item_id]['path'] 77 | print path 78 | if self.directory_tree[item_id]['is_dir']: 79 | if not self.directory_tree[item_id]['expanded']: 80 | self.add_item(item_id, path) 81 | else: 82 | if os.path.splitext(path)[1] in SANDBOX_FILE_READ_FORM: 83 | try: 84 | content = self._device_driver.get_sandbox_file_content(self._bundle_id, path) 85 | self.lbl.SetValue(base64.b64decode(content)) 86 | except: 87 | msg = traceback.format_exc() # 方式1 88 | print (msg) 89 | self.lbl.SetValue('该文件可能存在某些字符导致无法base64编码') 90 | else: 91 | self.lbl.SetValue('不支持该格式的文件显示') 92 | 93 | def add_item(self, root, path): 94 | self.directory_tree[root]['expanded'] = True 95 | try: 96 | file_list = self._device_driver.get_sandbox_path_files(self._bundle_id, path) 97 | for i in file_list: 98 | # 获得绝对路径 99 | tmpdir = os.path.join(path, i['path']) 100 | tmpdict = {} 101 | # 如果是路径的话 还需对该路径进行一次操作 102 | if i['is_dir']: 103 | child = self.treectrl.AppendItem(root, os.path.basename(i['path'])) 104 | tmpdict['path'] = tmpdir 105 | tmpdict['is_dir'] = i['is_dir'] 106 | tmpdict['expanded'] = False 107 | self.directory_tree[child] = tmpdict 108 | self.treectrl.SetItemImage(child, 0, which=wx.TreeItemIcon_Normal) 109 | tmpdict = {} 110 | # 如果是目录的话 111 | else: 112 | child = self.treectrl.AppendItem(root, os.path.basename(i['path'])) 113 | tmpdict['path'] = tmpdir 114 | tmpdict['is_dir'] = i['is_dir'] 115 | tmpdict['expanded'] = False 116 | self.directory_tree[child] = tmpdict 117 | self.treectrl.SetItemImage(child, 1, which=wx.TreeItemIcon_Normal) 118 | tmpdict = {} 119 | except: 120 | error = traceback.format_exc() 121 | Log.e('add_item', error) 122 | self._main_frame.create_tip_dialog(error.decode('utf-8')) 123 | 124 | def on_close(self, event): 125 | self._main_frame.on_close_treeFrame() 126 | self._device_driver.close_sandbox_client() 127 | event.Skip() 128 | -------------------------------------------------------------------------------- /rpc/driver.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | ''' 16 | ''' 17 | 18 | import base64 19 | import ConfigParser 20 | from functools import wraps 21 | import os 22 | import socket 23 | import sys 24 | import subprocess 25 | import threading 26 | 27 | from rpc.client import RPCClientProxy 28 | from settings import RESOURCE_PATH 29 | 30 | ENCODING = "utf-8" 31 | 32 | 33 | def sync(lockname): 34 | '''方法同步锁,保证driverserver的接口一次只访问一个 35 | ''' 36 | 37 | def _synched(func): 38 | @wraps(func) 39 | def _synchronizer(self,*args, **kwargs): 40 | tlock = self.__getattribute__(lockname) 41 | tlock.acquire() 42 | try: 43 | return func(self, *args, **kwargs) 44 | finally: 45 | tlock.release() 46 | return _synchronizer 47 | return _synched 48 | 49 | 50 | class HostDriver(object): 51 | '''设备主机的Driver 52 | ''' 53 | 54 | 55 | def __init__(self, host_ip, host_port): 56 | self._host_ip = host_ip 57 | self._host_url = 'http://%s:%s' % (host_ip, host_port) 58 | self._driver = RPCClientProxy('/'.join([self._host_url, 'host/']), allow_none=True, encoding=ENCODING) 59 | self._devices = None 60 | self._qt4i_manage = None 61 | 62 | @property 63 | def host_url(self): 64 | return self._host_url 65 | 66 | @property 67 | def devices(self): 68 | return self._devices 69 | 70 | @property 71 | def qt4i_manage(self): 72 | if sys.platform == 'darwin': 73 | config_parser = ConfigParser.ConfigParser() 74 | settings_file = os.path.join(RESOURCE_PATH, 'uispy.conf') 75 | config_parser.read(settings_file) 76 | try: 77 | self._qt4i_manage = config_parser.get('uispy', 'qt4i-manage') 78 | except ConfigParser.NoOptionError: 79 | self._qt4i_manage = os.path.expanduser("~/Library/Python/2.7/bin/qt4i-manage") 80 | config_parser.set("uispy", "qt4i-manage", self._qt4i_manage) 81 | with open(settings_file, 'w') as fd: 82 | config_parser.write(fd) 83 | return self._qt4i_manage 84 | else: 85 | raise Exception("Unsupported platform!") 86 | 87 | def connect_to_host(self, driver_type): 88 | socket.setdefaulttimeout(3) 89 | is_connected = False 90 | try: 91 | is_connected = self._driver.echo() 92 | except: 93 | is_connected = self.restart_host_driver(driver_type) 94 | finally: 95 | socket.setdefaulttimeout(None) 96 | return is_connected 97 | 98 | def restart_host_driver(self, driver_type): 99 | socket.setdefaulttimeout(3) 100 | is_connected = False 101 | if sys.platform == 'darwin' and self._host_ip == '127.0.0.1': 102 | try: 103 | if driver_type == 'instruments': 104 | subprocess.check_call('killall -9 instruments') 105 | xctestagent_path = os.path.join(os.path.expanduser('~'), 'XCTestAgent') 106 | if driver_type == 'xctest' and not os.path.exists(xctestagent_path): 107 | unzip_agent_cmd = '%s setup' % self.qt4i_manage 108 | subprocess.call(unzip_agent_cmd, shell=True) 109 | subprocess.call('%s restartdriver -t %s' % (self.qt4i_manage, driver_type), shell=True) 110 | self._driver = RPCClientProxy('/'.join([self.host_url, 'host/']), allow_none=True, encoding=ENCODING) 111 | is_connected = self._driver.echo() 112 | except: 113 | pass 114 | socket.setdefaulttimeout(None) 115 | return is_connected 116 | 117 | def list_devices(self): 118 | self._devices = self._driver.list_devices() 119 | return self._devices 120 | 121 | def start_simulator(self, udid): 122 | self._driver.start_simulator(udid) 123 | 124 | 125 | class DeviceDriver(object): 126 | '''iPhone真机或者模拟器的Driver 127 | ''' 128 | 129 | def __init__(self, host_url, device_udid): 130 | self._driver = RPCClientProxy('/'.join([host_url, 'device', '%s/' % device_udid]), allow_none=True, encoding='utf-8') 131 | self.udid = device_udid 132 | self.devicelock = threading.RLock() 133 | 134 | @sync('devicelock') 135 | def start_app(self, bundle_id): 136 | try: 137 | self._driver.device.stop_app(bundle_id) 138 | except: 139 | pass 140 | return self._driver.device.start_app(bundle_id, None, None) 141 | 142 | @sync('devicelock') 143 | def take_screenshot(self): 144 | base64_img = self._driver.device.capture_screen() 145 | return base64.decodestring(base64_img) 146 | 147 | @sync('devicelock') 148 | def get_element_tree(self): 149 | return self._driver.device.get_element_tree() 150 | 151 | @sync('devicelock') 152 | def install_app(self, ipa_path): 153 | return self._driver.device.install(ipa_path) 154 | 155 | @sync('devicelock') 156 | def uninstall_app(self, bundle_id): 157 | return self._driver.device.uninstall(bundle_id) 158 | 159 | @sync('devicelock') 160 | def get_screen_orientation(self): 161 | return self._driver.device.get_screen_orientation() 162 | 163 | @sync('devicelock') 164 | def get_app_list(self, app_type="user"): 165 | '''获取设备上的app列表 166 | :param app_type: app的类型(user/system/all) 167 | :type app_type: str 168 | :returns: list 例如:[{'com.tencent.rdm': 'RDM'}] 169 | ''' 170 | return self._driver.device.get_app_list(app_type) 171 | 172 | @sync('devicelock') 173 | def click(self, x, y, retry = 3): 174 | ''' 175 | 基于屏幕的点击操作 176 | :param x: 横向坐标(从左向右,屏幕百分比) 177 | :type x: float 178 | :param y: 纵向坐标(从上向下,屏幕百分比) 179 | :type y: float 180 | :param retry 重试次数 181 | :type int 182 | ''' 183 | self._driver.device.click(x, y) 184 | 185 | @sync('devicelock') 186 | def double_click(self, x, y): 187 | self._driver.device.double_click(x, y) 188 | 189 | @sync('devicelock') 190 | def long_click(self, x, y, duration=3): 191 | self._driver.device.long_click(x, y, duration) 192 | 193 | @sync('devicelock') 194 | def drag(self, x0, y0, x1, y1, duration=0, repeat=1, interval=0.5, velocity=1000, retry = 3): 195 | '''拖拽(全局操作) 196 | :param x0: 起始横向坐标(从左向右,屏幕百分比) 197 | :type x0: float 198 | :param y0: 起始纵向坐标(从上向下,屏幕百分比) 199 | :type y0: float 200 | :param x1: 终止横向坐标(从左向右,屏幕百分比) 201 | :type x1: float 202 | :param y1: 终止纵向坐标(从上向下,屏幕百分比) 203 | :type y1: float 204 | :param duration: 起始坐标按下的时间(秒) 205 | :type duration: float 206 | ''' 207 | self._driver.device.drag(x0, y0, x1, y1, duration, repeat, interval, velocity) 208 | 209 | @sync('devicelock') 210 | def sendkeys(self, text): 211 | self._driver.device.send_keys(text) 212 | 213 | @sync('devicelock') 214 | def get_sandbox_path_files(self, bundle_id, file_path): 215 | '''返回真机或者模拟器的沙盒路径 216 | 217 | :param bundle_id: 应用的bundle_id 218 | :type bundle_id: str 219 | :param file_path: 沙盒目录 220 | :type file_path: str 221 | ''' 222 | return self._driver.device.get_sandbox_path_files(bundle_id, file_path) 223 | 224 | @sync('devicelock') 225 | def is_sandbox_path_dir(self, bundle_id, file_path): 226 | '''判断一个sandbox路径是否是一个目录 227 | 228 | :param bundle_id: 应用的bundle_id 229 | :type bundle_id: str 230 | :param file_path: 沙盒目录 231 | :type file_path: str 232 | ''' 233 | return self._driver.device.is_sandbox_path_dir(bundle_id, file_path) 234 | 235 | @sync('devicelock') 236 | def get_sandbox_file_content(self, bundle_id, file_path): 237 | '''获取sandbox中文本文件的内容 238 | 239 | :param bundle_id: 应用的bundle_id 240 | :type bundle_id: str 241 | :param file_path: 沙盒目录 242 | :type file_path: str 243 | ''' 244 | return self._driver.device.get_sandbox_file_content(bundle_id, file_path) 245 | 246 | @sync('devicelock') 247 | def close_sandbox_client(self): 248 | '''销毁sandboxClient对象 249 | ''' 250 | self._driver.device.close_sandbox_client() 251 | 252 | @sync('devicelock') 253 | def get_xcode_version(self): 254 | '''查询Xcode版本 255 | 256 | ''' 257 | return self._driver.device.get_xcode_version() 258 | 259 | @sync('devicelock') 260 | def get_ios_version(self): 261 | '''查询ios版本 262 | 263 | ''' 264 | return self._driver.device.get_ios_version() 265 | -------------------------------------------------------------------------------- /rpc/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | '''RPC Framework 16 | ''' 17 | 18 | import json 19 | import random 20 | import string 21 | import SimpleXMLRPCServer 22 | import xmlrpclib 23 | try: 24 | import fcntl 25 | except ImportError: 26 | fcntl = None 27 | 28 | IDCHARS = string.ascii_lowercase+string.digits 29 | 30 | def random_id(length=8): 31 | return_id = '' 32 | for _ in range(length): 33 | return_id += random.choice(IDCHARS) 34 | return return_id 35 | 36 | class RPCClientProxy(object): 37 | '''RPC Client 38 | ''' 39 | 40 | def __init__(self, uri, transport=None, encoding=None, verbose=0, 41 | allow_none=0, use_datetime=0, context=None): 42 | # establish a "logical" server connection 43 | 44 | if isinstance(uri, unicode): 45 | uri = uri.encode('ISO-8859-1') 46 | 47 | # get the url 48 | import urllib 49 | protocol, uri = urllib.splittype(uri) 50 | if protocol not in ("http", "https"): 51 | raise IOError, "unsupported JSON-RPC protocol" 52 | self.__host, self.__handler = urllib.splithost(uri) 53 | if not self.__handler: 54 | self.__handler = "/RPC2" 55 | 56 | if transport is None: 57 | if protocol == "https": 58 | transport = SafeTransport(use_datetime=use_datetime, context=context) 59 | else: 60 | transport = Transport(use_datetime=use_datetime) 61 | self.__transport = transport 62 | 63 | self.__encoding = encoding 64 | self.__verbose = verbose 65 | self.__allow_none = allow_none 66 | 67 | def __close(self): 68 | self.__transport.close() 69 | 70 | def __request(self, methodname, params): 71 | # call a method on the remote server 72 | request = {"jsonrpc": "2.0"} 73 | if len(params) > 0: 74 | request["params"] = params 75 | request["id"] = random_id() 76 | request["method"] = methodname 77 | request = json.dumps(request) 78 | response = self.__transport.request( 79 | self.__host, 80 | self.__handler, 81 | request, 82 | verbose=self.__verbose 83 | ) 84 | response = json.loads(response) 85 | if not isinstance(response, dict): 86 | raise TypeError('Response is not dict') 87 | if 'error' in response.keys() and response['error'] is not None: 88 | raise DriverApiError(response['error']['message']) 89 | else: 90 | response = response['result'][0] 91 | if isinstance(response, dict): 92 | return self.encode_dict(response, "UTF-8") 93 | elif isinstance(response, list): 94 | return self.encode_list(response, "UTF-8") 95 | elif isinstance(response, unicode): 96 | return response.encode("UTF-8") 97 | return response 98 | 99 | def __repr__(self): 100 | return ( 101 | "" % 102 | (self.__host, self.__handler) 103 | ) 104 | 105 | __str__ = __repr__ 106 | 107 | def __getattr__(self, name): 108 | # magic method dispatcher 109 | return xmlrpclib._Method(self.__request, name) 110 | 111 | # note: to call a remote object with an non-standard name, use 112 | # result getattr(server, "strange-python-name")(args) 113 | 114 | def __call__(self, attr): 115 | """A workaround to get special attributes on the ServerProxy 116 | without interfering with the magic __getattr__ 117 | """ 118 | if attr == "close": 119 | return self.__close 120 | elif attr == "transport": 121 | return self.__transport 122 | raise AttributeError("Attribute %r not found" % (attr,)) 123 | 124 | 125 | def encode_dict(self, content, encoding="UTF-8"): 126 | '''将字典编码为指定形式 127 | 128 | :param content: 要编码内容 129 | :type content: dict 130 | :param encoding:编码类型 131 | :type encoding: str 132 | :returns: dict -- 编码后的字典 133 | ''' 134 | for key in content: 135 | if isinstance(content[key], dict): 136 | content[key] = self.encode_dict(content[key], encoding) 137 | elif isinstance(content[key], unicode): 138 | content[key] = content[key].encode(encoding) 139 | elif isinstance(content[key], list): 140 | content[key] = self.encode_list(content[key], encoding) 141 | return content 142 | 143 | def encode_list(self, content, encoding="UTF-8"): 144 | '''将列表编码为指定形式 145 | 146 | :param content: 要编码内容 147 | :type content: list 148 | :param encoding:编码类型 149 | :type encoding: str 150 | :returns: list -- 编码后的列表 151 | ''' 152 | for ind, item in enumerate(content): 153 | if isinstance(item, dict): 154 | content[ind] = self.encode_dict(item, encoding) 155 | elif isinstance(item, unicode): 156 | content[ind] = content[ind].encode(encoding) 157 | elif isinstance(item, list): 158 | content[ind] = self.encode_list(item, encoding) 159 | return content 160 | 161 | 162 | class DriverApiError(Exception): 163 | '''Driver API Error 164 | ''' 165 | 166 | class Fault(object): 167 | '''JSON-RPC Error 168 | ''' 169 | 170 | def __init__(self, code=-12306, message = None, rpcid=None): 171 | self.faultCode = code 172 | self.faultString = message 173 | self.rpcid = rpcid 174 | if not message: 175 | import traceback 176 | self.faultString = traceback.format_exc() 177 | 178 | def error(self): 179 | return {"code": self.faultCode, "message": self.faultString} 180 | 181 | def response(self): 182 | return json.dumps({"jsonrpc": "2.0", "error":self.error(), "id":self.rpcid}) 183 | 184 | def __repr__(self): 185 | return '' % (self.faultCode, self.faultString) 186 | 187 | 188 | class TransportMixIn(object): 189 | '''XMLRPC Transport extended API 190 | ''' 191 | user_agent = "jsonrpclib/0.1" 192 | _connection = (None, None) 193 | _extra_headers = [] 194 | 195 | def send_content(self, connection, request_body): 196 | connection.putheader("Content-Type", "application/json-rpc") 197 | connection.putheader("Content-Length", str(len(request_body))) 198 | connection.endheaders() 199 | if request_body: 200 | connection.send(request_body) 201 | 202 | def getparser(self): 203 | target = JSONTarget() 204 | return JSONParser(target), target 205 | 206 | 207 | class JSONParser(object): 208 | 209 | def __init__(self, target): 210 | self.target = target 211 | 212 | def feed(self, data): 213 | self.target.feed(data) 214 | 215 | def close(self): 216 | pass 217 | 218 | 219 | class JSONTarget(object): 220 | 221 | def __init__(self): 222 | self.data = [] 223 | 224 | def feed(self, data): 225 | self.data.append(data) 226 | 227 | def close(self): 228 | return ''.join(self.data) 229 | 230 | 231 | class Transport(TransportMixIn, xmlrpclib.Transport): 232 | 233 | def __init__(self, use_datetime): 234 | TransportMixIn.__init__(self) 235 | xmlrpclib.Transport.__init__(self, use_datetime) 236 | 237 | 238 | class SafeTransport(TransportMixIn, xmlrpclib.SafeTransport): 239 | 240 | def __init__(self, use_datetime, context): 241 | TransportMixIn.__init__(self) 242 | xmlrpclib.SafeTransport.__init__(self, use_datetime, context) 243 | 244 | 245 | class SimpleJSONRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): 246 | '''JSON-RPC请求处理器 247 | ''' 248 | def is_rpc_path_valid(self): 249 | return True 250 | 251 | def do_POST(self): 252 | '''处理HTTP的POST请求 253 | ''' 254 | if not self.is_rpc_path_valid(): 255 | self.report_404() 256 | return 257 | try: 258 | max_chunk_size = 10*1024*1024 259 | size_remaining = int(self.headers["content-length"]) 260 | L = [] 261 | while size_remaining: 262 | chunk_size = min(size_remaining, max_chunk_size) 263 | L.append(self.rfile.read(chunk_size)) 264 | size_remaining -= len(L[-1]) 265 | data = ''.join(L) 266 | response = self.server._marshaled_dispatch( 267 | data, getattr(self, '_dispatch', None), self.path 268 | ) 269 | self.send_response(200) 270 | except Exception: 271 | response = Fault().response() 272 | self.send_response(500, response) 273 | if response is None: 274 | response = '' 275 | self.send_header("Content-type", "application/json-rpc") 276 | if self.encode_threshold is not None: 277 | if len(response) > self.encode_threshold: 278 | q = self.accept_encodings().get("gzip", 0) 279 | if q: 280 | try: 281 | response = xmlrpclib.gzip_encode(response) 282 | self.send_header("Content-Encoding", "gzip") 283 | except NotImplementedError: 284 | pass 285 | self.send_header("Content-length", str(len(response))) 286 | self.end_headers() 287 | self.wfile.write(response) 288 | -------------------------------------------------------------------------------- /ui/mainframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | '''UISpy窗口控件 16 | ''' 17 | 18 | 19 | import ConfigParser 20 | import os 21 | import StringIO 22 | import sys 23 | import threading 24 | import traceback 25 | import wx 26 | import subprocess 27 | import time 28 | 29 | from util.logger import Log 30 | from rpc.driver import HostDriver 31 | from rpc.driver import DeviceDriver 32 | from ui.sandboxframe import TreeFrame 33 | from version import VERSION 34 | from settings import RESOURCE_PATH 35 | 36 | 37 | DEFAULT_BUNDLE_ID = 'com.tencent.sng.test.gn' 38 | DEBUG_PATH = 'UISpy.app/Contents/MacOS/UISpy' 39 | TIMER_ID = 10010 40 | 41 | 42 | class EnumDriverType(object): 43 | '''定义iOS测试框架的类型 44 | ''' 45 | XCTest, Instruments = ('xctest', 'instruments') 46 | 47 | 48 | class MainFrame(wx.Frame): 49 | 50 | def __init__(self): 51 | self._init_controls() 52 | self._driver_type = EnumDriverType.XCTest 53 | self._device = None 54 | self._host_driver = None 55 | self._device_driver = None 56 | self._element_tree = None 57 | self._focused_element = None 58 | self._app_started = False 59 | self._orientation = 1 60 | self._app_type = 'user' 61 | self._process_dlg_running = False 62 | self.treeframe = None 63 | 64 | def _init_controls(self): 65 | #设置MacOS X的偏移 66 | if sys.platform == 'darwin': 67 | osx_offset = 5 68 | else: 69 | osx_offset = 0 70 | uispy_stype = wx.DEFAULT_FRAME_STYLE & ~wx.MAXIMIZE_BOX & ~wx.RESIZE_BORDER #屏蔽掉最大化按钮和窗口缩放功能 71 | wx.Frame.__init__(self, None, wx.ID_ANY, "UISpy "+VERSION, size=(900, 750-osx_offset*6), style=uispy_stype) 72 | 73 | # 设置左上角图标(仅限windows系统) 74 | if sys.platform == 'win32': 75 | logo = wx.Icon(os.path.join(RESOURCE_PATH, 'qt4i_win.ico'), wx.BITMAP_TYPE_ICO) 76 | self.SetIcon(logo) 77 | 78 | # 设置菜单栏 79 | menu_bar = wx.MenuBar() 80 | #应用菜单 81 | app_menu = wx.Menu() 82 | install_menu = app_menu.Append(wx.ID_ANY, u"安装App", u"安装app到被测手机") 83 | uninstall_menu = app_menu.Append(wx.ID_ANY, u"卸载App", u"卸载被测手机上的app") 84 | sandbox_menu = app_menu.Append(wx.ID_ANY, u"浏览App沙盒", u"打开并查看设备的沙盒目录") 85 | app_menu.AppendSeparator() 86 | menu_bar.Append(app_menu, u'应用') 87 | #高级菜单 88 | advance_menu = wx.Menu() 89 | log_menu = advance_menu.Append(wx.ID_ANY, u"查看日志", u"打开日志文件夹") 90 | debug_menu = advance_menu.Append(wx.ID_ANY, u"Debug模式", u"使用Debug模式运行UISpy") 91 | setting_menu = advance_menu.Append(wx.ID_ANY, u"设置", u"环境参数设置") 92 | self.show_qpath_menu = advance_menu.Append(wx.ID_ANY, u'显示QPath', u"打开即可显示控件Qpath",kind=wx.ITEM_CHECK) 93 | self.remote_operator_menu = advance_menu.Append(wx.ID_ANY, u'远程控制', u"打开即可远程控制手机",kind=wx.ITEM_CHECK) 94 | 95 | advance_menu.AppendSeparator() 96 | menu_bar.Append(advance_menu, u'高级') 97 | 98 | self.Bind(wx.EVT_MENU, self.on_select_install_pkg, install_menu) 99 | self.Bind(wx.EVT_MENU, self.on_uninstall, uninstall_menu) 100 | self.Bind(wx.EVT_MENU, self.on_log, log_menu) 101 | self.Bind(wx.EVT_MENU, self.on_debug, debug_menu) 102 | self.Bind(wx.EVT_MENU, self.on_settings, setting_menu) 103 | self.Bind(wx.EVT_MENU, self.on_sandbox_view, sandbox_menu) 104 | self.SetMenuBar(menu_bar) 105 | 106 | # 设置主面板 107 | self.panel = wx.Panel(self, wx.ID_ANY) 108 | self.Bind(wx.EVT_CLOSE, self.on_close) 109 | 110 | # 第一行控件 111 | line1_y = 10 112 | wx.StaticText(self.panel, wx.ID_ANY, u"设备主机:", pos=(10,line1_y+osx_offset/2), size=wx.DefaultSize) 113 | self.tc_device_host_ip = wx.TextCtrl(self.panel, wx.ID_ANY, pos=(80, line1_y), size=(140, 24), style=wx.TE_LEFT) 114 | self.tc_device_host_ip.SetValue('127.0.0.1') 115 | self.tc_device_host_ip.SetToolTip(u"请输入设备主机或者设备DriverServer的IP") 116 | wx.StaticText(self.panel, wx.ID_ANY, u":", pos=(225, line1_y), size=wx.DefaultSize) 117 | self.tc_device_host_port = wx.TextCtrl(self.panel, wx.ID_ANY, pos=(235, line1_y), size=(60, 24), style=wx.TE_LEFT) 118 | self.tc_device_host_port.SetValue('12306') 119 | self.tc_device_host_port.SetToolTip(u"请输入设备主机或者设备DriverServer的端口号") 120 | self.btn_connect_device_host = wx.Button(self.panel, wx.ID_ANY, label=u'连接', pos=wx.Point(310, line1_y), size=wx.Size(50, 24), style=0) 121 | self.btn_connect_device_host.Bind(wx.EVT_BUTTON, self.on_connect_device_host) 122 | 123 | # 第二行控件 124 | line2_y = 45 125 | wx.StaticText(self.panel, wx.ID_ANY, u"设备:", pos=(10,line2_y+osx_offset/2), size=wx.DefaultSize) 126 | self.cb_devicelist = wx.ComboBox(self.panel, wx.ID_ANY, pos=(50, line2_y), size=(245+osx_offset/2, 24), style=wx.CB_READONLY) 127 | self.cb_devicelist.Bind(wx.EVT_COMBOBOX, self.on_select_device) 128 | self.btn_refresh = wx.Button(self.panel, wx.ID_ANY, u'刷新', pos=wx.Point(310, line2_y), size=wx.Size(50, 25), style=0) 129 | self.btn_refresh.Bind(wx.EVT_BUTTON, self.on_update_device_list) 130 | self.btn_refresh.Enable(False) 131 | 132 | # 第三行控件 133 | line3_y = 80 134 | wx.StaticText(self.panel, wx.ID_ANY, u"BundleID:", pos=(10,line3_y+osx_offset/2), size=wx.DefaultSize) 135 | self.tc_bundle_id = wx.ComboBox(self.panel, wx.ID_ANY, pos=(80-osx_offset, line3_y), size=(230+osx_offset*2, 24)) 136 | self._config_file_path = os.path.join(RESOURCE_PATH, 'uispy.conf') 137 | if os.path.exists(self._config_file_path): 138 | config_parser = ConfigParser.ConfigParser() 139 | config_parser.read(self._config_file_path) 140 | try: 141 | bunlde_id = config_parser.get('uispy', 'bundle_id') 142 | except ConfigParser.NoOptionError: 143 | bunlde_id = 'com.tencent.sng.test.gn' 144 | else: 145 | with open(self._config_file_path, 'w+') as fd: 146 | config_parser = ConfigParser.ConfigParser() 147 | config_parser.add_section('uispy') 148 | config_parser.write(fd) 149 | bunlde_id = DEFAULT_BUNDLE_ID 150 | self.tc_bundle_id.SetValue(bunlde_id) 151 | self.tc_bundle_id.SetToolTip(u"请选择被测App的Bundle ID") 152 | self.tc_bundle_id.Bind(wx.EVT_COMBOBOX, self.on_select_bundle_id) 153 | self.btn_refresh_applist = wx.Button(self.panel, wx.ID_ANY, u'刷新', pos=wx.Point(320, line3_y), size=wx.Size(40, 25), style=0) 154 | self.btn_refresh_applist.Bind(wx.EVT_BUTTON, self.on_update_app_list) 155 | self.btn_refresh_applist.Enable(False) 156 | self.tc_bundle_all = wx.CheckBox(self.panel, label = 'All',pos = (370, line3_y+osx_offset/2)) 157 | self.tc_bundle_all.Bind(wx.EVT_CHECKBOX, self.on_select_bundle_all) 158 | self.btn_start_app = wx.Button(self.panel, wx.ID_ANY, label=u'启动App', pos=wx.Point(420, line3_y), size=wx.Size(80, 24), style=0) 159 | self.btn_start_app.Bind(wx.EVT_BUTTON, self.on_start_app) 160 | 161 | # 第四行控件 162 | line4_y = 115 163 | wx.StaticText(self.panel, wx.ID_ANY, u"QPath:", pos=(10,line4_y+osx_offset/2), size=wx.DefaultSize) 164 | self.tc_qpath = wx.TextCtrl(self.panel, wx.ID_ANY, pos=(70-osx_offset, line4_y), size=(340, 24), style=wx.TE_LEFT) 165 | self.tc_qpath.SetValue('') 166 | self.btn_get_uitree = wx.Button(self.panel, wx.ID_ANY, label=u'获取控件', pos=wx.Point(420, line4_y), size=wx.Size(80, 24), style=0) 167 | self.btn_get_uitree.Bind(wx.EVT_BUTTON, self.on_get_uitree) 168 | 169 | #截屏区域的控件 170 | self.image_shown_width = 375 171 | self.image_shown_height = 667 172 | self.image_screenshot = wx.StaticBitmap(parent=self.panel, id=wx.ID_ANY, pos=(520, 5), size=(self.image_shown_width, self.image_shown_height)) 173 | self.image_screenshot.Bind(wx.EVT_MOUSE_EVENTS, self.on_screenshot_mouse_event) 174 | self.image_screenshot.Bind(wx.EVT_SET_FOCUS, self.on_screenshot_focus) 175 | startup_image = wx.Image(os.path.join(RESOURCE_PATH, 'startup.png'), wx.BITMAP_TYPE_ANY, index=-1) 176 | startup_image = startup_image.Scale(self.image_shown_width, self.image_shown_height) 177 | self.image_screenshot.SetBitmap(wx.Bitmap(startup_image)) 178 | self.mask_panel = CanvasPanel(self.panel, id=wx.ID_ANY, pos=(520, 5), size=(self.image_shown_width, self.image_shown_height)) 179 | self.mask_panel.Bind(wx.EVT_MOUSE_EVENTS, self.on_screenshot_mouse_event) 180 | #文件拖拽 181 | self.drop_target = FileDropTarget(self, self.image_screenshot) 182 | self.image_screenshot.SetDropTarget(self.drop_target) 183 | 184 | #控件树 185 | line5_y = 150 186 | self.tc_uitree = wx.TreeCtrl(self.panel, wx.ID_ANY,pos=(10, line5_y), size=(500, 380)) 187 | self.tc_uitree.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_uitree_node_click) 188 | self.tc_uitree.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.on_uitree_node_right_click) 189 | 190 | #控件详细属性 191 | line6_y = 525 192 | st_control_properties = wx.StaticBox(self.panel, wx.ID_ANY, u"控件属性", pos=(10, line6_y+osx_offset*2), size=(500, 140)) 193 | wx.StaticText(st_control_properties, wx.ID_ANY, u"name", pos=(10, 30-osx_offset*2), size=wx.DefaultSize) 194 | self.tc_id = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(55, 30-osx_offset*2), size=(135, 24), style=wx.TE_LEFT) 195 | wx.StaticText(st_control_properties, wx.ID_ANY, u"classname", pos=(200, 30-osx_offset*2), size=wx.DefaultSize) 196 | self.tc_classname = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(280, 30-osx_offset*2), size=(160, 24), style=wx.TE_LEFT) 197 | wx.StaticText(st_control_properties, wx.ID_ANY, u"label", pos=(10, 65-osx_offset*2), size=wx.DefaultSize) 198 | self.tc_label = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(55, 65-osx_offset*2), size=(135, 24), style=wx.TE_LEFT) 199 | wx.StaticText(st_control_properties, wx.ID_ANY, u"rect", pos=(200, 65-osx_offset*2), size=wx.DefaultSize) 200 | self.tc_rect = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(250, 65-osx_offset*2), size=(190, 24), style=wx.TE_LEFT) 201 | wx.StaticText(st_control_properties, wx.ID_ANY, u"value", pos=(10, 100-osx_offset*2), size=wx.DefaultSize) 202 | self.tc_value = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(55, 100-osx_offset*2), size=(135, 24), style=wx.TE_LEFT) 203 | wx.StaticText(st_control_properties, wx.ID_ANY, u"visible", pos=(200, 100-osx_offset*2), size=wx.DefaultSize) 204 | self.tc_visible = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(250, 100-osx_offset*2), size=(50, 24), style=wx.TE_LEFT) 205 | wx.StaticText(st_control_properties, wx.ID_ANY, u"enable", pos=(310, 100-osx_offset*2), size=wx.DefaultSize) 206 | self.tc_enable = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(370, 100-osx_offset*2), size=(50, 24), style=wx.TE_LEFT) 207 | 208 | #状态栏 209 | self.statusbar = self.CreateStatusBar() 210 | # 将状态栏分割为3个区域,比例为1:2:3 211 | self.statusbar.SetFieldsCount(3) 212 | self.statusbar.SetStatusWidths([-6, -6, -1]) 213 | 214 | def on_close(self, event): 215 | if self.treeframe: 216 | self.treeframe.Destroy() 217 | config_parser = ConfigParser.ConfigParser() 218 | config_parser.read(self._config_file_path) 219 | config_parser.set("uispy", "bundle_id", self.tc_bundle_id.GetValue()) 220 | with open(self._config_file_path, 'w') as fd: 221 | config_parser.write(fd) 222 | import atexit 223 | atexit._exithandlers = [] # 禁止退出时弹出错误框 224 | event.Skip() 225 | 226 | def create_tip_dialog(self, msg, title=u"错误"): 227 | dialog = wx.MessageDialog(self, msg, title, style=wx.OK) 228 | dialog.ShowModal() 229 | dialog.Destroy() 230 | 231 | def show_process_dialog(self, msg): 232 | progress_max = 100 233 | dialog = wx.ProgressDialog(u"提示", msg, progress_max, parent=self.panel, style=wx.PD_CAN_ABORT|wx.PD_APP_MODAL|wx.PD_AUTO_HIDE) 234 | count = 0 235 | while self._process_dlg_running and count < progress_max: 236 | count = (count + 1) % 100 237 | wx.MilliSleep(100) 238 | if not dialog.Update(count)[0]: 239 | break 240 | dialog.Destroy() 241 | 242 | def _run_in_main_thread(self, func, *args, **kwargs): 243 | wx.CallAfter(func, *args, **kwargs) 244 | 245 | def _run_in_work_thread(self, func, *args, **kwargs): 246 | t = threading.Thread(target=func, args=args, kwargs=kwargs) 247 | t.setDaemon(True) 248 | t.start() 249 | 250 | def _update_device_list(self): 251 | self._run_in_main_thread(self.statusbar.SetStatusText, u"正在获取设备列表......", 0) 252 | time.sleep(2) 253 | try: 254 | start_time = time.time() 255 | devices = self._host_driver.list_devices() 256 | end_time = time.time() 257 | print 'cost time:',(end_time - start_time) 258 | except: 259 | error = traceback.format_exc() 260 | Log.e('update_device_list', error) 261 | self._run_in_main_thread(self.statusbar.SetStatusText, u"更新设备列表失败", 0) 262 | self._run_in_main_thread(self.create_tip_dialog, error.decode('utf-8')) 263 | return 264 | self.cb_devicelist.Clear() 265 | for dev in devices: 266 | self.cb_devicelist.Append(dev['name'].decode('utf-8'), dev) 267 | self._device = devices[0] 268 | self._run_in_main_thread(self.btn_refresh.Enable) 269 | self._run_in_main_thread(self.btn_refresh_applist.Enable) 270 | self._run_in_main_thread(self.cb_devicelist.Select, 0) 271 | self._run_in_main_thread(self.statusbar.SetStatusText, u"更新设备列表完毕", 0) 272 | self._update_bundle_id_list() 273 | 274 | def _update_bundle_id_list(self): 275 | ''' 276 | 在连接设备或者更换设备时,更新bundle_id列表 277 | ''' 278 | print self._device 279 | self._device_driver = DeviceDriver(self._host_driver.host_url, self._device['udid']) 280 | original_bundle_id = self.tc_bundle_id.GetValue() 281 | self.tc_bundle_id.Clear() 282 | if self._device['simulator']: 283 | self._host_driver.start_simulator(self._device['udid']) 284 | self._app_list = self._device_driver.get_app_list(self._app_type) 285 | Log.i('app_list:', str(self._app_list)) 286 | bundle_list = [] 287 | remove_dev = None 288 | if self._app_list: 289 | for dev in self._app_list: 290 | bundle = dev.keys()[0] 291 | if bundle == 'com.apple.test.XCTestAgent-Runner': 292 | remove_dev = dev 293 | continue 294 | bundle_list.append(bundle) 295 | self.tc_bundle_id.Append(bundle.decode('utf-8'), dev) 296 | index = bundle_list.index(original_bundle_id) if original_bundle_id and original_bundle_id in bundle_list else 0 297 | self._run_in_main_thread(self.tc_bundle_id.Select, index) 298 | if remove_dev: 299 | self._app_list.remove(dev) 300 | if self.treeframe: 301 | self._run_in_main_thread(self.on_update_sandbox_view) 302 | else: 303 | self.create_tip_dialog(u'设备未启动或无可用APP') 304 | return 305 | 306 | def on_connect_device_host(self, event): 307 | '''连接设备主机 308 | ''' 309 | self.statusbar.SetStatusText(u"正在连接设备主机......", 0) 310 | host_ip = self.tc_device_host_ip.GetValue() 311 | host_port = self.tc_device_host_port.GetValue() 312 | self._host_driver = HostDriver(host_ip, host_port) 313 | if not self._host_driver.connect_to_host(self._driver_type): 314 | self.statusbar.SetStatusText(u"连接设备主机异常!", 0) 315 | config_parser = ConfigParser.ConfigParser() 316 | config_parser.read(os.path.join(RESOURCE_PATH, 'uispy.conf')) 317 | qt4i_manage = config_parser.get('uispy', 'qt4i-manage') 318 | if not os.path.exists(qt4i_manage): 319 | self.create_tip_dialog(u"请检查qt4i是否安装或者qt4i-manage路径是否配置正确!\n" 320 | u"1.qt4i安装方法:pip install qt4i --user\n" 321 | u"2.qt4i-manage配置方法:高级->设置") 322 | else: 323 | self.create_tip_dialog(u"连接设备主机异常,请检查设备主机地址") 324 | return 325 | self._run_in_work_thread(self._update_device_list) 326 | 327 | def on_select_device(self, event): 328 | '''从设备列表中选择设备 329 | ''' 330 | index = self.cb_devicelist.GetSelection() 331 | selected_device = self.cb_devicelist.GetClientData(index) 332 | 333 | if self._device != selected_device: 334 | self._device = selected_device 335 | print 'current_device:', self._device 336 | self._run_in_work_thread(self._update_bundle_id_list) 337 | 338 | def on_update_device_list(self, event): 339 | '''刷新设备列表 340 | ''' 341 | self.statusbar.SetStatusText(u"正在更新设备列表......", 0) 342 | self._run_in_work_thread(self._update_device_list) 343 | 344 | def _update_screenshot(self, img_data): 345 | image = wx.Image(StringIO.StringIO(img_data)) 346 | if self._orientation == 3 and image.GetWidth() > image.GetHeight(): 347 | image = image.Rotate90() 348 | w = image.GetWidth() 349 | h = image.GetHeight() 350 | print 'Width:', w 351 | print 'Height:', h 352 | image = image.Scale(self.image_shown_width, self.image_shown_height) 353 | self.image_screenshot.SetBitmap(wx.Bitmap(image)) 354 | 355 | def _add_child(self, parent, children, depth): 356 | for child in children: 357 | properties = dict.copy(child) 358 | del properties['children'] 359 | new_item = self.tc_uitree.AppendItem(parent, child['classname'], data = properties) 360 | child['item_id'] = new_item 361 | child['depth'] = depth 362 | self._add_child(new_item, child['children'], depth+1) 363 | 364 | def _update_uitree(self): 365 | self.tc_uitree.DeleteAllItems() 366 | if 'UIATarget' == self._element_tree['classname']: 367 | self._element_tree = self._element_tree['children'][0] 368 | app = dict.copy(self._element_tree) 369 | del app['children'] 370 | 371 | self._app_shown_width = app['rect']['size']['width'] 372 | self._app_shown_height = app['rect']['size']['height'] 373 | 374 | if self._scale_rate is None: 375 | self._scale_rate = ((self.image_shown_width * 1.0) / self._app_shown_width, 376 | (self.image_shown_height * 1.0) / self._app_shown_height) 377 | # root = self.tc_uitree.AddRoot(self._element_tree['classname'], data = wx.TreeItemData(app)) 378 | root = self.tc_uitree.AddRoot(self._element_tree['classname'], data = app) 379 | self._element_tree['item_id'] = root 380 | self._element_tree['depth'] = -1 381 | self._root_item = root 382 | self._add_child(root, self._element_tree['children'], 0) 383 | 384 | def _start_app(self): 385 | bundle_id = self.tc_bundle_id.GetValue() 386 | 387 | while(True): 388 | if self._process_dlg_running: 389 | break 390 | dlg = self._dialog 391 | try: 392 | xcode_version = self._device_driver.get_xcode_version() 393 | ios_version = self._device_driver.get_ios_version() 394 | Log.i('xcode_version:%s ios_version:%s' % (xcode_version, ios_version)) 395 | # 增加版本检测 396 | self._run_in_main_thread(dlg.on_update) 397 | self._app_started = self._device_driver.start_app(bundle_id) 398 | if self._app_started: 399 | self._run_in_main_thread(self.statusbar.SetStatusText, u"App启动成功", 0) 400 | self._run_in_main_thread(dlg.on_update_title_msg, '抓取App屏幕中......') 401 | img_data = self._device_driver.take_screenshot() 402 | self._orientation = self._device_driver.get_screen_orientation() 403 | self._run_in_main_thread(self._update_screenshot, img_data) 404 | self._run_in_main_thread(dlg.on_update_title_msg, '抓取App控件树中......') 405 | self._element_tree = self._device_driver.get_element_tree() 406 | self._run_in_main_thread(self._update_uitree) 407 | else: 408 | self._run_in_main_thread(self.statusbar.SetStatusText, u"App启动失败:", 0) 409 | except: 410 | error = traceback.format_exc() 411 | Log.e('start_app', error) 412 | self._run_in_main_thread(self.create_tip_dialog, error.decode('utf-8')) 413 | self._process_dlg_running = False 414 | self._run_in_main_thread(dlg.on_destory) 415 | 416 | def on_start_app(self, event): 417 | if self._host_driver is None: 418 | self.create_tip_dialog(u"未连接设备主机,请连接设备主机") 419 | return 420 | self._device_driver = DeviceDriver(self._host_driver.host_url, self._device['udid']) 421 | self.statusbar.SetStatusText(u"App正在启动......", 0) 422 | self._scale_rate = None 423 | self._run_in_work_thread(self._start_app) 424 | self.show_dialog('App启动中......') 425 | 426 | def show_dialog(self, msg): 427 | self._dialog = MyProgressDialog(msg, self.panel) 428 | self._process_dlg_running = True 429 | 430 | def on_get_uitree(self, event): 431 | img_data = self._device_driver.take_screenshot() 432 | self._orientation = self._device_driver.get_screen_orientation() 433 | print 'orientation: ', self._orientation 434 | self._update_screenshot(img_data) 435 | try: 436 | self._element_tree = self._device_driver.get_element_tree() 437 | except: 438 | self.create_tip_dialog(u'获取控件树失败:%s' % traceback.format_exc()) 439 | return 440 | self._update_uitree() 441 | self.statusbar.SetStatusText(u"获取控件树成功", 0) 442 | 443 | def _check_pos_in_element(self, pos, element): 444 | rect = element['rect'] 445 | if pos[0] >= rect['origin']['x'] and pos[0] <= rect['origin']['x'] + rect['size']['width'] \ 446 | and pos[1] >= rect['origin']['y'] and pos[1] <= rect['origin']['y'] + rect['size']['height'] \ 447 | and element['visible']: 448 | return True 449 | else: 450 | return False 451 | 452 | def _select_closest_element(self, pos, element1, element2): 453 | '''选择距离最近的控件,具体规则如下: 454 | 1、如果element1和element2的位置是包含关系(父子关系),则选择子节点 455 | 2、如果element1和element2的位置是部分重叠,则选择距离控件中心最近的控件 456 | ''' 457 | rect1 = element1['rect'] 458 | rect2 = element2['rect'] 459 | if rect1['origin']['x'] >= rect2['origin']['x'] and \ 460 | rect1['origin']['x'] + rect1['size']['width'] <= rect2['origin']['x'] + rect2['size']['width'] and \ 461 | rect1['origin']['y'] >= rect2['origin']['y'] and \ 462 | rect1['origin']['y'] + rect1['size']['height'] <= rect2['origin']['y'] + rect2['size']['height']: 463 | return element1 464 | 465 | if rect2['origin']['x'] >= rect1['origin']['x'] and \ 466 | rect2['origin']['x'] + rect2['size']['width'] <= rect1['origin']['x'] + rect1['size']['width'] and \ 467 | rect2['origin']['y'] >= rect1['origin']['y'] and \ 468 | rect2['origin']['y'] + rect2['size']['height'] <= rect1['origin']['y'] + rect1['size']['height']: 469 | return element2 470 | 471 | center1 = (rect1['origin']['x'] + rect1['size']['width'] / 2.0, rect1['origin']['y'] + rect1['size']['height'] / 2.0) 472 | center2 = (rect2['origin']['x'] + rect2['size']['width'] / 2.0, rect2['origin']['y'] + rect2['size']['height'] / 2.0) 473 | distance1 = (pos[0] - center1[0]) ** 2 + (pos[1] - center1[1]) ** 2 474 | distance2 = (pos[0] - center2[0]) ** 2 + (pos[1] - center2[1]) ** 2 475 | if distance1 <= distance2: 476 | return element1 477 | else: 478 | return element2 479 | 480 | def _get_focused_element(self, pos, root): 481 | '''优先查找叶子节点的控件(深度遍历) 482 | ''' 483 | for e in root['children']: 484 | self._get_focused_element(pos, e) 485 | if self._check_pos_in_element(pos, e): 486 | if self._focused_element is None: 487 | self._focused_element = e 488 | else: 489 | self._focused_element = self._select_closest_element(pos, self._focused_element, e) 490 | 491 | # if self._check_pos_in_element(pos, root): 492 | # if self._focused_element is None: 493 | # self._focused_element = root 494 | # else: 495 | # self._focused_element = self._select_closest_element(pos, self._focused_element, root) 496 | 497 | def _expand_uitree(self, item_id): 498 | '''展开控件树 499 | ''' 500 | if item_id != self._root_item: 501 | parent = self.tc_uitree.GetItemParent(item_id) 502 | self._expand_uitree(parent) 503 | self.tc_uitree.Expand(item_id) 504 | else: 505 | self.tc_uitree.Expand(self._root_item) 506 | 507 | def _dfs_traverse(self, element_tree, element_list=None): 508 | if element_list is None: 509 | element_list = [] 510 | element_list.append(element_tree) 511 | for e in element_tree['children']: 512 | self._dfs_traverse(e, element_list) 513 | return element_list 514 | 515 | def _recommend_qpath(self, element): 516 | '''推荐QPath,具体策略如下: 517 | 1、id唯一,则推荐id作为QPath 518 | 2、待补充 519 | ''' 520 | self.tc_qpath.SetValue("") 521 | if element['name']: 522 | element_id = element['name'] 523 | element_list = self._dfs_traverse(self._element_tree) 524 | count = 0 525 | for e in element_list: 526 | if e['name'] == element_id: 527 | count += 1 528 | if count == 1: 529 | self.tc_qpath.SetValue(element_id.decode('utf-8')) 530 | return 531 | 532 | if self.show_qpath_menu.IsChecked(): 533 | if element['classname'] == 'Other' and not element['label'] and not element['name'] and not element['value']: 534 | return 535 | 536 | qpath = '/classname = \'' + element['classname'] + '\'' 537 | if element['label']: 538 | qpath += ' & label = \'' +element['label'] + '\'' 539 | elif element['name']: 540 | qpath += ' & name = \'' +element['name'] + '\'' 541 | elif element['value']: 542 | qpath += ' & value = \'' +element['value'] + '\'' 543 | 544 | qpath += ' & visible = %s & maxdepth = %s' % (str(element['visible']).lower(), str(element['depth']).lower()) 545 | self.tc_qpath.SetValue(qpath.decode('utf-8')) 546 | 547 | 548 | def on_screenshot_focus(self, event): 549 | print 'screenshot focus' 550 | # self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) 551 | 552 | def on_screenshot_single_click(self, event): 553 | self.timer.Stop() 554 | click_action = 'click' 555 | if not self._mouse_up: 556 | click_action = 'long_click' 557 | if not self._element_tree: 558 | Log.e('on_screenshot_single_click', 'failed to get element tree') 559 | return 560 | self._focused_element = None 561 | self._get_focused_element(self._single_click_position, self._element_tree) 562 | print 'focus_element:', self._focused_element ['rect'] 563 | rect = (self._focused_element ['rect']['origin']['x'], self._focused_element ['rect']['origin']['y'], \ 564 | self._focused_element ['rect']['size']['width'], self._focused_element ['rect']['size']['height']) 565 | #红色标记截图中鼠标位置的控件 566 | if self._orientation == 3: 567 | rect = (self.image_shown_width/self._scale_rate[0]-rect[1] - rect[3], rect[0], rect[3], rect[2]) 568 | self.mask_panel.highlight_element(rect, self._scale_rate) 569 | #展开控件树 570 | self._expand_uitree(self._focused_element['item_id']) 571 | #选中对应的控件 572 | self.tc_uitree.SelectItem(self._focused_element['item_id']) 573 | self.tc_uitree.SetFocus() 574 | #推荐控件的QPath 575 | self._recommend_qpath(self._focused_element) 576 | #判断是否进行远程控制 577 | if self.remote_operator_menu.IsChecked() and self._device_driver: 578 | driver_method = getattr(self._device_driver, click_action) 579 | driver_method(self.x, self.y) 580 | time.sleep(1.5) 581 | self._run_in_work_thread(self.on_get_uitree,event) 582 | 583 | def on_screenshot_mouse_event(self, event): 584 | pos = event.GetPosition() 585 | if self._orientation == 3: 586 | pos = (pos[1], self.image_shown_width - pos[0]) 587 | 588 | self.statusbar.SetStatusText(str(pos),2) 589 | if not self._app_started: 590 | self.statusbar.SetStatusText(u'App未启动',0) 591 | return 592 | 593 | if event.ButtonDown(): 594 | self._mouse_up = False 595 | self._event_start = time.time() 596 | pos = (pos[0]/self._scale_rate[0], pos[1]/self._scale_rate[1]) 597 | self.x = float(pos[0])/self._app_shown_width 598 | self.y = float(pos[1])/self._app_shown_height 599 | if not event.RightDown(): 600 | self.timer = wx.Timer(self) 601 | self.timer.Start(200) # 0.2 seconds delay 602 | self.Bind(wx.EVT_TIMER, self.on_screenshot_single_click, self.timer) 603 | 604 | elif event.Dragging(): 605 | self.timer.Stop() 606 | 607 | elif event.LeftDClick(): 608 | self.timer.Stop() 609 | 610 | elif event.LeftUp(): 611 | self._mouse_up = True 612 | self._event_interval = time.time() - self._event_start 613 | new_pos = (pos[0]/self._scale_rate[0], pos[1]/self._scale_rate[1]) 614 | x1 = float(new_pos[0])/self._app_shown_width 615 | y1 = float(new_pos[1])/self._app_shown_height 616 | self._single_click_position = new_pos 617 | if abs(self.x - x1) > 0.02 or abs(self.y - y1) > 0.02: 618 | if self.remote_operator_menu.IsChecked() and self._device_driver: 619 | self._device_driver.drag(self.x, self.y, x1, y1, self._event_interval) 620 | time.sleep(1.5) 621 | self._run_in_work_thread(self.on_get_uitree,event) 622 | 623 | def on_uitree_node_click(self, event): 624 | 625 | item_id = event.GetItem() 626 | 627 | item_data = self.tc_uitree.GetItemData(item_id) 628 | 629 | item_data['depth'] = self.get_element_depth(item_id) 630 | 631 | self.tc_classname.SetValue(item_data['classname']) 632 | name = item_data['name'] if item_data['name'] else 'null' 633 | if isinstance(name, basestring): 634 | name = name.decode('utf-8') 635 | else: 636 | name = str(name) 637 | self.tc_id.SetValue(name) 638 | label = item_data['label'] if item_data['label'] else 'null' 639 | if isinstance(label, basestring): 640 | label = label.decode('utf-8') 641 | else: 642 | label = str(label) 643 | self.tc_label.SetValue(label) 644 | value = item_data['value'] if item_data['value'] else 'null' 645 | if isinstance(value, basestring): 646 | value = value.decode('utf-8') 647 | else: 648 | value = str(value) 649 | self.tc_value.SetValue(value) 650 | rect = (item_data['rect']['origin']['x'], item_data['rect']['origin']['y'], \ 651 | item_data['rect']['size']['width'], item_data['rect']['size']['height']) 652 | self.tc_rect.SetValue(str(rect)) 653 | self.tc_visible.SetValue('true' if item_data['visible'] else 'False') 654 | self.tc_enable.SetValue('true' if item_data['enabled'] else 'False') 655 | 656 | if self._orientation == 3: 657 | rect = (self.image_shown_width/self._scale_rate[0]-rect[1]-rect[3], rect[0], rect[3], rect[2]) 658 | self.mask_panel.highlight_element(rect, self._scale_rate) 659 | 660 | self._recommend_qpath(item_data) 661 | 662 | def on_uitree_node_right_click(self, event): 663 | self.tc_uitree.PopupMenu(TreeNodePopupMenu(self, event.GetItem()), event.Point) 664 | 665 | def on_select_install_pkg(self, event): 666 | if self._device is None: 667 | self.create_tip_dialog(u'未选择设备,请连接设备主机并选择设备!') 668 | return 669 | dlg = wx.FileDialog(self, u"选择app的安装包", "", "", 670 | "app files (*.zip,*.ipa)|*.zip;*.ipa", wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) 671 | if dlg.ShowModal() == wx.ID_CANCEL: 672 | return 673 | pkg_path = dlg.GetPath().encode('utf-8') 674 | dlg.Destroy() 675 | self._run_in_work_thread(self.on_install, pkg_path) 676 | 677 | def on_install(self, pkg_path): 678 | 679 | self._run_in_main_thread(self.show_dialog, 'APP正在安装中......') 680 | while(True): 681 | if self._process_dlg_running: 682 | break 683 | dlg = self._dialog 684 | self._run_in_main_thread(dlg.on_update) 685 | 686 | if not self._device_driver: 687 | self._device_driver = DeviceDriver(self._host_driver.host_url, self._device['udid']) 688 | try: 689 | result = self._device_driver.install_app(pkg_path) 690 | self._process_dlg_running = False 691 | self._run_in_main_thread(dlg.on_destory) 692 | if result: 693 | self._run_in_main_thread(wx.MessageBox, '安装成功', '信息提示', wx.OK|wx.ICON_INFORMATION) 694 | self._update_bundle_id_list() 695 | else: 696 | self._run_in_main_thread(wx.MessageBox, '安装失败', '信息提示', wx.OK|wx.ICON_INFORMATION) 697 | except: 698 | error = traceback.format_exc() 699 | Log.e('start_app', error) 700 | self.create_tip_dialog(error.decode('utf-8')) 701 | 702 | def on_uninstall(self, event): 703 | if self._device is None: 704 | self.create_tip_dialog(u'未选择设备,请连接设备主机并选择设备!') 705 | return 706 | if not self._device_driver: 707 | self._device_driver = DeviceDriver(self._host_driver.host_url, self._device['udid']) 708 | index = self.tc_bundle_id.GetSelection() 709 | bundle_id = self.tc_bundle_id.GetClientData(index).keys()[0] 710 | result = self._device_driver.uninstall_app(bundle_id) 711 | 712 | if result: 713 | uninstall_app = self.get_app(bundle_id) 714 | message_title = uninstall_app[bundle_id]+':'+bundle_id 715 | wx.MessageBox(u"卸载成功",message_title.decode('utf-8')) 716 | self._update_bundle_id_list() 717 | else: 718 | wx.MessageBox(u"卸载失败") 719 | 720 | def on_log(self, event): 721 | ''' 722 | 打开日志所在文件夹 723 | ''' 724 | log_path = Log.gen_log_path() 725 | if os.path.exists(log_path): 726 | if sys.platform == 'darwin': 727 | log_cmd = 'open %s' % log_path 728 | subprocess.call(log_cmd,shell=True) 729 | elif sys.platform == 'win32': 730 | os.system("explorer.exe %s" % log_path) 731 | else: 732 | self.create_tip_dialog(u'日志文件被移除') 733 | return 734 | 735 | def on_select_bundle_id(self, event): 736 | ''' 737 | 当bundle_id切换选择时,更新沙盒目录 738 | ''' 739 | if self.treeframe: 740 | self.treeframe.update_directory_tree(self.tc_bundle_id.GetValue()) 741 | 742 | def on_update_app_list(self, event): 743 | ''' 744 | 更新app列表 745 | ''' 746 | if self._device: 747 | self._run_in_work_thread(self._update_bundle_id_list) 748 | 749 | def on_select_bundle_all(self, event): 750 | ''' 751 | 根据按钮打开多有或者用户APP 752 | ''' 753 | bundle_check = event.GetEventObject() 754 | if bundle_check.GetValue(): 755 | self._app_type = 'all' 756 | else: 757 | self._app_type = 'user' 758 | if self._device: 759 | self._run_in_work_thread(self._update_bundle_id_list) 760 | 761 | def get_element_depth(self, item_id): 762 | ''' 763 | 获取element的depth 764 | :param item_id element的属性 765 | 766 | return depth 767 | ''' 768 | depth = 0 769 | while item_id != self._root_item: 770 | item_id = self.tc_uitree.GetItemParent(item_id) 771 | depth += 1 772 | return depth 773 | 774 | def get_app(self, bundle_id): 775 | ''' 776 | 根据bundle_id从app_list中获取app信息 777 | ''' 778 | for app in self._app_list: 779 | if app.has_key(bundle_id): 780 | return app 781 | 782 | def on_debug(self, event): 783 | ''' 784 | ''' 785 | current_path = os.getcwd() 786 | debug_path = os.path.join(current_path, DEBUG_PATH) 787 | if current_path.endswith('ui'): 788 | wx.MessageBox('本模式下不支持') 789 | else: 790 | self.Close(force=True) 791 | wx.Exit() 792 | cmd = 'open %s' % debug_path 793 | subprocess.call(cmd,shell=True) 794 | 795 | def on_settings(self, event): 796 | setting_dlg = Settings(self._config_file_path, None, title=u'环境参数设置') 797 | setting_dlg.ShowModal() 798 | setting_dlg.Destroy() 799 | 800 | def on_sandbox_view(self, event): 801 | ''' 802 | 查看沙盒的目录结构 803 | ''' 804 | if self._device is None: 805 | self.create_tip_dialog(u'未选择设备,请连接设备主机并选择设备!') 806 | return 807 | self.treeframe = TreeFrame(self, '/', '%s沙盒目录' % self._device['name'], self._device_driver, self.tc_bundle_id.GetValue()) 808 | self.treeframe.Show(show=True) 809 | 810 | def on_update_sandbox_view(self): 811 | if self.treeframe: 812 | self.treeframe.update_tree_frame('%s沙盒目录' % self._device['name'], self._device_driver, self.tc_bundle_id.GetValue()) 813 | 814 | def on_close_treeFrame(self): 815 | '''监控沙盒frame是否关闭 816 | ''' 817 | self.treeframe = None 818 | 819 | 820 | class CanvasPanel(wx.Panel): 821 | '''绘图面板 822 | ''' 823 | def __init__(self, *args, **kwargs): 824 | wx.Panel.__init__(self, *args, **kwargs) 825 | self.Bind(wx.EVT_PAINT, self.on_paint) 826 | self._draw_points = None 827 | self._last_draw_points = None 828 | 829 | def draw_rectangle(self, p1, p2): 830 | '''画长方形 831 | ''' 832 | self._draw_points = (p1, p2) 833 | if self._last_draw_points and self._last_draw_points[0] == p1 and self._last_draw_points[1] == p2: 834 | pass 835 | else: 836 | self.Hide() 837 | self.Show() 838 | 839 | def highlight_element(self, rect, scale_rate): 840 | p1 = rect[0] * scale_rate[0], rect[1] * scale_rate[1] 841 | p2 = (rect[0] + rect[2]) * scale_rate[0], (rect[1] + rect[3]) * scale_rate[1] 842 | self.draw_rectangle(p1, p2) 843 | 844 | def on_paint(self, evt): 845 | if self._draw_points: 846 | left, top = self._draw_points[0] 847 | right, bottom = self._draw_points[1] 848 | dc = wx.PaintDC(self) 849 | dc.SetPen(wx.Pen('red', 2)) 850 | dc.DrawLine(left, top, right, top) 851 | dc.DrawLine(right, top, right, bottom) 852 | dc.DrawLine(right, bottom, left, bottom) 853 | dc.DrawLine(left, bottom, left, top) 854 | self._last_draw_points = self._draw_points 855 | self._draw_points = None 856 | 857 | 858 | class TreeNodePopupMenu(wx.Menu): 859 | '''控件树节点的弹出菜单 860 | ''' 861 | 862 | def __init__(self, parent, select_node, *args, **kwargs): 863 | super(TreeNodePopupMenu, self).__init__(*args, **kwargs) 864 | self._parent = parent 865 | self._select_node = select_node 866 | 867 | item1 = wx.MenuItem(self, wx.NewId(), u'打开WebInspector') 868 | self.AppendItem(item1) 869 | self.Bind(wx.EVT_MENU, self.on_open_web_inspector, item1) 870 | 871 | def on_open_web_inspector(self, event): 872 | if sys.platform == 'darwin': 873 | import webbrowser 874 | safari = webbrowser.get('safari') 875 | safari.open('', new=0, autoraise=True) 876 | else: 877 | self._parent.create_tip_dialog(u'WebInspector仅支持MacOS系统下Safari浏览器') 878 | 879 | 880 | class MyProgressDialog(wx.ProgressDialog): 881 | ''' 882 | 弹出显示框可更新内容显示 883 | ''' 884 | 885 | def __init__(self, msg, panel, progress_max = 100): 886 | super(MyProgressDialog, self).__init__(u"提示", msg, progress_max, parent=panel, style=wx.PD_CAN_ABORT|wx.PD_APP_MODAL|wx.PD_AUTO_HIDE) 887 | self._title_msg = msg 888 | self._is_destoryed = False 889 | self._progress_max = progress_max 890 | self._dialog_destory = False 891 | 892 | def on_update_title_msg(self, msg): 893 | self._title_msg = msg 894 | 895 | def on_destory(self): 896 | self._dialog_destory = True 897 | 898 | def on_close(self): 899 | if not self._is_destoryed: 900 | self.Destroy() 901 | self._is_destoryed = True 902 | 903 | def on_update(self): 904 | count = 0 905 | while not self._is_destoryed and count < self._progress_max: 906 | count = (count + 1) % 100 907 | wx.MilliSleep(100) 908 | if self._dialog_destory or not self.Update(count,self._title_msg)[0]: 909 | break 910 | self.on_close() 911 | 912 | 913 | class FileDropTarget(wx.FileDropTarget): 914 | def __init__(self, frame_window, target): 915 | wx.FileDropTarget.__init__(self) 916 | self.frame_window = frame_window 917 | self.target = target 918 | 919 | def OnDropFiles(self, x, y, filepath): 920 | 921 | if self.frame_window._device is None: 922 | self.frame_window.create_tip_dialog(u'未选择设备,请连接设备主机并选择设备!') 923 | return False 924 | 925 | for path in filepath: 926 | path = path.encode('utf-8') 927 | basename = os.path.basename(path) 928 | if path.endswith(('.ipa', '.zip')): 929 | self.frame_window._run_in_work_thread(self.frame_window.on_install, path) 930 | else: 931 | self.frame_window.create_tip_dialog(u'%s格式不符合,请选择.ipa(真机)或者.zip(模拟器)类型安装包' % basename) 932 | return filepath is None 933 | 934 | 935 | class Settings(wx.Dialog): 936 | '''设置系统参数 937 | ''' 938 | 939 | def __init__(self, settings_file, *args, **kw): 940 | super(Settings, self).__init__(*args, **kw) 941 | self._settings_file = settings_file 942 | self._settings = {} 943 | self._load_settings() 944 | self._init_dialog() 945 | 946 | def _load_settings(self): 947 | config_parser = ConfigParser.ConfigParser() 948 | config_parser.read(self._settings_file) 949 | try: 950 | qt4i_manage_path = config_parser.get('uispy', 'qt4i-manage') 951 | except ConfigParser.NoOptionError: 952 | qt4i_manage_path = os.path.expanduser("~/Library/Python/2.7/bin/qt4i-manage") 953 | self._settings['qt4i-manage'] = qt4i_manage_path 954 | 955 | def _init_dialog(self): 956 | 957 | vertical_box = wx.BoxSizer(wx.VERTICAL) 958 | 959 | row_box = wx.BoxSizer(wx.HORIZONTAL) 960 | 961 | label = wx.StaticText(self, -1, "qt4i-manage:") 962 | row_box.Add(label, 0, wx.ALIGN_CENTRE | wx.ALL, 5) 963 | 964 | self.tc_qt4i_manage = wx.TextCtrl(self, -1, "", size=(400, -1)) 965 | self.tc_qt4i_manage.SetToolTip(u"qt4i-manage的安装路径") 966 | row_box.Add(self.tc_qt4i_manage, 1, wx.ALIGN_CENTRE | wx.ALL, 5) 967 | self.tc_qt4i_manage.SetValue(self._settings['qt4i-manage']) 968 | 969 | vertical_box.Add(row_box, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) 970 | 971 | line = wx.StaticLine(self, -1, size=(20, -1), style=wx.LI_HORIZONTAL) 972 | vertical_box.Add(line, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.TOP, 5) 973 | 974 | btnsizer = wx.StdDialogButtonSizer() 975 | 976 | btn_apply = wx.Button(self, wx.ID_APPLY) 977 | btn_apply.SetToolTip(u"设置生效") 978 | btn_apply.SetDefault() 979 | btn_apply.Bind(wx.EVT_BUTTON, self.on_apply_settings) 980 | btnsizer.AddButton(btn_apply) 981 | 982 | btn_cancel = wx.Button(self, wx.ID_CANCEL) 983 | btn_cancel.SetToolTip(u"取消设置") 984 | btnsizer.AddButton(btn_cancel) 985 | btnsizer.Realize() 986 | 987 | vertical_box.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) 988 | 989 | self.SetSizer(vertical_box) 990 | vertical_box.Fit(self) 991 | 992 | def on_apply_settings(self, event): 993 | config_parser = ConfigParser.ConfigParser() 994 | config_parser.read(self._settings_file) 995 | config_parser.set("uispy", "qt4i-manage", self.tc_qt4i_manage.GetValue()) 996 | with open(self._settings_file, 'w') as fd: 997 | config_parser.write(fd) 998 | --------------------------------------------------------------------------------