一、暴露问题——窗体假死
新建一个Windown窗体应用程序,在窗体代码里写入如下代码:
Public Class MainForm
Dim intTime As Integer = 0
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click, Button2.Click
'运行计时器
intTime = 0
Me.Timer1.Enabled = True
'******************************
'模拟长时间工作代码
Me.Label1.Text = "开始工作!"
For i As Integer = 0 To 100
'模拟长时间工作
Threading.Thread.Sleep(100)
'显示工作进度
Me.Label1.Text = i.ToString & "%"
Next
'******************************
'关闭计时器
Me.Timer1.Enabled = False
End Sub
Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.Timer1.Interval = 1000 '设置引发Timer1.Tick事件的时间间隔
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
intTime += Me.Timer1.Interval
'实时显示运行时间
Me.Label2.Text = String.Format("{0}秒", intTime / 1000)
End Sub
End Class
因为需长时间运行代码,所以想整个显示工作进度的界面,2种实现方式:百分比方式适用于循环等知道总工作量的代码、计时显示适用于无法事先计算总工作量的代码,可是测试时却发现事与愿为,代码并没有依我们所想象的那样在窗体界面实时显示工作进度或工作时间,而且窗体失去响应,无法拖动,点击按钮也没有反应,甚至计时器压根就没有启动计时工作。因为不知道是否成功运行了按钮事件代码,所以忍不住的多次重复点击按钮,又发现程序会重复运行按钮事件代码,即使将按钮的Enalbe 属性设置为 false还是会触发点击事件,这可不是我想的结果。
于是从网上搜索得知:winfrom会有消息排队,当程序繁忙工作(本代码中,程序忙于处理循环代码,无法分心去处理其他事情了)的时候消息被堵住了,所以整个窗体界面就处于假死状态,这时候当你点击按钮时,系统还是会记录你的点击事件,等程序响应回来后,继续后续的点击事件。
二、使用Application.DoEvents()解决窗体假死
代码繁忙工作的时候,可以使用Application.DoEvents()语句让程序喘口气去解决其他事情,如下在循环里加入该语句。
'******************************
'模拟长时间工作代码
Me.Label1.Text = "开始工作!"
For i As Integer = 0 To 100
'模拟长时间工作
Threading.Thread.Sleep(100)
Application.DoEvents()'关键语句,解决大问题
'显示工作进度
Me.Label1.Text = i.ToString & "%"
Next
'MessageBox.Show("工作完成!")
'******************************
这时窗体可以正常实时的显示进度和工作时间,而且窗体也可以及时响应了,但当你不断点击按钮时程序就会不断的多次重复执行事件代码,如何避免重复点击按钮呢?可以设置按钮的Enalbe 属性,如下代码:
'******************************
'设置按钮不响应点击
Me.Button1.Enabled = False
'模拟长时间工作代码
Me.Label1.Text = "开始工作!"
For i As Integer = 0 To 100
'模拟长时间工作
Threading.Thread.Sleep(100)
Application.DoEvents()
'显示工作进度
Me.Label1.Text = i.ToString & "%"
Next
'工作结束再设置回来,使其响应点击
Me.Button1.Enabled = True
'******************************
当按钮的Enalbe 属性设置为False时,其样式会变成灰色不太美观,使用AddHandler和RemoveHandler语句可以解决此问题,按钮还是跟正常的样式一样,只是在工作期间不会响应点击,代码如下:
'******************************
'暂时移除Button1按钮点击事件
RemoveHandler Button1.Click, AddressOf Button1_Click
'模拟长时间工作代码
Me.Label1.Text = "开始工作!"
For i As Integer = 0 To 100
'模拟长时间工作
Threading.Thread.Sleep(100)
Application.DoEvents()
'显示工作进度
Me.Label1.Text = i.ToString & "%"
Next
'工作结束后记得恢复Button1按钮点击事件
AddHandler Button1.Click, AddressOf Button1_Click
'******************************
有关AddHandler和RemoveHandler语句的使用请参阅《VB.NET学习笔记:事件处理及自定义事件》。
三、异步调用委托解决窗体假死
据说Application.DoEvents()会影响程序性能,要尽量的少用,所以想着把原按钮Button1的单击事件代码改为如下代码:
'定义委托
Private Delegate Sub SubDelegate(ByVal text As String)
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'运行计时器
intTime = 0
Me.Label2.Text = "开始工作!"
Me.Timer1.Enabled = True
'******************************
'暂时移除Button1按钮点击事件
RemoveHandler Button1.Click, AddressOf Button1_Click
'模拟长时间工作代码
'定义委托
Dim mi As MethodInvoker = New MethodInvoker(AddressOf LongTimeSub)
'异步调用委托
mi.BeginInvoke(Nothing, Nothing)
'工作结束后记得恢复Button1按钮点击事件
AddHandler Button1.Click, AddressOf Button1_Click
'******************************
'关闭计时器
Me.Timer1.Enabled = False
End Sub
Private Sub LongTimeSub()
Me.Invoke(New SubDelegate(AddressOf SetText), "开始工作")
For i As Integer = 0 To 100
'模拟长时间工作
Threading.Thread.Sleep(100)
'显示工作进度
'Me.Label1.Text = i.ToString & "%"
'改用委托
Me.Invoke(New SubDelegate(AddressOf SetText), i.ToString & "%")
Next
End Sub
Private Sub SetText(ByVal text As String) '此处是接受委托,执行函数
Me.Label1.Text = text
End Sub
长时间工作代码封装到一个LongTimeSub方法中,然后在 Button1_Click方法中使用异步调用委托的方式执行该方法,注意Label1显示也必须用委托,否则会报错:System.InvalidOperationException:“线程间操作无效: 从不是创建控件“Label1”的线程访问它。”,如图:
界面能正常显示进度了,但计时器并没有工作,而且多次重复点击按钮进度显示会很乱,一下80%,一下又75%,好像是每次点击就马上执行代码?我们在按钮的 Button1_Click方法结束处设置断点,运行发现执行代码时并没有在mi.BeginInvoke(Nothing, Nothing)这句卡住,而是直接向下运行到 Button1_Click方法结束。如图:
原来异步调用委托时,委托关联的LongTimeSub方法是在另一个线程中工作,并不会阻塞当前线程,所以当前线程中执行的Button1_Click方法能够顺利的执行完毕。
把 Button1_Click方法中的AddHandler Button1.Click, AddressOf Button1_Click和Me.Timer1.Enabled = False语句搬到LongTimeSub方法中,成功解决问题,代码正如所想象的那样执行。代码修改如下:
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
'运行计时器
intTime = 0
Me.Label2.Text = "开始工作!"
Me.Timer1.Enabled = True
'******************************
'暂时移除Button1按钮点击事件
RemoveHandler Button3.Click, AddressOf Button3_Click
'模拟长时间工作代码
'定义委托
Dim mi As MethodInvoker = New MethodInvoker(AddressOf LongTimeSub)
'异步调用委托
mi.BeginInvoke(Nothing, Nothing)
'******************************
End Sub
Private Sub LongTimeSub()
Me.Invoke(New SubDelegate(AddressOf SetText), "开始工作")
For i As Integer = 0 To 100
'模拟长时间工作
Threading.Thread.Sleep(100)
'显示工作进度
'Me.Label1.Text = i.ToString & "%"
'改用委托
Me.Invoke(New SubDelegate(AddressOf SetText), i.ToString & "%")
Next
'工作结束后记得恢复Button1按钮点击事件
AddHandler Button3.Click, AddressOf Button3_Click
'关闭计时器
Me.Timer1.Enabled = False
End Sub
四、思考
如果要在长时间工作的代码弹出一个等待窗口来显示进度,而代码又要能继续往下执行,类似于正在加载网页、游戏啊等这样的效果,可否做到?