利用一个计时器Timer类,实时更新界面上的控件内容,但是一直遇到抛出异常:System.InvalidOperationException {"调用线程无法访问此对象,因为另一个线程拥有该对象。"} 。
测试代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Timers; namespace TimerTest { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private Timer aTimer = null; public MainWindow() { InitializeComponent(); aTimer = new Timer(); aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent); // Set the Interval to 5 seconds. aTimer.Interval = 1000; aTimer.Enabled = true; aTimer.Start(); } private void OnTimedEvent(object source, ElapsedEventArgs e) { timeLabel.Content = DateTime.Now.ToUniversalTime(); } } }
Debug的时候,发现在第38行的时候,timeLabel.Content 赋值的时候抛出了该异常
Google搜索了一下发现:”访问 Windows 窗体控件本质上不是线程安全的。如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的 bug,包括争用情况和死锁。确保以线程安全方式访问控件非常重要。“——来自MSDN http://msdn.microsoft.com/zh-cn/library/ms171728(en-us,VS.80).aspx
哎,在群上回答说了一句:子线程无法访问界面线程的对象。被指没学过C#(的确,我学C#就是在一周之内看完一本几百页的书),这句话说的的确不够严谨,因为是有方法的,可以通过委托的方式进行访问,同时,这个界面线程的对象是指控件,如Label,TextBox之类。之前都写一个Qt程序的时候也遇到类似的问题,也是类似的情况:在子线程中试图直接更新界面上的一张图片,结果程序奔溃了,调试了很久才发现问题所在。
这时候我就想起来前阵子做过的一个练习,使用DispatcherTimer来实现更新。测试了一下,发现可行啊!!
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Timers; using System.Windows.Threading; namespace TimerTest { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private DispatcherTimer dispatcherTimer = null; public MainWindow() { InitializeComponent(); dispatcherTimer = new System.Windows.Threading.DispatcherTimer(); dispatcherTimer.Tick += new EventHandler(OnTimedEvent); dispatcherTimer.Interval = new TimeSpan(0, 0, 1); dispatcherTimer.Start(); } private void OnTimedEvent(object sender, EventArgs e) { timeLabel.Content = DateTime.Now.ToUniversalTime(); } } }
于是好奇这两者有什么不同?
仔细阅读MSDN上的文档后,可以得知:DispatcherTimer在界面线程中实现的,当然可以安全地访问,修改界面内容。
If a System.Timers.Timer is used in a WPF application, it is worth noting that the System.Timers.Timer runs on a different thread then the user interface (UI) thread. In order to access objects on the user interface (UI) thread, it is necessary to post the operation onto the Dispatcher of the user interface (UI) thread using Invoke or BeginInvoke. Reasons for using a DispatcherTimer opposed to a System.Timers.Timer are that theDispatcherTimer runs on the same thread as the Dispatcher and a DispatcherPriority can be set on the DispatcherTimer.
呵呵,区别就是在这里了!!
附:感谢群里高手的指点,采用Timer,使用Invoke或者BeginInvoke的方式进行UI的更新的方式(好处在于:在DispatcherTimer里面执行等待动作或者时间过长,可能会导致UI假死):
using System; using System.Windows; using System.Timers; using System.Windows.Threading; namespace TimerTest { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private Timer aTimer = null; private delegate void TimerDispatcherDelegate(); public MainWindow() { InitializeComponent(); aTimer = new Timer(1000); aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent); aTimer.Interval = 1000; aTimer.Enabled = true; } private void OnTimedEvent(object sender, EventArgs e) { this.Dispatcher.Invoke(DispatcherPriority.Normal, new TimerDispatcherDelegate(updateUI)); } private void updateUI() { timeLabel.Content = DateTime.Now.ToUniversalTime(); } } }