> 文章列表 > VC++如何获取所有运行中的Word实例的COM对象

VC++如何获取所有运行中的Word实例的COM对象

VC++如何获取所有运行中的Word实例的COM对象

目录

  • 一 问题的提出
  • 二 工程创建
    • 2.1 创建一个基于对话框的MFC工程
    • 2.2 导入word相关的自动化包装类
  • 三 代码实例
    • 3.1 初始化COM库
    • 3.2 对话框类头文件修改
    • 3.3 对话框类实现文件
      • 1.根据进程名称获取进程ID
      • 2. 获取一个进程下所有的窗口
      • 3. 判断某个窗口是否为主窗口
      • 4. 判断word进程下面哪个窗口是word客户区所对应的窗口
      • 5. 获取所有word文档的信息
      • 6.对话框其他接口
  • 参考文章

一 问题的提出

  在自动化编程中,一个常见的问题是:如何获取运行中的Word/Excel实例的COM对象,一般来说,可以采取以下代码:

	CLSID IDExcel;::CLSIDFromProgID(L"Excel.Application", &IDExcel);LPUNKNOWN pUnkEx = NULL;::GetActiveObject(IDExcel, NULL, &pUnkEx);

  上述代码可以获取ROT表(Running Object Table运行实例表)中第一个对应的实例对象,但是很遗憾,可能并不是你想要的。比如当一个WPS word对象和MicroSoft office word对象同时打开时,上述代码返回的竟然是WPS对象的接口指针
  本文利用另外一种办法,可以获取所有运行中的word实例COM对象。最后的运行效果如下。
在这里插入图片描述
本文编译环境为VS2017+Office2016 ,涉及的项目源码链接为:
https://download.csdn.net/download/mary288267/87719124

二 工程创建

2.1 创建一个基于对话框的MFC工程

  按照下图修改对话框模板,ClistCtrl为报表形式,CEdit为多行模式,并在附加依赖项中增加一个导入库Oleacc.lib。
在这里插入图片描述

2.2 导入word相关的自动化包装类

  在VS中进入类向导->添加类->类型库中的MFC类
在这里插入图片描述
  在可用的类型库列表中找到word的类型库,单击,即可显示该类型库中所有的接口。

4.
  选出其中的_Application、_Document、Documents、Paragraph、Paragraphs、Window、Selection、range等接口,可以根据自己偏好修改上述类的名称。
在这里插入图片描述
  最终导出的包装类为
在这里插入图片描述
  请注意,包装类导出后,需要删掉每个包装类起始处的#Import指令行,即类似下面的指令语句
VC++如何获取所有运行中的Word实例的COM对象

三 代码实例

3.1 初始化COM库

  首先,应该在app类的InitInstance函数中加入AfxOleInit函数,用于初始化COM库。

BOOL CGetAllWordInstancesApp::InitInstance()
{//首先,必须初始化COM库if (!AfxOleInit()){AfxMessageBox(_T("Can't initilize COM!"));return TRUE;}//........省略
}

3.2 对话框类头文件修改

  对话框类的头文件为:


// GetAllWordInstancesDlg.h: 头文件
//#pragma once
#include <map>#define MAXTITLELEN 256
#define MAXCLASSLEN 256//窗口信息结构体
struct SWinInfo
{
public:HWND hWnd;HWND hParent;HWND hOwner;LONG lStyle;DWORD idProcess;  // process idDWORD idThread;  // creator thread idTCHAR pszTitle[MAXTITLELEN];	//Window titleTCHAR pszWinClass[MAXCLASSLEN];// window class name.void Reset() {hWnd = hParent = hOwner = NULL;idProcess = idThread = NULL;lStyle = 0;memset(pszTitle, 0, sizeof(pszTitle));memset(pszWinClass, 0, sizeof(pszWinClass));}
};// CGetAllWordInstancesDlg 对话框
class CGetAllWordInstancesDlg : public CDialog
{
public:typedef std::map<CString, CString> MapDocTitle2Cont;typedef MapDocTitle2Cont::iterator mapIter;CGetAllWordInstancesDlg(CWnd* pParent = nullptr);enum { IDD = IDD_GETALLWORDINSTANCES_DIALOG };protected:HICON m_hIcon;virtual void DoDataExchange(CDataExchange* pDX);virtual BOOL OnInitDialog();afx_msg void OnSysCommand(UINT nID, LPARAM lParam);afx_msg void OnPaint();afx_msg HCURSOR OnQueryDragIcon();afx_msg void OnBnClickedButton1();afx_msg void OnNMClickList1(NMHDR *pNMHDR, LRESULT *pResult);DECLARE_MESSAGE_MAP()protected:void GetDocsCont(MapDocTitle2Cont& mapDocTitle2String);private:CListCtrl m_wndLstProcess;MapDocTitle2Cont m_mapDocTitle2String;CEdit m_wndEdt;
};

  在头文件中,我们加入了一个结构体SWinInfo,这个结构体主要用来保存窗口的信息,包括窗口句柄、父窗口句柄、窗口所在进程和线程ID、窗口标题以及窗口类名称。

3.3 对话框类实现文件

  对话框类的实现文件中需要加入以下关键函数。

1.根据进程名称获取进程ID

//根据进程名拿到进程id
DWORD GetProcessIDByName(CString strName, std::vector<DWORD> &vtcUid)
{HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);if (INVALID_HANDLE_VALUE == hSnapshot) {return NULL;}PROCESSENTRY32 pe = { sizeof(pe) };for (BOOL ret = Process32First(hSnapshot, &pe); ret; ret = Process32Next(hSnapshot, &pe)){CString strTemp = pe.szExeFile;if (strTemp == strName)vtcUid.push_back(pe.th32ProcessID);}CloseHandle(hSnapshot);return 0;
}

  GetProcessIDByName的第一个参数为进程的名称,例如word就是“WINWORD.EXE”;第二个参数就是该进程ID的数组。这个函数里面调用了CreateToolhelp32Snapshot,用于拍摄指定进程以及这些进程使用的堆、模块和线程的快照;其中第一个标志变量TH32CS_SNAPPROCES指示拍摄系统中所有进程的快照。

2. 获取一个进程下所有的窗口

  在Windows API中,EnumWindows函数可以枚举屏幕上的所有顶级窗口,并把窗口的句柄传递给一个回调函数。它的原型是:

BOOL EnumWindows(
WNDENUMPROC lpEnumFunc,
LPARAM lParam
);

  该函数的第一个参数就是枚举窗口时所调用的回调函数。根据上述函数,获取一个进程下所有窗口的例程如下:

//该结构体用做回调函数的参数
typedef struct EnumHWndsArg
{std::vector<HWND> *vecHWnds;DWORD dwProcessId;
}EnumHWndsArg, *LPEnumHWndsArg;//回调函数
BOOL CALLBACK lpEnumFunc(HWND hwnd, LPARAM lParam)
{EnumHWndsArg *pArg = (LPEnumHWndsArg)lParam;if (pArg){DWORD  idPprocess = 0;//注意这个函数,引用参数返回的是创建窗口的进程ID,而函数本身返回的是线程ID::GetWindowThreadProcessId(hwnd, &idPprocess);if (idPprocess == pArg->dwProcessId)pArg->vecHWnds->push_back(hwnd);}return TRUE;
}//获取一个进程下所有的窗口
void GetHWndsByProcessID(DWORD processID, std::vector<HWND> &vecHWnds)
{EnumHWndsArg infoWin;infoWin.dwProcessId = processID;infoWin.vecHWnds = &vecHWnds;::EnumWindows(lpEnumFunc, (LPARAM)&infoWin);
}

3. 判断某个窗口是否为主窗口

  在一个Word进程中,一般有一个或者多个主窗口,主窗口的特点是具有标题、可见、没有父窗口,因此判断一个窗口是否为主窗口的函数为:

//判断一个窗口是否为主窗口
bool IsMainWindow(HWND hWnd)
{if (!::IsWindow(hWnd))return false;SWinInfo cWndInfo;GetWindowInfo(hWnd, cWndInfo);DWORD dwVisibleStyle = WS_VISIBLE;bool bRet = _tcslen(cWndInfo.pszTitle)&& (cWndInfo.lStyle & dwVisibleStyle)&& !cWndInfo.hOwner;return bRet;
}

4. 判断word进程下面哪个窗口是word客户区所对应的窗口

  上述标题起的有点绕口,其实也表明这个问题有点复杂。首先,我们可以获取一个word进程下的所有顶级窗口,也可以判断这些窗口中哪些是主窗口。但现在的问题是:我们如何根据主窗口得到word文档对应的COM对象?
  经过反复查阅资料,得出了一个基本思路:首先,找到word进程下的主窗口(可能有多个,例如你同时打开几个word文档,在任务管理器上可以看到只有一个word进程);然后,依次迭代主窗口下的子窗口,并找到其中一个名字为"_WwG"的窗口,这个窗口实际上就是word的客户区(就是我们编辑文本的那个窗口),至于为什么窗口类名称为"_WwG",下文会有解释;最后,利用COM提供的AccessibleObjectFromWindow函数返回客户区的接口指针。
  以下为实现例程。

//根据窗口句柄获取窗口信息
void GetWindowInfo(HWND hWnd, SWinInfo& cWndInfo)
{cWndInfo.hWnd = hWnd;cWndInfo.hParent = GetParent(hWnd);cWndInfo.hOwner = GetWindow(hWnd, GW_OWNER);cWndInfo.lStyle = GetWindowLong(hWnd, GWL_STYLE);::GetWindowText(hWnd, cWndInfo.pszTitle, MAXTITLELEN);::GetClassName(hWnd, cWndInfo.pszWinClass, MAXCLASSLEN);cWndInfo.idThread = ::GetWindowThreadProcessId(hWnd, &cWndInfo.idProcess);
}BOOL CALLBACK NextExcelChildWindow(HWND hWnd, LPARAM lParam)
{SWinInfo* pWinInfo = (SWinInfo*)lParam;TCHAR psz[MAXCLASSLEN] = { 0 };::GetClassName(hWnd, psz, MAXCLASSLEN);if (_tcscmp(psz, _T("EXCEL7")) == 0){GetWindowInfo(hWnd, *pWinInfo);return FALSE;}return TRUE;
}//根据主窗口的句柄得到EXCEL的COM对象
LPDISPATCH ExcelComFromMainWindowHandle(HWND hMainWin)
{SWinInfo cWinInfo;::EnumChildWindows(hMainWin, NextExcelChildWindow, (LPARAM)&cWinInfo);if (_tcscmp(cWinInfo.pszWinClass, _T("EXCEL7")) == 0){void* pVoid = NULL;if (S_OK == AccessibleObjectFromWindow(cWinInfo.hWnd, OBJID_NATIVEOM, IID_IDispatch, &pVoid))return (LPDISPATCH)pVoid;}return NULL;
}//获取Word的窗口COM对象
BOOL CALLBACK NextWordChildWindow(HWND hWnd, LPARAM lParam)
{SWinInfo* pWinInfo = (SWinInfo*)lParam;TCHAR psz[MAXCLASSLEN] = { 0 };::GetClassName(hWnd, psz, MAXCLASSLEN);if (_tcscmp(psz, _T("_WwG")) == 0) //word文档窗口的窗口类名称为"_WwG"{GetWindowInfo(hWnd, *pWinInfo);return FALSE;}return TRUE;
}//根据主窗口的句柄得到Word的窗口COM对象
LPDISPATCH WordComFromMainWindowHandle(HWND hMainWin, SWinInfo& cWinInfo)
{cWinInfo.Reset();::EnumChildWindows(hMainWin, NextWordChildWindow, (LPARAM)&cWinInfo);if (_tcscmp(cWinInfo.pszWinClass, _T("_WwG")) == 0){void* pVoid = NULL;if (S_OK == AccessibleObjectFromWindow(cWinInfo.hWnd, OBJID_NATIVEOM, IID_IDispatch, &pVoid))return (LPDISPATCH)pVoid;}return NULL;
}

  上述例程中WordComFromMainWindowHandle接口可以获取某个主窗口下对应的word客户区对象接口指针。
  我们重点解释下下面几个函数

BOOL EnumChildWindows( HWND hWndParent,
WNDENUMPROC lpEnumFunc,
LPARAM lParam
);

  EnumChildWindows会枚举父窗口的所有子窗口,并且将子窗口的句柄传给回调函数lpEnumFunc。因此在函数WordComFromMainWindowHandle中我们枚举主窗口所有的子窗口,并且在回调函数NextWordChildWindow中判断这个窗口的窗口类名称是不是"_WwG",如果是,我们把这个子窗口句柄保存下来。

STDAPI AccessibleObjectFromWindow(
HWND hwnd,
DWORD dwObjectID,
REFIID riid,
void** ppvObject);

  AccessibleObjectFromWindow函数可以获取指定窗口关联的COM对象接口,这里面第二个参数是对象ID,是标准对象标识符常量值之一;或者是自定义的对象ID,比如OBJID_NATIVEOM,就是Microsoft Office本机对象模型的ID。
  若要获取指向本机对象模型支持的类的 IDispatch 接口指针,请在 dwObjectID 中指定OBJID_NATIVEOM。使用此对象标识符时,hwnd 参数必须与以下窗口类类型匹配。从下表可以看出,word对应的窗口类名称为"_WwG"。第三个参数是IID_IAccessible 或者 IID_IDispatch,这里取IID_IDispatch。
在这里插入图片描述

5. 获取所有word文档的信息

  做完了上述工作,基本就大功告成了,接下来,我们来获取所有正在运行的word文档的内容,在对话框类中添加以下成员函数。在GetDocsCont函数中,我们取出所有名称为"WINWORD.EXE"的进程ID,然后依次取出每个进程ID下面所有的窗口,并找到其中的主窗口,然后根据主窗口的句柄,得到word客户区窗口的COM对象,进而读取对应的文档文字内容。

void CGetAllWordInstancesDlg::GetDocsCont(MapDocTitle2Cont& mapDocTitle2String)
{mapDocTitle2String.clear();std::vector<DWORD> aridProcess;GetProcessIDByName(_T("WINWORD.EXE"), aridProcess);	//获取WORD进程for (int i = 0; i < aridProcess.size(); i++){DWORD idProcess = aridProcess[i];std::vector<HWND> vtHWnds;GetHWndsByProcessID(idProcess, vtHWnds);	//取出该进程中所有对话框HWND hMainWin;for (int i = 0; i < vtHWnds.size(); i++){hMainWin = vtHWnds[i];if (IsMainWindow(hMainWin)){LPDISPATCH pDispatch = NULL;SWinInfo cWinInfo;pDispatch = (LPDISPATCH)WordComFromMainWindowHandle(hMainWin, cWinInfo);if (pDispatch){CString sContent;VARIANT vt;vt.vt = VT_I4;vt.lVal = i;CWordWindow wordDocWindow;wordDocWindow.AttachDispatch(pDispatch);CWordDocument doc = wordDocWindow.get_Document();CString sTitle = doc.get_FullName();CWordParagraphs paragraphs = doc.get_Paragraphs();for (int i = 1; i < paragraphs.get_Count() + 1; i++){CWordParagraph paragraph = paragraphs.Item(i);CWordRange range = paragraph.get_Range();sContent += range.get_Text();}sContent.Replace(_T("\\r"), _T("\\r\\n"));mapDocTitle2String[sTitle] = sContent;}}}}
}void CGetAllWordInstancesDlg::OnBnClickedButton1()
{GetDocsCont(m_mapDocTitle2String);m_wndLstProcess.DeleteAllItems();mapIter it;CString str;int iItem = 0;for (it = m_mapDocTitle2String.begin(); it != m_mapDocTitle2String.end(); it++){str.Format(_T("%d"), iItem +1);m_wndLstProcess.InsertItem(iItem, str);m_wndLstProcess.SetItemText(iItem, 1, it->first);iItem++;}
}

  在OnBnClickedButton1方法中,我们调用了GetDocsCont,后者找到了不同文档名称对应的文档内容。

6.对话框其他接口

  对话框还需要补充一个接口,根据用户点击ClistCtrl中的不同项目,读取对应文档的文字内容(不含图片、表格等,仅是文字)。

void CGetAllWordInstancesDlg::OnNMClickList1(NMHDR *pNMHDR, LRESULT *pResult)
{LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);//进行单击检测,这个结构已经被扩展为能够适应子项的单击检测。int iCurRow;LVHITTESTINFO cHitTest;cHitTest.pt = pNMItemActivate->ptAction;if (-1 !=m_wndLstProcess.SubItemHitTest(&cHitTest))	//检测给定坐标位于哪个单元格上{if (cHitTest.flags & LVHT_ONITEMLABEL){iCurRow = cHitTest.iItem;CString sWinHandle = m_wndLstProcess.GetItemText(iCurRow, 1);if (m_mapDocTitle2String.end() != m_mapDocTitle2String.find(sWinHandle)){CString sCont = m_mapDocTitle2String[sWinHandle];m_wndEdt.SetWindowText(sCont);}}}*pResult = 0;
}

OK, that is all!写作不易,如果大家觉得对自己有点帮助,麻烦点个赞吧!

参考文章

  1. 《Get list of all open word documents in all Word instances》 https://social.microsoft.com/Forums/zh-CN/fd0411cb-dba4-48a9-acf7-2575ade4e597/get-list-of-all-open-word-documents-in-all-word-instances
  2. 《Get a Collection of All Running Excel Instances》 https://www.codeproject.com/Tips/1080611/Get-a-Collection-of-All-Running-Excel-Instances
  3. 《C++通过COM操作EXCEL》 https://blog.csdn.net/litterCooker/article/details/81538461
  4. 《VBA关于Word Windows对象参考》 https://learn.microsoft.com/en-us/office/vba/api/word.window