MFC單文檔視圖中嵌入GLFW窗口


開始學習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,所以。。。放棄這種做法,就當留個踩坑記錄。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM