|
| 1 | +# -*-coding:utf-8-*- |
| 2 | +# sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030') |
| 3 | + |
| 4 | + |
| 5 | +import uiautomation as uia |
| 6 | +import win32gui, win32con |
| 7 | +import win32clipboard as wc |
| 8 | +import time |
| 9 | +import os |
| 10 | + |
| 11 | +PUBLISH_ID = '公众号:程序员晚枫' |
| 12 | + |
| 13 | +COPYDICT = {} |
| 14 | + |
| 15 | +class WxParam: |
| 16 | + SYS_TEXT_HEIGHT = 33 |
| 17 | + TIME_TEXT_HEIGHT = 34 |
| 18 | + RECALL_TEXT_HEIGHT = 45 |
| 19 | + CHAT_TEXT_HEIGHT = 52 |
| 20 | + CHAT_IMG_HEIGHT = 117 |
| 21 | + SpecialTypes = ['[文件]', '[图片]', '[视频]', '[音乐]', '[链接]'] |
| 22 | + |
| 23 | + |
| 24 | +class WxUtils: |
| 25 | + def SplitMessage(MsgItem): |
| 26 | + uia.SetGlobalSearchTimeout(0) |
| 27 | + MsgItemName = MsgItem.Name |
| 28 | + if MsgItem.BoundingRectangle.height() == WxParam.SYS_TEXT_HEIGHT: |
| 29 | + Msg = ('SYS', MsgItemName, ''.join([str(i) for i in MsgItem.GetRuntimeId()])) |
| 30 | + elif MsgItem.BoundingRectangle.height() == WxParam.TIME_TEXT_HEIGHT: |
| 31 | + Msg = ('Time', MsgItemName, ''.join([str(i) for i in MsgItem.GetRuntimeId()])) |
| 32 | + elif MsgItem.BoundingRectangle.height() == WxParam.RECALL_TEXT_HEIGHT: |
| 33 | + if '撤回' in MsgItemName: |
| 34 | + Msg = ('Recall', MsgItemName, ''.join([str(i) for i in MsgItem.GetRuntimeId()])) |
| 35 | + else: |
| 36 | + Msg = ('SYS', MsgItemName, ''.join([str(i) for i in MsgItem.GetRuntimeId()])) |
| 37 | + else: |
| 38 | + Index = 1 |
| 39 | + User = MsgItem.ButtonControl(foundIndex=Index) |
| 40 | + try: |
| 41 | + while True: |
| 42 | + if User.Name == '': |
| 43 | + Index += 1 |
| 44 | + User = MsgItem.ButtonControl(foundIndex=Index) |
| 45 | + else: |
| 46 | + break |
| 47 | + Msg = (User.Name, MsgItemName, ''.join([str(i) for i in MsgItem.GetRuntimeId()])) |
| 48 | + except: |
| 49 | + Msg = ('SYS', MsgItemName, ''.join([str(i) for i in MsgItem.GetRuntimeId()])) |
| 50 | + uia.SetGlobalSearchTimeout(10.0) |
| 51 | + return Msg |
| 52 | + |
| 53 | + def SetClipboard(data, dtype='text'): |
| 54 | + '''复制文本信息或图片到剪贴板 |
| 55 | + data : 要复制的内容,str 或 Image 图像''' |
| 56 | + if dtype.upper() == 'TEXT': |
| 57 | + type_data = win32con.CF_UNICODETEXT |
| 58 | + elif dtype.upper() == 'IMAGE': |
| 59 | + from io import BytesIO |
| 60 | + type_data = win32con.CF_DIB |
| 61 | + output = BytesIO() |
| 62 | + data.save(output, 'BMP') |
| 63 | + data = output.getvalue()[14:] |
| 64 | + else: |
| 65 | + raise ValueError('param (dtype) only "text" or "image" supported') |
| 66 | + wc.OpenClipboard() |
| 67 | + wc.EmptyClipboard() |
| 68 | + wc.SetClipboardData(type_data, data) |
| 69 | + wc.CloseClipboard() |
| 70 | + |
| 71 | + def Screenshot(hwnd, to_clipboard=True): |
| 72 | + '''为句柄为hwnd的窗口程序截图 |
| 73 | + hwnd : 句柄 |
| 74 | + to_clipboard : 是否复制到剪贴板 |
| 75 | + ''' |
| 76 | + import pyscreenshot as shot |
| 77 | + bbox = win32gui.GetWindowRect(hwnd) |
| 78 | + win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, \ |
| 79 | + win32con.SWP_SHOWWINDOW | win32con.SWP_NOMOVE | win32con.SWP_NOSIZE) |
| 80 | + win32gui.SetWindowPos(hwnd, win32con.HWND_NOTOPMOST, 0, 0, 0, 0, \ |
| 81 | + win32con.SWP_SHOWWINDOW | win32con.SWP_NOMOVE | win32con.SWP_NOSIZE) |
| 82 | + win32gui.BringWindowToTop(hwnd) |
| 83 | + im = shot.grab(bbox) |
| 84 | + if to_clipboard: |
| 85 | + WxUtils.SetClipboard(im, 'image') |
| 86 | + return im |
| 87 | + |
| 88 | + def SavePic(savepath=None, filename=None): |
| 89 | + Pic = uia.WindowControl(ClassName='ImagePreviewWnd', Name='图片查看') |
| 90 | + Pic.SendKeys('{Ctrl}s') |
| 91 | + SaveAs = Pic.WindowControl(ClassName='#32770', Name='另存为...') |
| 92 | + SaveAsEdit = SaveAs.EditControl(ClassName='Edit', Name='文件名:') |
| 93 | + SaveButton = Pic.ButtonControl(ClassName='Button', Name='保存(S)') |
| 94 | + PicName, Ex = os.path.splitext(SaveAsEdit.GetValuePattern().Value) |
| 95 | + if not savepath: |
| 96 | + savepath = os.getcwd() |
| 97 | + if not filename: |
| 98 | + filename = PicName |
| 99 | + FilePath = os.path.realpath(os.path.join(savepath, filename + Ex)) |
| 100 | + SaveAsEdit.SendKeys(FilePath) |
| 101 | + SaveButton.Click() |
| 102 | + Pic.SendKeys('{Esc}') |
| 103 | + |
| 104 | + def ControlSize(control): |
| 105 | + locate = control.BoundingRectangle |
| 106 | + size = (locate.width(), locate.height()) |
| 107 | + return size |
| 108 | + |
| 109 | + def ClipboardFormats(unit=0, *units): |
| 110 | + units = list(units) |
| 111 | + wc.OpenClipboard() |
| 112 | + u = wc.EnumClipboardFormats(unit) |
| 113 | + wc.CloseClipboard() |
| 114 | + units.append(u) |
| 115 | + if u: |
| 116 | + units = WxUtils.ClipboardFormats(u, *units) |
| 117 | + return units |
| 118 | + |
| 119 | + def CopyDict(self): |
| 120 | + Dict = {} |
| 121 | + for i in WxUtils.ClipboardFormats(): |
| 122 | + if i == 0: |
| 123 | + continue |
| 124 | + wc.OpenClipboard() |
| 125 | + try: |
| 126 | + content = wc.GetClipboardData(i) |
| 127 | + wc.CloseClipboard() |
| 128 | + except: |
| 129 | + wc.CloseClipboard() |
| 130 | + raise ValueError |
| 131 | + if len(str(i)) >= 4: |
| 132 | + Dict[str(i)] = content |
| 133 | + return Dict |
| 134 | + |
| 135 | + |
| 136 | +class WeChat: |
| 137 | + def __init__(self): |
| 138 | + self.UiaAPI = uia.WindowControl(ClassName='WeChatMainWndForPC') |
| 139 | + |
| 140 | + self.SessionList = self.UiaAPI.ListControl(Name='会话') |
| 141 | + self.EditMsg = self.UiaAPI.EditControl(Name='输入') |
| 142 | + self.SearchBox = self.UiaAPI.EditControl(Name='搜索') |
| 143 | + self.MsgList = self.UiaAPI.ListControl(Name='消息') |
| 144 | + self.SessionItemList = [] |
| 145 | + self.Group = self.UiaAPI.ButtonControl().Name |
| 146 | + |
| 147 | + def GetSessionList(self, reset=False): |
| 148 | + '''获取当前会话列表,更新会话列表''' |
| 149 | + self.SessionItem = self.SessionList.ListItemControl() |
| 150 | + SessionList = [] |
| 151 | + if reset: |
| 152 | + self.SessionItemList = [] |
| 153 | + for i in range(100): |
| 154 | + try: |
| 155 | + name = self.SessionItem.Name |
| 156 | + except: |
| 157 | + break |
| 158 | + if name not in self.SessionItemList: |
| 159 | + self.SessionItemList.append(name) |
| 160 | + if name not in SessionList: |
| 161 | + SessionList.append(name) |
| 162 | + self.SessionItem = self.SessionItem.GetNextSiblingControl() |
| 163 | + return SessionList |
| 164 | + |
| 165 | + def Search(self, keyword): |
| 166 | + ''' |
| 167 | + 查找微信好友或关键词 |
| 168 | + keywords: 要查找的关键词,str * 最好完整匹配,不完全匹配只会选取搜索框第一个 |
| 169 | + ''' |
| 170 | + self.UiaAPI.SetFocus() |
| 171 | + time.sleep(0.1) |
| 172 | + self.UiaAPI.SendKeys('{Ctrl}f{Ctrl}a', waitTime=0.2) |
| 173 | + self.SearchBox.SendKeys(keyword, waitTime=0.2) |
| 174 | + self.SearchBox.SendKeys('{Enter}') |
| 175 | + |
| 176 | + def ChatWith(self, who, RollTimes=None): |
| 177 | + ''' |
| 178 | + 打开某个聊天框 |
| 179 | + who : 要打开的聊天框好友名,str; * 最好完整匹配,不完全匹配只会选取搜索框第一个 |
| 180 | + RollTimes : 默认向下滚动多少次,再进行搜索 |
| 181 | + ''' |
| 182 | + self.UiaAPI.SwitchToThisWindow() |
| 183 | + RollTimes = 1 if not RollTimes else RollTimes |
| 184 | + |
| 185 | + def roll_to(who=who, RollTimes=RollTimes): |
| 186 | + for i in range(RollTimes): |
| 187 | + if who not in self.GetSessionList()[:-1]: |
| 188 | + self.SessionList.WheelDown(wheelTimes=1, waitTime=0.01 * i) |
| 189 | + else: |
| 190 | + time.sleep(0.1) |
| 191 | + self.SessionList.ListItemControl(Name=who).Click(simulateMove=False) |
| 192 | + return 1 |
| 193 | + return 0 |
| 194 | + |
| 195 | + rollresult = roll_to() |
| 196 | + if rollresult: |
| 197 | + return 1 |
| 198 | + else: |
| 199 | + self.Search(who) |
| 200 | + return roll_to(RollTimes=1) |
| 201 | + |
| 202 | + def SendMsg(self, msg, clear=True): |
| 203 | + '''向当前窗口发送消息 |
| 204 | + msg : 要发送的消息 |
| 205 | + clear : 是否清除当前已编辑内容 |
| 206 | + ''' |
| 207 | + self.UiaAPI.SwitchToThisWindow() |
| 208 | + if clear: |
| 209 | + self.EditMsg.SendKeys('{Ctrl}a', waitTime=0) |
| 210 | + self.EditMsg.SendKeys(msg, waitTime=0) |
| 211 | + self.EditMsg.SendKeys('{Ctrl}{Enter}', waitTime=0) |
| 212 | + |
| 213 | + def SendEnd(self, clear=False): |
| 214 | + '''向当前窗口发送消息 |
| 215 | + msg : 要发送的消息 |
| 216 | + clear : 是否清除当前已编辑内容 |
| 217 | + ''' |
| 218 | + self.UiaAPI.SwitchToThisWindow() |
| 219 | + self.EditMsg.SendKeys('{Enter}', waitTime=0) |
| 220 | + |
| 221 | + def SendFiles(self, *filepath, not_exists='ignore'): |
| 222 | + """向当前聊天窗口发送文件 |
| 223 | + not_exists: 如果未找到指定文件,继续或终止程序 |
| 224 | + *filepath: 要复制文件的绝对路径""" |
| 225 | + global COPYDICT |
| 226 | + key = '' |
| 227 | + for file in filepath: |
| 228 | + file = os.path.realpath(file) |
| 229 | + if not os.path.exists(file): |
| 230 | + if not_exists.upper() == 'IGNORE': |
| 231 | + print('File not exists:', file) |
| 232 | + continue |
| 233 | + elif not_exists.upper() == 'RAISE': |
| 234 | + raise FileExistsError('File Not Exists: %s' % file) |
| 235 | + else: |
| 236 | + raise ValueError('param not_exists only "ignore" or "raise" supported') |
| 237 | + key += '<EditElement type="3" filepath="%s" shortcut="" />' % file |
| 238 | + if not key: |
| 239 | + return 0 |
| 240 | + if not COPYDICT: |
| 241 | + self.EditMsg.SendKeys(' ', waitTime=0) |
| 242 | + self.EditMsg.SendKeys('{Ctrl}a', waitTime=0) |
| 243 | + self.EditMsg.SendKeys('{Ctrl}c', waitTime=0) |
| 244 | + self.EditMsg.SendKeys('{Delete}', waitTime=0) |
| 245 | + while True: |
| 246 | + try: |
| 247 | + COPYDICT = WxUtils.CopyDict() |
| 248 | + break |
| 249 | + except: |
| 250 | + pass |
| 251 | + wc.OpenClipboard() |
| 252 | + wc.EmptyClipboard() |
| 253 | + wc.SetClipboardData(13, '') |
| 254 | + wc.SetClipboardData(16, b'\x04\x08\x00\x00') |
| 255 | + wc.SetClipboardData(1, b'') |
| 256 | + wc.SetClipboardData(7, b'') |
| 257 | + for i in COPYDICT: |
| 258 | + copydata = COPYDICT[i].replace(b'<EditElement type="0" pasteType="0"><![CDATA[ ]]></EditElement>', |
| 259 | + key.encode()).replace(b'type="0"', b'type="3"') |
| 260 | + wc.SetClipboardData(int(i), copydata) |
| 261 | + wc.CloseClipboard() |
| 262 | + self.SendClipboard() |
| 263 | + return 1 |
| 264 | + |
| 265 | + def SendClipboard(self): |
| 266 | + '''向当前聊天页面发送剪贴板复制的内容''' |
| 267 | + self.SendMsg('{Ctrl}v') |
| 268 | + |
| 269 | + @property |
| 270 | + def GetAllMessage(self): |
| 271 | + '''获取当前窗口中加载的所有聊天记录''' |
| 272 | + MsgDocker = [] |
| 273 | + MsgItems = self.MsgList.GetChildren() |
| 274 | + for MsgItem in MsgItems: |
| 275 | + MsgDocker.append(WxUtils.SplitMessage(MsgItem)) |
| 276 | + return MsgDocker |
| 277 | + |
| 278 | + @property |
| 279 | + def GetLastMessage(self): |
| 280 | + '''获取当前窗口中最后一条聊天记录''' |
| 281 | + try: |
| 282 | + uia.SetGlobalSearchTimeout(1.0) |
| 283 | + MsgItem = self.MsgList.GetChildren()[-1] |
| 284 | + ChatName = MsgItem.ButtonControl() # 聊天对象应当从消息子目录获取 |
| 285 | + uia.SetGlobalSearchTimeout(2.0) |
| 286 | + return MsgItem.Name, ChatName.Name # 返回正确的聊天信息和聊天对象 |
| 287 | + except LookupError: |
| 288 | + pass |
| 289 | + |
| 290 | + def LoadMoreMessage(self, n=0.1): |
| 291 | + '''定位到当前聊天页面,并往上滚动鼠标滚轮,加载更多聊天记录到内存''' |
| 292 | + n = 0.1 if n < 0.1 else 1 if n > 1 else n |
| 293 | + self.MsgList.WheelUp(wheelTimes=int(500 * n), waitTime=0.1) |
| 294 | + |
| 295 | + def SendScreenshot(self, name=None, classname=None): |
| 296 | + '''发送某个桌面程序的截图,如:微信、记事本... |
| 297 | + name : 要发送的桌面程序名字,如:微信 |
| 298 | + classname : 要发送的桌面程序类别名,一般配合 spy 小工具使用,以获取类名,如:微信的类名为 WeChatMainWndForPC''' |
| 299 | + if name and classname: |
| 300 | + return 0 |
| 301 | + else: |
| 302 | + hwnd = win32gui.FindWindow(classname, name) |
| 303 | + if hwnd: |
| 304 | + WxUtils.Screenshot(hwnd) |
| 305 | + self.SendClipboard() |
| 306 | + return 1 |
| 307 | + else: |
| 308 | + return 0 |
| 309 | + |
| 310 | + def SavePic(self, savepath=None, filename=None): |
| 311 | + WxUtils.SavePic() |
| 312 | + # Pic = uia.WindowControl(ClassName='ImagePreviewWnd', Name='图片查看') |
0 commit comments