一、WPF的线程
对于初学wpf的人来说,一般会把所有的程序都在一个线程中运行,当数据量较大,需要频繁刷新界面时,界面会出现卡顿的情况。
1、当我们打开一个WPF应用程序即开启了一个进程,该进程中都会加载两个重要的线程:一个用于呈现用户界面,另一个用于管理用户界面。呈现线程是一个在后台运行的隐藏线程,因此您通常面对的唯一线程 就是 UI线程。WPF 要求将其大多数对象与 UI 线程进行关联,这称之为线程关联,意味着要使用一个 WPF 对象,只能在创建它的线程上使用,在其他线程上使用它会导致引发运行时异常。
2、在 WPF 中绝大部分控件都继承自 DispatcherObject,甚至包括 Application。这些继承自 DispatcherObject 的对象具有线程关联特征,也就意味着只有创建这些对象实例,且包含了 Dispatcher 的线程(通常指默认 UI 线程)才能直接对其进行更新操作。
在 WPF 中,DispatcherObject 只能通过与它关联的 Dispatcher 进行访问。 例如,后台线程不能更新由 UI 线程创建的 Label的内容。
二、 Dispatcher类
1、在UI线程中有一个Dispatcher对象,管理每一个需要执行的工作项。Dispatcher会根据每个工作项的优先级排队。向Dispatcher列队中添加工作项时可指定10个不同的级别。那么问题来了,如果遇到耗时操作的时候,该操作如果依旧发生在UI线程中,Dispatcher 列队中其他的需要执行的工作项都要等待,从而造成界面假死的现象。为了加快响应速度,提高用户体验,我们应该尽量保证Dispatcher 列队中工作项要小。所以,对于耗时操作,我们应该开辟一个新的子线程去处理,在操作完成后,通过向UI线程的Dispatcher列队注册工作项,来通知UI线程更新结果。
Dispatcher类详细介绍
2、Dispatcher提供两个注册工作项的方法:Invoke 和 BeginInvoke。
这两个方法均调度一个委托来执行。Invoke 是同步调用,也就是说,直到 UI 线程实际执行完该委托它才返回。BeginInvoke是异步的,将立即返回。
3、UI线程中创建的对象,如何在非UI线程中更新
参考:https://www.cnblogs.com/chillsrc/p/4482691.html
(1)前台,创建对象
(2)后台创建非UI线程,若直接更新UI线程的对象就会报错
namespace WpfApp1
{
///
/// WindowThd.xaml 的交互逻辑
///
public partial class WindowThd : Window
{
public WindowThd()
{
InitializeComponent();
}
//更新UI线程创建的对象
private void ModifyUI()
{
// 模拟一些工作正在进行
Thread.Sleep(TimeSpan.FromSeconds(2));
lblHello.Content = "欢迎你光临WPF的世界,Dispatcher";
}
//开启非UI线程
private void btnThd_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(ModifyUI);
thread.Start();
}
}
}
程序报错:
正确方法:
修改上面的ModifyUI()函数
方法一:使用Invoke
private void ModifyUI()
{
// 模拟一些工作正在进行
Thread.Sleep(TimeSpan.FromSeconds(2));
//lblHello.Content = "欢迎你光临WPF的世界,Dispatcher";
this.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate()
{
lblHello.Content = "欢迎你光临WPF的世界,Dispatche 同步方法 !!";
});
}
方法二:使用 BeginInvoke
private void btnAppBeginInvoke_Click(object sender, RoutedEventArgs e)
{
new Thread(() =>
{
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new Action(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(2));
this.lblHello.Content = "欢迎你光临WPF的世界,Dispatche 异步方法!!"+ DateTime.Now.ToString();
}));
}).Start();
}
三、实验测试
1、简单测试
做了一个上位机,接收光谱仪的数据,并将线阵CMOS的数据绘制成图,一直存在界面卡顿的问题。
ShowCCD()函数用来将串口接收的数据绘制成图。在串口接收处理事件中,接收完一帧数据后再调用ShowCCD()函数绘图。
方式一:
直接调用:
ShowCCD();
测试效果:
整个界面非常卡顿,CPU占到了61%。
方法二:使用Dispatcher.Invoke
this.Dispatcher.Invoke(ShowCCD);
测试效果:
页面相对较为卡顿,CPU占用42%.
方法三:
使用Dispatcher.BeginInvoke
this.Dispatcher.BeginInvoke((Action)delegate(){ShowCCD();});
出现报错。
降低发送的帧数后,可以显示。
通过以上分析程序卡顿的原因的我在UI线程上接收数据,并使用Dispatcher类显示数据,实际上还是运行在UI线程上,造成卡顿。
四、优化方案
1、原来的主窗口还是用来进行数据的接收及数据的运算处理,即数据处理过程还是运行在UI线程。
2、新建一个wpf窗口,这个窗口使用zedgraph控件出图,开启一个Timer定时器线程定时刷新界面,定时20ms,即刷新频率为50HZ,若刷新的速率和接收数据的速率设置为一样快,我认为也没有必要,太快的刷新速率人眼也分辨不出来。
WPF下使用ZedGraph控件
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// savepicture_th = new Thread(new ThreadStart(savepicture));
// savepicture_th.Start(); //启动线程
// savepicture();
//这种定时器不是工作在UI线程
System.Timers.Timer timer = new System.Timers.Timer(20);//实例化Timer类,设置间隔时间为20毫秒;
timer.Elapsed += new System.Timers.ElapsedEventHandler(theout); //注册中断事件
timer.Start();//启动定时器
}
//时间中断事件 以50Hz的频率刷新界面
public void theout(object source, System.Timers.ElapsedEventArgs e)
{
showChart1(MainWindow.aveccdData0);
}
PointPairList list2 = new PointPairList();
private void showChart1(double[] value)
{
list2.Clear();//清空数组
zedGraphControl.GraphPane.Title.Text = "";
zedGraphControl.GraphPane.XAxis.Title.Text = "";
zedGraphControl.GraphPane.YAxis.Title.Text = "";
zedGraphControl.GraphPane.XAxis.Scale.Min = 0; //X轴最小值0
//zedGraphControl1.GraphPane.XAxis.Scale.MaxAuto = true; //X轴最大30
zedGraphControl.GraphPane.XAxis.Scale.Max = 520;
zedGraphControl.GraphPane.XAxis.Scale.MinorStep = 10;
zedGraphControl.GraphPane.XAxis.Scale.MajorStep = 100;
zedGraphControl.GraphPane.YAxis.Scale.Min = 0;
zedGraphControl.GraphPane.YAxis.Scale.Max = 2000;
zedGraphControl.GraphPane.YAxis.Scale.MinorStep = 20;
zedGraphControl.GraphPane.YAxis.Scale.MajorStep = 500;
zedGraphControl.AxisChange();
for (int j = 0; j < value.Length; j++)
list2.Add(j, value[j]);
this.Dispatcher.Invoke(RefreshInterface);//切换到UI线程更新界面
}
//刷新界面
private void RefreshInterface()
{
zedGraphControl.GraphPane.CurveList.Clear();
zedGraphControl.GraphPane.AddCurve("", list2, System.Drawing.Color.Red, SymbolType.None);//绘制图表
// zedGraphControl.AxisChange();//刷新界面
zedGraphControl.Refresh();
}
Timer定时器线程里刷新界面时,必须使用Dispatcher类,若在Timer定时器线程里直接刷新界面就会报错,这违反了WPF下用其他线程刷新界面的规则。
如果使用定时器DispatcherTimer则不需要使用Dispatcher类,因为DispatcherTimer就是在UI线程中,仅是猜测未经实验。
测试效果:界面完全不卡顿,CPU占用率10%。