目录
- 一 问题的提出
- 二 工程创建
- 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的类型库,单击,即可显示该类型库中所有的接口。
选出其中的_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!写作不易,如果大家觉得对自己有点帮助,麻烦点个赞吧!
参考文章
- 《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
- 《Get a Collection of All Running Excel Instances》 https://www.codeproject.com/Tips/1080611/Get-a-Collection-of-All-Running-Excel-Instances
- 《C++通过COM操作EXCEL》 https://blog.csdn.net/litterCooker/article/details/81538461
- 《VBA关于Word Windows对象参考》 https://learn.microsoft.com/en-us/office/vba/api/word.window