C# Note20: 制作延时改变显示的标题栏
前言
在使用wpf构建一个窗体时,其中有这样一个功能,在保存数据或加载数据时,我们希望在改变标题栏的显示以标志当前保存成功的状态或者加载数据的名称信息,而且标题信息更新显示几秒后,再恢复到默认的状态。那么为了满足这个需求我们要解决两个问题:
(1)如何在点击保存或选择相应数据后,改变标题栏的显示?
(2)如何保证让标题栏显示变化后延迟一定时间后再变回来?在这期间,需要阻止用户的其他操作吗?(即是否考虑多线程操作)
首先,我们先说一种简单的解决方案吧:
- 标题栏根据情况变化,并延迟显示一定时间,期间阻止用户的其他操作
code1:
private void DelayChangeTitle() { DateTime start = DateTime.Now; while (true) { DateTime end = DateTime.Now; TimeSpan ts = end.Subtract(start); if (ts.TotalSeconds > 1.5) break; } this.Title = $"Recipe Editor [ {CurrentName} ]"; }
需要使用的地方,调用DelayChangeTitle()函数即可。这种思路利用TimeSpan计算两个时间差,来达到延迟变化标题栏显示的目的(记录开始时间,然后读取当前时间,循环判断时间差延时)。其实,这种解决方案,我们最容易想到的就是利用线程。
code2:
private void XXX() { ....... this.Title = $"Title Name [ {CurrentName} saved successfully ]"; DelayChangeTitle(); } private void DelayChangeTitle() { Thread t = new Thread(new ThreadStart(ThreadWorker)); t.Start(); t.Join(); //阻止线程 } private void ThreadWorker() { Thread.Sleep(200);
this.Title = $"Title Name [ {CurrentName} ]";
}
再就是可能会想到利用一个计时器Timer类,实时更新界面上的控件内容
code3:
private void DelayChangeTitle() { System.Timers.Timer t = new System.Timers.Timer(500);//实例化Timer类,设置时间间隔 t.Elapsed += new System.Timers.ElapsedEventHandler(Method2);//到达时间时执行事件 t.AutoReset = true;//设置是执行一次(false)还是一直执行(true) t.Enabled = true;//是否执行System.Timers.Timer.Elapsed事件 t.Start(); } private void Method2(object source, System.Timers.ElapsedEventArgs e) { Title = $"Title Name [ {CurrentName} ]"; }
但是code2和code3都会遇到抛出异常:
System.InvalidOperationException {"调用线程无法访问此对象,因为另一个线程拥有该对象。"} 。
追究其根本:(MSDN的解释:https://msdn.microsoft.com/zh-cn/library/ms171728(en-us,VS.80).aspx)
访问 Windows 窗体控件本质上不是线程安全的。如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的 bug,包括争用情况和死锁。确保以线程安全方式访问控件非常重要。 .NET Framework 有助于在以非线程安全方式访问控件时检测到这一问题。在调试器中运行应用程序时,如果创建某控件的线程之外的其他线程试图调用该控件,则调试器会引发一个 InvalidOperationException,并提示消息:“从不是创建控件 control name 的线程访问它。
那么自然地引出了下面的解决方案。该过程可参考WPF中Timer与DispatcherTimer类的区别一文!
- 标题栏根据情况变化,并延迟显示一定时间,允许用户做其他操作
code4(使用DispatcherTimer实现):
using System.Windows.Threading; DispatcherTimer timer = new DispatcherTimer(new TimeSpan(0, 0, 2), DispatcherPriority.Normal, DelayChangeTitle, Dispatcher); private void DelayChangeTitle(object sender, EventArgs e) { this.Title = $"Title Name [ {CurrentName} ]"; ((DispatcherTimer)sender).Stop(); }
DispatcherTimer是在界面线程中实现的,可以安全地访问,并修改界面内容。
--那么,还有其他方法吗?答案是Yes。
code5:
在C# 5.0中引入了async 和 await,利用他们可以更方便地写出异步代码。
private async void SaveAsXXX() { ....... this.Title = $"Title Name [ {CurrentName} saved successfully ]";
// The await operator suspends SeeTheDotNets_Click, returning control to its caller.
// This is what allows the app to be responsive and not hang on the UI thread.
await Task.Delay(2000);
this.Title = $"Recipe Editor [ {CurrentName} ]";
}
MSDN的Asynchronous programming中有这么一段话:
可参考相关文章:
wpf中应该使用c#四种定时器中的DispatcherTimer(讲明了为什么使用DispatcherTimer的理由)
The DispatcherTimer(a simple example where we use a DispatcherTimer to create a digital clock)
DispatcherTimer Class(MSDN说明)