MDI窗口嵌入,我爱惨你了

简介: 小五在WinForm项目中遇到多线程问题。起初误以为MDI子窗体与主窗体异步,但实际上它们共享同一线程。在尝试解决耗时操作影响关键方法执行时,小五先错误地使用MDI子窗体,后通过`Task.Factory.StartNew`创建新线程。然而,更新界面时遇到跨线程操作错误。最终,小五学会使用`Invoke`处理UI线程同步,但发现`Invoke`使线程回到主线程,导致问题复发。总结:MDI子窗体与主窗体同线程,需谨慎处理UI线程同步。
前言:

这是我在实际中发生的一件事,想告诉大家,如果不知道它的一些基本概念,一定要先测试一下自己想要的功能再去放到正式代码中用

切记切记

背景:

工具及程序:VS2022,WinForm程序,.net4.8

人物:小五(C#小白),领导(多年C#开发经验,但是很忙,此软件由领导开发完成初步功能)

正文:

某天上班的时候,小五发现了一个问题,于是......

小五:领导,我发现在这个TabControl下面有几页存在一些耗时操作,这些耗时操作会影响我们最重要的那个方法(此方法是被封装成dll文件的,轻易不改动,下面统称ImportentVoid)的执行啊,给你看看

(一波演示过后💻)

领导(大惊失色😲):那这个不行啊,这个必须要改,不然肯定会出问题的!

小五(抬头问道🤔):要不把这些耗时的放进一个新的线程里面?

领导(沉思😔):可以,之前这些代码(跳转到另外的代码了)用的是MDI窗口嵌入的方式,你可以试试这个,好像也是一个新的线程

小五:好的👌......

于是小五就去测试了,并没有去打印线程ID,而是直接放了一个for(int i=0;i<100000000;i++)去模拟耗时操作,主窗体放ImportentVoid,子窗体放耗时操作,结果小五发现ImportentVoid直接就执行完了,看起来并没有影响,然后就得出结论:MDI窗口嵌入的方式,主窗体和子窗体是两个不同的线程(敲黑板!他们是同一个线程,这里要考)

小五(兴致冲冲😄):领导(繁忙中),我刚刚试了一下,MDI嵌入的方式,主窗体和子窗体是不同的线程

领导(点头道👍):好,那你就好那些tabpage页面迁到子窗体中,然后再测试看看

小五:好的

于是小五就开始了漫长的迁移工作

MDI方法使用:

//在主窗体的里的方法写
this.IsMdiContainer = true;
Form frm = new ChildForm();
foreach (Form childForm in MdiChildren)
{
    childForm.Close();
}
frm.MdiParent = this;
frm.Show();
frm.WindowState = FormWindowState.Maximized;

迁移完成之后,小五又用for(int i=0;i<100000000;i++)测试了一下,发现没问题,于是和领导报告之后就放到生产环境了

过了一段时间后,小五在执行耗时操作的时候,发现ImportentVoid方法并没有被执行,然后小李突然奇想,用while(true){}模拟耗时操作,哦豁,完蛋了😨,方法没有被执行,于是小五就去网上找到如何打印当前线程,然后找到了打印当前线程的方法:

Console.WriteLine("当前线程ID: " + Thread.CurrentThread.ManagedThreadId);

结果发现主窗体和子窗体的线程ID是同一个,于是小五赶紧去和领导报告

小五😑:领导,我刚刚又测试了一下,发现MDI嵌入的方式,主窗体和子窗体是一个线程

领导😶:一个线程啊,我看看

(一顿演示💻)

领导😕:那这样吧,你把那些耗时操作放到一个新线程里面,不过要抓紧了

小五(连忙点头😦):好的

于是小五又去借鉴怎么开新线程了,最后决定使用Task.Factory.StartNew(() =>方法()),然后小五一顿操作,将第一个耗时操作(没有涉及到绑定到界面控件)放入新线程后,打印主窗体线程和新线程ID,发现他们是不一样的,于是大喜,又测试使用while(true){}模拟耗时操作,发现ImportentVoid能够被正常执行,然后就去迁移其他的耗时操作了,然后发现如果涉及到了绑定界面控件的耗时操作时就会报错“从不是创建此控件的线程操作它”,然后小五直接打开浏览器找寻方法

最后发现解决方法是这样的:

delegate void VoidInvoke(Dictionary<string, DataTable> _dataList);
//实际上调用的方法为RunImportentVoid
private void RunVoid(Dictionary<string, DataTable> _dataList)
{
   
   VoidInvoke iv = new VoidInvoke(Void);
   if (this.InvokeRequired)
   {
   
       Object[] parametors = new Object[] {
    _dataList };
        this.Invoke(iv, parametors);
   }
    else
    {
   
       Void(_dataList);
    }
}
private void Void(Dictionary<string, DataTable> _dataList)
{
   
    //耗时
}

于是小五继续更改,发现没有报错了,心里直呼终于好了,然后小五又把所有的耗时操作改了一遍,但是小五没有测试了,因为小五已经下意识的认为他们不是同一个线程了,改完后,小五发现,如果当一个耗时没有执行完的时候,用户打开了另外一个子窗体,就会报错,然后小五就想把在耗时操作执行之前让主窗体和子窗体的界面不可以点击,想到把Enabled属性改一下不就好了😁:

this.Enabled=false
this.Mdiparent.Enabled=false

想好之后,小五使用了ContinueWith来接受当前的耗时操作完成信号,最后的方法是

Task.Factory.StartNew(() => RunVoid(dataList))
    .ContinueWith(o=>OnTaskCompleted(o));

在测试的时候,发现同样报错“从不是创建此控件的线程操作它”,于是继续和之前一样改造,改造完成之后,先进行正常测试,没有问题,于是来到了模拟耗时操作while(true){},哦豁,又完蛋了,ImportentVoid方法没有被执行,小五头大了😱,这哪里出了问题啊,于是小五在每一个方法里面都加了一个打印当前线程ID

最后发现是上面解决方案(“从不是创建此控件的线程操作它”)中的this.Invoke(iv, parametors);出了问题,在执行这一步之后,子窗体中耗时方法的线程ID和主窗体的线程ID是一样的,小五直接原地炸裂🤡,于是小五苦笑😦找到领导,给他说明了情况,然后领导说,那只能改底层了......

总结:

1:MDI嵌入的方式主窗体和子窗体是同一个线程

2:Task.Factory.StartNew(() =>方法())是可以开一个新的线程并启动的

3:this.Invoke(); 会把线程强制拉到主线程中来

4:不清楚一个方法时,想要用它之前先测试!!!

相关文章
|
C++ 计算机视觉 Python
Qt+C++堆叠多窗口界面切换
这篇博客针对<<Qt+C++堆叠多窗口界面切换>>编写代码,代码整洁,规则,易读。 学习与应用推荐首选。
136 0
|
API
机房收费系统之【只允许一个MDI窗体 错误:426】
机房收费系统之【只允许一个MDI窗体 错误:426】
77 0
|
计算机视觉
一个窗口显示多个画面【附代码】
在有些项目中需要在一个窗口画面中显示多个子画面【这里说的不是plt.subplot()】,比如像下面这种,可以将狗头在画面的右下角进行显示。比如你是做目标检测或者跟踪等,你现在想要将检测后的目标在画面右下角显示或要进一步处理,那么这篇文章可以帮到你
154 0
一个窗口显示多个画面【附代码】
|
C#
[WPF疑难] 模式窗口被隐藏后重新显示时变成了非模式窗口
原文:[WPF疑难] 模式窗口被隐藏后重新显示时变成了非模式窗口                            [WPF疑难] 模式窗口被隐藏后重新显示时变成了非模式窗口                                              周银辉 现象: 大家可以...
1237 0