近日,在做一个将一个第三方应用窗口嵌入自己的程序窗口内部的功能,经过几次摸索,终于能比较好的嵌入进去了,为方便叙述,以下称需要被嵌入应用窗口为目标窗口,承载嵌入应用窗口的为宿主窗口,目标窗口为任意第三方应用,宿主窗口为自己开发。
将目标窗口完美嵌入,关键在于将目标窗口做为宿主窗口的子窗口,这样操作系统就会帮我们完成移动,绘制等操作,为目标窗口设置父窗口,也很简单,调用一个API即可:
HWND SetParent(
HWND hWndChild,
HWND hWndNewParent
);
第一个参数是子窗口的句柄,也即目标窗口的句柄,第二个窗口为我们想设置的目标窗口的父窗口,即宿主窗口,由于宿主窗口的程序为自己开发,要拿到这个句柄就很简单,关键在于如何找到目标窗口的句柄。
找到目标窗口的句柄,也有一个API:
HWND FindWindow(
LPCTSTR lpClassName,
LPCTSTR lpWindowName
);
第一个参数为窗口的类名,创建窗口之前,首先需要使用RegisterClass向系统注册一个窗口类WNDCLASS,这个结构体中有个lpszClassName字段,就代表这个窗口类的名称。第二个参数为目标窗口的标题。具体这两个值多少,可以使用vs自带的spy++工具,查看下目标窗口,就能获得。
下面分步骤,结合代码,详细展示下整体流程。
这个可以通过响应WM_CTLCOLOR消息,并返回一个透明画刷完成,代码很简单如下:
HBRUSH CDlgMainIntelligent::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
CDetachDlg::OnCtlColor(pDC, pWnd, nCtlColor);
pDC->SetBkMode(TRANSPARENT);
return (HBRUSH)GetStockObject(NULL_BRUSH);
}
这里又分为两种情景,一种是第三方应用尚未启动,需要我们自己去启动,并等待其启动完成后,查找其窗口句柄,另一种情形是第三方应用已启动,这样可以直接去查找窗口句柄。如何区分这两种情形,很简单,先去查找下窗口,如果能找到就证明已启动,否则就去启动下。
启动应用进程可以使用CreateProcess,这里有个问题,如何判断这个进程已经启动好了呢?可以使用
DWORD WINAPI WaitForInputIdle(
__in HANDLE hProcess,
__in DWORD dwMilliseconds
);
不过这个仅仅是判断进程启动好,有可能主窗口还没创建完成,也有可能第三方程序崩了,压根拿不到。试过几种方法,都不太理想,我采用了最简单有效的方法,就是循环探测等待,并设置超时时间,具体看代码,我封装了一个函数,可以拿到第三方进程的句柄及主窗口句柄。
HWND GetIVAWindowAndProcess(HANDLE& hProcess)
{
HWND hWnd = NULL;
hWnd = ::FindWindow("XXXXClient3",NULL);
//是否能找到,找不到就去启动下
if(hWnd == NULL)
{
int nNumberDely = 10;
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
// Start the child process.
if( !CreateProcess( NULL, // No module name (use command line)
"XXX\\XXX.exe", // Command line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
0, // No creation flags
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi ) // Pointer to PROCESS_INFORMATION structure
)
{
AfxMessageBox(_T("启动XXX进程失败!"));
return NULL;
}
CloseHandle(pi.hThread);
WaitForInputIdle(pi.hProcess,INFINITE);
hProcess = pi.hProcess;
while((hWnd = ::FindWindow("XXXXClient3",NULL)) == NULL && nNumberDely > 0)
{
Sleep(500);
nNumberDely --;
}
}
else
{
DWORD dwProcessId = 0;
DWORD dwThreadId = 0;
dwThreadId = GetWindowThreadProcessId(hWnd,&dwProcessId);
hProcess = OpenProcess(PROCESS_ALL_ACCESS,NULL,dwProcessId);
}
return hWnd;
}
通过以上函数,就能拿到句柄了。接下来就可以设置其父窗口了。
前面说过,将第三方窗口设置为子窗口,移动绘制这些我们不用管了,系统会帮忙完成,不过WM_SIZE是需要我们处理的,有了窗口句柄,这个也变得很简单。只要在OnSize里面调用MoveWindow即可
void CDlgMainIntelligent::OnSize(UINT nType, int cx, int cy)
{
CDetachDlg::OnSize(nType, cx, cy);
if(GetSafeHwnd() == NULL)
{
return ;
}
if(!IsWindow(GetSafeHwnd()))
{
return;
}
if(!m_bIsInit)
{
return ;
}
CRect rect;
GetClientRect(&rect);
if(m_hIVAWnd != NULL && !m_bIsAttached)
{
::SetParent(m_hIVAWnd,GetSafeHwnd());
m_bIsAttached = TRUE;
}
if(m_hIVAWnd)
{
::PostMessage(m_hIVAWnd,WM_SYSCOMMAND,SC_MAXIMIZE,NULL);
::MoveWindow(m_hIVAWnd,rect.left,rect.top,rect.Width(),rect.Height(),TRUE);
}
}
好的体验就是,目标窗口完全做为宿主窗口的一部分,同生共死,因此当宿主窗口销毁的时候,目标窗口也要一并销毁。这个也很简单,只要在宿主窗口的OnCLose中做处理即可。
void CDlgMainIntelligent::OnClose()
{
// TODO: Add your message handler code here and/or call default
if(m_hIVAWnd!= NULL)
{
::PostMessage(m_hIVAWnd,WM_CLOSE,NULL,NULL);
m_hIVAWnd = NULL;
}
if(m_hIVAProcess != NULL)
{
::TerminateProcess(m_hIVAProcess,0);
CloseHandle(m_hIVAProcess);
m_hIVAProcess = NULL;
}
CDetachDlg::OnClose();
}
经过这样处理,基本能完美嵌入第三方应用的主窗口了。
另外还有一些细节上的体验,一般第三方应用窗口,也是有自己的最大最小化按钮的,需要把这个屏蔽掉,否则看起来不美观。我使用的方法是,向第三方应用注入一个DLL,在该DLL中创建一个小窗口,把应用窗口的右上角遮挡住。这样界面看起来就非常统一和协调了。
注入DLL的代码就是windows核心编程上的,没什么好说的,如下:
BOOL CDlgMainIntelligent::InjectHookDLL(HANDLE hProcess)
{
if(hProcess == NULL)
{
return FALSE;
}
PCWSTR pszLibFile = L"MsgHookDLL.Dll";
if(hProcess != NULL)
{
int cch = 1 + lstrlenW(pszLibFile) ;
int cb = cch * sizeof(wchar_t);
PWSTR PszLibFileRemote = (PWSTR) VirtualAllocEx(hProcess,NULL,cb,MEM_COMMIT,PAGE_READWRITE);
WriteProcessMemory(hProcess,PszLibFileRemote,(PVOID)pszLibFile,cb,NULL);
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(_T("Kernel32")),"LoadLibraryW");
HANDLE hThread = CreateRemoteThread(hProcess,NULL,0,pfnThreadRtn,PszLibFileRemote,0,NULL);
WaitForSingleObject(hThread,INFINITE);
return TRUE;
}
return FALSE;
}