C# WPF调用QT窗口的方法

WPF 程序内嵌 QT 窗体

1、目标:将QT控件(Qwiget)(或则基于QWiget的控件)(或则任何第三方C++控件)封装为WPF可调用的用户控件。简单来说就是WPF程序调用QT窗体控件。

本人需要使用3D控件显示一些3D点云等功能。但是又找不到好的兼容WPF的控件(大点云和效率原因)。最后选用CloudConpare作为点云显示控件(该控件基于QT的QWiget)。
目前网络上这方面的内容并不是太多,而且多数还都是雷同。
个人感觉 C# 跨平台并不是太友好(或则我水平不够),特别是3D方面。

本人对QT不是很熟悉,但是CloudCompare是基于QT写的界面,所以没办法只能慢慢摸索,好在以前有MFC编程经验,对C++还有点积累,过程中关于配置方面的问题我尽量详细说明(对于C#编程者来说还是挺恶心的,QT的配置和CloudCompare的配置挺麻烦的)。

实现过程如下(可能需要稍微有点基础才能看懂,我认为有意思的会写多点 ,写的不是太系统),可能有点乱,有点啰嗦,想到哪里写到哪,见谅。

心路:
一开始查询资料想通过中间库 CLR链接C#和C++,简单demo也测试OK,后期碰到控件句柄传递问题调试也比较麻烦,遂放弃(现在想来应该也是可以实现的)。
最终直接使用 C形式函数导出 的形式来创建DLL,也挺方便的。专业点叫P/Invoke(推荐大家一本书《精通.NET互操作:P/Invoke,C++Interop和COM Interop》)。

大概逻辑:WPF控件句柄–>传递给C++生成的QT库–>动态库根据传递过来的句柄创建一个QWiget–>根据这个QWiget生成一个GLWindow(真实的3D显示窗体)–>C#保存这个指针方便下次访问。
数据传逻辑:主要是C#这边发给C++ Dll数据,主要涉及到托管和非托管资源的问题、字节对齐等。

实现过程中可能有几个主要问题:
1、C#和C++混合编程;
2、QT程序(QT事件循环QApplication.exec())如何封装为Dll;
3、QT的UI线程和WPF的UI线程冲突问题(目前本人也没完全弄懂,欢迎沟通);

工具:VS2019(Qt Visual Studio Tool 2.7.0)、QT5.14.2、CloudCompare2.6.12源码。。。。

上代码:

1、创建一个VS-QT的动态库。

C# WPF调用QT窗口的方法_第1张图片

选择Qtwidgets application,然后修改配置生成为dll。网上资料很多,不详细描述(大概流程:修改输出类型为dll,将没必要的文件删除即可(Main)(所有文件都可以删除,然后新增一个QtWidgets class也行))。如下:

C# WPF调用QT窗口的方法_第2张图片

我们这里界面就不需要设置(因为我们不是直接用QT界面),只需要一个空白的Qwidget。(同志们只需要用Qwiget就可以在这里绘制了)
右键 编译这个.ui文件。会生成一个ui_xxxx.h文件。添加这个文件到项目中,(可能在生成目录的uic文件夹下,如有必要还需要将这个文件路径加到包含目录中。若是不添加可能会生成失败)。
首次生成失败很正常,各种QT环境问题。不要心急。

C# WPF调用QT窗口的方法_第3张图片

这里我们就获得了一个带QT的界面的C++库。

2、WPF中引用它:

若是你们用的是Winform,那就比较方便了,直接拿到控件句柄(我这里使用的panel控件)传递给动态库就行了。
若是你用的是WPF,会麻烦一点(因为WPF控件获取不到“控件”句柄),这里在下试了很多,获得的都是窗体句柄。所以只能使用WindowsFormsHost。上代码

C# WPF调用QT窗口的方法_第4张图片

这里是本人在WPF创建了一个用户控件的xaml文件。上代码

public partial class Window3DControl : UserControl
    {
        public Window3DControl()
        {
            InitializeComponent();
        }

        /// 
        /// 3D显示 窗口
        /// 
        public Window3DControl_Net Control3D = new Window3DControl_Net();


        /// 
        /// 创建真实的3D窗体,GlWindow
        /// 
        /// 
        public bool Create3DWindow()
        {
            int res = Control3D.Creat3DWindow_Net((long)Panel3D.Handle, Panel3D.Width, Panel3D.Height);

            return res == 0;
        }

        /// 
        /// 控件发生变化
        /// 
        /// 
        /// 
        private void Window3DControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if(Control3D.IsInitWindow)
            {
                int width = (int)(WindowForm3D.ActualWidth * 1.25);
                int Heigth = (int)(WindowForm3D.ActualHeight * 1.25);
                int res = Control3D.WindowSizeChanged_Net(width, Heigth);
            }
        }

        /// 
        /// 3D窗体切换
        /// 
        public void ChangeWindowModel()
        {
            if(WindowForm3D.Visibility == Visibility.Visible)
            {
                WindowForm3D.Visibility = Visibility.Collapsed;
                Button2D.Visibility = Visibility.Visible;
            }
            else
            {
                WindowForm3D.Visibility = Visibility.Visible;
                Button2D.Visibility = Visibility.Collapsed;
            }
        }

    }

上面是用户控件的代码部分。也很简单只有几个简单功能。其中Control3D对象是我封装的一个调用动态库的接口类(只是封装了一层),功能上直接调用C形式的函数接口也是可以的。上代码

C# WPF调用QT窗口的方法_第5张图片

这里是封装的3D对象的部分接口,C#这边就很简单了,都封装成用户控件了,那不是想怎么玩怎么玩。(关于C#定义函数接口就不描述了,C++那边导出函数接口也不描述了)。上代码

C# WPF调用QT窗口的方法_第6张图片

还有一点很重要(我感觉)如何显示QT窗口正好覆盖WPF中句柄对应的控件(可能会碰到直接显示QT窗口在主界面的左上角)。有两步很重要,通过如上图,可以看到我们将控件句柄和对应的尺寸传递过去,并且将生成的QT窗口的实例对应的指针传回来,为了下次能够直接拿到这个QT窗口。可以看下我的创建窗口函数是如何实现的,上代码

C# WPF调用QT窗口的方法_第7张图片

图中标记部分,很重要 。(忘记从哪个大神哪里偷学来的)
另外为了C# 那边能拿到C++这边的对象指针。使用了二级指针概念(这个不太好解释,要自己细细理解,这个不是偷学的,在下耗尽脑子想的)。
还有一个问题QT事件循环,(若是你调用过QT的dll,可能就会发现这个问题),我这边解决办法就是,在程序开始(你认为的合适的地方)调用InitQTEnvironment接口,上代码。

#include "qmfcapp.h"
int InitQtEnvironment()
{
	try
	{
		//if (QApplication::instance() == NULL)
		//{
		//	int argc = 0;
		//	g_app = new QApplication(argc, NULL);
		//	//QWidget* GroudWidget = new QWidget(NULL);
		//	//GroudWidget->setGeometry(0, 0,0, 1);//设置widget的大小
		//	//GroudWidget->showMinimized();
		//	//g_app->exec();
		//}
		if (QApplication::instance() == NULL)
		{
			return  QMfcApp::pluginInstance();
		}
		return 0;
	}
	catch(QException ex )
	{
		printf("Init Error");

		printf(ex.what());
		return -1;
	}
	
}

两种方式都行,目的是创建出来QApplication这个静态对象即可。(其中QMfcApp是偷学某个大佬)。
OK,其实到这里整个流程已经基本打通了,路走通了,其余的都好办了。
关于数据传递问题,这里也说一下。C#和C++的基础数据类型在内存中占位不尽相同,有兴趣可以看下对照表(网上一大堆)。
若是使用CLR包装C++dll,可以解决类型不一致问题(个人感觉CLR就是一次形参类型转换而已,可能是我太浅薄了)。
若是我们这种直接调用C形式的函数接口。需要传递参数需要注意几点。
1、基础类型也需要注意内存占位是否一致。
2、结构体类型,需要考虑字节对齐和托管资源问题。思路:两边定义相同类型的结构体。直接传递结构体指针(首地址)本质上还是内存要一样,不能乱。先上代码(C++)

C# WPF调用QT窗口的方法_第8张图片

C#中数据结构

C# WPF调用QT窗口的方法_第9张图片

仔细对比(字节对齐,还要考虑编译器可能优化)。
数组必须确定传递长度(内存分配才能连续),并且C#这边申请的内存必须是非托管内存(使用Marsh申请)。关于C++传递数据到C#,上文也提到了二级指针,基础类型或确定内存长度的可以直接传递,不确定的对象使用二级指针。关于内存释放问题。大家可以网上找下,有几种常用方法,此处不赘述。

按照惯例,上效果图。

C# WPF调用QT窗口的方法_第10张图片

遗留问题:
玩过WPF的都知道,WPF有一个隐藏的UI线程,我们的属性绑定控件和刷线界面都是
这个线程来更新的。QT也有一个类似的界面线程。因为我这边主程序是WPF,QWiget只是作为一个控件来用的。
目前发现他们两个界面线程会出现抢资源情况(我也是猜的,因为出现WPF的源已经改变了但是目标控件上没有刷新(及时刷新)的现象)。各位大佬若有这方面的见解,请一定联系我。抱拳!!!

到此这篇关于C# WPF调用QT窗口的方法的文章就介绍到这了,更多相关C# WPF调用QT窗口内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(C# WPF调用QT窗口的方法)