內嵌第三方EXE程式視窗
近日,在做一個將一個第三方應用視窗嵌入自己的程式視窗內部的功能,經過幾次摸索,終於能比較好的嵌入進去了,為方便敘述,以下稱需要被嵌入應用視窗為目標視窗,承載嵌入應用視窗的為宿主視窗,目標視窗為任意第三方應用,宿主視窗為自己開發。
將目標視窗完美嵌入,關鍵在於將目標視窗做為宿主視窗的子視窗,這樣作業系統就會幫我們完成移動,繪製等操作,為目標視窗設定父視窗,也很簡單,呼叫一個API即可:
HWND SetParent(
HWND hWndChild,
HWND hWndNewParent
);
第一個引數是子視窗的控制代碼,也即目標視窗的控制代碼,第二個視窗為我們想設定的目標視窗的父視窗,即宿主視窗,由於宿主視窗的程式為自己開發,要拿到這個控制代碼就很簡單,關鍵在於如何找到目標視窗的控制代碼。
找到目標視窗的控制代碼,也有一個API:
HWND FindWindow(
LPCTSTR lpClassName,
LPCTSTR lpWindowName
);
第一個引數為視窗的類名,建立視窗之前,首先需要使用RegisterClass向系統註冊一個視窗類WNDCLASS,這個結構體中有個lpszClassName欄位,就代表這個視窗類的名稱。第二個引數為目標視窗的標題。具體這兩個值多少,可以使用vs自帶的spy++工具,檢視下目標視窗,就能獲得。
下面分步驟,結合程式碼,詳細展示下整體流程。
1.首先準備好宿主視窗的窗體,並且使其透明。
這個可以通過響應WM_CTLCOLOR訊息,並返回一個透明畫刷完成,程式碼很簡單如下:
HBRUSH CDlgMainIntelligent::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
CDetachDlg::OnCtlColor(pDC, pWnd, nCtlColor);
pDC->SetBkMode(TRANSPARENT);
return (HBRUSH)GetStockObject(NULL_BRUSH);
}
2.拿到第三方應用的視窗控制代碼
這裡又分為兩種情景,一種是第三方應用尚未啟動,需要我們自己去啟動,並等待其啟動完成後,查詢其視窗控制代碼,另一種情形是第三方應用已啟動,這樣可以直接去查詢視窗控制代碼。如何區分這兩種情形,很簡單,先去查詢下視窗,如果能找到就證明已啟動,否則就去啟動下。
啟動應用程序可以使用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;
<span style="font-size:14px;">}</span>
通過以上函式,就能拿到控制代碼了。接下來就可以設定其父視窗了。
3.處理視窗WM_SIZE訊息
前面說過,將第三方視窗設定為子視窗,移動繪製這些我們不用管了,系統會幫忙完成,不過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);
}
}
4.退出時,將第三方程式關掉
好的體驗就是,目標視窗完全做為宿主視窗的一部分,同生共死,因此當宿主視窗銷燬的時候,目標視窗也要一併銷燬。這個也很簡單,只要在宿主視窗的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;
}