Files
wechatauto-replica/wechatauto/ui/main.py
T

269 lines
8.8 KiB
Python

from .base import BaseUISubWnd, BaseUIWnd
from .navigationbox import NavigationBox
from .sessionbox import SessionBox
from .chatbox import ChatBox
from wechatauto.utils.win32 import (
FindWindow,
GetAllWindows,
GetPathByHwnd,
get_windows_by_pid
)
from wechatauto.param import WxParam, WxResponse, PROJECT_NAME
from wechatauto.logger import wxlog
from wechatauto import uia
from typing import (
Union,
List,
Literal
)
import os
import re
import sys
def find_wechat_windows(ui_cls_name: str) -> List[uia.Control]:
"""从所有顶层窗口中查找指定 UIA 类名的微信窗口控件。
Args:
ui_cls_name: 目标 UIA 类名,如 ``mmui::MainWindow``
Returns:
匹配到的窗口控件列表
"""
targets = []
for hwnd, clsname, winname in GetAllWindows():
# 跳过 Qt 内部消息泵等隐藏窗口,其 UIA 服务可能阻塞调用
if clsname.startswith(uia.QT_INTERNAL_WIN_CLASS_PREFIX):
continue
if uia.GetUiClassNameWithTimeout(hwnd) != ui_cls_name:
continue
try:
control = uia.ControlFromHandle(hwnd)
except Exception:
continue
if control is not None:
targets.append(control)
return targets
class WeChatSubWnd(BaseUISubWnd):
"""微信独立聊天子窗口"""
_ui_cls_name: str = 'mmui::FramelessMainWindow'
_win_cls_name: str = 'Qt51514QWindowIcon'
_chat_api: ChatBox = None
nickname: str = ''
def __init__(
self,
key: Union[str, int],
parent: 'WeChatMainWnd',
timeout: int = 3
):
self.root = self
self.parent = parent
if isinstance(key, str):
hwnd = FindWindow(classname=self._win_cls_name, name=key, timeout=timeout)
else:
hwnd = key
self.control = uia.ControlFromHandle(hwnd)
if self.control is not None:
chatbox_control = self.control.\
GroupControl(ClassName="mmui::ChatMessagePage").\
CustomControl(ClassName="mmui::XSplitterView")
self._chat_api = ChatBox(chatbox_control, self)
self.nickname = self.control.Name
def __repr__(self):
return f'<{PROJECT_NAME} - {self.__class__.__name__} object("{self.nickname}")>'
@property
def pid(self):
if not hasattr(self, '_pid'):
self._pid = self.control.ProcessId
return self._pid
def _get_chatbox(
self,
nickname: str = None,
exact: bool = False
) -> ChatBox:
return self._chat_api
def _get_windows(self):
wins = []
for hwnd in get_windows_by_pid(self.pid):
try:
wins.append(uia.ControlFromHandle(hwnd))
except Exception:
pass
ignore_cls = ['basepopupshadow', 'popupshadow']
return [win for win in wins if win.ClassName not in ignore_cls]
def chat_info(self):
return self._chat_api.get_info()
def send_msg(
self,
msg: str,
who: str = None,
clear: bool = True,
at: Union[str, List[str]] = None,
exact: bool = False,
) -> WxResponse:
chatbox = self._get_chatbox(who, exact)
if chatbox is None:
return WxResponse.failure(f"未找到聊天窗口:{who}")
return chatbox.send_msg(msg, clear, at)
def send_files(
self,
filepath,
who=None,
exact=False
) -> WxResponse:
chatbox = self._get_chatbox(who, exact)
if chatbox is None:
return WxResponse.failure(f"未找到聊天窗口:{who}")
return chatbox.send_file(filepath)
def get_msgs(self):
chatbox = self._get_chatbox()
if chatbox:
return chatbox.get_msgs()
return []
def get_new_msgs(self):
return self._get_chatbox().get_new_msgs()
def get_msg_by_id(self, msg_id):
chatbox = self._get_chatbox()
if chatbox:
return chatbox.get_msg_by_id(msg_id)
def get_msg_by_hash(self, msg_hash: str):
chatbox = self._get_chatbox()
if chatbox:
return chatbox.get_msg_by_hash(msg_hash)
def get_last_msg(self):
chatbox = self._get_chatbox()
if chatbox:
return chatbox.get_last_msg()
class WeChatMainWnd(WeChatSubWnd):
"""微信主窗口"""
_ui_cls_name: str = 'mmui::MainWindow'
_win_cls_name: str = 'Qt51514QWindowIcon'
_ui_name: str = '微信'
def __init__(self, nickname: str = None, hwnd: int = None):
self.root = self
self.parent = self
if hwnd:
self._setup_ui(hwnd)
else:
# 优先按 UIA 类名定位主窗口,兼容不同 Qt 版本的客户端
controls = find_wechat_windows(self._ui_cls_name)
if not controls:
controls = find_wechat_windows(self._ui_cls_name.replace('MainWindow', 'FramelessMainWindow'))
if not controls:
raise Exception('未找到已登录的微信主窗口')
target = None
for control in controls:
if nickname and control.Name != nickname:
continue
target = control
break
if target is None:
target = controls[0]
self._setup_ui(target.NativeWindowHandle)
print(f'初始化成功,获取到已登录窗口:{self.nickname}')
def _setup_ui(self, hwnd: int):
self.HWND = hwnd
self.control = uia.ControlFromHandle(hwnd)
if self.control is not None:
navigation_control = self.control.\
ToolBarControl(ClassName="mmui::MainTabBar", AutomationId='main_tabbar')
sessionbox_control = self.control.\
GroupControl(ClassName="mmui::ChatMasterView")
chatbox_control = self.control.\
GroupControl(ClassName="mmui::ChatMessagePage").\
CustomControl(ClassName="mmui::XSplitterView")
self._navigation_api = NavigationBox(navigation_control, self)
self._session_api = SessionBox(sessionbox_control, self)
self._chat_api = ChatBox(chatbox_control, self)
self.nickname = self.control.Name
def __repr__(self):
return f'<{PROJECT_NAME} - {self.__class__.__name__} object("{self.nickname}")>'
def _get_wx_path(self):
return GetPathByHwnd(self.HWND)
def _get_wx_dir(self):
wxdir = os.path.dirname(self._get_wx_path())
for d in os.listdir(wxdir):
if re.match(r'\d+\.\d+\.\d+\.\d+', d):
return os.path.join(wxdir, d)
def _get_chatbox(
self,
nickname: str = None,
exact: bool = False
) -> ChatBox:
if nickname and (chatbox := WeChatSubWnd(nickname, self, timeout=0)).control:
return chatbox._chat_api
else:
if nickname:
switch_result = self._session_api.switch_chat(keywords=nickname, exact=exact)
if not switch_result:
return None
if self._chat_api.msgbox.Exists(0.5):
return self._chat_api
def switch_chat(
self,
keywords: str,
exact: bool = True,
force: bool = False,
force_wait: Union[float, int] = 0.5
):
return self._session_api.switch_chat(keywords, exact, force, force_wait)
def get_all_sub_wnds(self):
sub_wxs = GetAllWindows(classname=WeChatSubWnd._win_cls_name)
return [
sub_win
for i in sub_wxs
if (
uia.ControlFromHandle(i[0]).ClassName == WeChatSubWnd._ui_cls_name
and (sub_win := WeChatSubWnd(i[0], self)).pid == self.pid
)
]
def get_sub_wnd(self, who: str):
subwins = self.get_all_sub_wnds()
for subwin in subwins:
if subwin.nickname == who:
return subwin
def open_separate_window(self, keywords: str) -> WeChatSubWnd:
if subwin := self.get_sub_wnd(keywords):
wxlog.debug(f"{keywords} 获取到已存在的子窗口: {subwin}")
return subwin
if nickname := self._session_api.switch_chat(keywords):
wxlog.debug(f"{keywords} 切换到聊天窗口: {nickname}")
if subwin := self.get_sub_wnd(nickname):
wxlog.debug(f"{nickname} 获取到已存在的子窗口: {subwin}")
return subwin
else:
keywords = nickname
if result := self._session_api.open_separate_window(keywords):
find_nickname = result['data'].get('nickname', keywords)
return WeChatSubWnd(find_nickname, self)