有经验的程序员们都知道:不能在ui线程上进行耗时操作,那样会造成界面卡顿,如下就是一个简单的示例:
public partial class mainwindow : window { public mainwindow() { initializecomponent(); this.dispatcher.invoke(new action(()=> { })); this.loaded += mainwindow_loaded; } private void mainwindow_loaded(object sender, routedeventargs e) { this.content = new usercontrol1(); } } class usercontrol1 : usercontrol { textblock textblock; public usercontrol1() { textblock = new textblock(); this.content = textblock; this.dispatcher.begininvoke(new action(updatetime), null); } private async void updatetime() { while (true) { thread.sleep(900); //模拟耗时操作 textblock.text = datetime.now.tostring(); await task.delay(100); } } }
当我们运行这个程序的时候,就会发现:由于主线程大部分的时间片被占用,无法及时处理系统事件(如鼠标,键盘等输入),导致程序变得非常卡顿,连拖动窗口都变得不流畅;
如何解决这个问题呢,初学者可能想到的第一个方法就是新启一个线程,在线程中执行更新:
public usercontrol1() { textblock = new textblock(); this.content = textblock; threadpool.queueuserworkitem(_ => updatetime()); }
但很快就会发现此路不通,因为wpf不允许跨线程访问程序,此时我们会得到一个:”the calling thread cannot access this object because a different thread owns it.”的invalidoperationexception异常
那么该如何解决这一问题呢?通常的做法是把耗时的函数放在线程池执行,然后切回主线程更新ui显示。前面的updatetime函数改写如下:
private async void updatetime() { while (true) { await task.run(() => thread.sleep(900)); textblock.text = datetime.now.tostring(); await task.delay(100); } }
这种方式能满足我们的大部分需求。但是,有的操作是比较耗时间的。例如,在多窗口实时监控的时候,我们就需要同时多十来个屏幕每秒钟各进行几十次的刷新,更新图像这个操作必须在ui线程上进行,并且它有非常耗时间,此时又会回到最开始的卡顿的情况。
看起来这个问题无法解决,实际上,wpf只是不允许跨线程访问程序,并非不允许多线程更新界面。我们大可以对每个视频监控窗口单独其一个独立的线程,在那个线程中进行更新操作,此时就不会影响到主线程。msdn上有篇文章介绍了详细的操作:multithreaded ui: hostvisual。用这种方式将原来的程序改写如下:
private void mainwindow_loaded(object sender, routedeventargs e) { hostvisual hostvisual = new hostvisual(); uielement content = new visualhost(hostvisual); this.content = content; thread thread = new thread(new threadstart(() => { visualtarget visualtarget = new visualtarget(hostvisual); var control = new usercontrol1(); control.arrange(new rect(new point(), content.rendersize)); visualtarget.rootvisual = control; system.windows.threading.dispatcher.run(); })); thread.setapartmentstate(apartmentstate.sta); thread.isbackground = true; thread.start(); } public class visualhost : frameworkelement { visual child; public visualhost(visual child) { if (child == null) throw new argumentexception("child"); this.child = child; addvisualchild(child); } protected override visual getvisualchild(int index) { return (index == 0) ? child : null; } protected override int visualchildrencount { get { return 1; } } }
这个里面用来了两个新的类:hostvisual、visualtarget。以及自己写的一个visualhost。msdn上相关的解释,也不算难理解,这里就不多介绍了。最后,再来重构一下代码,把在新线程中创建控件的方式改写如下:
private void mainwindow_loaded(object sender, routedeventargs e) { createchildinnewthread<usercontrol1>(this); } void createchildinnewthread<t>(contentcontrol container) where t : uielement , new() { hostvisual hostvisual = new hostvisual(); uielement content = new visualhost(hostvisual); container.content = content; thread thread = new thread(new threadstart(() => { visualtarget visualtarget = new visualtarget(hostvisual); var control = new t(); control.arrange(new rect(new point(), content.rendersize)); visualtarget.rootvisual = control; system.windows.threading.dispatcher.run(); })); thread.setapartmentstate(apartmentstate.sta); thread.isbackground = true; thread.start(); }
当然,我这个函数多了一些不必要的的限制:容器必须是contentcontrol,子元素必须是uielement。可以根据实际需要进行相关修改。这里有一个完整的示例,也可以参考一下。
到此这篇关于wpf使用多线程更新ui的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持。