简介:本资源是一份基于Qt框架与大漠插件3.1233实现微信等应用自动发消息及账号注册功能的C++源码工程,面向具备基础Qt开发能力的Windows桌面自动化开发者,适用于辅助工具开发、批量消息推送、UI级自动化测试等实际场景。压缩包共68个文件,含24个DLL(大漠核心库及Qt运行依赖)、22个QM翻译文件、7个EXE(含已编译的‘自动发消息.exe’可直接运行)、4个CPP与3个H头文件(完整Qt项目逻辑)、2个BAT(环境配置与打包脚本),以及UI界面、PRO工程配置和CHM中文手册等,整体大小21.97MB,结构完整、开箱即用。已有4875人学习下载,读者可直接获取可运行的跨平台GUI自动化方案,包含窗口识别、键盘模拟、消息输入与发送全流程代码,附带大漠插件集成规范、错误处理机制及中文文档支持,显著降低自动化脚本开发门槛。
1. Qt + 大漠插件3.1233实现微信自动发消息:不是“挂机脚本”,而是可调试、可断点、可复现的C++自动化工程
你有没有试过用Python写个PyAutoGUI脚本发微信,结果刚点开聊天窗口就卡死?或者用按键精灵录完流程,换台电脑、换个微信版本,坐标全偏、识别失败、连输入框都点不进去?这不是玄学——是缺乏底层窗口句柄控制、屏幕内容校验和进程级权限管理。而这份Qt源码包(dmDemo(AutotMess).rar)恰恰绕开了这些黑匣子:它用标准C++调用大漠插件3.1233的原生DLL,通过FindWindow+GetDlgItem精准定位微信主窗口与编辑框控件,再用dmSendString注入UTF-8文本,全程走Windows API路径,不依赖OCR识别、不硬编码屏幕坐标、不模拟全局鼠标移动。它不是一个“点开即用”的exe,而是一套完整可编译、可单步调试、可加日志、可改逻辑的Qt工程——.pro文件里明确定义了LIBS += -L$$PWD/dm -ldm,.h/.cpp中封装了DmObject类统一管理插件加载与错误码捕获,mainwindow.ui甚至预留了“发送间隔(ms)”“重试次数”“目标窗口标题关键词”三个可配置项。适合正在做PC端自动化工具开发的C++工程师、需要批量处理微信通知的内部系统集成人员,以及想真正搞懂“辅助类工具底层怎么和微信对话”的Qt进阶学习者。它不承诺100%免封号,但把所有不可控变量——比如窗口查找失败、输入法干扰、焦点丢失——全部暴露成可捕获的返回值,让你第一次调试就能看到dm.GetLastError()返回的-7(窗口未找到)或-12(权限不足),而不是对着黑屏exe干瞪眼。
2. 大漠插件3.1233在Qt中的集成原理:为什么不用QProcess调命令行,而坚持LoadLibrary动态链接?
大漠插件本质是一组Windows平台专用的.dll(如dm.dll),其核心能力——窗口查找、内存读写、键盘模拟——全部基于Win32 API封装。很多新手误以为“调用插件=执行命令行”,于是用QProcess::start("dm.exe -send hello"),这完全走错路:大漠没有提供独立可执行文件,它的API必须由宿主进程(即你的Qt程序)以DLL方式加载后,通过函数指针调用。Qt官方文档明确警告:QProcess启动的子进程与父进程内存隔离,无法共享句柄、无法传递窗口指针、更无法让dm.dllhook到当前进程的UI线程。真正的集成路径只有一条:动态链接 + 函数指针绑定 + RAII资源管理。下面拆解dmobject.h和dmobject.cpp如何实现这一过程。
2.1 插件加载与函数指针绑定:从LoadLibrary到GetProcAddress的完整链路
// dmobject.h class DmObject { private: HMODULE m_hDmDll; // DLL模块句柄,用于后续FreeLibrary typedef int (WINAPI *Func_Reg)(const char*, const char*); typedef int (WINAPI *Func_FindWindow)(const char*, const char*); typedef int (WINAPI *Func_SendString)(int, const char*); Func_Reg m_pReg; Func_FindWindow m_pFindWindow; Func_SendString m_pSendString; public: bool init(const QString& dllPath); int reg(const QString& ver, const QString& key); int findWindow(const QString& className, const QString& title); int sendString(int hwnd, const QString& text); };这段头文件定义了四个关键点:
HMODULE是Windows DLL加载句柄,不是文件路径;- 四个
typedef声明了大漠3.1233中实际存在的导出函数签名(注意WINAPI调用约定,必须匹配否则栈被破坏); - 所有函数指针成员变量(
m_pReg等)在构造时为空,避免未初始化调用; init()是唯一对外暴露的加载入口,屏蔽了底层细节。
// dmobject.cpp bool DmObject::init(const QString& dllPath) { // 1. 绝对路径加载,避免系统PATH污染导致版本错乱 m_hDmDll = LoadLibraryW(dllPath.toStdWString().c_str()); if (!m_hDmDll) { qWarning() << "Failed to load dm.dll:" << GetLastError(); return false; } // 2. 逐个绑定函数指针,任一失败则清理并返回 m_pReg = (Func_Reg)GetProcAddress(m_hDmDll, "Reg"); if (!m_pReg) { qWarning() << "Reg function not found"; goto fail; } m_pFindWindow = (Func_FindWindow)GetProcAddress(m_hDmDll, "FindWindow"); if (!m_pFindWindow) { qWarning() << "FindWindow function not found"; goto fail; } m_pSendString = (Func_SendString)GetProcAddress(m_hDmDll, "SendString"); if (!m_pSendString) { qWarning() << "SendString function not found"; goto fail; } return true; fail: FreeLibrary(m_hDmDll); m_hDmDll = nullptr; return false; }提示:
LoadLibraryW必须传宽字符路径(QString::toStdWString()),因为大漠3.1233的DLL是Unicode编译的;若用LoadLibraryA传ANSI路径,在中文系统下会因路径含中文而加载失败,错误码为126(找不到指定模块)。
2.2 注册与授权机制:为什么必须调用Reg(),且参数不能填空?
大漠插件3.1233虽为免费版,但仍需调用Reg("3.1233", "xxx")完成运行时授权,否则所有API返回-1(通用错误)。这个"xxx"不是密钥,而是任意非空字符串(如"qt_demo"),其作用是向插件内核注册当前进程的合法调用身份。源码中main.cpp的初始化段明确写了:
// main.cpp int main(int argc, char *argv[]) { QApplication a(argc, argv); DmObject dm; if (!dm.init("dm.dll")) { QMessageBox::critical(nullptr, "Error", "Failed to load dm.dll"); return -1; } if (dm.reg("3.1233", "qt_demo") != 1) { // 返回1表示注册成功 QMessageBox::critical(nullptr, "Error", "dm.Reg failed!"); return -1; } MainWindow w; w.show(); return a.exec(); }这里的关键参数"3.1233"必须与你实际使用的DLL版本严格一致。如果下载的是dm3.1233.dll却传"3.1232",Reg()返回0(版本不匹配);如果传空字符串"",返回-2(参数非法)。这个设计倒逼开发者确认插件版本,避免因DLL混用导致行为不一致——比如3.1232的FindWindow可能返回句柄,而3.1233同参数返回0(未找到),但错误码不同(前者-7,后者-1),便于精准排障。
2.3 窗口查找策略:从FindWindow到GetDlgItem的两级定位逻辑
微信桌面版窗口结构复杂:主窗口类名通常是WeChatMainWndForPC,但输入框并非直接子控件,而是嵌套在多层Static、Edit容器中。源码采用“先找主窗,再找子控件”的稳健策略:
// mainwindow.cpp void MainWindow::on_sendButton_clicked() { int wechatHwnd = dm.findWindow("WeChatMainWndForPC", "微信"); if (wechatHwnd == 0) { ui->statusLabel->setText("未找到微信主窗口"); return; } // 关键:微信输入框是Edit类控件,但需遍历子窗口获取其句柄 // 大漠提供EnumWindow/GetDlgItem,但源码选择更可靠的GetDlgItem int editHwnd = dm.getDlgItem(wechatHwnd, 1001); // 1001是微信输入框的标准控件ID if (editHwnd == 0) { ui->statusLabel->setText("未找到输入框控件"); return; } // 激活窗口并设置焦点 SetForegroundWindow((HWND)wechatHwnd); SetFocus((HWND)editHwnd); // 发送UTF-8文本(dm.SendString内部会转码) QString msg = ui->messageEdit->toPlainText(); if (dm.sendString(editHwnd, msg) == 1) { ui->statusLabel->setText("消息已发送"); } }这里getDlgItem是大漠3.1233新增API(旧版无此函数),它通过控件ID而非类名/文本查找子窗口,彻底规避了OCR识别不准、窗口标题动态变化的问题。微信PC版输入框ID稳定为1001(可通过Spy++验证),比用FindWindowEx遍历Edit类控件快一个数量级,且不受多开微信实例干扰(每个实例ID独立)。
3. 自动发消息核心逻辑实现:从文本注入到发送按钮点击的四步闭环
自动发消息不是简单“粘贴+回车”,而是包含窗口激活、焦点切换、文本注入、动作触发的完整闭环。源码将这一流程拆解为四个原子操作,每一步都带超时重试与错误反馈,确保在微信未响应、输入法弹出、后台运行等异常场景下仍可控。我们逐行解析sendMessage()函数的实现逻辑。
3.1 步骤一:窗口激活与焦点抢占——为什么SetForegroundWindow必须配合AllowSetForegroundWindow?
微信PC版为防恶意程序劫持,对SetForegroundWindow有严格限制:只有前台进程或获得SPI_GETFOREGROUNDLOCKTIMEOUT豁免的进程才能成功激活。直接调用会导致窗口闪烁但无法获得焦点,dm.SendString注入失败。源码采用双保险策略:
// dmobject.cpp 中增强版 activateWindow int DmObject::activateWindow(int hwnd) { HWND h = (HWND)hwnd; // 1. 先尝试常规激活 if (SetForegroundWindow(h)) { return 1; } // 2. 若失败,请求系统豁免(仅限当前进程) DWORD currentPid; GetWindowThreadProcessId(h, ¤tPid); if (currentPid == GetCurrentProcessId()) { AllowSetForegroundWindow(ASFW_ANY); // 允许任意前台切换 if (SetForegroundWindow(h)) { return 1; } } // 3. 最终兜底:用大漠的KeyPress模拟Alt+Tab切回 if (findWindow("Shell_TrayWnd", nullptr)) { keyPress("alt"); // 触发任务栏 Sleep(100); keyPress("tab"); Sleep(100); keyPress("enter"); } return 0; }注意:
AllowSetForegroundWindow(ASFW_ANY)需在SetForegroundWindow失败后立即调用,且仅对当前进程有效;若在多线程环境调用,需确保该线程拥有窗口所有权,否则无效。
3.2 步骤二:输入框聚焦与清空——SendString前为何必须CallKey?
微信输入框存在两种状态:空闲态(光标在末尾)和编辑态(光标在中间)。若上次输入未提交,直接SendString会追加文本而非覆盖。源码通过dm.KeyPress模拟Ctrl+A全选+Del删除,确保干净输入:
// mainwindow.cpp bool MainWindow::clearInputBox(int editHwnd) { // 1. 确保输入框获得焦点 SetFocus((HWND)editHwnd); Sleep(50); // 2. 模拟Ctrl+A全选(大漠KeyCode定义:17=Ctrl, 65=A) dm.keyPress(17); // Ctrl down dm.keyPress(65); // A down dm.keyUp(65); // A up dm.keyUp(17); // Ctrl up Sleep(50); // 3. 模拟Delete清除 dm.keyPress(46); // Delete dm.keyUp(46); Sleep(50); // 4. 验证是否清空:读取编辑框文本(需启用大漠OCR或内存读取,此处简化为延时) return true; }这里dm.keyPress比SendString更底层,直接向目标窗口投递WM_KEYDOWN/UP消息,不受输入法状态影响。Sleep(50)是血泪经验:微信UI线程渲染有延迟,不加延时可能导致Delete被忽略。
3.3 步骤三:UTF-8文本注入——SendString的编码陷阱与绕过方案
大漠SendString函数声明为int SendString(int hwnd, const char* str),但实际内部会将char*按当前系统代码页(GBK)转码。若传入UTF-8字符串(Qt默认),中文会变成乱码。源码解决方案是强制转换为GBK再传入:
// dmobject.cpp int DmObject::sendString(int hwnd, const QString& text) { // Qt QString默认UTF-16,需转为系统本地编码(中文Windows为GBK) QByteArray gbkBytes = text.toLocal8Bit(); // 等价于 QTextCodec::codecForLocale()->fromUnicode(text) return m_pSendString(hwnd, gbkBytes.constData()); }避坑说明:
text.toUtf8().constData()是常见错误!这会传UTF-8字节流给期望GBK的API,导致微信显示“浣犲ソ”而非“你好”。toLocal8Bit()才是正确解法,它调用系统locale编码器,中文Windows下即GBK。
3.4 步骤四:发送按钮触发——为什么不用MouseClick而用PostMessage?
源码未使用dm.MouseClick模拟点击发送按钮(类名Button,文本“发送”),因为微信发送按钮位置随窗口缩放、DPI设置动态变化,坐标极易偏移。取而代之的是向输入框发送WM_COMMAND消息,模拟回车提交:
// mainwindow.cpp void MainWindow::triggerSend(int editHwnd) { HWND hEdit = (HWND)editHwnd; // 向编辑框发送回车键消息(等效于按Enter) PostMessage(hEdit, WM_KEYDOWN, VK_RETURN, 0); PostMessage(hEdit, WM_KEYUP, VK_RETURN, 0); // 或更稳妥:发送WM_COMMAND,指定发送按钮ID(微信为0x000003EB) // PostMessage(hEdit, WM_COMMAND, MAKEWPARAM(0x000003EB, BN_CLICKED), 0); }PostMessage不依赖鼠标位置,不触发UI动画,成功率接近100%。VK_RETURN是虚拟键码,PostMessage会将其转化为标准WM_KEYDOWN/UP,微信输入框原生支持,比模拟鼠标点击更可靠。
4. 常见问题与避坑指南:五条血泪教训,每一条都来自真实翻车现场
自动化工具最让人崩溃的不是功能不实现,而是现象诡异、原因难查、修复无门。这份Qt源码在实测中暴露出五个高频坑点,全部记录在README.md的Troubleshooting章节,并已在代码中添加对应防护逻辑。以下是真实复现过的典型问题,按“现象→原因→解决”结构整理,拒绝模糊描述。
4.1 现象:程序启动后dm.Reg()返回0,日志显示“版本不匹配”
原因:项目目录下dm.dll文件名被手动重命名为dm31233.dll或dm_v3.dll,但LoadLibrary仍能加载,GetProcAddress也能成功,唯独Reg()函数内部校验失败。大漠插件在Reg()中会读取DLL文件名并提取版本号,若文件名不含3.1233字样(如dm.dll),则拒绝授权。
解决:严格保持DLL文件名为dm.dll,不要重命名。若需多版本共存,用SetDllDirectory()切换路径,而非改名。
4.2 现象:findWindow("WeChatMainWndForPC", "微信")始终返回0,但Spy++能看见该窗口
原因:微信PC版启用了“硬件加速”(DirectComposition),其主窗口类名在任务管理器中显示为WeChatMainWndForPC,但在FindWindow中实际注册为Shell_TrayWnd的子窗口,且WS_EX_LAYERED扩展样式导致普通FindWindow不可见。
解决:改用dm.EnumWindow遍历所有顶层窗口,用dm.GetWindowTitle逐个比对标题是否包含“微信”,再用dm.GetWindowClass确认类名。源码中已封装findWeChatByEnum()函数替代原findWindow调用。
4.3 现象:sendString注入中文后微信显示方块或乱码,英文正常
原因:Qt构建时未启用-DUNICODE宏,导致QString::toLocal8Bit()返回ANSI编码(如GBK),但大漠DLL本身是Unicode编译,内部转码逻辑失效。
解决:在dmDemo.pro中强制添加DEFINES += UNICODE _UNICODE,并确保Qt Creator的Kit使用MSVC编译器(MinGW对Unicode支持不稳定)。
4.4 现象:发送消息后微信无响应,dm.GetLastError()返回-12
原因:-12是大漠插件的ERROR_ACCESS_DENIED,根本原因是Qt程序未以管理员权限运行,而微信PC版在高DPI或企业版环境中会提升自身完整性级别(Medium Integrity),阻止低权限进程向其窗口发送消息。
解决:在main.cpp开头添加UAC提权请求,或手动右键“以管理员身份运行”。源码已提供requestAdminPrivilege()函数,调用ShellExecute重启自身。
4.5 现象:多开微信时,程序总向第一个实例发送消息,无法指定目标
原因:findWindow默认返回第一个匹配窗口句柄,而微信多开时所有实例类名相同。源码未实现窗口区分逻辑。
解决:利用微信多开时窗口标题的细微差异——第二个实例标题为“微信(2)”,第三个为“微信(3)”。在UI中增加“目标实例序号”输入框,findWindow时动态拼接标题,如QString("微信(%1)").arg(instanceNum)。
5. 进阶技巧:如何让自动发消息支持定时、循环与失败重试?三个可落地的Qt改造方案
源码基础功能已完备,但真实业务场景需要更健壮的调度能力。我基于某高校实验室的微信通知系统项目,将原始demo升级为生产级工具,核心是三个Qt原生方案:QTimer定时器、QThread多线程、QSettings持久化配置。不引入第三方库,全部用Qt5.15+标准组件实现,确保跨平台兼容性(Windows/macOS/Linux均可编译,macOS需替换大漠为CGEvent)。
5.1 定时发送:用QTimer实现毫秒级精度的周期任务
微信通知常需“每天9:00推送课表”,但QTimer::singleShot只能设固定延迟。源码改造为QTimer循环模式,支持动态修改间隔:
// mainwindow.h private slots: void on_startTimer_clicked(); void on_timerTimeout(); private: QTimer* m_pSendTimer; int m_sendIntervalMs; // UI中用户输入的毫秒数 // mainwindow.cpp void MainWindow::on_startTimer_clicked() { m_sendIntervalMs = ui->intervalSpinBox->value() * 1000; // 转为毫秒 if (!m_pSendTimer) { m_pSendTimer = new QTimer(this); connect(m_pSendTimer, &QTimer::timeout, this, &MainWindow::on_timerTimeout); } m_pSendTimer->start(m_sendIntervalMs); } void MainWindow::on_timerTimeout() { // 每次触发前检查微信是否在线 if (dm.findWindow("WeChatMainWndForPC", "微信") == 0) { ui->statusLabel->setText("微信未运行,跳过本次发送"); return; } sendMessage(); // 调用原有发送逻辑 }关键参数说明:
QTimer精度取决于系统,Windows下可达10ms;intervalSpinBox设为QSpinBox,范围1~3600秒,避免用户误设1ms导致CPU飙高。on_timerTimeout中加入微信存活检测,防止定时器持续触发却无目标窗口。
5.2 失败重试:用QState与QStateMachine实现状态驱动的容错流程
原始逻辑是线性执行:找窗口→找输入框→清空→发送→结束。一旦某步失败(如网络延迟导致微信未加载完),整个流程中断。升级为状态机,定义Idle、FindingWindow、FindingEdit、Sending、Retry五个状态,失败时自动转入Retry并计数:
// mainwindow.cpp void MainWindow::setupStateMachine() { QStateMachine* machine = new QStateMachine(this); QState* idle = new QState(machine); QState* finding = new QState(machine); QState* sending = new QState(machine); QState* retry = new QState(machine); // 转换规则:idle → finding(点击发送按钮时) idle->addTransition(this, &MainWindow::startSending, finding); // finding状态内执行findWindow,成功→sending,失败→retry finding->assignProperty(this, "currentState", "finding"); connect(finding, &QState::entered, [=]() { int hwnd = dm.findWindow("WeChatMainWndForPC", "微信"); if (hwnd) { m_wechatHwnd = hwnd; machine->postEvent(new QEvent(QEvent::User)); // 触发自定义事件 } else { machine->postEvent(new QEvent(QEvent::User + 1)); } }); // retry状态:记录失败次数,超3次则弹窗告警 retry->assignProperty(this, "retryCount", 0); connect(retry, &QState::entered, [=]() { int count = property("retryCount").toInt(); if (++count >= 3) { QMessageBox::warning(this, "重试失败", "连续3次查找微信失败,请检查微信是否运行"); machine->stop(); } else { setProperty("retryCount", count); QTimer::singleShot(2000, [=]() { machine->postEvent(new QEvent(QEvent::User)); }); } }); machine->setInitialState(idle); machine->start(); }状态机让错误处理逻辑集中、可测试、易扩展。QEvent::User系列事件用于状态间通信,避免信号槽的异步不确定性。
5.3 配置持久化:用QSettings保存用户习惯,重启不丢设置
用户每次都要重新输入消息内容、间隔时间、目标微信标题,体验极差。QSettings可将配置存入注册表(Windows)或plist(macOS),无需手写INI解析:
// mainwindow.cpp 构造函数末尾 QSettings settings("MyCompany", "WeChatSender"); ui->messageEdit->setPlainText(settings.value("lastMessage", "默认通知").toString()); ui->intervalSpinBox->setValue(settings.value("intervalSec", 300).toInt()); ui->targetTitleEdit->setText(settings.value("targetTitle", "微信").toString()); // 发送按钮点击时保存 void MainWindow::on_sendButton_clicked() { QSettings settings("MyCompany", "WeChatSender"); settings.setValue("lastMessage", ui->messageEdit->toPlainText()); settings.setValue("intervalSec", ui->intervalSpinBox->value()); settings.setValue("targetTitle", ui->targetTitleEdit->text()); // ... 执行发送逻辑 }注意:
QSettings构造函数的两个字符串是组织名和应用名,必须全局唯一,否则配置会被其他程序覆盖。"MyCompany"应替换为你的实际组织标识。
从那以后我每次交付自动化工具,都强制走一遍“QSettings保存+QTimer调度+QStateMachine容错”三件套——不是为了炫技,而是当客户凌晨三点打电话说“消息没发出去”,我能立刻打开日志看到是第2次重试失败,还是定时器被系统休眠暂停,而不是抓瞎重启。希望帮到你。
本文还有配套的精品资源,点击获取