灵感来源
近期在Bilibili刷到这样一个视频:
有网友制作了这样一个网页,网页是一个伪装的Word,这个Word没有什么编辑功能,但是有一个主要作用,那就是摸鱼。这个网页可以导入txt文本(比如小说),然后到伪装的Word打字就能一点一点地把小说内容显示出来,假装自己在努力工作。
这个想法真的惊到我了,只不过现成的网页功能方面不太完善,用户体验感不太好,于是我决定自己动手丰衣足食。
那么话不多说,直接进入主题,开始动手!
方案选择与文档资料,Q&A
我目前采用的方案是C++ + MFC + COM的方案。
选用C++不如Python或者vue/react好写? 使用C++纯纯是因为个人喜好,如果你不喜欢,没关系,下面的原理依然可以参考。
为什么使用MFC而不是Qt? 主要原因是这个框架节省存储占用,而我的这款软件又属于小工具。
COM方案? 考虑到自己设计一个类似于Word的界面的软件,成本较高,且学业繁忙时间有限,于是我决定直接采用COM组件方案,只需稍微查阅Microsoft Office 的 Visual Basic for Application文档即可:
Office Visual Basic for Applications (VBA) 参考 | Microsoft Learn
这是Word相关的文档
Word Visual Basic for Applications (VBA) 参考 | Microsoft Learn
声明
软件代码发布至Github,请在遵循开源协议的情况下使用!请勿侵权!
GitHub - lyxyz5223/FakeWordMfc: Word工作学习伪装,可以在Word中偷偷看小说
界面设计:
- 主窗口:

- 设置窗口:

- 状态显示窗口:

设计思路与代码讲解
设计完毕,开始Coding.
首先需要包含COM相关的库:
需要注意的是,下面代码中“导入Word库”不是一次性完成的,步骤如下:
- 打开电脑上的Word/WPS Office,通过任务管理器找到其安装路径,如图:

- 在相同目录下找到MSWORD.OLB文件,复制其路径,然后写入代码中,使用#import 导入:
#import "C:\\Program Files\\Microsoft Office\\root\\Office16\\MSWORD.OLB"
- 直接编译,编译器会自动生成相关文件,会发现编译失败且出现99+个错误,不必理会,双击任意一个与Word相关的报错,进入头文件MSWORD.tlh,手动回到该文件开头(Ctrl + Home),发现如下模板内容
// Created by Microsoft (R) C/C++ Compiler Version 14.40.33813.0 (9dc93bb3).
//
// D:\C++\FakeWordMfc\FakeWordMfc\x64\Debug\MSWORD.tlh
//
// C++ source equivalent of Win32 type library C:\\Program Files\\Microsoft Office\\root\\Office16\\MSWORD.OLB
// compiler-generated file - DO NOT EDIT!//
// Cross-referenced type libraries:
// #import "C:\Program Files\Microsoft Office\Root\VFS\ProgramFilesCommonX64\Microsoft Shared\OFFICE16\MSO.DLL"
// #import "C:\Program Files\Microsoft Office\Root\VFS\ProgramFilesCommonX86\Microsoft Shared\VBA\VBA6\VBE6EXT.OLB"
//#pragma once
#pragma pack(push, 8)#include <comdef.h>namespace Word {...}
其中 Cross-referenced type libraries: 部分有一个/多个#import语句,直接复制粘贴到步骤2语句的上一行并去掉注释,但是不要着急立即编译,立即编译同样失败,这时候请看步骤4
4. 回到MSWORD.tlh头文件,直接修改该头文件,编写任意内容保存再次编译即可,例如:

至此,恭喜你完成了COM组件Word库的导入。
完整导入代码如下,注意添加#include <atlbase.h>和#include <atlcom.h>:
// 先保存并取消相关宏定义(Windows.h)
#pragma push_macro("FindText")
#pragma push_macro("ExitWindows")
#pragma push_macro("RGB")
#undef FindText
#undef ExitWindows
#undef RGB
// 导入Word库
#import "C:\\Program Files\\Microsoft Office\\Root\\VFS\\ProgramFilesCommonX64\\Microsoft Shared\\OFFICE16\\MSO.DLL"
#import "C:\\Program Files\\Microsoft Office\\Root\\VFS\\ProgramFilesCommonX86\\Microsoft Shared\\VBA\\VBA6\\VBE6EXT.OLB"
#import "C:\\Program Files\\Microsoft Office\\root\\Office16\\MSWORD.OLB"/* rename("FindText", "FindText") rename("ExitWindows", "ExitWindows")*/
// 恢复相关宏定义
#pragma pop_macro("RGB")
#pragma pop_macro("ExitWindows")
#pragma pop_macro("FindText")#include <atlbase.h>
#include <atlcom.h>
接下来就是编写Word的控制,比如打开Word,打开文档...
首先编写WordEventSink类,用于接收Word事件(如文档打开、选中区域更改...)
WordEventSink类继承自IDispatch,IDispatch是COM的底层接口,用于管理WordEventSink对象本身的生命周期(引用计数接口实现),还有QueryInterface方法用于查询当前类支持的接口有哪些,实现的时候只要return S_OK便是支持,并且需要手动赋值对应接口的地址*ppvObject = static_cast<IDispatch*>(this),Invoke方法用于分发事件,实现Invoke方法可以捕获到Word触发的事件并进行处理。
具体事件的绑定方法为,代码包含详细注释
Word::_ApplicationPtr pWordApp;
HRESULT hr = pWordApp.CreateInstance(__uuidof(Word::Application)); // 创建Word应用实例
// 挂接事件第一步:通过任一Word的接口地址(如Word::_ApplicationPtr类型变量pWordApp的地址或者Word::_Document类型变量document的地址)获取连接点容器,用于获取事件连接点
CComQIPtr<IConnectionPointContainer> pCPC(pWordApp);
if (!pCPC)return E_NOINTERFACE;
// 第二步:通过连接点容器查找对应事件接口的连接点
CComPtr<IConnectionPoint> pCP;
HRESULT hr = pCPC->FindConnectionPoint(eventUuid, &pCP);
if (FAILED(hr))return hr;
// 第三步:将当前WordEventSink类挂接上去,从而WordEventSink对象能够收到事件通知
return pCP->Advise(this, &dwCookie); // 挂接
完整代码如下:
WordController.h
#include "ComResultAndCatch.h"std::wstring HResultToWString(HRESULT hr);// 定义事件接收器类
class WordEventSink : public IDispatch
{
public:// IUnknown 方法STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) override{if (riid == IID_IUnknown || riid == IID_IDispatch || riid == eventUuid){*ppvObject = static_cast<IDispatch*>(this);AddRef();return S_OK;}*ppvObject = nullptr;return E_NOINTERFACE;}STDMETHOD_(ULONG, AddRef)() override{return InterlockedIncrement(&_refCount);}STDMETHOD_(ULONG, Release)() override{ULONG count = InterlockedDecrement(&_refCount);if (count == 0){delete this;}return count;}// IDispatch 方法STDMETHOD(GetTypeInfoCount)(UINT* pctinfo) override{*pctinfo = 0;return S_OK;}STDMETHOD(GetTypeInfo)(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) override{*ppTInfo = nullptr;return E_NOTIMPL;}STDMETHOD(GetIDsOfNames)(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId) override{return E_NOTIMPL;}STDMETHOD(Invoke)(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) override{std::cout << "dispIdMember: " << dispIdMember << std::endl;// 进行事件处理函数分发if (callbackInfoMap.count(dispIdMember)){auto [begin, end] = callbackInfoMap.equal_range(dispIdMember);for (auto &it = begin; it != end; it++){auto& info = it->second;info.callback(pDispParams, info.userData);}return S_OK;}return E_NOTIMPL;}public://enum class CallbackExecType {// Sync,// Async//};typedef std::any UserDataType;typedef std::function<Result<bool>(DISPPARAMS* pDispParams, UserDataType userData)> DispatchInvokeCallbackFunction;private:struct DispatchInvokeCallbackInfo {DISPID dispIdMember;//CallbackExecType execType;DispatchInvokeCallbackFunction callback;UserDataType userData;};typedef std::unordered_multimap<DISPID, DispatchInvokeCallbackInfo> CallbackInfoMapType;ULONG _refCount = 1;IUnknown* ptr = nullptr;DWORD dwCookie = 0;IID eventUuid;CallbackInfoMapType callbackInfoMap;
public:/*** @param p 需要绑定的接口* @param eventUuid 需要绑定的事件的uuid*/WordEventSink(IUnknown* p, const IID eventUuid) {this->ptr = p;this->eventUuid = eventUuid;}// 一键绑定HRESULT attach(){try {if (!ptr)return E_POINTER;if (dwCookie)return E_PENDING;CComQIPtr<IConnectionPointContainer> pCPC(ptr);if (!pCPC)return E_NOINTERFACE;CComPtr<IConnectionPoint> pCP;HRESULT hr = pCPC->FindConnectionPoint(eventUuid, &pCP);if (FAILED(hr))return hr;return pCP->Advise(this, &dwCookie); // 挂接}catch (...) {return E_POINTER;}}void detach(){try {if (dwCookie != 0){CComQIPtr<IConnectionPointContainer> pCPC(ptr);if (pCPC){CComPtr<IConnectionPoint> pCP;if (SUCCEEDED(pCPC->FindConnectionPoint(eventUuid, &pCP)))pCP->Unadvise(dwCookie);}dwCookie = 0;}}catch (...) {}}/*** 注册事件* @param eventDispIdMember 注册的事件号码* @param callback 回调函数*/void registerEvent(DISPID dispIdMember, DispatchInvokeCallbackFunction callback, UserDataType userData) {DispatchInvokeCallbackInfo info{dispIdMember,callback,userData};this->callbackInfoMap.insert(CallbackInfoMapType::value_type(dispIdMember, info));}
};#define ERRORRESULT_IDNOTFOUND(type) ErrorResult<type>(E_FAIL, L"Id not found")class WordController
{typedef unsigned long long IdType;public:~WordController();WordController(bool bCreateApp = true, bool bComInit = false, bool bCreateAppIfComInitFail = true);// 初始化com组件static Result<bool> comInitialize() {HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);//HRESULT hr = CoInitialize(0);return Result<bool>::CreateFromHRESULT(hr, 0);}// com组件取消初始化static void comUninitialize() {CoUninitialize();}// 获取当前对象的Word app实例Word::_ApplicationPtr getApp() const {return pWordApp;}// 通过id获取打开的文档地址Result<Word::_DocumentPtr> getDocument(IdType id) const {if (pWordDocumentMap.count(id))return SuccessResult(0, pWordDocumentMap.at(id));return ERRORRESULT_IDNOTFOUND(Word::_DocumentPtr);}// 通过id获取打开的Word窗口Result<Word::WindowPtr> getWindow(IdType id) const {if (pWordWindowMap.count(id))return SuccessResult(0, pWordWindowMap.at(id));return ERRORRESULT_IDNOTFOUND(Word::WindowPtr);}// 获取当前激活的文档Result<Word::_DocumentPtr> getActiveDocument() const;// 创建Word app实例Result<Word::_ApplicationPtr> createApp();/*** 打开 Word 文档* @param file 要打开的文档路径* @param ConfirmConversions 是否显示转换对话框(默认 false)* @param ReadOnly 是否以只读方式打开(默认 false)* @param AddToRecentFiles 是否添加到最近文件列表(默认 true)* @param PasswordDocument 打开文档所需密码(默认空)* @param PasswordTemplate 打开模板所需密码(默认空)* @param Revert 如果文档已打开,是否放弃未保存更改并重新打开(默认 false)* @param WritePasswordDocument 保存文档所需密码(默认空)* @param WritePasswordTemplate 保存模板所需密码(默认空)* @param Format 文档格式(默认 wdOpenFormatAuto)* @param Encoding 文档编码(默认 msoEncodingAutoDetect)* @param Visible 打开后是否可见(默认 true)* @param OpenAndRepair 是否尝试修复损坏文档(默认 false)* @param DocumentDirection 文档方向(默认从左到右)* @param NoEncodingDialog 是否隐藏编码对话框(默认 false)* @param XMLTransform 指定XML文件,用于将 XML 数据转换为格式化的 Word 文档*/Result<Word::_DocumentPtr> openDocument(IdType id,std::wstring file,bool ConfirmConversions = false,bool ReadOnly = false,bool AddToRecentFiles = true,std::wstring PasswordDocument = L"",std::wstring PasswordTemplate = L"",bool Revert = false,std::wstring WritePasswordDocument = L"",std::wstring WritePasswordTemplate = L"",Word::WdOpenFormat Format = Word::WdOpenFormat::wdOpenFormatAuto,Office::MsoEncoding Encoding = Office::MsoEncoding::msoEncodingAutoDetect,bool Visible = true,bool OpenAndRepair = false,Word::WdDocumentDirection DocumentDirection = Word::WdDocumentDirection::wdLeftToRight,bool NoEncodingDialog = false,std::wstring XMLTransform = L"");struct DocumentOpenOptions {bool confirmConversions = false;bool readOnly = false;bool addToRecentFiles = true;std::wstring passwordDocument = L"";std::wstring passwordTemplate = L"";bool revert = false;std::wstring writePasswordDocument = L"";std::wstring writePasswordTemplate = L"";Word::WdOpenFormat format = Word::WdOpenFormat::wdOpenFormatAuto;Office::MsoEncoding encoding = Office::MsoEncoding::msoEncodingAutoDetect;bool visible = true;bool openAndRepair = false;Word::WdDocumentDirection documentDirection = Word::WdDocumentDirection::wdLeftToRight;bool noEncodingDialog = false;std::wstring xmlTransform = L"";};Result<Word::_DocumentPtr> openDocument(IdType docId, std::wstring filePath, DocumentOpenOptions options);struct DocumentCloseOptions {Word::WdSaveOptions saveChanges = Word::wdPromptToSaveChanges;Word::WdOriginalFormat originalFormat = Word::wdWordDocument;bool routeDocument = false;};Result<bool> closeDocument(IdType docId, DocumentCloseOptions options);Result<Word::_DocumentPtr> addDocument(IdType id, bool visible = false, std::wstring templateName = L"", bool asNewTemplate = false, Word::WdNewDocumentType documentType = Word::wdNewBlankDocument);Result<Word::WindowPtr> newWindow(IdType id);Result<bool> setAppVisible(bool visible) const;/*** 注册事件分发槽* @param parent 事件隶属的接口* @param eventUuid 事件*/Result<bool> registerEventSink(IUnknown* parent, const IID& eventUuid) {// 实例化并存储事件槽对象WordEventSink* pWordAppSink = new WordEventSink(parent, eventUuid);//// 获取Word事件连接点容器//CComPtr<IConnectionPointContainer> icpc;//HRESULT hr = pWordApp.QueryInterface(IID_IConnectionPointContainer, &icpc);//Result icpcResult = Result<int>::CreateFromHRESULT(hr, 0);//if (!icpcResult)//{// MessageBox(0, icpcResult.getMessage().c_str(), L"error", MB_ICONERROR);// return;//}//// 获取Word事件连接点//CComPtr<IConnectionPoint> icp;//hr = icpc->FindConnectionPoint(__uuidof(Word::ApplicationEvents4), &icp);//Result icpResult = Result<int>::CreateFromHRESULT(hr, 0);//if (!icpResult)//{// MessageBox(0, icpResult.getMessage().c_str(), L"error", MB_ICONERROR);// return;//}//// 将事件接收器绑定到连接点//DWORD dwCookie;//hr = icp->Advise(pWordAppSink, &dwCookie);//Result adviseResult = Result<int>::CreateFromHRESULT(hr, 0);//if (!adviseResult)//{// MessageBox(0, adviseResult.getMessage().c_str(), L"error", MB_ICONERROR);// return;//}// 在EventSink中实现了Advise,因此不需要重新写一遍上述流程,改为下面步骤HRESULT hr = dynamic_cast<WordEventSink*>(pWordAppSink)->attach();Result pSinkAttachResult = Result<bool>::CreateFromHRESULT(hr, 0);if (!pSinkAttachResult){delete pWordAppSink;std::wstring msg = L"无法注册Word事件槽!\n";msg += pSinkAttachResult.getMessage();MessageBox(0, msg.c_str(), L"Error", MB_ICONERROR);}elseeventSinkMap[eventUuid] = pWordAppSink;return pSinkAttachResult;}/*** 注册事件* @param eventUuid 之前注册事件槽使用的eventUuid* @param */Result<bool> registerEvent(const IID& eventUuid, DISPID dispIdMember, WordEventSink::DispatchInvokeCallbackFunction callback, WordEventSink::UserDataType userData) {if (!eventSinkMap.count(eventUuid)){LPOLESTR guidStr = NULL;HRESULT hr = StringFromCLSID(eventUuid, &guidStr);std::wstring errMsg = L"该事件槽未注册:"; errMsg += guidStr ? guidStr : L"";std::wstring msg = L"无法注册Word事件!\n"; msg += errMsg;if (SUCCEEDED(hr)){CoTaskMemFree(guidStr);}MessageBox(0, msg.c_str(), L"Error", MB_ICONERROR);return ErrorResult<bool>(0, errMsg);}eventSinkMap[eventUuid]->registerEvent(dispIdMember, callback, userData);return SuccessResult(true);}private:// 哈希函数struct IIDHash {std::size_t operator()(const IID& iid) const noexcept {// 把 16 字节当成两个 64 位整数来 hashconst std::uint64_t* p = reinterpret_cast<const std::uint64_t*>(&iid);std::hash<std::uint64_t> h;return h(p[0]) ^ (h(p[1]) + 0x9e3779b97f4a7c15ULL);}};// 相等比较(IID 是 POD,直接按位比)struct IIDEqual {bool operator()(const IID& a, const IID& b) const noexcept {return InlineIsEqualGUID(a, b) != FALSE; // 用 MS 提供的内联函数// 或者 return memcmp(&a, &b, sizeof(GUID)) == 0;}};Word::_ApplicationPtr pWordApp;std::unordered_map<IdType, Word::_DocumentPtr> pWordDocumentMap;std::unordered_map<IdType, Word::WindowPtr> pWordWindowMap;std::unordered_map<const IID, WordEventSink*, IIDHash, IIDEqual> eventSinkMap;
};
WordController.cpp
#include "pch.h"
#include "WordController.h"std::wstring HResultToWString(HRESULT hr)
{WCHAR* lpBuffer = new WCHAR[sizeof(WCHAR*) * 8];// 获取错误消息的长度DWORD messageSize = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,NULL,hr,MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),lpBuffer,0,NULL);delete[] lpBuffer;lpBuffer = new WCHAR[messageSize];//MessageBox(0, std::to_wstring(messageSize).c_str(), L"messageSize", 0);// 将错误消息复制到字符串FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,NULL,hr,MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),lpBuffer,messageSize,NULL);std::wstring message = lpBuffer;delete[] lpBuffer;return message;
}WordController::~WordController()
{// 析构时自动调用Release,因此不需要手动调用//pWordApp.Release();for (auto it = eventSinkMap.begin(); it != eventSinkMap.end(); it++){it->second->detach();it->second->Release();//delete it->second;}eventSinkMap.clear();// 智能指针不需要手动调用ReleasepWordDocumentMap.clear();pWordWindowMap.clear();if (pWordApp){try {pWordApp->Application->Quit();}COM_CATCH_NORESULT(L"Word quit error.")COM_CATCH_ALL_NORESULT(L"Word quit error.")}
}WordController::WordController(bool bCreateApp, bool bComInit, bool bCreateAppIfComInitFail)
{Result<bool> comInitResult{ true };if (bComInit){comInitResult = comInitialize();if (!comInitResult){std::wstring msg = L"无法初始化COM服务!\n原因:\n";msg += comInitResult.getMessage();MessageBeep(MB_ICONERROR);MessageBox(0, msg.c_str(), L"Error", MB_ICONERROR);}}if (bCreateApp && (comInitResult.getSuccess() || bCreateAppIfComInitFail)){auto createResult = createApp();if (!createResult){std::wstring msg = L"无法打开Word!\n原因:\n";msg += createResult.getMessage();MessageBeep(MB_ICONERROR);MessageBox(0, msg.c_str(), L"Error", MB_ICONERROR);}}
}Result<Word::_DocumentPtr> WordController::getActiveDocument() const
{Result<Word::_DocumentPtr> docRst(false, 0, L"Unknown error.");try {Word::_DocumentPtr docPtr = pWordApp->ActiveDocument;return SuccessResult(0, docPtr);}COM_CATCH_NOMSGBOX(L"无法获取当前活动文档!", docRst)COM_CATCH_ALL_NOMSGBOX(L"无法获取当前活动文档!", docRst)return docRst;
}Result<Word::_ApplicationPtr> WordController::createApp()
{auto& app = pWordApp;HRESULT hr = app.CreateInstance(__uuidof(Word::Application)); // 创建Word应用实例return Result<Word::_ApplicationPtr>::CreateFromHRESULT(hr, app);
}Result<Word::_DocumentPtr> WordController::openDocument(IdType docId, std::wstring file, bool ConfirmConversions, bool ReadOnly, bool AddToRecentFiles, std::wstring PasswordDocument, std::wstring PasswordTemplate, bool Revert, std::wstring WritePasswordDocument, std::wstring WritePasswordTemplate, Word::WdOpenFormat Format, Office::MsoEncoding Encoding, bool Visible, bool OpenAndRepair, Word::WdDocumentDirection DocumentDirection, bool NoEncodingDialog, std::wstring XMLTransform)
{// 原生类型 -> _variant_t,必须小心,这里极易容易出现错误导致无法正常运行_variant_t vFile(file.empty() ? vtMissing : file.c_str());_variant_t vConfirm(ConfirmConversions);_variant_t vReadOnly(ReadOnly);_variant_t vAddRecent(AddToRecentFiles);_variant_t vPwdDoc(PasswordDocument.empty() ? vtMissing : PasswordDocument.c_str());_variant_t vPwdTpl(PasswordTemplate.empty() ? vtMissing : PasswordTemplate.c_str());_variant_t vRevert(Revert);_variant_t vWrPwdDoc(WritePasswordDocument.empty() ? vtMissing : WritePasswordDocument.c_str());_variant_t vWrPwdTpl(WritePasswordTemplate.empty() ? vtMissing : WritePasswordTemplate.c_str());_variant_t vFormat((long)Format); // 枚举类型必须是long_variant_t vEnc((long)Encoding); // 枚举类型必须是long_variant_t vVisible(Visible);_variant_t vRepair(OpenAndRepair);_variant_t vDir((long)DocumentDirection); // 枚举类型必须是long_variant_t vNoEnc(NoEncodingDialog);_variant_t vXMLTransform(XMLTransform.empty() ? vtMissing : XMLTransform.c_str());Result<Word::_DocumentPtr> result(true);try {if (pWordDocumentMap.count(docId)) // 如果存在先关闭{//try {// auto& pDoc = pWordDocumentMap[docId];// pDoc->Close();//}//catch (...) {}closeDocument(docId, DocumentCloseOptions());}//pWordApp->Documents->Open(// _com_util::ConvertStringToBSTR(wstr2str_2ANSI(file).c_str()),// ConfirmConversions, ReadOnly, AddToRecentFiles, PasswordDocument, PasswordTemplate,// Revert, WritePasswordDocument, WritePasswordTemplate, Format, Encoding,// Visible, OpenConflictDocument, OpenAndRepair, DocumentDirection, NoEncodingDialog//);Word::DocumentsPtr dsp = pWordApp->Documents;Word::_DocumentPtr docPtr = pWordApp->Documents->Open(&vFile, &vConfirm, &vReadOnly, &vAddRecent,&vPwdDoc, &vPwdTpl, &vRevert,&vWrPwdDoc, &vWrPwdTpl,&vFormat, &vEnc,&vVisible, &vRepair,&vDir, &vNoEnc,&vXMLTransform);pWordDocumentMap[docId] = docPtr;return SuccessResult(0, docPtr);}COM_CATCH(L"无法打开文档!", result)COM_CATCH_ALL(L"无法打开文档!", result)return result;
}Result<Word::_DocumentPtr> WordController::openDocument(IdType docId, std::wstring filePath, DocumentOpenOptions options)
{return openDocument(docId, filePath, options.confirmConversions, options.readOnly, options.addToRecentFiles, options.passwordDocument, options.passwordTemplate, options.revert, options.writePasswordDocument, options.writePasswordTemplate, options.format, options.encoding, options.visible, options.openAndRepair, options.documentDirection, options.noEncodingDialog, options.xmlTransform);
}Result<bool> WordController::closeDocument(IdType docId, DocumentCloseOptions options)
{Result<bool> result(true);_variant_t vSaveChanges((long)options.saveChanges);_variant_t vOriginalFormat((long)options.originalFormat);_variant_t vRouteDocument(options.routeDocument);try {if (pWordDocumentMap.count(docId)){pWordDocumentMap[docId]->Close(&vSaveChanges, &vOriginalFormat, &vRouteDocument);pWordDocumentMap.erase(docId);}elsereturn ERRORRESULT_IDNOTFOUND(bool);}COM_CATCH(L"无法关闭文档!", result)COM_CATCH_ALL(L"无法关闭文档!", result)return result;
}Result<Word::_DocumentPtr> WordController::addDocument(IdType id, bool visible, std::wstring templateName, bool asNewTemplate, Word::WdNewDocumentType documentType)
{_variant_t vTemplateName(templateName.empty() ? vtMissing : templateName.c_str());_variant_t vAsNewTemplate(asNewTemplate);_variant_t vDocumentType((long)documentType);_variant_t vVisible(visible);Result<Word::_DocumentPtr> result(true);try {Word::DocumentsPtr dsp = pWordApp->Documents;//Word::_DocumentPtr docPtr = pWordApp->Documents->Add();Word::_DocumentPtr docPtr = pWordApp->Documents->Add(&vTemplateName, &vAsNewTemplate, &vDocumentType, &vVisible);pWordDocumentMap[id] = docPtr;return SuccessResult(0, docPtr);}COM_CATCH(L"无法添加新文档!", result)COM_CATCH_ALL(L"无法添加新文档!", result)return result;
}Result<Word::WindowPtr> WordController::newWindow(IdType id)
{Result<Word::WindowPtr> result(true);try {Word::WindowPtr windowPtr = pWordApp->NewWindow();pWordWindowMap[id] = windowPtr;return SuccessResult(0, windowPtr);}COM_CATCH(L"无法创建新窗口!", result)COM_CATCH_ALL(L"无法创建新窗口!", result)return result;
}Result<bool> WordController::setAppVisible(bool visible) const
{_variant_t vVisible(visible);Result<bool> result(true);try {pWordApp->PutVisible(vVisible.boolVal);return SuccessResult(0, true);}COM_CATCH(L"无法设置窗口可见性", result)COM_CATCH_ALL(L"无法设置窗口可见性", result)return result;
}//// Define type info structure
//_ATL_FUNC_INFO EventSink::OnDocChangeInfo = { CC_STDCALL, VT_EMPTY, 0 };
//_ATL_FUNC_INFO EventSink::OnQuitInfo = { CC_STDCALL, VT_EMPTY, 0 };
//// (don't actually need two structure since they're the same)
通过在WordEventSink的Invoke函数中打印dispIdMember值,发现dispIdMember与具体事件名称的对应值为
- 6 -> 文档即将关闭
- 10 -> 窗口激活
- 11 -> 窗口去激活
- 12 -> 窗口选中区域改变
查阅文档,或者使用Qt的COM功能生成接口表,可以发现这4个事件的函数原型:
HRESULT DocumentBeforeClose(struct _Document* Doc, VARIANT* Cancel/*设置为true则取消关闭,否则继续关闭*/); // dispIdMember: 6
HRESULT WindowActivate(struct _Document* Doc, struct Window* Wn); // dispIdMember: 10
HRESULT WindowDeactivate(struct _Document* Doc, struct Window* Wn); // dispIdMember: 11
HRESULT WindowSelectionChange(struct Selection* Sel); // dispIdMember: 12
当然Invoke显然不存在上述函数原型的参数,必须通过pDispParams参数通过QueryInterface方法查询接口才能得到Doc真正的接口地址才能够使用,方法如下(以WindowActivate事件回调函数为例):
先看Invoke函数原型与相关定义:
typedef struct tagDISPPARAMS
{/* [size_is] */ VARIANTARG *rgvarg; // 参数数组。注意:这些参数以相反的顺序显示/* [size_is] */ DISPID *rgdispidNamedArgs; // 命名参数的调度 ID。UINT cArgs; // 自变量的数量。UINT cNamedArgs; // 命名参数的数目。
} DISPPARAMS;virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke( /* [annotation][in] */ _In_ DISPID dispIdMember, // 事件唯一id,每个事件对应不同的id // 标识成员。 使用 GetIDsOfNames 或对象的文档获取调度标识符。/* [annotation][in] */ _In_ REFIID riid, // 留待将来使用。 必须为 IID_NULL。/* [annotation][in] */ _In_ LCID lcid, // 要在其中解释自变量的区域设置上下文。 lcid 由 GetIDsOfNames 函数使用,并且还传递给 Invoke,以允许对象解释其特定于区域设置的参数。不支持多个国家/地区语言的应用程序可以忽略此参数。 有关详细信息,请参阅 支持多个国家/地区语言 和 公开 ActiveX 对象。/* [annotation][in] */ _In_ WORD wFlags, // 描述 Invoke 调用上下文的标志。标志都有如下:// DISPATCH_METHOD 成员作为方法调用。 如果属性具有相同的名称,则可以设置此属性和DISPATCH_PROPERTYGET标志。// DISPATCH_PROPERTYGET 成员作为属性或数据成员进行检索。// DISPATCH_PROPERTYPUT 成员将更改为属性或数据成员。// DISPATCH_PROPERTYPUTREF 成员由引用赋值而不是值赋值更改。 仅当属性接受对 对象的引用时,此标志才有效。/* [annotation][out][in] */ _In_ DISPPARAMS *pDispParams, // 事件处理的函数原型的参数 // 指向 DISPPARAMS 结构的指针,该结构包含参数数组、命名参数 DISPID 数组以及数组中元素数的计数。/* [annotation][out] */ _Out_opt_ VARIANT *pVarResult, // 事件处理结果 // 指向存储结果的位置的指针;如果调用方不需要任何结果,则为 NULL。 如果指定了DISPATCH_PROPERTYPUT或DISPATCH_PROPERTYPUTREF,则忽略此参数。/* [annotation][out] */ _Out_opt_ EXCEPINFO *pExcepInfo, // 异常信息 // 指向一个包含异常信息的结构的指针。 如果返回DISP_E_EXCEPTION,则应填充此结构。 可以为 NULL。/* [annotation][out] */ _Out_opt_ UINT *puArgErr) = 0; // 错误码 // 具有错误的第一个参数的 rgvarg 中的索引。 参数以相反的顺序存储在 pDispParams-rgvarg> 中,因此第一个参数是数组中索引最高的参数。 仅当生成的返回值DISP_E_TYPEMISMATCH或DISP_E_PARAMNOTFOUND时,才会返回此参数。 此参数可以设置为 null。 有关详细信息,请参阅 返回错误。
/*返回值此方法可以返回其中一个值。返回代码 说明S_OK 成功。DISP_E_BADPARAMCOUNT 提供给 DISPPARAMS 的元素数不同于方法或属性接受的参数数。DISP_E_BADVARTYPE DISPPARAMS 中的一个参数不是有效的变体类型。DISP_E_EXCEPTION 应用程序需要引发异常。 在这种情况下,应填充在 pexcepinfo 中传递的结构。DISP_E_MEMBERNOTFOUND 请求的成员不存在。DISP_E_NONAMEDARGS IDispatch 的此实现不支持命名参数。DISP_E_OVERFLOW DISPPARAMS 中的一个参数无法强制为指定的类型。DISP_E_PARAMNOTFOUND 其中一个参数 ID 与 方法上的参数不对应。 在这种情况下, puArgErr 设置为包含错误的第一个参数。DISP_E_TYPEMISMATCH 无法强制一个或多个参数。 rgvarg 中类型不正确的第一个参数的索引在 puArgErr 中返回。DISP_E_UNKNOWNINTERFACE riid 中传递的接口标识符未IID_NULL。DISP_E_UNKNOWNLCID 正在调用的成员根据 LCID 解释字符串参数,并且无法识别 LCID。 如果不需要 LCID 来解释参数,则不应返回此错误DISP_E_PARAMNOTOPTIONAL 省略了必需的参数。
*/// 下面是一个简单的Invoke实现
STDMETHOD(Invoke)(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) override
{std::cout << "dispIdMember: " << dispIdMember << std::endl;// 进行事件处理函数分发if (callbackInfoMap.count(dispIdMember)){auto [begin, end] = callbackInfoMap.equal_range(dispIdMember);for (auto &it = begin; it != end; it++){auto& info = it->second;info.callback(pDispParams, info.userData);}return S_OK;}return E_NOTIMPL;
}
参考:IDispatch::Invoke (oaidl.h) - Win32 apps | Microsoft Learn
查阅官方文档可知内容我已标注到上述代码中,值得注意的是虽然rgvarg保存的参数是以相反的顺序显示,但是经过摸索尝试,我发现rgvarg中保存的参数依旧与Word事件参数顺序保持一致。
auto paramCount = params->cArgs;
if (paramCount < 2)return Result<bool>::CreateFromHRESULT(E_INVALIDARG, 0);
IDispatch* documentDisp = params->rgvarg[0].pdispVal; // 获取第一个参数的disp地址值
IDispatch* windowDisp = params->rgvarg[1].pdispVal; // 获取第二个参数的disp地址值
Word::_DocumentPtr docPtr;
Word::WindowPtr windowPtr;
HRESULT hr = documentDisp->QueryInterface(&docPtr); // 查询第一个参数的真正接口地址
// 错误处理if (FAILED(hr)) {...}
hr = windowDisp->QueryInterface(&windowPtr); // 查询第二个参数的真正接口地址
// 错误处理if (FAILED(hr)) {...}
不过本程序最主要还是依赖于WindowSelectionChange事件(在活动窗口中的所选内容更改时发生),只要使用中文编辑文本或者光标位置或者选择内容都会发出该事件,只要收到该事件,说明需要检测文本是否更改,然后进行小说文本的替换,检测原理可以自己研究,这里仅仅提供一个简单方法:使用std::mismatch检测纯文本内容变动。
Result<bool> CFakeWordMfcDlg::onWordWindowSelectionChangeEvent(DISPPARAMS* params, WordEventSink::UserDataType userData)
{IDispatch* disp = params->rgvarg[0].pdispVal;CComPtr<IDispatch> selDisp(disp);DISPID dispid;OLECHAR* name = (OLECHAR*)L"Text";HRESULT hr = selDisp->GetIDsOfNames(IID_NULL, &name, 1, LOCALE_NAME_USER_DEFAULT, &dispid);if (FAILED(hr))return Result<bool>::CreateFromHRESULT(hr, true);VARIANT result = { 0 };DISPPARAMS invokeDispParams = { 0 };hr = selDisp->Invoke(dispid, IID_NULL, LOCALE_NAME_USER_DEFAULT, DISPATCH_PROPERTYGET, &invokeDispParams, &result, nullptr, nullptr);if (FAILED(hr))return Result<bool>::CreateFromHRESULT(hr, true);std::wstring selectedText;if (result.bstrVal)selectedText += result.bstrVal;VariantClear(&result);std::cout << "Selected text: " << wstr2str_2ANSI(selectedText) << std::endl;return processTextChange();
}
Result<bool> CFakeWordMfcDlg::processTextChange()
{auto rstGetActiveDoc = wordCtrller->getActiveDocument();if (!rstGetActiveDoc)return ErrorResult<bool>(rstGetActiveDoc);Result<bool> rst(true, 0, true);try {Word::_DocumentPtr docPtr = rstGetActiveDoc.getData();docPtr->Application->ScreenUpdating = VARIANT_FALSE;try {docPtr->Application->UndoRecord->StartCustomRecord(_bstr_t(L"Fish update"));try {Word::RangePtr mainTextRangePtr = docPtr->Range();std::wstring currentText = mainTextRangePtr->Text;auto lenCur = currentText.size() - 1;auto lenLast = wordLastText.size() - 1;// 寻找差异位置std::pair<std::wstring::iterator, std::wstring::iterator> diffResult;std::reference_wrapper<std::wstring> repTxtFullText(processedFishFileTxtWordText);WordController* repWordController{ fishFileWordController };SupportedFileType selectedRepFileType = selectedFishFile.fileType;switch (replaceMode){case ReplaceMode::Work:{diffResult = std::mismatch(currentText.begin(), currentText.end(), processedWorkFileTxtWordText.begin(), processedWorkFileTxtWordText.end());repTxtFullText = processedWorkFileTxtWordText;repWordController = workFileWordController;selectedRepFileType = selectedWorkFile.fileType;break;}default:case ReplaceMode::SlackOff:{diffResult = std::mismatch(currentText.begin(), currentText.end(), processedFishFileTxtWordText.begin(), processedFishFileTxtWordText.end());repTxtFullText = processedFishFileTxtWordText;repWordController = fishFileWordController;selectedRepFileType = selectedFishFile.fileType;break;}}// 返回第一个不匹配字符的位置auto diffStart = std::distance(currentText.begin(), diffResult.first);//if (lenCur > lenLast)//{// // 新增小说文本// wordTextUpdateTxtSubTextRange.start = diffStart;// //wordTextUpdateTxtSubTextRange.start = lenLast - 1;// //wordTextUpdateTxtSubTextRange.length = lenCur - lenLast;// wordTextUpdateTxtSubTextRange.end = lenCur;// wordTextUpdateTxtSubTextRange.newLength = (lenCur - diffStart) * GetEditNumber(IDC_EDITOIRATIO);// updateWordText(docPtr, txtFullText.get(), wordTextUpdateTxtSubTextRange.start, wordTextUpdateTxtSubTextRange.end, wordTextUpdateTxtSubTextRange.newLength);//}//else if (lenCur < lenLast)//{// wordTextUpdateTxtSubTextRange.start = 0;// wordTextUpdateTxtSubTextRange.end = lenCur;// wordTextUpdateTxtSubTextRange.newLength = lenCur - 0;// updateWordText(docPtr, txtFullText.get(), wordTextUpdateTxtSubTextRange.start, wordTextUpdateTxtSubTextRange.end, wordTextUpdateTxtSubTextRange.newLength);//}auto& rangeStart = wordTextUpdateTxtSubTextRange.start = diffStart;auto& rangeEnd = wordTextUpdateTxtSubTextRange.end = lenCur;auto& newLen = wordTextUpdateTxtSubTextRange.newLength = (static_cast<long double>(lenCur - diffStart)) * oiRatio;if (!(rangeStart == rangeEnd && newLen == 0)){updateWordText(docPtr, [docPtr, selectedRepFileType, repWordController, toRepFullText=repTxtFullText.get(), this](Word::RangePtr range, size_t newRangeStart, size_t newRangeLen, std::any userData) {auto&& rangeText = range->Text;std::wstring oldText = rangeText.GetBSTR() ? rangeText.GetBSTR() : L"";std::wstring newText;switch (selectedRepFileType){case SupportedFileType::Word:{auto rst = repWordController->getDocument(0);if (rst){auto oriFullRange = range->Document->Range();auto repDocPtr = rst.getData();auto repRange = repDocPtr->Range(&_variant_t(0), &_variant_t(newRangeStart + newRangeLen));//auto repRange = repDocPtr->Range(&_variant_t(newRangeStart), &_variant_t(newRangeStart + newRangeLen));auto repText = repRange->Text;if (repText.GetBSTR())newText = repText;oriFullRange->Delete();oriFullRange->InsertXML(repRange->GetXML(VARIANT_FALSE), &_variant_t(false));//range->Collapse(&_variant_t((long)Word::wdCollapseStart));// 使用FormattedText属性复制格式化的文本//range->FormattedText = repRange->FormattedText;//HRESULT hr = S_OK;//hr = repRange->Copy();//hr = range->Paste();}break;}default:case SupportedFileType::Txt:{newText = toRepFullText.substr(newRangeStart, newRangeLen);//range->Text = _bstr_t(addText.c_str());range->Delete();range->InsertAfter(newText.c_str());break;}}// 插入文本后将输入光标位置移动到刚刚输入的内容后面Word::SelectionPtr selection = docPtr->Application->Selection;if (selection) {_variant_t vSelEnd(Word::wdStory);selection->EndKey(&vSelEnd, &vtMissing);//selection->SetRange(from + length, from + length);//selection->SetRange(from + length, from + length);}//selection->Text = _bstr_t(txtFullText.substr(0, from + length).c_str());//selection->InsertAfter(_bstr_t(txtFullText.substr(0, from + length).c_str()));//variant_t vCollapseEnd(Word::wdCollapseEnd);//selection->Collapse(&vCollapseEnd);std::cout << "Document text updating: \n\tRange: [" << newRangeStart << "," << range->End - range->Start + newRangeStart << ") "<< "\tOld text: " << wstr2str_2ANSI(oldText) << " -> "<< "\tNew text: " << wstr2str_2ANSI(newText)<< std::endl;}, repTxtFullText.get().size(), rangeStart, rangeEnd, newLen, 0);Word::RangePtr newMainTextRangePtr = docPtr->Range();std::wstring newCurText = newMainTextRangePtr->Text;wordLastText = newCurText;}}COM_CATCH_NOMSGBOX(L"", rst)COM_CATCH_ALL_NOMSGBOX(L"", rst)docPtr->Application->UndoRecord->EndCustomRecord();}COM_CATCH_NOMSGBOX(L"", rst)COM_CATCH_ALL_NOMSGBOX(L"", rst)docPtr->Application->ScreenUpdating = VARIANT_TRUE;}COM_CATCH_NOMSGBOX(L"", rst)COM_CATCH_ALL_NOMSGBOX(L"", rst)return rst;
}// 一定会更新,即使rangeStart == rangeEnd && newLen == 0
void CFakeWordMfcDlg::updateWordText(Word::_DocumentPtr doc, UpdateWordContentsReplaceFunction replaceFunction, size_t fullTextLen, size_t rangeStart, size_t rangeEnd, size_t newLen, std::any userData)
{try {// 修改输入的文本//Word::_DocumentPtr doc = wordCtrller->getActiveDocument().getData();_variant_t vFrom(rangeStart);_variant_t vEnd(rangeEnd);Word::RangePtr range = doc->Range(&vFrom, &vEnd);if (rangeStart < fullTextLen){if (rangeStart + newLen > fullTextLen)newLen = fullTextLen - rangeStart;replaceFunction(range, rangeStart, newLen, userData);}else{range->Text = L"";}}COM_CATCH_NORESULT(L"Word文本更新失败!")COM_CATCH_ALL_NORESULT(L"Word文本更新失败!")
}
恭喜
至此,恭喜你完成了通过替换Word文本实现的工作摸鱼神器!