開始學習OpenGL由於有一段時間,但是glfw只有窗口區,雖然通過某種手段(移步這里)可以加入工具欄,但仍然無法作為一個標准的GUI,而直接在MFC或Qt里面使用OpenGL API感覺有諸多制肘,各有利弊,所以打算將其嵌入GUI框架,此處以MFC為例
參考博文:https://blog.csdn.net/sunbibei/article/details/51783783
注意:本文使用的是MFC主程序調用GLFW子進程的方式嵌入窗口,略顯繁瑣。經道友提醒,原來也可以用多線程方式嵌入渲染,詳情請移步 將GLFW窗口嵌入Win32 SDK窗口及其多線程渲染方法。其方式應該也可以用在MFC窗口中。
1、准備工作
由於要通過CreateProcess創建子進程的方式調用第三方exe程序,所以有必要知道創建的子進程信息,此處exe來自GLFW示例程序
1.1、查看打開窗口程序進程PID
windows任務管理器 -> 進程 -> 查看 -> 選擇列 -> 進程勾選PID選項
如圖,同一個exe窗口程序多次重復打開之后其PID是唯一的,其他信息(名稱)相同,所以首先拿到以CreateProcess方法創建子進程時的進程PID
2、對CreateProcess函數進行封裝
/* * 創建子進程 * @program 被調用進程的路徑 * @args 需傳入的參數列表 */ HANDLE StartNewProcess(LPCTSTR program, LPCTSTR args) { HANDLE hProcess = NULL; PROCESS_INFORMATION pi; STARTUPINFO si; ::ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; // 創建子進程 if (::CreateProcess( program, // 參數1.應用程序的名稱,絕對路徑,也可以是相對路徑,可為NULL,若為NULL,則執行lpCommandLine (LPTSTR)args, // 參數2.命令行參數,可為NULL,一般為應用程序傳參,若為NULL,函數則使用 lpApplicationName字符串為運行命令行 NULL, // 參數3.進程的屬性,指向一個SECURITY_ATTRIBUTES結構,結構體決定返回的句柄是否被子進程繼承,一般為NULL NULL, // 參數4.線程的屬性,同參數3.但是這個參數決定的是 線程 是否被繼承,一般為NULL FALSE, // 參數5.是否繼承父進程的屬性,TRUE\FALSE ,一般為FALSE ,若為TRUE 進程中每個可被繼承的打開句柄都被繼承,被繼承者有相同的值和訪問權限 0, // 參數6.標志位信息,參數太多,具體見MSDN,或者百度百科,一般默認為 0 NULL, // 參數7.環境變量,指向新進程的環境塊,一般為NULL,為NULL則新進程使用調用進程的環境 NULL, // 參數8.程序當前目錄,為指定子進程的工作路徑,如果是啟動Exe程序,則為應用程序所在的目錄 &si, // 參數9.傳給新進程的信息,指向新進程主窗口如何顯示的STARUPINFO 結構體 &pi) // 參數10.進程返回的信息,用來接收新進程識別信息的PROCESS_INFORMATION結構體 ) { Sleep(100); // 此處要是不等待或等待時間過短,子進程窗口未創建完成,則無法在回調中枚舉到當前創建的窗口句柄 TRACE("PID = >>>>> = %d, TID = >>>>> = %d, hProcess = >>> %d\n", pi.dwProcessId, pi.dwThreadId, pi.hProcess); // 枚舉所有屏幕上的頂層窗口,並將窗口句柄傳送給應用程序定義的回調函數 ::EnumWindows(&EnumWindowsProc, pi.dwThreadId); hProcess = pi.hProcess; } else { TRACE("CreateProcess failed (%d).\n", GetLastError()); return 0; } return hProcess; // 返回進程句柄 }
任務管理器顯示如下,進程PID為10172
VS打印輸出
d:\vsworkspace\mfcglfwtest\mfcglfwtest\mfcglfwtestview.cpp(80) : atlTraceGeneral - PID = >>>>> = 1072, TID = >>>>> = 6288, hProcess = >>> 300
過程無誤,由PROCESS_INFORMATION 結構體對象pi拿到進程pid和句柄,兩個結構體詳情查看這里。
3、枚舉桌面窗口句柄的回調函數
/* * 回調函數, 枚舉獲取窗口句柄 * @hwnd 枚舉獲取的頂層窗口句柄 * @param 進程名稱 */ BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM param) { DWORD pID; // 進程ID DWORD TpID = GetWindowThreadProcessId(hwnd, &pID); // 返回值為創建窗口的線程ID if (TpID == (DWORD)param) { TRACE("TpID = %d, > ===== > param = %d, > ===== > hwnd = %d\n", TpID, param, hwnd); apphwnd = hwnd; return FALSE; // 停止枚舉,返回FALSE } return TRUE; // 繼續枚舉,返回TRUE }
4、進程關閉函數
/* * 關閉進程 */ BOOL CloseProcess() { return TerminateProcess(handle, 0); }
5、入口函數
此處加入了一個工具欄按鈕事件,點擊觸發此函數,客戶區位置rect和上下文m_pDC定義為CXXXView類的成員
public: CClientDC *m_pDC = NULL; // 客戶區上下文 CRect rect;
事件處理函數
void CMFCGlfwTestView::OnGlfwsimple() { // TODO: 在此添加命令處理程序代碼 handle = StartNewProcess(_T("..\\Debug\\simple.exe"), NULL); // 獲取客戶區位置 GetClientRect(&rect); // 更改窗口的位置和尺寸,此處填滿父窗口 ::MoveWindow(apphwnd, rect.left, rect.top, rect.Width(), rect.Height(), false); // ::SetWindowLong(apphwnd, GWL_STYLE, WS_VISIBLE); // 更改窗口屬性 // 獲取客戶區上下文 m_pDC = new CClientDC(this); HDC c_DC = m_pDC->GetSafeHdc(); HWND viewWnd = WindowFromDC(c_DC); TRACE("child_hwnd >> %d, parent_hwnd >> %d\n", apphwnd, viewWnd); ::SetWindowLong(apphwnd, GWL_STYLE, WS_VISIBLE); // 獲取客戶區所在窗口句柄 ::SetParent(apphwnd, viewWnd); }
關鍵在於通過::SetParent(apphwnd, viewWnd)函數設置子窗口的父窗口為單文檔客戶區,所以要用WindowFromDC(c_DC)函數經客戶區上下文拿到客戶區句柄。
6、全局變量與函數說明
上面代碼涉及到兩個全局變量子窗口句柄和子進程句柄,這兩個變量被定義在CXXXView.cpp中而不是頭文件中,還有2、3、4三個函數也是直接在CXXXView.cpp中定義而沒有進行頭文件聲明,因為將其作為CXXXView類的成員或對象成員都會使得::EnumWindows(&EnumWindowsProc, pi.dwThreadId)枚舉函數參數錯誤,暫未找到解決之法。
HWND apphwnd; // 子窗口句柄 HANDLE handle; // 進程句柄
7、關鍵的窗口銷毀事件
子進程窗口應該隨着父窗口的銷毀而關閉,所以需要為當前類添加一個OnDestroy消息處理函數,在其中調用上面定義的進程關閉函數
/* * 窗口銷毀 */ void CMFCGlfwTestView::OnDestroy() { // TODO: 在此處添加消息處理程序代碼 if (CloseProcess() != 0) { TRACE("child process has been specified ... \n"); } // 釋放客戶區上下文對象 if (m_pDC) { delete m_pDC; } TRACE("m_pDC has been free ... \n"); CView::OnDestroy(); }
注意:由於是通過該函數關閉子進程,所以正常點擊主窗口上的×按鈕是沒問題的,但是通過VS的停止調試按鈕關閉程序不會觸發該函數,子進程仍會在后台運行,需要手動關閉
8、子窗口大小改變
要使得父窗口大小變化時子窗口隨之改變,需要再給當前類添加一個OnSize消息處理函數
/* * 窗口大小改變監聽 */ void CMFCGlfwTestView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); // TODO: 在此處添加消息處理程序代碼 GetClientRect(&rect); ::MoveWindow(apphwnd, rect.left, rect.top, rect.Width(), rect.Height(), true); }
每次父窗口改變后獲取窗體大小,重新設置子窗口大小。
9、效果展示
注意:子窗口本身具有按鍵監聽函數,按ESC可關閉子窗口,該功能嵌入MFC后依然有效,由於沒有設置點擊打開子窗口的次數,所以多次打開會重疊,無法關閉前面進程,需要手動關閉。
由於子窗口是創建后再被移動縮放到指定位置的,所以會有閃爍,可以先創建隱藏窗口-->移動到指定位置-->窗口顯示,在創建子進程時將父窗口的位置和大小傳給子進程,在子進程main函數中接收
// TODO: 在此添加控件通知處理程序代碼 STARTUPINFO startupinfo; memset(&startupinfo, '\0', sizeof(startupinfo)); startupinfo.cb = sizeof(startupinfo); //設置進程創建時不顯示窗口 // startupinfo.dwFlags = STARTF_USESHOWWINDOW; /*startf_useposition*/ // startupinfo.wShowWindow = SW_HIDE; char* CommandLine = new char[128]; memset(CommandLine, '\0', 128); // 主進程窗口句柄 HWND mainWnd = AfxGetMainWnd()->m_hWnd; // 顯示控件句柄 HWND viewWnd = GetDlgItem(IDC_STATIC)->m_hWnd; CRect rect; GetDlgItem(IDC_STATIC)->GetWindowRect(&rect); // 將參數寫入命令行, 傳遞給馬上要創建的進程 sprintf(CommandLine, "%d %d %d %d", mainWnd, viewWnd, rect.Width(), rect.Height()); BOOL b = CreateProcess("..\\Debug\\OpenCVProc.exe", CommandLine, NULL, NULL, FALSE, NULL, NULL, NULL, &startupinfo, &pi); if (!b) MessageBox("創建進程失敗!");
10、進程間通信
既然是通過子進程方式調用OpenGL程序,那么某些復雜交互應該需要進程間通信,關於C++進程間通信的方法比較多,常見的管道、共享內存等,作為一個新手,我這里選擇了UDP通信方式,即在exe程序中加個線程函數,啟動一個UDPServer,然后在MFC程序中需要的位置啟動UDPClient,很簡單就可以實現兩者之間通信了。
需要注意的是UDP通信有數據大小限制(網絡MTU,1500-20-8=1472),對於基本的傳輸應該是可以滿足的,有需要可以改成TCP通信。
關於C++ UDP通信的Demo各種博客比較多,就不復制代碼了。
11、更好的方式 dear imgui
參考文章:現代OpenGL教程(一):繪制三角形(imgui+OpenGL3.3)
imgui 是一個開源的GUI框架,見 github,對opengl,d3d圖形接口,glfw,glut、sdl等界面庫進行了一定的封裝,可以看看其演示程序(直接點擊下載)。
寫這篇glfw嵌入MFC的文章的初衷是為了給glfw窗口添加菜單欄,以便實現更好的交互功能。因為以前一直不明白為什么GLFW庫只有一個窗口,就不能像MFC,Qt等框架一樣也帶上其他界面元素,直到看到dear imageui的演示程序,忽然悟到原來菜單欄,按鈕,輸入框等界面元素其實與窗體本身沒有必然關系(Window != UI,窗口是UI的基礎),是后來繪制出來的。MFC,Qt框架通過其自帶的UI設計器很方便的在窗口上可視化繪制這些UI元素。GLFW只是一個繪圖板,沒有UI設計器,就需要自己繪制元素,寫事件交互。而dear imgui就是再窗口的基礎上用圖形接口做了這些事情。下面是其demo演示:
這個OpenGL 3 + GLFW的演示程序只有623K。沒有任dll依賴。里面有各種常用界面元素的演示。
總結:如果是更側重於圖形渲染的程序,那么這種方式可能是更好的選擇,滿足輕量級、性能、跨平台。如果是側重於一般的條條框框,數據展示,Qt和MFC顯然更快速方便。
GLFW + MFC 可能更適合將 GLFW 程序作為 MFC/Qt程序的插件使用的場景,而我的初衷是更好的使用GLFW,所以。。。放棄這種做法,就當留個踩坑記錄。