线程间操作无效: 从不是创建控件“textBox1”的线程访问它(未处理System.InvalidOperationException)(委托)delegate,Invoke,BeginInvoke

 

目录

1.问题复现

2.分析问题

3.解决办法

4.写法拓展

5.总结与源码

//长话短说 //本文将逐步的的引导并解决此问题//大家互相交流,有不足处请指正

//源码在本文最后面

 

  1. 问题复现

新建WinForm程序 ,界面 Form1,拖个 button1 和textBox1,然后Button1中增加click事件,其中Form1.cs中部分代码如下:

private void button1_Click(object sender, EventArgs e)

{

    System.Threading.Thread Thread_1 = new System.Threading.Thread(ThreadOne);

    Thread_1.IsBackground = true//设置为后台线程,软件关闭则线程关闭

    Thread_1.Start();  //线程中若想传值可此处将值传过去,比如我想传个字符串的值,此处可 Thread_1.Start("传值")

}

private void ThreadOne() //线程中若想传值可设置为object类型,即 ThreadOne(object obj)

{

    MessageBox.Show("即将运行报错的代码!");

    textBox1.Text = " 在这个线程里改变控件的值"; //会报异常   Message=线程间操作无效: 从不是创建控件“textBox1”的线程访问它。

}

编译运行后如下图,复现此BUG:

文字提示如下:

未处理System.InvalidOperationException

  Message=线程间操作无效: 从不是创建控件“textBox1”的线程访问它。

  Source=System.Windows.Forms

  StackTrace:

       在 System.Windows.Forms.Control.get_Handle()

       在 System.Windows.Forms.Control.set_WindowText(String value)

       在 System.Windows.Forms.TextBoxBase.set_WindowText(String value)

2.分析问题

仔细看错误提示为: 线程间操作无效: 从不是创建控件“textBox1”的线程访问它。

这句话字面意思是这个控件只能被创建它的线程访问,即线程A中创建了控件A1,若线程B中想修改控件A1的属性,则不允许!(如下图)

3.解决办法

那么能否找个中间变量或者在A线程中有某种方法可以让线程B中执行的某段代码可以从线程B中跳到线程A中,然后线程A改控件A1中的信息 ,这样就是A改A中创建的控件A1了,如下图

那么这个中间变量是什么呢?此时就用到了关键字 delegate(委托),

然后我们试试 通过用这个关键字可以解决我们遇到的问题吗?同时我们也得确认下他提示的”不是从创建的线程中访问它”这句话是否正确,既然是线程,那么我们就在System.Threading.Thread下面搜,然后就我们搜到了如下信息:

可获得当前唯一标识符(ID)的代码:

int NowThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;

可获得当前工作的线程的名字(Name)的代码:

 string NowThreadName = System.Threading.Thread.CurrentThread.Name;

仔细看描述,发现唯一标识符(ID)只读,不可设,而当前工作的线程的名字(Name)可以设置的,那我们就选择Name来判断当前工作的线程是哪个。然后我们新建个WinForm程序,然后在第一个桌面程序Form1的构造函数中提前设置主线程Name为MainThread

代码如下

public Form1()

{

   InitializeComponent();

   System.Threading.Thread.CurrentThread.Name = "MainThread"; //备注:软件开启后只能设置一次

}

然后设计器中拖个button2新增个click事件,

提前设置好子线程的Name为:ThreadTwo声明一个叫SetText_delegate的委托,但是目前这个委托不知道怎么调用啊,既然不知道,那先按普通的方法那样调用,

相关代码如下

private void button2_Click(object sender, EventArgs e)

{

    MessageBox.Show("当前线程Name:" + System.Threading.Thread.CurrentThread.Name);

    System.Threading.Thread Thread_2 = new System.Threading.Thread(ThreadTwo);

    Thread_2.Name = "ThreadTwo";   //设置子线程Name

Thread_2.IsBackground = true//设置为后台线程,软件关闭则线程关闭

    Thread_2.Start();

}

private delegate void SetText_delegate(string sssext); //声明一个委托  //相当于在此线程中定义一个方法p

rivate void SetText(string str)

{

    MessageBox.Show("哈哈,跳到这里来了,当前线程Name:" + System.Threading.Thread.CurrentThread.Name);

    textBox1.Text = str + ",赋值部分!";

}

private void ThreadTwo()

{

    MessageBox.Show("正常定义委托,即将更改值!!" + "当前线程Name:" + System.Threading.Thread.CurrentThread.Name);

    SetText_delegate aInThread = new SetText_delegate(SetText);  //核心代码

aInThread("线程中直接传递参数,报错"); //报错:此处直接调用则当前工作线程还是子线程

}

以上代码运行过后发现在aInThread("线程中直接传递参数,报错"); 处还是会报同样的错误

提取部分报错内容如下:

Message=线程间操作无效: 从不是创建控件“textBox1”的线程访问它。

在 WindowsFormsApplication1.Form1.SetText(String str) 位置 d:\Projects\C#\WindowsFormsApplication1\WindowsFormsApplication1\Form1.cs:行号 91

在 WindowsFormsApplication1.Form1.ThreadTwo() 位置 d:\Projects\C#\WindowsFormsApplication1\WindowsFormsApplication1\Form1.cs:行号 100

上面报错验证了子线程中委托方法实例化后直接调用主线程中的方法是不可行的,仔细查看发现控件textBox1赋值前的MessageBox.Show中提示当前工作线程还是: ThreadTwo

直接调用走不通,那么是否是委托不行或者是我们不会用这个委托?

这时我们查到了关键字Invoke/BeginInvoke那么这个关键字的作用是什么?怎么用?

按F12或则在Invoke处右键选择转到定义,然后我们发现了下图信息,

关键字:

英文截图如下:

OK,这里写出了 这个Invoke使用的前提如下

  1. 在拥有控件的基础窗口句柄的线程上,(Executes the specified delegate,
  2. 用指定的参数列表执行指定委托(on the thread that owns the control's underlying window handle
  3. Invoke运行需要两个参数,一个是Delegate类型的,另一个是Object类型,即普通参数

可是我们如何用如何用Invoke呢,突然想到线程ThreadTwo也是处于MainThread中(即线程的Name,前文有写),

然后我们将线程中ThreadTwo方法内的aInThread方法注释掉,然后可改为如下:

private void ThreadTwo()

{

   MessageBox.Show("正常定义委托,即将更改值!!" + "当前线程Name:" + System.Threading.Thread.CurrentThread.Name);

   SetText_delegate aInThread = new SetText_delegate(SetText);  //核心代码

   MessageBox.Show("正常定义委托,即将更改值!!当前线程Name:" + System.Threading.Thread.CurrentThread.Name); ;

  //aInThread("线程中直接传递参数,报错"); //报错:此处直接调用则当前工作线程还是子线程

   this.Invoke(aInThread, "第一种写法,我要赋值了呦!");  //核心代码

   MessageBox.Show("正常定义委托,当前线程Name:" + System.Threading.Thread.CurrentThread.Name);

}

然后编译运行,成功赋值!成功将值赋给了textBox1

那么下图应改为如下:

准确来说应该如下图

通过以上我们知道了Invoke和委托二者缺一不可,只有运行到Invoke才会触发方法SetText给控件textBox1赋值,那么软件运行的顺序是什么呢?每运行一步我都加个MessageBox来查看软件的运行顺序,实际弹窗的顺序如下:

从上图中标记处,我们就很容易得出委托实例化时没有运行到方法内部,只有运行到Invoke时才会运行到方法SetText,进而改变textBox1属性,

或者看着麻烦也可以改成这样儿

即线程ThreadTwo中调用方法Test,方法Test仍然在子线程ThreadTwo中,而只有Invoke调用的委托函数SetTest在MainThread中,

 

那么现在我们定义了两个方法,方法SetText和方法Test,两个方法可否合二为一?

我们可以试着判断,若是仍然在线程ThreadTwo中则继续运行目前方法Test中的内容,若当前运行的线程为MainThread就对textBox1进行赋值,

主线程赋值代码:System.Threading.Thread.CurrentThread.Name = "MainThread"; //备注:软件开启后只能设置一次

那么方法可改为  

private void button2_Click(object sender, EventArgs e)

{

    MessageBox.Show("当前线程Name:" + System.Threading.Thread.CurrentThread.Name, "1");

    System.Threading.Thread Thread_2 = new System.Threading.Thread(ThreadTwo);

    Thread_2.Name = "ThreadTwo";   //设置子线程Name

    Thread_2.IsBackground = true//设置为后台线程,软件关闭则线程关闭

    Thread_2.Start();

}

private void ThreadTwo()

{

    NewTest("我要赋值了呦!");

}

private delegate void SetText_delegate(string sssext); //声明一个委托  //相当于在此线程中定义一个方法

private void NewTest(string str)

{

    if (!System.Threading.Thread.CurrentThread.Name.Equals("MainThread"))

    {

        SetText_delegate aInThread = new SetText_delegate(SetText);  //核心代码

        this.Invoke(aInThread, str + ",调用Invoke调用");  //核心代码

    }

    else

    {

        textBox1.Text = str + ",赋值了!";

    }

}

然后运行结果如下

那么如果我不会设置主线程的Name呢?

有方法:麻烦将

if (!System.Threading.Thread.CurrentThread.Name.Equals("MainThread"))

替换成

if (this.InvokeRequired)

试试看,哇塞,居然也可以,而且貌似替换后就变成了网上主流的方式。

4.多种写法

4.1网络正常写法及变种

网上主流的方式基本上是如下:

private void button2_Click(object sender, EventArgs e)

{

    System.Threading.Thread Thread_2 = new System.Threading.Thread(ThreadTwo);

    Thread_2.IsBackground = true//设置为后台线程,软件关闭则线程关闭

    Thread_2.Start();

}

private delegate void SetText_delegate(string sssext); //声明一个委托  //相当于在此线程中定义一个方法

private void ThreadTwo()

{

    NewTest("我要赋值了呦!");

}

private void NewTest(string str)

{

    //if (!System.Threading.Thread.CurrentThread.Name.Equals("MainThread"))

    if (this.InvokeRequired)

    {

        SetText_delegate aInThread = new SetText_delegate(SetText);  //核心代码

        this.Invoke(aInThread, str + ",调用Invoke调用");  //核心代码

    }

    else

    {

        textBox1.Text = str + ",赋值了!";

    }

}

那么有没有些变种写法呢?

有在Invoke/BeginInvoke处按F12或则在Invoke处右键选择转到定义,然后我们发现了下图信息,

    //public object Invoke(Delegate method);

    //public object Invoke(Delegate method, params object[] args);

    //public IAsyncResult BeginInvoke(Delegate method);

    //public IAsyncResult BeginInvoke(Delegate method, params object[] args);

意思就是无论什么方法,只要第一个参数是Delegate就行,

如果我们把网上的方法当作第0种写法,我前面写的当作第一种写法,其他的依次往后排:

private void ThreadTwo()

{

    //第0种写法

    NewTest("我要赋值了呦!");

 

    第一种写法

    SetText_delegate aInThread = new SetText_delegate(SetText_NoReturn);  //核心代码

    this.Invoke(aInThread, "第一种写法,我要赋值了呦!");  //核心代码

 

    //第二种写法

    SetText_delegate SetText_d = SetText_NoReturn;

    this.Invoke(SetText_d, new object[] { "第二种写法,我还要赋值了呦!" }); //变种方式如上

 

    //第三种写法

    this.Invoke(new SetText_delegate(SetText_NoReturn), "第三种写法,我又要赋值了呦!"); //变种方式如上

 

    //第四种写法        

    this.Invoke((SetText_delegate)SetText_NoReturn, "第四种写法,我再次要赋值了呦!"); //变种方式如上

 

    //第五种写法

    Action<string> ffaff = new Action<string>(SetText_NoReturn);

    this.Invoke(ffaff, "5555555555");

 

    //第六种写法

    Action<string> fffaff = SetText_NoReturn;

    this.Invoke(fffaff, "666666666666");

 

    //第七种写法

    this.Invoke(new Action<string>(SetText), "7777777777");

 

    //第八种写法   //Func是.NET里面的内置委托,它有很多重载。//第一个值是参数,第二个值是返回值

    Func<string, string> ssfada = new Func<string, string>(SetText_ReturnStr);

    this.Invoke(ssfada, "8888888888");

 

    //第九种写法

    Func<string, string> ssfaa = SetText_ReturnStr;

    this.Invoke(ssfaa, "999999999999");

 

    //第十种写法

    this.Invoke(new Func<string, string>(SetText_ReturnStr), "无论何种写法,Invoke第一个参数必须为Delegate类型");

 

    //以上this.textBox1.Invoke 可改为 this.textBox1.BeginInvoke 或者 this.Invoke 或者 this.BeginInvoke 功能相同

}

private delegate void SetText_delegate(string sssext); //声明一个委托  //相当于在此线程中定义一个方法

private void SetText_NoReturn(string str)

{

    textBox1.Text = str;

    MessageBox.Show(str);

}

private string SetText_ReturnStr(string str)

{

    textBox1.Text = str;

    MessageBox.Show(str);

    return str; //随便返回个字符串就行

}

那么有没有直接在线程里面直接可以操作的?

4.2 BeginInvoke的作用

相关代码如下,其中SetText_delegate本文前面定义过

private void button3_Click(object sender, EventArgs e)

{

    MessageBox.Show("当前线程Name:" + System.Threading.Thread.CurrentThread.Name, "1");

    System.Threading.Thread Thread_3 = new System.Threading.Thread(ThreadThree);

    Thread_3.Name = "ThreadThree";

    Thread_3.IsBackground = true;

    Thread_3.Start();

}

private void ThreadThree()

{                  //System.Threading.Thread.CurrentThread.Name 值为 ThreadThree

    MessageBox.Show("进入线程中,当前线程Name:" + System.Threading.Thread.CurrentThread.Name, "2");

    string strargs = "我要赋 值了呦! " + button3.Text;

    SetText_delegate setText = delegate(string str)

    {                                                        //MainThread  //这就说明delegate只是起到了函数指针的作用

        MessageBox.Show("即将改值,当前线程Name:" + System.Threading.Thread.CurrentThread.Name, "4");

        this.textBox1.Text = str;

    };

    MessageBox.Show("线程运行中,线程:" + System.Threading.Thread.CurrentThread.Name, "3");

    this.BeginInvoke(setText, new object[] { "赋值" });

    MessageBox.Show("线程运行中,线程:" + System.Threading.Thread.CurrentThread.Name, "5");

}

 

BeginInvoke运行结果如下:

 

4.3 超简写

那么有没有可以用最小行数的代码来实现此功能呢?

答:有,代码如下(本文最后有源码下载链接)

#region 委托(超简写)

private delegate void delegate_ParameterOne(string s);  //带一个参数的

private delegate void delegate_ParameterNo();         //不带参数的

private void button4_Click(object sender, EventArgs e)

{

    System.Threading.Thread Thread_4 = new System.Threading.Thread(ThreadFour);

    Thread_4.Name = "ThreadFour";

    Thread_4.IsBackground = true;

    Thread_4.Start();

}

private void ThreadFour()

{

    //我们都知道Invoke有两种用法,那么什么时候用一个参数,什么时候用两个参数呢?            //请往下看

    //public object Invoke(Delegate method);

    //public object Invoke(Delegate method, params object[] args);

    MessageBox.Show("即将开始改变textBox1的值");

    int n = 0; string strMessage = "";

    //以下方法中,通过Invoke的第二个参数来传值    strMessage 等同于  new object[] { strMessage }

    n = 1; strMessage = string.Format("这是第 {0} 种写法", n);

    delegate_ParameterOne setText = delegate(string str) { this.textBox1.Text = str; };

    this.Invoke(setText, strMessage);

    MessageBox.Show(strMessage);

 

    n = 2; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke((delegate_ParameterOne)(delegate(string str) { this.textBox1.Text = str; }), strMessage);

    MessageBox.Show(strMessage);

 

    n = 3; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke((delegate_ParameterOne)((string str) => { this.textBox1.Text = str; }), strMessage);

    MessageBox.Show(strMessage);

 

    n = 4; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke(new delegate_ParameterOne(delegate(string str) { this.textBox1.Text = str; }), strMessage);

    MessageBox.Show(strMessage);

 

    n = 5; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke(new delegate_ParameterOne((string str) => { this.textBox1.Text = str; }), strMessage);

    MessageBox.Show(strMessage);

 

    //以下是不带参数而传值的写法

    n = 6; strMessage = string.Format("这是第 {0} 种写法", n);

    delegate_ParameterNo setT = delegate() { this.textBox1.Text = strMessage; };

    this.Invoke(setT);

    MessageBox.Show(strMessage);

 

    n = 7; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke((delegate_ParameterNo)(delegate() { this.textBox1.Text = strMessage; }));

    MessageBox.Show(strMessage);

 

    n = 8; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke((delegate_ParameterNo)(() => { this.textBox1.Text = strMessage; }));

    MessageBox.Show(strMessage);

 

    n = 9; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke(new delegate_ParameterNo(delegate() { this.textBox1.Text = strMessage; }));

    MessageBox.Show(strMessage);

 

    n = 10; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke(new delegate_ParameterNo(() => { this.textBox1.Text = strMessage; }));

    MessageBox.Show(strMessage);

 

    n = 11; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke(new delegate_ParameterOne((string str) => { this.textBox1.Text = strMessage; }), new object[] { null });

    //注意:第二个参数直接接写null会报错

    MessageBox.Show(strMessage);

 

    //通过以上十一个方法我们总结出了如何用哪种Invoke中取决于 定义delegate类型的方法时候带不带参数

    //如不带,即像定义delegate_ParameterNo时需要用一个参数的Invoke,即

    //public object Invoke(Delegate method);

    //如带参数,即像定义delegate_ParameterOne时需要用两个个参数的Invoke,即

    //public object Invoke(Delegate method, params object[] args);

 

    n = 12; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke((Func<string, string>)delegate(string str) { textBox1.Text = str; return ""; }, strMessage);

    MessageBox.Show(strMessage);  //Func中第二个参数为返回值类,可改

 

    n = 13; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke((Func<string, string>)delegate { textBox1.Text = strMessage + "," + strMessage; return ""; }, "此变量无作用");

    MessageBox.Show(strMessage);  //Func中第二个参数为返回值类,可改

 

    n = 14; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke((Func<string, string>)delegate { textBox1.Text = strMessage; return ""; }, "");

    MessageBox.Show(strMessage);

 

    n = 15; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke(new Func<string>(() => { textBox1.Text = strMessage; return ""; }));

    MessageBox.Show(strMessage);

 

    n = 16; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke(new MethodInvoker(delegate() { textBox1.Text = strMessage; }));

    MessageBox.Show(strMessage);

 

    n = 17; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke(new Action(delegate { textBox1.Text = strMessage; }));

    MessageBox.Show(strMessage);

 

    n = 18; strMessage = string.Format("这是第 {0} 种写法", n);//lambda表达式

    this.Invoke(new Action(() => { textBox1.Text = strMessage; }));

    MessageBox.Show(strMessage);

 

    n = 19; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke(new EventHandler(delegate { textBox1.Text = strMessage; }));

    MessageBox.Show(strMessage);

 

    n = 20; strMessage = string.Format("这是第 {0} 种写法", n);

    this.Invoke((EventHandler)delegate { textBox1.Text = strMessage; });

    MessageBox.Show(strMessage);

 

    MessageBox.Show("总结,写法再多也是为了实现功能,记不住就记网上最经典的方法好了,一法通万法通!", "总结");

}

#endregion

 

5.总结与源码

5.1总结

对于本文题目的问题,个人认为我们应该主要了解及怎么使用Invoke/BeginVoke,其次才是delegate的使用。

总结本博文前半部分主要写了delegate的用法,及如何使用Invoke,什么情况下使用Invoke,怎么使用Invoke。

本文中间写了InvokeRequired的作用,

后半部分写了Beginvoke的作用,捎带提了Action的用法,Func的用法,EventHandler的用法。

其实还是有种方法关闭线程安全检查,即

System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false; //关闭安全检查

极其不建议如此做,原因关闭安全检查是就相当于 消防检查出问题了,但是你每次都忽略。

5.2源码:

源代码为Visual Studio 2012环境下编译的

源代码链接:

https://download.csdn.net/download/shengmingzaiyuxuexi/12274875(推荐)

https://download.csdn.net/download/shengmingzaiyuxuexi/12264023(前一版本,仅供借鉴)

若没积分,请留言或私信。

 

 

本文参考链接:https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2008/ms171728(v=vs.90)?redirectedfrom=MSDN

 

 

有错误,请指正

有疑问,请留言

与君共勉,共同进步!

  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值