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

一 问题的提出

  在自动化编程中,一个常见的问题是:如何获取运行中的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指令行,即类似下面的指令语句
在这里插入图片描述

三 代码实例

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 id
	DWORD idThread;  // creator thread id
	TCHAR pszTitle[MAXTITLELEN];	//Window title
	TCHAR 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
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Santiago

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值