在 WinForms 中使用 foreach 循环保持 UI 响应
Keeping UI Responsive with foreach loop in WinForms
我创建了一个 Winforms 应用程序,它使用 Open Hardware Monitor 使用实时图表以仪表格式显示 PC 温度。我知道以下代码会导致 UI 在图表更新时变得无响应,但我无法弄清楚如何实现任何形式的线程来处理它或如何更改我的编码方式保持 UI 响应的应用程序。计时器将每两秒计时一次,以获取值并更新仪表。
添加到项目的引用是:
- OpenHardwareMonitorLib
- LiveCharts.Winforms + 依赖项
请参阅下面的代码,如有任何建议,我们将不胜感激。
UserControls.TempGauge
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Media;
namespace PCDashboard.UserControls
{
public partial class TempGauge : UserControl
{
public TempGauge(string name)
{
try
{
//Initialize
InitializeComponent();
//Gauge Properties
this.Name = name;
gaugeName.Text = name;
gauge.Uses360Mode = true;
gauge.From = 0;
gauge.To = 110;
gauge.Value = 0;
gauge.HighFontSize = 60;
gauge.Base.Foreground = System.Windows.Media.Brushes.White;
gauge.InnerRadius = 0;
gauge.GaugeBackground = Classes.Colours.Blue;
}
catch (Exception)
{
}
}
}
}
主窗体
using OpenHardwareMonitor.Hardware;
using PCDashboard.Classes;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace PCDashboard.Forms
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void MainForm_Load(object sender, EventArgs e)
{
try
{
GetSystemTemps(false);
timer.Enabled = true;
}
catch (Exception)
{
}
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
Close();
}
private void GetSystemTemps(bool UpdateGauge)
{
try
{
UpdateVisitor updateVisitor = new UpdateVisitor();
Computer computer = new Computer();
computer.Open();
//Which components to read?
computer.CPUEnabled = true;
computer.GPUEnabled = true;
computer.MainboardEnabled = true;
computer.RAMEnabled = true;
computer.Accept(updateVisitor);
foreach (IHardware hardware in computer.Hardware)
{
foreach (ISensor sensor in hardware.Sensors.Where(
x => x.SensorType == SensorType.Temperature))
{
if (UpdateGauge)
{
UserControls.TempGauge tempGauge =
((UserControls.TempGauge)gaugeContainer
.Controls[sensor.Name]);
if (tempGauge != null)
{
tempGauge.gauge.Value = Math.Round(
double.Parse(sensor.Value.ToString()), 0);
}
}
else
{
UserControls.TempGauge tempGauge =
new UserControls.TempGauge(sensor.Name);
gaugeContainer.Controls.Add(tempGauge);
}
}
}
computer.Close();
}
catch (Exception)
{
}
}
private void timer_Tick(object sender, EventArgs e)
{
try
{
GetSystemTemps(true);
}
catch (Exception)
{
}
}
}
}
我的建议是将数据检索与表示分开。这样您就可以将繁重的数据检索工作卸载到后台线程,而不必担心如何同时更新 UI 控件。以下是如何制作一个 GetSensorData
方法,该方法将数据作为 ValueTuple<string, double>
元素的数组获取,代表每个传感器的名称和值:
private (string, double)[] GetSensorData()
{
var list = new List<(string, double)>();
Computer computer = new Computer();
computer.Open();
//...
foreach (IHardware hardware in computer.Hardware)
{
foreach (ISensor sensor in hardware.Sensors.Where(
x => x.SensorType == SensorType.Temperature))
{
list.Add(sensor.Name, double.Parse(sensor.Value.ToString()));
}
}
computer.Close();
return list.ToArray();
}
然后您可以使用 Task.Run
method to offload the heavy work of retrieving the data to a background thread (a ThreadPool
thread). This method returns a Task
, that could be awaited asynchronously so that the code below the await
有可用的数据。
private async Task UpdateSystemTempsAsync(bool updateGauge)
{
var data = await Task.Run(() => GetSensorData()); // Offload to thread-pool
// While awaiting the UI remains responsive
// After the await we are back in the UI thread
foreach (var (sensorName, sensorValue) in data)
{
if (updateGauge)
{
UserControls.TempGauge tempGauge =
((UserControls.TempGauge)gaugeContainer.Controls[sensorName]);
if (tempGauge != null)
{
tempGauge.gauge.Value = Math.Round(sensorValue, 0);
}
}
else
{
var tempGauge = new UserControls.TempGauge(sensorName);
gaugeContainer.Controls.Add(tempGauge);
}
}
}
最后你必须制作事件处理程序 async
:
private async void MainForm_Load(object sender, EventArgs e)
{
try
{
await UpdateSystemTempsAsync(false);
timer.Enabled = true;
}
catch { }
}
private async void timer_Tick(object sender, EventArgs e)
{
timer.Enabled = false;
try
{
await UpdateSystemTempsAsync(true);
timer.Enabled = true;
}
catch { }
}
为此使用计时器是完全合理的。但是您可以做三件事来帮助您的应用保持响应速度。
首先,我会检查计时器上的 Interval
属性。任何小于 100 的值都可能太小,您可能会更高一些。即使是250也还是每秒4次,400多了每秒两次
其次,我将调用 SuspendLayout()
和 ResumeLayout()
来括起循环。这将有助于您的表单更高效地批量重绘。
最后,IIRC 您可以将定时器的 Enabled
属性 设置为 false
形式 Move
事件,并再次返回 true 为 ResizeEnd
事件, 在有人四处移动表单时禁用更新。不过,这对我来说是很久以前的事了,所以你会想测试一下这个。
我创建了一个 Winforms 应用程序,它使用 Open Hardware Monitor 使用实时图表以仪表格式显示 PC 温度。我知道以下代码会导致 UI 在图表更新时变得无响应,但我无法弄清楚如何实现任何形式的线程来处理它或如何更改我的编码方式保持 UI 响应的应用程序。计时器将每两秒计时一次,以获取值并更新仪表。
添加到项目的引用是:
- OpenHardwareMonitorLib
- LiveCharts.Winforms + 依赖项
请参阅下面的代码,如有任何建议,我们将不胜感激。
UserControls.TempGauge
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Media;
namespace PCDashboard.UserControls
{
public partial class TempGauge : UserControl
{
public TempGauge(string name)
{
try
{
//Initialize
InitializeComponent();
//Gauge Properties
this.Name = name;
gaugeName.Text = name;
gauge.Uses360Mode = true;
gauge.From = 0;
gauge.To = 110;
gauge.Value = 0;
gauge.HighFontSize = 60;
gauge.Base.Foreground = System.Windows.Media.Brushes.White;
gauge.InnerRadius = 0;
gauge.GaugeBackground = Classes.Colours.Blue;
}
catch (Exception)
{
}
}
}
}
主窗体
using OpenHardwareMonitor.Hardware;
using PCDashboard.Classes;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace PCDashboard.Forms
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void MainForm_Load(object sender, EventArgs e)
{
try
{
GetSystemTemps(false);
timer.Enabled = true;
}
catch (Exception)
{
}
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
Close();
}
private void GetSystemTemps(bool UpdateGauge)
{
try
{
UpdateVisitor updateVisitor = new UpdateVisitor();
Computer computer = new Computer();
computer.Open();
//Which components to read?
computer.CPUEnabled = true;
computer.GPUEnabled = true;
computer.MainboardEnabled = true;
computer.RAMEnabled = true;
computer.Accept(updateVisitor);
foreach (IHardware hardware in computer.Hardware)
{
foreach (ISensor sensor in hardware.Sensors.Where(
x => x.SensorType == SensorType.Temperature))
{
if (UpdateGauge)
{
UserControls.TempGauge tempGauge =
((UserControls.TempGauge)gaugeContainer
.Controls[sensor.Name]);
if (tempGauge != null)
{
tempGauge.gauge.Value = Math.Round(
double.Parse(sensor.Value.ToString()), 0);
}
}
else
{
UserControls.TempGauge tempGauge =
new UserControls.TempGauge(sensor.Name);
gaugeContainer.Controls.Add(tempGauge);
}
}
}
computer.Close();
}
catch (Exception)
{
}
}
private void timer_Tick(object sender, EventArgs e)
{
try
{
GetSystemTemps(true);
}
catch (Exception)
{
}
}
}
}
我的建议是将数据检索与表示分开。这样您就可以将繁重的数据检索工作卸载到后台线程,而不必担心如何同时更新 UI 控件。以下是如何制作一个 GetSensorData
方法,该方法将数据作为 ValueTuple<string, double>
元素的数组获取,代表每个传感器的名称和值:
private (string, double)[] GetSensorData()
{
var list = new List<(string, double)>();
Computer computer = new Computer();
computer.Open();
//...
foreach (IHardware hardware in computer.Hardware)
{
foreach (ISensor sensor in hardware.Sensors.Where(
x => x.SensorType == SensorType.Temperature))
{
list.Add(sensor.Name, double.Parse(sensor.Value.ToString()));
}
}
computer.Close();
return list.ToArray();
}
然后您可以使用 Task.Run
method to offload the heavy work of retrieving the data to a background thread (a ThreadPool
thread). This method returns a Task
, that could be awaited asynchronously so that the code below the await
有可用的数据。
private async Task UpdateSystemTempsAsync(bool updateGauge)
{
var data = await Task.Run(() => GetSensorData()); // Offload to thread-pool
// While awaiting the UI remains responsive
// After the await we are back in the UI thread
foreach (var (sensorName, sensorValue) in data)
{
if (updateGauge)
{
UserControls.TempGauge tempGauge =
((UserControls.TempGauge)gaugeContainer.Controls[sensorName]);
if (tempGauge != null)
{
tempGauge.gauge.Value = Math.Round(sensorValue, 0);
}
}
else
{
var tempGauge = new UserControls.TempGauge(sensorName);
gaugeContainer.Controls.Add(tempGauge);
}
}
}
最后你必须制作事件处理程序 async
:
private async void MainForm_Load(object sender, EventArgs e)
{
try
{
await UpdateSystemTempsAsync(false);
timer.Enabled = true;
}
catch { }
}
private async void timer_Tick(object sender, EventArgs e)
{
timer.Enabled = false;
try
{
await UpdateSystemTempsAsync(true);
timer.Enabled = true;
}
catch { }
}
为此使用计时器是完全合理的。但是您可以做三件事来帮助您的应用保持响应速度。
首先,我会检查计时器上的 Interval
属性。任何小于 100 的值都可能太小,您可能会更高一些。即使是250也还是每秒4次,400多了每秒两次
其次,我将调用 SuspendLayout()
和 ResumeLayout()
来括起循环。这将有助于您的表单更高效地批量重绘。
最后,IIRC 您可以将定时器的 Enabled
属性 设置为 false
形式 Move
事件,并再次返回 true 为 ResizeEnd
事件, 在有人四处移动表单时禁用更新。不过,这对我来说是很久以前的事了,所以你会想测试一下这个。