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

Posted 小贝也沉默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了cv::namedWindow, GLFWwindow以及其他程序嵌入到MFC中的教程相关的知识,希望对你有一定的参考价值。

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

MFC虽然很老, 不美观, 不跨平台, 但是在Windows系统中, 利用MFC做功能验证的界面, 还是很快很方便的. 因为它老, 所以有很多解决方案可以利用, 因为它是MS提供的界面库, 所以在Windows上很容易实现, 并且和Windows系统结合很紧密. 比如说, 窗口消息等, 在MFC中是很方便实现的. 基于上面的种种原因, 利用MFC作为功能验证的一个”壳” 是很好的工具.

当然, 难免就会遇到不少工程问题. 例如利用glfwCreateWindow创建出来的窗口, 怎么让它嵌入到MFC中. 以及经常使用OpenCV的朋友, 利用cv::namedWindow函数, 创建的图像/视频显示窗口也是弹出式的, 怎么让它嵌入在MFC中的某个位置. 以及, 有时候想创建一个多进程程序, 让创建的进程嵌入在MFC中运行等.

1. 准备工作

下面所有示例, 我都集成到一个VS13的解决方案中, 下载链接: http://download.csdn.net/detail/sunbibei/9563524

glfw源码我也编译成VS13版本的, 解压之后可以直接使用VS13打开, 下载链接: http://download.csdn.net/detail/sunbibei/9563534

如果积分不够的朋友, 可以给我留言, 留下邮箱或者QQ, 我可以直接发给你. 或者, 你按照下述内容, 一步一步的进行也是可以的. 第二个下载是不需要积分的.

首先, 你首先得有 glfw 的源码 , OpenCV库, 以及一个Visual Studio(我使用的是VS2013). 另外, 在VS13中, MFC已经抛弃了多字节字符集, 如果在MFC工程中想要使用多字节字符集, 需要下载一个多字节字符集支持包. 下载好了, 安装即可.

然后, 创建一个基于对话框的MFC工程, 创建好功能后, 编辑界面, 简单的添加一个控件就行, 我添加的是Picture Control, 在工具箱里面拖进来调整大小就好. 再给 <开始> 按键添加一个按键响应函数. 双击<开始> 按键就行了. 界面示图如下:




最后, 在工程里面配置一下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程序, 点击开始, 效果如下:

作为调试用时, 命令行的调试信息输出是必不可少的. 所以难看的黑框就只有先忍着吧. 到最后展示阶段, 该黑框可以将按键响应函数中两行注释掉的代码打开注释即可让难看的黑框不再弹出来了. 最终结果如下所示:




代码中需要说明的三点, 首先, 在命令行参数传递时, 窗口句柄 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.0 新增内容

前一次写该博客的时候, 该部分内容没有很好的完成, 效果验证部分出现了一个错误. 今天有空, 就将最近几天的一点点小工作进行了整理, 并可以作为该部分的效果验证.

通过新建MFC程序, 利用自己编写的两个类来进行xyz文件和stl文件的显示, 并显示窗口嵌入到MFC中. 源码下载地址: http://download.csdn.net/detail/sunbibei/9605194

源码中包含已经修改好的GLFW, 并且对上述两个功能进行了集成. 以及一些键盘按键响应, 鼠标左右键以及滚轮的响应等工作, 具体内容见代码.

xyz文件显示效果如下:



stl文件显示效果如下:



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源码文件夹如下图所示:




下载好CMake之后, 安装. 在开始菜单能够找到 CMake(cmake-gui)的快捷方式. 打开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勾选上, 再次点击 :



然后点击 , 会提示 Generating done. 搞定之后, 打开 E:/glfw/glfw-build后你会看到如下画面, 熟练的双击 GLFW.sln就可以使用VS打开该工程了. VS打开之后, 爽快的按下 F7. VS就开始工作了. 在 E:\\glfw\\glfw-build\\src\\Debug路径下, 会看到生成的一些文件. 都是很熟悉的东西吧. lib文件以及dll文件. VS示图如下, 并按照图中选项找到simple示例:



按照上图, 找到simple, 右击弹出下拉菜单, 依次选择 <调试> –> <启动新实例>. 会得到下图展示的一个DEMO效果. 也许你会得到一个错误, 提示在 E:/glfw/glfw-huild/src/Debug 里面看到的 glfw3.dll文件找不到. 解决办法很简单, 将该文件复制到 C:\\Windows\\System32中去, 或者将 E:/glfw/glfw-huild/src/Debug 目录加入环境变量 Path中. 第一种办法好像需要重启一次才行.


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声明, 如下:




在原本 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);

截图如下:




修改好了之后, 对代码进行编译, 还是运行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, 打完收工.

以上是关于cv::namedWindow, GLFWwindow以及其他程序嵌入到MFC中的教程的主要内容,如果未能解决你的问题,请参考以下文章

谷歌 Colab 中的 Trackbars 和 cv2.namedWindow()

Opencv '未定义的对 `cv::namedWindow....' 的引用(链接错误)

关于opencv的cv2.WINDOW_一类

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

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

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