65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF/MVVM 倒计时器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (19投票s)

2012年2月14日

CPOL

8分钟阅读

viewsIcon

110246

downloadIcon

8642

使用 MVVM 模式在 WPF 中实现的倒计时器

引言

本文介绍了使用 C# 和 WPF 编写的倒计时器应用程序的构建过程,该应用程序使用了 Laurent Bugnion 的 MVVMLight Toolkit。本文基于我之前撰写的一些文章。

与往常一样,本文中的大部分代码都省略了,尤其因为没有人喜欢滚动五屏 XAML 来查看一行感兴趣的代码。请下载源 zip 文件以查看完整内容。

本文是在 Windows 7 64 位系统上编写并更重要的是经过测试的,但使用了 .NET 4.0 框架,所以应该“直接可用”!

要求和功能

倒计时器将相对简单

  • 用户可以选择开始时间。
  • 以视觉(应用程序和任务栏)和声音方式通知用户。
  • 该应用程序有用户可以更改的设置。
  • Windows 7 任务栏图标显示计时器的进度。

最初的动机是我在浏览网页时偶然发现了 番茄工作法,并认为编写一个可以用于此的倒计时器会很有趣。简而言之,这是一种“把事情做好”的思想,可以归结为:

  • 工作 25 分钟
  • 休息 5 分钟
  • 重复

所以我决定计时器的默认设置为 25 分钟,并且应该以不显眼的方式记录完成的倒计时次数,以便有人希望以这种方式使用此应用程序。

选择底层计时器

我们使用 WPF 的 DispatchTimer 来执行计数。我们不保证 计时器 的准确性。

事实上,文档也没有

计时器不保证在时间间隔发生时准确执行,但保证不会在时间间隔发生之前执行。这是因为 DispatcherTimer 的操作与其他操作一样被放入 Dispatcher 队列中。DispatcherTimer 操作的执行取决于队列中的其他作业及其优先级。

通过利用 .NET Framework,我们使用 TimeSpan,它允许我们按指定量递增,尤其重要的是递减。然后,我们只需在 DispatchTimer 滴答时递减我们的起始值,直到我们得到负的 TimeSpan,然后停止。

代码的编写方式是,TimerModel 只是 ITimerModel 的一个具体实现,而 ITimerModel 的具体实例化是从单个工厂方法生成的:换句话说,您可以编写自己的 ITimerModel 派生类,并根据需要更新工厂方法(例如,改用 System.Threading.Timer)。

MVVM

由于这是一个 WPF 应用程序,我们将使用 MVVM 模式来布局代码。

这是什么意思?应用程序将被划分为:

  • 视图 - 仅 XAML 的布局,应用程序使用:即 GUI 及其所有窗口!
  • 视图模型 - 在视图和模型之间传递数据。
  • 模型 - 执行工作(以及其他所有工作)的实际代码。

如果您发现这令人困惑或想了解更多信息,请参阅我的另一篇文章:WPF/MVVM 快速入门教程

应用程序设置

所有应用程序都有设置,这个也不例外:为了持久化应用程序的设置,我们利用了 System.Configuration.ApplicationSettingsBase 类。当您创建 WPF 应用程序时,它会被继承,因此您可以像这样通过编程方式直接访问应用程序设置:

_timer.Duration = Properties.Settings.Default.Duration;

在这里,我们创建了一个 Duration 属性。

与我们通过 ITimerModel 接口隐藏 Timer 的实现一样,我们也使用了一个名为 ISettingsModel 的接口,并使用了一个名为 SettingsModel 的具体实例,以及一个构建方法来检索该类的实例。这使我们有机会像以前一样,将设置的后端存储更改为其他内容(例如 ini 文件?)。

更新应用程序版本之间的设置

为了应对应用程序的更新,我们可以使用以下方法:在我们的设置中定义 UpgradeRequired,并默认设置为 True。然后我们使用

if (Properties.Settings.Default.UpgradeRequired)
{
  Properties.Settings.Default.Upgrade();
  Properties.Settings.Default.UpgradeRequired = false;
  Properties.Settings.Default.Save();
}

仅当 UpgradeRequired 标志为 true 时强制升级应用程序设置。对于新版本化的程序集,所有设置都采用默认值,此代码将被触发,并将设置从先前的应用程序版本(如果存在)复制到新版本。

值得注意的是,要使此“技巧”生效,您必须始终在应用程序的第一个版本的应用程序设置中定义此字段。

视图和视图模型

应用程序有几个视图,它们都是 UserControl 并由 MainWindow 承载。这意味着没有弹出对话框!它们是:

  • TimerView
  • SettingsView
  • AboutView

以及相应的视图模型:

  • TimerViewModel
  • SettingsViewModel
  • AboutViewModel

更改视图和消息传递

由于我们希望使用“关注点分离”或“封装”(如果您愿意),我们不希望视图模型直接通信。为了做到这一点,我们只需使用消息传递,换句话说:

330073/Messaging.png

MVVMLight Toolkit 为我们提供了一个单例 Messenger 类,我们可以向其注册消息消费者和消息生产者。因此,要从一个视图模型在一个视图模型中引发一个“事件”,我们只需传递一个消息,例如:

public class MainViewModel : ViewModelBase
{
    public MainViewModel()
    {
        //  Lastly, listen for messages from other view models.
        Messenger.Default.Register<SimpleMessage>(this, ConsumeMessage);
    }

    private void ConsumeMessage(SimpleMessage message)
    {
        switch (message.Type)
        {
            case MessageType.TimerTick:
                WindowTitle = message.Message;
                break;
            // ....
        }
    }
}

而在 TimerViewModel 中:

public class TimerViewModel : ViewModelBase
{
    private void OnTick(object sender, TimerModelEventArgs e)
    {
        Messenger.Default.Send(new SimpleMessage(MessageType.TimerTick, TimerValue));
    }
}

这样做的好处是:TimerViewModel 更新主窗口 ContentControl 中的 TimerView 倒计时时钟,但我们也希望更新窗口标题以显示倒计时。主窗口 View 绑定到 MainViewModel,因此为了做到这一点并保持视图模型的分离,我们传递一条包含剩余时间的的消息。稍后会讨论更新窗口标题栏的原因。

任务栏预览和窗口标题

从这个截图可以看出:

330073/CountdownTimer2.png

倒计时值显示在任务栏项目缩略图主窗口的标题中。更新窗口标题的原因是,当窗口最小化时,Windows 不会更新任务栏项目缩略图,所以如果您将鼠标指针悬停在任务栏上的图标上,当项目最小化时,缩略图预览将显示窗口最小化时的倒计时。幸运的是,窗口的标题在缩略图预览中更新,所以我们确保更新它以提供视觉提示给用户。

任务栏消息

我们需要第二种消息类型来在 Windows 7 中通信任务栏进度更新:由于 MainWindow“视图”绑定到 MainViewModel,我们需要从 TimerViewModel 接收适合更新任务栏进度指示器的消息。幸运的是,这相对简单,我们再次利用了我们之前看到的 Messenger.Default.RegisterMessenger.Default.Send 模式。

第二条消息类就是:

public class TaskbarItemMessage
{
    public TaskbarItemMessage()
    {
        State = TaskbarItemProgressState.None;
        Value = -1.0;
    }
    public TaskbarItemProgressState State { get; set; }

    public double Value { get; set; }

    public bool HasValue { get { return ! (Value < 0.0); } }
}

我们的 TimerViewModel 只发送这些消息的实例,而 MainViewModel 接收它们,并通过数据绑定的魔力,在视图模型 (MainViewModel) 和视图 (MainWindow) 之间,任务栏进度指示器会自动更新。

<Window x:Class="Btl.MainWindow"
        DataContext="{Binding Main,
                              Source={StaticResource Locator}}">
    <Window.TaskbarItemInfo>
        <TaskbarItemInfo ProgressState="{Binding ProgressState}" 
                      ProgressValue="{Binding ProgressValue}">
            <TaskbarItemInfo.ThumbButtonInfos>
                <ThumbButtonInfoCollection>
                    <ThumbButtonInfo Command="{Binding PlayCommand}"
                                     Description="Start"
                                     DismissWhenClicked="False"
                                     ImageSource="Resources\icon.play.png" />
                    <ThumbButtonInfo Command="{Binding PauseCommand}"
                                     Description="Pause"
                                     DismissWhenClicked="False"
                                     ImageSource="Resources\icon.pause.png" />
                </ThumbButtonInfoCollection>
            </TaskbarItemInfo.ThumbButtonInfos>
        </TaskbarItemInfo>
    </Window.TaskbarItemInfo>
    <!-- ELIDED  -->
</Window>

由于 TaskBarItemInfo 缩略图预览提供了更多功能,我们可以添加缩略图“开始”和“暂停”按钮(就像媒体播放器一样),这样我们就可以从缩略图预览控制倒计时器,因此有了上面的 ThumbButtonInfo 元素。

关于 UI 设计的说明

倒计时器 UI 的设计有一定的道理:由于播放和暂停按钮很可能是最常用的,所以它们最大,然后是设置和重置按钮,它们较小,不太可能被意外点击。“关于”窗口通过右下角的一个小“?”来访问。

同样,设置视图中的“确定”和“取消”按钮也分开得较远,以确保您清楚要点击哪个按钮。

330073/CountdownTimer3.png

最后,除了按钮图标(播放、暂停等)之外,我没有更改应用程序的主题,让操作系统来选择如何主题化它。当然,由于这是一个 MVVM 应用程序,您可以获取源代码,启动 Blend,并按您喜欢的方式进行更改。

甚至还有一些第三方库可以为您完成大量工作,例如 MahApps.Metro

奖励功能

在文章开头的下载部分,还有一个 MSI 安装程序,供任何想安装计时器而不想深入研究的人使用。

所有源代码都在 github 上,您可以自由地 fork、复制、破解等。MSI 安装程序项目使用 InstallShield,因此它是 github 上托管的解决方案的一部分,但不包含在上面的 zip 文件中(以防您,读者,未安装它)。

最后

如果您觉得这篇文章有帮助或有趣,请投票和/或在下方添加任何评论。谢谢!

修订历史

  • 2012 年 2 月 22 日:根据读者评论,对代码库进行了轻微更新。
© . All rights reserved.