cv::namedWindow, GLFWwindow以及其他程序嵌入到MFC中的教程

分類:IT技術 時間:2016-10-08

cv::namedWindow, GLFWwindow以及其他程序嵌入到MFC中的教程

MFC雖然很老, 不美觀, 不跨平臺, 但是在Windows系統中, 利用MFC做功能驗證的界面, 還是很快很方便的. 因為它老, 所以有很多解決方案可以利用, 因為它是MS提供的界面庫, 所以在Windows上很容易實現, 並且和Windows系統結合很緊密. 比如說, 窗口消息等, 在MFC中是很方便實現的. 基於上面的種種原因, 利用MFC作為功能驗證的一個”殼” 是很好的工具.

當然, 難免就會遇到不少工程問題. 例如利用

glfwCreateWindow
創建出來的窗口, 怎麽讓它嵌入到MFC中. 以及經常使用OpenCV的朋友, 利用
cv::namedWindow
函數, 創建的圖像/視頻顯示窗口也是彈出式的, 怎麽讓它嵌入在MFC中的某個位置. 以及, 有時候想創建一個多進程程序, 讓創建的進程嵌入在MFC中運行等.

1. 準備工作

下面所有示例, 我都集成到一個VS13的解決方案中, 下載鏈接:

glfw源碼我也編譯成VS13版本的, 解壓之後可以直接使用VS13打開, 下載鏈接:

如果積分不夠的朋友, 可以給我留言, 留下郵箱或者QQ, 我可以直接發給你. 或者, 你按照下述內容, 一步一步的進行也是可以的. 第二個下載是不需要積分的.

首先, 你首先得有 glfw 的源碼 , OpenCV庫, 以及一個Visual Studio(我使用的是VS2013). 另外, 在VS13中, MFC已經拋棄了多字節字符集, 如果在MFC工程中想要使用多字節字符集, 需要下載一個多字節字符集支持包. 下載好了, 安裝即可.

然後, 創建一個基於對話框的MFC工程, 創建好功能後, 編輯界面, 簡單的添加一個控件就行, 我添加的是Picture Control, 在工具箱裏面拖進來調整大小就好. 再給 <開始> 按鍵添加一個按鍵響應函數. 雙擊<開始> 按鍵就行了. 界面示圖如下:


UI

最後, 在工程裏面配置一下OpenCV相關的包含目錄和庫目錄以及依賴項.

2. OpenCV窗口嵌入MFC

對當前我要分享問題感興趣的朋友, 應該不會對OpenCV的配置有問題吧. 如果有問題的話, 搜索一下, CSDN上面也有很多人對相關問題由詳細的描述.

在前面添加的<開始>按鍵響應函數中, 添加入下述代碼.

#include <opencv2\opencv.hpp>
void CaboutMFCDlg::OnBnClickedButton1()
{
    // TODO:  在此添加控件通知處理程序代碼
    CRect rect;
    // IDC_STATIC是剛剛在界面中加入的Picture Control的ID
    GetDlgItem(IDC_STATIC)->GetWindowRect(&rect);

    // 創建cv窗口並重置窗口大小
    cv::namedWindow("view", cv::WINDOW_NORMAL);
    cv::resizeWindow("view", rect.Width(), rect.Height());

    // 設置依附關系, 將cv窗口嵌入MFC主要是下述代碼起作用了.
    HWND hWnd = (HWND)cvGetWindowHandle("view");
    HWND hParent = ::GetParent(hWnd);
    ::SetParent(hWnd, GetDlgItem(IDC_STATIC)->m_hWnd);
    ::ShowWindow(hParent, SW_HIDE);

    // 循環讀取文件夾中的圖片並顯示. 僅僅作為功能驗證而已.
    cv::Mat img;
    int index = 0;
    char filename[128] = { 0 };
    while (true) {
        sprintf_s(filename, "..\\DragonBaby\\0%03d.jpg", ++index);
        img = cv::imread(filename);
        if ((img.cols <= 0) || (img.rows <= 0)) {
            break;
        }
        cv::imshow("view", img);
        cv::waitKey(30);
    }

    cv::destroyWindow("view");
}

其中真正關鍵的代碼就六行, 別的都是一些可有可無的代碼. 當然, 這只是一個簡單的示例而已. 當你要使用OpenCV時, 肯定不單是為了這樣循環查看圖片而已. 但, 通過上面的示例可以給我們一個啟發, 就是完全可以將OpenCV的處理進程與界面分離, 兩者相互沒有過多的影響. MFC只是作為一個”殼”用來展示而已. 因此, 可以將上述代碼再進行完善一下.

在該解決方案下, 再創建一個命令行工程. 配置好OpenCV. 因為我們要使用命令行參數進行參數傳遞, 所以需要把工程的改為使用多字節字符集. 更改方式: 右擊工程名–> 屬性 –> 配置屬性 –> 常規 –> 字符集, 選擇使用多字節字符集.

好, 下面開始寫代碼, 整理如下:
首先還是改MFC中按鍵響應函數, 修改如下:

PROCESS_INFORMATION pi;
void CaboutMFCDlg::OnBnClickedButton1()
{
    // 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("創建進程失敗!");

然後, 在新創建的命令行工程中, 添加下述代碼:

#include <opencv2\opencv.hpp>
#include <Windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
    int width = 0;
    int height = 0;
    HWND mainWnd = NULL;
    HWND viewWnd = NULL;
    char* commandline = GetCommandLine();
    // 從命令行中獲取主進程傳遞來的參數
    sscanf(commandline, "%d %d %d %d", &mainWnd, &viewWnd, &width, &height);

    // 創建cv窗口並重置窗口大小
    cv::namedWindow("view", cv::WINDOW_NORMAL);
    cv::resizeWindow("view", width, height);

    // 設置依附關系, 將cv窗口嵌入MFC主要是下述代碼起作用了.
    HWND hWnd = (HWND)cvGetWindowHandle("view");
    HWND hParent = ::GetParent(hWnd);
    ::SetParent(hWnd, viewWnd);
    ::ShowWindow(hParent, SW_HIDE);

    // 循環讀取文件夾中的圖片並顯示. 僅僅作為功能驗證而已.
    cv::Mat img;
    int index = 0;
    char filename[128] = { 0 };
    while (true) {
        sprintf_s(filename, "..\\DragonBaby\\0%03d.jpg", ++index);
        img = cv::imread(filename);
        if ((img.cols <= 0) || (img.rows <= 0)) {
            break;
        }
        cv::imshow("view", img);
        cv::waitKey(30);
    }
    cv::destroyWindow("view");

    return 0;
}

分別編譯, 然後就運行MFC程序, 點擊開始, 效果如下:

效果1

作為調試用時, 命令行的調試信息輸出是必不可少的. 所以難看的黑框就只有先忍著吧. 到最後展示階段, 該黑框可以將按鍵響應函數中兩行註釋掉的代碼打開註釋即可讓難看的黑框不再彈出來了. 最終結果如下所示:


結果

代碼中需要說明的三點, 首先, 在命令行參數傳遞時, 窗口句柄
HWND
是作為
int
來傳遞的. 具體參見MSDN該段說明, 可以知道, 在Win32中,
HWND
是32位的一個ID. 所以可以使用
"%d"
格式來進行傳輸. 其次, 在MFC的按鍵響應函數中, 有一個變量
PROCESS_INFORMATION pi
是作為全局變量放在函數體外面的. 原因是創建新進程之後, 難免會涉及到通信問題, 最簡單的辦法就是窗口消息. 通過該變量可以實現消息傳遞. 使用
PostThreadMessage(pi.dwThreadId, WM_TEST, wParam, lParam)
函數傳遞消息到新進程中, 其中
WM_TEST
是自定義的消息. 最後, 在命令行進程中, 命令行第一個參數, 是MFC進程的窗口句柄, 可以利用該句柄發送消息到MFC進程. 使用
SendMessage(mainWnd, WM_TEST, wParam, lParam)
函數.

3. GLFWwindow嵌入MFC

3.1 配置GLFW

之前在一個小項目中, 用到了TI所提供的DLP-ALC-LIGHTCRAFTER-SDK-2.0(簡稱DLP), 該SDK提供源碼, 通過結構光projector + Point Grey攝像頭進行掃描, 得到點雲, 然後進行三維重建. 需要創建一個界面用於展示. 而在DLP中, 點雲顯示是將

GLFWwindow
進行了封裝用於顯示. 源碼中, 用於創建窗口的函數是
glfwCreateWindow(width, height, title.c_str(), NULL, NULL)
, 使用上述方法得不到理想的效果. 所以只能另辟蹊徑. 很慶幸, Google到一個比較好的解決方案. 很感謝該博主, 成功的完成了預期的功能.

由於我原始項目錯綜復雜, 不利於直接呈現出該問題的解決. 所以下面我們一步一步的完成所需要的功能. 在前面的鏈接中下載glfw的源碼, 在GitHub上面下載下來即可. 另外, 需要下載CMake, 並且假定你電腦已經安裝了VS.

下載下來的glfw源碼文件夾如下圖所示:


glfw

下載好CMake之後, 安裝. 在開始菜單能夠找到 CMake(cmake-gui)的快捷方式. 打開CMake, 如下圖所示:

cmake

在 “Where is the source code:” 之後選擇你下載的glfw路徑, 如我上圖所示, 我的路徑就是
E:/glfw/glfw-master
, 在
E:/glfw
目錄下新建一個文件夾, 命名為
glfw-build
, 將該文件夾路徑填入”Where to build the binaries:”, 然後點擊 . 會出現下述選擇窗口, 選擇(當然, 我電腦安裝的VS13, 所以選擇該條目, 你對應選擇你所安裝的VS就好). 然後選擇 . 然後將下圖中
BUILD_SHARED_LIBS
勾選上, 再次點擊:

configure

然後點擊, 會提示 Generating done. 搞定之後, 打開
E:/glfw/glfw-build
後你會看到如下畫面, 熟練的雙擊
GLFW.sln
就可以使用VS打開該工程了. VS打開之後, 爽快的按下F7. VS就開始工作了. 在
E:\glfw\glfw-build\src\Debug
路徑下, 會看到生成的一些文件. 都是很熟悉的東西吧. lib文件以及dll文件. VS示圖如下, 並按照圖中選項找到simple示例:

VS

按照上圖, 找到simple, 右擊彈出下拉菜單, 依次選擇<調試> –> <啟動新實例>. 會得到下圖展示的一個DEMO效果. 也許你會得到一個錯誤, 提示在
E:/glfw/glfw-huild/src/Debug
裏面看到的
glfw3.dll
文件找不到. 解決辦法很簡單, 將該文件復制到
C:\Windows\system32
中去, 或者將
E:/glfw/glfw-huild/src/Debug
目錄加入環境變量
Path
中. 第一種辦法好像需要重啟一次才行.

simple

3.2 修改代碼

在simple工程中, 提供了源碼, 打開simple.c可以看到其實現代碼. 能夠找到下述代碼:

glfwWindowHint(GLFW_CONTEXT_version_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);

window = glfwCreateWindow(640, 480, "Simple example", NULL, NULL);
if (!window)
{
    glfwTerminate();
    exit(EXIT_FAILURE);
}

其中窗口的創建, 就是使用函數

glfwCreateWindow
. 在VS中, 找到
glfwCreateWindow
函數的定義位置, 是在
glfw3.h
文件中, 新加入一個函數
glfwCreateWindowEx
聲明, 如下:


define

在原本
glfwCreateWindow
函數的參數列表中新加入了參數
int hParent
. 新加入的參數, 本應該是
HWND
類型, 但該類型定義於Windows.h中, 本著盡可能少的改動代碼, 以
int
代替了
HWND
類型, 具體原因類似於第二節中所述.

現在打開

win32_platform.h
文件, 找到其中
struct _GLFWwindowWin32
定義所在的位置, 新加入
HWND handleParent
, 用來保存父窗口的句柄作為參數傳遞給創建窗口的函數. 如下圖所示:


新加入參數

修改好參數結構體之後, 現在定位
glfwCreateWindow
函數的定義, 定義於文件
window.c
中. 復制
glfwCreateWindow
函數的定義, 粘貼在
glfwCreateWindow
函數的定義的下方, 更改函數名為
glfwCreateWindowEx
並加入參數
int hParent
. 在該函數的實現中找到
_glfwPlatformCreateWindow
函數的調用地方, 在其前方加入下述代碼:

window->win32.handleParent = hParent;

效果如下:


傳參

現在, 沿著
_glfwPlatformCreateWindow
函數的函數調用一直找到API
CreateWindowExW
函數的調用地方, 位於
win32_window.c
文件定義的
static int createWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig)
函數中被調用. 在
CreateWindowExW
函數前加入下述代碼, 並將
CreateWindowExW
函數的倒數第四個參數改成
window->win32.handleParent
.

if (NULL != window->win32.handleParent) {
    exStyle = 0;
    style = WS_CHILDWINDOW | (wndconfig->visible ? WS_VISIBLE : 0);
}

截圖如下:


code1

修改好了之後, 對代碼進行編譯, 還是運行simple示例進行驗證. 仍然可以得到前面原始代碼所展示的效果. 說明我們代碼的修改沒有對原本性能產生破壞.

3.3 效果驗證

本來, 預想是如同前一個例子, 在MFC按鍵響應函數中通過

CreateProcess
調用已經編譯好的simple.exe可執行程序, 完成界面的顯示. 可是一直無法成功. 通過
CreateProcess
調用sample文件夾中任意示例均無法成功調用. 一直沒有找到具體是為什麽. 由於該步驟只是驗證功能. 所以就通過另一個辦法來完成驗證.

首先, 在原始MFC界面中加入三個Edit Control, 重新編寫<開始>按鍵響應函數, 代碼如下:

void CaboutMFCDlg::OnBnClickedButton1()
{
    // TODO:  在此添加控件通知處理程序代碼

    HWND viewWnd = GetDlgItem(IDC_STATIC)->m_hWnd;
    CRect rect;
    GetDlgItem(IDC_STATIC)->GetWindowRect(&rect);

    CString str;
    str.Format("%d", viewWnd);
    GetDlgItem(IDC_EDIT1)->SetWindowText(str);

    str.Format("%d", rect.Width());
    GetDlgItem(IDC_EDIT2)->SetWindowText(str);

    str.Format("%d", rect.Height());
    GetDlgItem(IDC_EDIT3)->SetWindowText(str);
}

點擊<開始>, 獲取用於顯示的Picture Control的HWND, 以及長寬. 分別顯示在三個Edit Control中. 然後在simple的代碼中寫死代碼完成該功能(恕小弟無能, 當前只能用這樣無奈的方式完成功能驗證了). simple.c中的代碼片段截取如下:

glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);

// 將MFC中獲取到的三個值分別替代這三個變量.
int viewWnd = 5114720; // Picture Control的HWND
int width = 536; // Picture Control的寬
int height = 294; // Picture Control的高
window = glfwCreateWindowEx(width, height, "Simple example", NULL, NULL, viewWnd);
// 註釋掉原本創建窗口所調用的函數, 換作我們新增的創建窗口函數glfwCreateWindowEx
// window = glfwCreateWindow(640, 480, "Simple example", NULL, NULL);
if (!window)
{
    glfwTerminate();
    exit(EXIT_FAILURE);
}

編譯simple工程生成可執行文件, 然後按照上面圖示的方式運行simple.可以看到如下結果.


這裏寫圖片描述

4. 其他程序嵌入MFC

該話題, 也是無意間在網站上瀏覽到相關的資料, 感覺很有趣, 就試著做了一下, 現在也整理出來和大家分享一下. 原作者 在他的博客中描述了一些, 但是不夠具體. 我在這兒更具體的描述一下. 另外, 在原作者描述的實現方式中, 主要是考慮所有功能均在一端實現, 被調用的exe完全不知道自己是被嵌入到MFC中在運行.

其中, 主要思想是, 調用

CreateProcess
後是可以得到被創建進程的信息, 其中就包括進程ID. 則可以通過枚舉進程ID進而得到被創建進程的窗口句柄. 然後就可以對該進程的窗口進行上述內容中的
SetParent
操作了. 在示例代碼中, 我是直接調用Windows自帶的記事本進行演示.

為了方便展示, 此處直接使用全局變量. 定義了兩個變量, 分別保存進程句柄以及進程窗口句柄. 定義如下:

HWND apphwnd;
HANDLE handle;

然後, 定義創建進程的函數, 創建成功後利用進程ID枚舉獲取窗口句柄, 代碼如下:

// 回調函數, 枚舉獲取窗口句柄
int CALLBACK EnumWindowsProc(HWND hwnd, LPARAM param)
{
    DWORD pID;
    DWORD TpID = GetWindowThreadProcessId(hwnd, &pID);
    if (TpID == (DWORD)param)
    {
        apphwnd = hwnd;
        return false;
    }
    return true;
}
// 第一個參數是被調用進程的路徑, 第二格參數是需傳入的參數列表
HANDLE StartNewProcess(LPCTSTR program, LPCTSTR args)
{
    HANDLE hProcess = NULL;
    PROCESS_INFORMATION processInfo;
    STARTUPINFO startupInfo;
    ::ZeroMemory(&startupInfo, sizeof(startupInfo));
    startupInfo.cb = sizeof(startupInfo);
    startupInfo.dwFlags = STARTF_USESHOWWINDOW;
    startupInfo.wShowWindow = SW_HIDE;
    if (::CreateProcess(program, (LPTSTR)args,
        NULL,  // process security
        NULL,  // thread security
        FALSE, // no inheritance
        0,     // no startup flags
        NULL,  // no special environment
        NULL,  // default startup directory
        &startupInfo,
        &processInfo))
    { /* success */
        Sleep(50);//wait for the window of exe application created
        ::EnumWindows(&EnumWindowsProc, processInfo.dwThreadId);
        hProcess = processInfo.hProcess;
    } /* success */
    return hProcess;//Return HANDLE of process.
}

當然, 該進程的關閉也是需要定義相關的函數.

BOOL CloseProcess() {
    return TerminateProcess(handle, 0);
}

現在, 我們再次重新編寫<開始>按鍵響應函數, 代碼如下:

void CaboutMFCDlg::OnBnClickedButton1()
{
    // TODO:  在此添加控件通知處理程序代碼
    handle = StartNewProcess("C:\\Windows\\notepad.exe", NULL);

    // CRect rect;
    // GetDlgItem(IDC_STATIC)->GetWindowRect(&rect);
    // ::MoveWindow(apphwnd, rect.left, rect.top, rect.Width(), rect.Height(), false);
    ::SetWindowLong(apphwnd, GWL_STYLE, WS_VISIBLE);

    HWND viewWnd = GetDlgItem(IDC_STATIC)->GetSafeHwnd();
    ::SetParent(apphwnd, viewWnd);
}

代碼很簡單, 我就沒有寫註釋了. 其中被註釋掉的三行代碼, 本來應該是完成將新建的進程移動到指定位置, 但是移動之後, 會出現錯誤. 也沒有找到原因. 希望哪位朋友知道原因並成功解決了的話, 告訴我一聲. 謝謝.

該內容幾乎和上面給出原作者的示例一樣. 感興趣的朋友, 可以查看原作者的表述.

最後, 結果示例如下:


這裏寫圖片描述

OK, 打完收工.


Tags: Windows 留下郵箱 解決方案 視頻顯示 對話框

文章來源:


ads
ads

相關文章
ads

相關文章

ad