WPF频繁更新UI卡顿问题
我的WPF程序,需要连接PLC、CCD、RFID、扫码枪、控制卡
所以我写了
InitHardware();
private void InitHardware()
{
var tasks = new Task[]
{
//后台线程长连接,不取消令牌
Task.Factory.StartNew(() => InitConnPLC(),CancellationToken.None,TaskCreationOptions.LongRunning,TaskScheduler.Default),
Task.Factory.StartNew(() => InitConnRFId(),CancellationToken.None,TaskCreationOptions.LongRunning,TaskScheduler.Default),
Task.Factory.StartNew(() => InitConnCCd(),CancellationToken.None,TaskCreationOptions.LongRunning,TaskScheduler.Default),
Task.Factory.StartNew(() => InitScanCode(),CancellationToken.None,TaskCreationOptions.LongRunning,TaskScheduler.Default),
Task.Factory.StartNew(() => InitControlCard(),CancellationToken.None,TaskCreationOptions.LongRunning,TaskScheduler.Default),
};
Task.Factory.ContinueWhenAll(tasks, completedTasks =>
{
// 当所有任务完成时的逻辑
foreach (var task in completedTasks)
{
if (task.IsFaulted)
{
// 处理任务异常
Dispatcher.CurrentDispatcher.Invoke(new Action(() =>
{
HandyOrgMessageBox.Info("Task。PLC连接失败"); //这段代码先放着,我在其他地方做了异常处理
}));
}
// 其他所有任务完成时的逻辑
}
});
}
/// <summary>
/// 初始化PLC,为了应对不同公司的风格
/// PLC读取Ini文件
/// </summary>
public void InitConnPLC()
{
PlcClient.Current.PlcCount = SysPlc.PlcCount = IniHelper.Current.GetPath("Config/setting.ini").GetSection("PlcCount").ToInt();
PlcClient.Current.DefaultPlc = ObPLC.Where(it => it.IP == IPHelper.LocalIP4FromPLC).First(); //如果一个PLC走这条线,用于前期测试,正式发布可以不写DefaultPlc,会自动跟据PlcCount的多少连接
PlcClient.Current.FindRuningPlc = ObPLC.ToArray(); //如果多个PLC走这条线
while (true)
{
Task.Delay(200);
Dispatcher.CurrentDispatcher.Invoke(
new Action(() =>
{
PlcInfo.IsConn = result.IsSuccess;
PlcInfo.PlcName = PlcInfo.IsConn ? "已连接PLC" : "未连接PLC";
})
);
}
}
可以看出我在这5个类似的方法中频繁更新UI,因为我要和设备长连接
解决方案1、将更新UI从while中剔除出来,写在UI线程
首先增加一个静态的全局变量
public class SysPlc
{
public static bool IsConn {get;set;}
}
那么 Dispatcher.CurrentDispatcher.Invoke不必频繁去写。
修改while
while (true)
{
Task.Delay(200);
var result = PlcClient.Current.ConnectionPLC;
SysPlc.IsConn = result.IsSuccess;
}
然后加入定时器
_dispatcherTimer = new DispatcherTimer();
_dispatcherTimer.Interval = TimeSpan.FromMilliseconds(500);
_dispatcherTimer.Tick += OnTick;
_dispatcherTimer.Start();
在UI线程通过全局静态传递过来的数据更新UI
private void OnTick(object sender, EventArgs e)
{
PlcInfo.PlcName = SysPlc.IsConn ? "已连接PLC" : "未连接PLC";
}
对于不必要的操作,改为轮询
使用ManualResetEvent结合轮询机制确实适合于读取PLC传来的周期性数据(如CT,即Control Table),特别是当数据更新不是非常频繁,或者你不需要实时响应每次数据变化时。这种方法的优点是实现简单,并且不会持续占用CPU资源。
除此之外,这种模式还适用于以下场景:
后台任务调度:执行一些不需要实时性的任务,例如定期的日志记录、数据备份等。
资源访问同步:当多个线程需要访问同一资源,并且该资源在某一时刻只能由一个线程访问时,可以使用ManualResetEvent来控制访问权限。
异步等待:在异步编程中,当需要等待某个操作完成,但又不想阻塞当前线程时,可以使用ManualResetEvent。
用户界面更新:在UI应用程序中,可以定期检查后台任务的状态,并在适当的时候更新UI元素。
网络通信:在客户端和服务器之间进行周期性的心跳检测,以确保连接的活跃性。
定时器替代:在某些情况下,如果不想使用System.Windows.Threading.DispatcherTimer或其他定时器类,可以使用ManualResetEvent实现简单的定时功能。
条件变量:在多线程编程中,当一个或多个线程需要等待某个条件成立时,可以使用ManualResetEvent作为条件变量。
任务协调:在复杂的任务执行流程中,可以使用ManualResetEvent来协调不同任务的执行顺序。
节能模式:在移动设备或嵌入式系统中,为了节省电量,可以定期唤醒设备执行任务,然后再次进入休眠状态。
模拟硬件行为:在某些模拟或测试环境中,可能需要模拟硬件的周期性行为,使用ManualResetEvent可以实现这种周期性的触发。
使用ManualResetEvent时,需要注意以下几点:
确保在适当的时候调用Set方法来触发事件,否则循环可能会无限期地继续。
考虑使用Reset方法来重置事件状态,特别是在循环中需要多次等待时。
避免在高频率更新的场景中使用,因为这可能会导致性能问题或响应延迟。
确保在所有可能的退出点正确地处理和清理资源,例如使用Dispose方法释放ManualResetEvent。
根据你的具体需求和场景,选择最合适的同步和通信机制,以确保程序的效率和响应性。