1. 程式人生 > >內嵌第三方EXE程式視窗

內嵌第三方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;
}