有一个主线程,我Controls在 winforms 中创建所有内容。还有第二个线程,通过事件的触发调用。由于此事件,我需要更改DataSourcey的值DataGridView。因此,会发生错误,即创建控件的主线程未尝试访问该控件。解决方案是使用方法Invoke\BeginInvoke。但是我不明白这些方法的本质以及如何在代码中实现它们。创建的第二个线程使用访问函数来dgv
private void RefreshTables()
{
try
{
if(con.State == ConnectionState.Closed)
{
con.Open();
}
sql = "select rowid, * from OpenPos";
adapOpenPos = new SQLiteDataAdapter(sql, con);
dsOpenPos = new DataSet();
adapOpenPos.Fill(dsOpenPos);
dataGridView1.DataSource = dsOpenPos.Tables[0];
dataGridView1.Columns[0].Visible = false;
dataGridView1.Columns[15].Visible = false;
dataGridView1.Update();
con.Close();
if (con.State == ConnectionState.Closed)
{
con.Open();
}
sql = "select rowid, * from ClosePos";
adapClosePos = new SQLiteDataAdapter(sql, con);
dsClosePos = new DataSet();
adapClosePos.Fill(dsClosePos);
dataGridView2.DataSource = dsClosePos.Tables[0];
dataGridView2.Columns[0].Visible = false;
dataGridView2.Columns[14].Visible = false;
dataGridView2.Update();
con.Close();
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}
}
该方法的本质
Invoke非常简单——它接受一个委托并在创建控件的线程中执行它,在该线程中Invoke。您可能已经注意到,如果从创建它们的线程以外的线程访问WinForms控件,则会抛出异常。因此,该方法Invoke在需要使用来自其他线程的控件的情况下很有用。该方法BeginInvoke执行相同的操作,但是是异步的。一个小的用法示例
Invoke:还值得注意的是
async/await,在 C# 5 中添加的 , 可以绕过Invoke:在窗口化的 Windows 应用程序中,有一个所谓的用户界面线程(UI thread),实际上是应用程序的主线程,最先创建。
GetMessage它通过循环调用or函数来处理窗口消息(Windows messages)PeekMessage。如果您想对用户界面做一些事情,比如更改输入字段中的字符串,您可以向它发送一个窗口消息。在 WinForms 中,更改输入字段中的文本是通过赋值完成的:
但在较低级别,此代码会导致将窗口消息发送到使用 .handle的
WM_SETTEXT窗口。textbox1.HandleSendMessage微妙之处在于,通过发送消息,您可能希望收到结果。这对 来说并不明显
WM_SETTEXT,但对WM_GETTEXT您来说您肯定想知道答案。这意味着如果调用
SendMessage,则必须等待函数完成。SendMessage如果您在只有一个线程时调用,这不是什么大问题,因为它会SendMessage调用处理程序,该处理程序通常会相当快地处理窗口消息。但是如果你
SendMessage从另一个线程调用呢?与流行的看法相反,这不会导致错误。您可以这样做,但调用线程将停止直到SendMessage它完成。如果程序中运行了多个线程,它们想要在用户界面上改变一些东西,它们将被挂起,每个SendMessage线程将依次由主线程执行。在现代窗口程序中,您几乎肯定不会直接使用线程,而是使用更高级别的抽象:异步回调。在 .NET 中,您可以使用一个名为 TPL 的便捷包装器,它为您提供
Task和类Task<TResult>,这样您就不必处理异步函数。异步函数在线程之上工作。为了让它们工作,.NET 在池中创建了多个线程,每个线程都挂在一个无限循环中并等待来自您的应用程序的信号。信号一出现,池中的某个线程就会执行您的回调函数并再次进入睡眠状态。
这一切都很好,但通常这些小函数想要对您的用户界面做些事情。例如,您正在处理一个大文件并要求 Windows 在读取下一个 64Kb 时调用您的函数。该块被读取,Windows 唤醒线程并将您的函数传递给它执行。该函数做了有用的工作,然后想要增加进度条。它调用
SendMessage可能与其他异步函数冲突。而如果异步函数开始互相等待,相应的线程就忙了,管理线程池的程序就不得不创建新的线程。
这是一个问题,因为 Windows 会阻止
SendMessage来自其他线程的异步调用。问题出现了如何解决这个问题。Windows 开发人员建议将异步函数一分为二。第一个做有用的工作,然后告诉线程池:调用第二个异步函数,但在主线程上执行。第二个函数更新进度条,由于线程池在主线程调用它,所以不会发生冲突。
是的,界面更新仍然是顺序发生的,但所有其他异步功能完全不会相互干扰。
为了在 UI 线程上执行函数,WinForms 开发人员提供了
Invoke和BeginInvoke.例如,您可以像这样使用它们:
在这里,参数
Invoke是一个匿名委托,它实际上是一个将从 UI 线程调用的函数。有几种方法可以简化此代码(在 Stack Overflow 上搜索)。我会注意到主要思想:代码分别位于表单的一个方法中,
InvokeRequired这Invoke是表单的一个属性和一个方法。Invoke如果属性InvokeRequired为真,您只需要通过调用属性更改。在调用之前,最好准备好您需要使用的所有数据,这样您就不会减慢 UI 线程。首先准备好数据,然后通过Invoke/里面的控件的属性来赋值BeginInvoke——这是桌面应用程序异步编程的主要模式。除了其他答案之外,我会尽量回答尽可能容易理解。
通过创建一个新线程,您向它传递了一些必须在这个新线程中执行的代码。例如,通过 Task 创建时,它看起来像这样。
另一方面,Invoke 方法用作进入主线程的窗口,否则它的声明几乎完全相同。
更改控件的属性和调用方法必须在主线程中进行,并包装在 Invoke 中。
我有时会使用另一种方式,它涉及主要操作之后的变化。它可能有一天会派上用场,我用它来锁定/解锁操作期间的界面。