在多线程软件开发中,你是不是经常为跨线程更新界面UI而烦恼?看完这篇文章后保证你收获满满!
在软件或APP开发过程中,往往会将耗时操作放到子线程中完成,这样可以有更好的用户体验,增加软件的稳定性。因为耗时的操作(如下载和数据库事务)在长时间运行时可能会导致用户界面 (UI) 始终处于停止响应的状态或者非常卡顿。在子线程执行过程中往往会在主界面报告并显示执行进度等信息。这时候就涉及到跨线程更新控件(UI)的问题。
下面以C#窗体应用程序开发举例,如何“优雅”地更新界面UI。
如上图所示,在软件界面上添加一个名为label1的Label控件,添加一个名为sbtn_thread的Button控件。点击该按钮“子线程设置Label标签Text属性”时会创建一个线程,在该线程中直接操作界面上标签控件属性,会报错“线程间操作无效:从不是创建控件‘label1’的线程访问它”。关键代码及报错信息如下图:
那么应该怎么在子线程中更新主界面中控件属性呢,下面给出2种方案:
第一种 使用BackgroundWorker控件
BackgroundWorker是.NET Framework 里面用来执行多线程任务的控件,它允许开发人员在一个单独的线程上执行一些操作。如需要能进行及时响应的用户界面,而且面临与这类操作相关的长时间延迟,则可以使用 BackgroundWorker 类方便地解决问题。
若要在后台执行耗时的操作,需要创建一个 BackgroundWorker,侦听那些报告操作进度并在操作完成时发出信号的事件。 可以通过编程方式创建 BackgroundWorker,也可以将它从“工具箱”的“组件”选项卡中拖到窗体上。
软件界面上添加一个名为 label1的 Label 控件并添加两个名为 sbtn_bgworker和 sbtn_bgworker_stop 的 Button 控件,创建按钮点击事件如下:
从工具箱中的“组件”选项卡中,添加命名为 backgroundWorker1 的backgroundWorker 组件,创建 DoWork、 ProgressChanged 和 RunWorkerCompleted 事件如下:
点击按钮“backgroundWorder设置Label标签Text属性”,可以看到界面上label1控件的值不停的发生变化,且窗体主界面并未发生卡顿,延迟更新等现象:
第二种 使用委托
C#中委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。委托是C#实现回调函数的一种机制。
当我们用delegate关键字声明委托时,编译器自动为我们生成类。类的名字即为委托变量名,访问类型为定义的委托访问类型。你就可以把委托看成是用来执行方法(函数)的一个东西。
如下图所示,声明一个委托设置Label标签的Text属性,然后定义对应的函数,代码如下:
演示效果如下图所示:
总结
从上面的示例可以看出:
第一种使用BackgroundWorker控件的方案,操作复杂,需要为每个需要更新的控件分别增加不同属性(比如设置Text、Enabled、ForeColor、BackColor等属性)对应的BackgroundWorker控件,并完成对应的DoWork、 ProgressChanged 和 RunWorkerCompleted事件。使用起来非常不方便,且编码量非常大。
第二种使用委托的方案比较方便,代码量少,虽然也需要为每个需要更新的控件分别增加不同属性操作的委托。但是这种方案可以进一步封装,将同一类具有Text属性的控件操作封装到一个委托中完成,只需要传输控件参数即可。比如Button、Label、TextBox等具有Text属性的控件,就可以使用同样的委托函数来完成Text属性的更新操作。完整代码如下:
终极方案
以上经过初步封装的委托实现了具有相同属性控件的操作。但事实上,平时跨线程更新的不仅仅是一个种类的控件或者一个属性,也就是会同时更新多种类的控件的多种属性。并且为了更方便使用,让代码更简洁,高效,移植性强,以下终极方案将常用控件以及常用的属性封装为一个标准的委托函数。完整代码如下:
如下图所示,添加不同种类型的控件,每种类型的控件分别开启一个线程,用来更新控件的属性(Text、ForeColor、BackColor、Enabled、Visible等):
有了以上功能齐全的委托,还可以根据需求创建一些常用的函数以方便灵活地使用:
以上是我目前能想到的最“优雅”的多线程更新界面UI的解决方案,如果大家有更好的方案或不同的看法,欢迎在评论区留言!