前回の記事で公開した簡易Timerプログラムのソースコードをご紹介します。
WPFといいつつ、MVVMは使っておらず、WindowsFormでおなじみのイベントドリブン方式となっています。
WPFのバインド機能を使ったり、イベントハンドラに処理を記述したりしているため、プログラミングの書き方としてはあまり推奨されない書き方になっていますが、WindowsFormの技術者がWPFの便利なところを活用しつつプログラミングした結果と思っていただければ幸いです。
プログラムの概要
今回公開するTimerプログラムは以下のようなものになります。

使い方は以下の記事をご覧ください。
プロジェクト一式のダウンロード先
Visual Stuido 2019のプロジェクト一式は下記からダウンロード可能です。
XAMLのソースコード
XAML側はコメントを全く入れていないのですが、単純に必要なコントロール(テキストボックス、ラベル、コンボボックス、コンテキストメニュー、スタックパネル、グリッドなど)を張り付けているだけです。
以下のような構造になっていますので、これを念頭にXAMLを見ていただければ理解が深まるかと思います。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
<Window x:Class="MyTimer.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:MyTimer" mc:Ignorable="d" Title="MainWindow" Height="71" Width="203" WindowStyle="None" AllowsTransparency="False" MouseLeftButtonDown="Window_MouseLeftButtonDown" WindowStartupLocation="CenterScreen" Topmost="True" Closed="Window_Closed" Loaded="Window_Loaded"> <Window.Resources> <Style TargetType="Button"> <Setter Property="BorderThickness" Value="0" /> <Setter Property="Background" Value="{x:Null}" /> </Style> <Style TargetType="Image"> <Setter Property="Stretch" Value="None" /> <Setter Property="Width" Value="16" /> <Setter Property="Height" Value="16" /> </Style> <BooleanToVisibilityConverter x:Key="BoolVisibilityConverter" /> <ColorConverter x:Key="ColorConverter"/> </Window.Resources> <Grid Margin="0,0,0,0"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="30"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> </Grid.RowDefinitions> <Viewbox Stretch="Uniform" Grid.Row="0" > <StackPanel> <TextBox x:Name="uxEditPanel" Visibility="Collapsed" TextWrapping="Wrap" Margin="2,0,0,2" Text="00:00:00" PreviewKeyDown="uxEditPanel_PreviewKeyDown" HorizontalAlignment="Center" VerticalAlignment="Center" FontFamily="{Binding ElementName=uxFontList,Path=Text}"/> <TextBlock x:Name="uxCounter" TextWrapping="Wrap" Margin="2,0,0,2" Text="00:00:00" HorizontalAlignment="Center" VerticalAlignment="Center" PreviewMouseDown="uxCounter_PreviewMouseDown" FontFamily="{Binding ElementName=uxFontList,Path=Text}"> <TextBlock.ContextMenu> <ContextMenu> <MenuItem Header="レイアウト" > <StackPanel Orientation="Horizontal"> <Label Content="フォント" Width="50" /> <ComboBox Width="200" x:Name="uxFontList" Margin="0,2,10,2" HorizontalAlignment="Left" ItemsSource="{Binding}"> <ComboBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding}" FontFamily="{Binding}"/> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox> <Button x:Name="uxDefaultFont" Height="20" VerticalContentAlignment="Center" Click="uxDefaultFont_Click" BorderThickness="1" Content="初期値に戻す"></Button> </StackPanel> <StackPanel Orientation="Horizontal"> <Label Content="文字色" Width="50"/> <ComboBox x:Name="uxFontColor" Width="200" Margin="0,2,10,2" ItemsSource="{Binding}" SelectedValue="{Binding Color}" Height="24" VerticalAlignment="Top" VerticalContentAlignment="Center" HorizontalAlignment="Left" SelectionChanged="uxFontColor_SelectionChanged"> <ComboBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Border Width="20" Height="Auto" Margin="5,0" BorderThickness="1" BorderBrush="Black" > <Border.Background> <SolidColorBrush Color="{Binding Color}"/> </Border.Background> </Border> <TextBlock Text="{Binding Name}"></TextBlock> </StackPanel> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox> <Button x:Name="uxDefaultColor" Height="20" VerticalContentAlignment="Center" Click="uxDefaultColor_Click" BorderThickness="1" Content="初期値に戻す"></Button> </StackPanel> </MenuItem> <MenuItem Header="現在のカウントダウン値を変更" Click="EditCountDownMenuItem_Click" ></MenuItem> <MenuItem Header="カウントダウンの開始時間を設定" Click="SetCountDownMenuItem_Click" ></MenuItem> <MenuItem Header="カウントアップ後の動作" > <MenuItem Header="カウントダウンを停止する" x:Name="uxAfterStop" Click="uxCountUpMode_Click" IsCheckable="True"></MenuItem> <MenuItem Header="カウントダウンをループする" x:Name="uxAfterLoop" Click="uxCountUpMode_Click" IsCheckable="True"></MenuItem> <MenuItem Header="マイナスになってもカウントダウンを続ける" x:Name="uxAfterNone" Click="uxCountUpMode_Click" IsCheckable="True"></MenuItem> </MenuItem> <MenuItem Header="カウントアップ時の処理"> <MenuItem Header="プログラムを終了する" x:Name="uxIsShutdown" IsCheckable="True"></MenuItem> <MenuItem Header="コマンドを実行する" x:Name="uxIsCommand" IsCheckable="True"/> <StackPanel Orientation="Horizontal"> <TextBlock Text="(実行したいコマンド" Margin="0,0,10,0"/> <TextBox x:Name="uxCommand" Width="200" /> <TextBlock Text=" )" /> </StackPanel> </MenuItem> <MenuItem x:Name="uxShutdownMenuItem" Header="終了" Click="uxShutdownMenuItem_Click" ></MenuItem> </ContextMenu> </TextBlock.ContextMenu> </TextBlock> </StackPanel> </Viewbox> <StackPanel Grid.Column="1" Height="63" VerticalAlignment="Top" HorizontalAlignment="Right" Width="30"> <Button x:Name="uxStart" HorizontalAlignment="Center" VerticalAlignment="Top" Click="uxStart_Click"> <Image Source="Images/arrow_run_16xLG.png" /> </Button> <Button x:Name="uxStop" HorizontalAlignment="Center" VerticalAlignment="Top" Click="uxStop_Click"> <Image Source="Images/Pause_17x16.png" /> </Button> <Button x:Name="uxReset" HorizontalAlignment="Center" VerticalAlignment="Top" Click="uxReset_Click"> <Image Source="Images/Restart_16x.png" /> </Button> </StackPanel> </Grid> </Window> |
C#側のソースコード
C#側のコードについては、次のような構造になっています。

Timerについては、こちらの記事で紹介した関数を使っています。
一定間隔で実行させたい処理はTimerMethodというメソッドに記述していますが、カウントアップ時の処理分岐(タイマー停止、ループ、マイナスカウント、プログラム終了、任意のコマンド実行)が必要なので、その分が少しだけ複雑になっています。
また、任意のコマンド実行時に例外が発生するとプログラムが異常終了してしまいますので、それを防ぐために try~catch で例外を握りつぶしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; 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.Windows.Threading; using System.ComponentModel; using System.Reflection; using System.Globalization; namespace MyTimer { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { const string LOOP_MODE = "Loop"; const string STOP_MODE = "Stop"; const string NONE_MODE = "None"; const string EDIT_COUNTER = "Counter"; const string EDIT_TIMER = "Timer"; const string EDIT_NONE = ""; const string DEFAULT_FONT = "Harlow Solid Italic"; const int DEFAULT_COLOR_NO = 7; //black private DispatcherTimer _timer; private TimeSpan Timer = new TimeSpan(0, 3, 0); private TimeSpan _needle; private TimeSpan _interval = new TimeSpan(0, 0,1); private TimeSpan _zero = new TimeSpan(0, 0, 0); private string _mode = STOP_MODE; private string _target = EDIT_NONE; public MainWindow() { InitializeComponent(); //時間間隔を1秒に設定し、呼び出したいメソッドを指定 _timer = CreateTimer(1000, TimerMethod); //初期値を設定 _needle = Timer; uxCounter.Text = Timer.ToString(); //フォント一覧の取得 uxFontList.DataContext = Fonts.SystemFontFamilies; //色の一覧を取得 uxFontColor.DataContext = GetColorList(); } /// <summary> /// 画面表示時のイベント /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Window_Loaded(object sender, RoutedEventArgs e) { //App.config から値を取得 _needle = Timer = TimeSpan.TryParse(Properties.Settings.Default.Timer,out TimeSpan ts) ? ts : Timer; uxCounter.Text = Timer.ToString(); uxCommand.Text = Properties.Settings.Default.Command; uxIsCommand.IsChecked = Properties.Settings.Default.IsCommand; uxIsShutdown.IsChecked = Properties.Settings.Default.IsShutdown; uxFontList.Text = Properties.Settings.Default.FontName; uxFontColor.SelectedIndex = Properties.Settings.Default.FontColorNo; _mode = Properties.Settings.Default.Mode; //動作モードを復元 switch(_mode) { case STOP_MODE: uxAfterStop.IsChecked = true; break; case LOOP_MODE: uxAfterLoop.IsChecked = true; break; case NONE_MODE: uxAfterNone.IsChecked = true; break; default: uxAfterStop.IsChecked = true;break; } } /// <summary> /// 画面終了時のイベント /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Window_Closed(object sender, EventArgs e) { _timer?.Stop(); //App.configに値を保存 Properties.Settings.Default.Timer = Timer.ToString(); Properties.Settings.Default.Command = uxCommand.Text; Properties.Settings.Default.IsCommand = uxIsCommand.IsChecked; Properties.Settings.Default.IsShutdown = uxIsShutdown.IsChecked; Properties.Settings.Default.Mode = _mode; Properties.Settings.Default.FontName = uxFontList.Text; Properties.Settings.Default.FontColorNo = uxFontColor.SelectedIndex; Properties.Settings.Default.Save(); } /// <summary> /// フォントを元に戻すボタンクリック時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void uxDefaultFont_Click(object sender, RoutedEventArgs e) { uxFontList.Text = DEFAULT_FONT; } /// <summary> /// 終了メニュークリック時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void uxShutdownMenuItem_Click(object sender, EventArgs e) { Application.Current.Shutdown(); } /// <summary> /// タイマー割り込み時の処理 /// </summary> private void TimerMethod() { //現在の経過時間から1秒マイナスする _needle = _needle.Add(-_interval); //現在の経過時間が0より小さくなったら初期値に戻す if (_needle < _zero) { switch (_mode) { case STOP_MODE: _needle = _zero; _timer.Stop(); break; case LOOP_MODE: _needle = Timer; break; } //カウントアップ時の処理で「コマンドを実行する」がチェックされ、コマンドが記述されている場合 if ((bool)uxIsCommand.IsChecked && uxCommand.Text.Trim() != "") { try { System.Diagnostics.Process.Start(uxCommand.Text); } catch { } } //カウントアップ時の処理で「プログラムを終了する」がチェックされている場合 if ((bool)uxIsShutdown.IsChecked) { Application.Current.Shutdown(); } } //現在の経過時間を表示 uxCounter.Text = _needle.ToString(); } /// <summary> /// 開始ボタンクリック時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void uxStart_Click(object sender, RoutedEventArgs e) { //編集内容のコミット CommitCounterValue(); // タイマを開始 _timer?.Start(); } /// <summary> /// 停止ボタンクリック時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void uxStop_Click(object sender, RoutedEventArgs e) { //編集内容のコミット CommitCounterValue(); //カウンターの開始 _timer?.Stop(); } /// <summary> /// カウントアップ動作モードを選択時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void uxCountUpMode_Click(object sender, RoutedEventArgs e) { var menu = (MenuItem)sender; var header = menu.Header.ToString(); if (header.Contains("停止") && (bool)menu.IsChecked) { _mode = STOP_MODE; uxAfterLoop.IsChecked = uxAfterNone.IsChecked = false; } if (header.Contains("ループ") && (bool)menu.IsChecked) { _mode = LOOP_MODE; uxAfterStop.IsChecked = uxAfterNone.IsChecked = false; } if (header.Contains("マイナス") && (bool)menu.IsChecked) { _mode = NONE_MODE; uxAfterStop.IsChecked = uxAfterLoop.IsChecked = false; } } /// <summary> /// フォントのリセットボタンクリック時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void uxReset_Click(object sender, RoutedEventArgs e) { //編集内容のコミット CommitCounterValue(); //カウンターの停止 _timer?.Stop(); _needle = Timer; uxCounter.Text = Timer.ToString(); } /// <summary> /// 色のリセットボタンクリック時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void uxDefaultColor_Click(object sender, RoutedEventArgs e) { uxFontColor.SelectedIndex = DEFAULT_COLOR_NO; } /// <summary> /// マウス左ボタンクリック時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.ButtonState != MouseButtonState.Pressed) return; this.DragMove(); } /// <summary> /// 時間表示部分のクリック処理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void uxCounter_PreviewMouseDown(object sender, MouseButtonEventArgs e) { if(e.ClickCount == 2) { EditCounterValue(EDIT_COUNTER); } } /// <summary> /// タイマー生成 /// </summary> /// <param name="interval"></param> /// <param name="action"></param> /// <returns></returns> private DispatcherTimer CreateTimer(int interval, Action action) { // 優先順位を指定してタイマのインスタンスを生成 DispatcherTimer tm = new DispatcherTimer(DispatcherPriority.Background); // インターバルを設定 tm.Interval = new TimeSpan(0, 0, 0, 0, interval); // タイマメソッドを設定 tm.Tick += (e, s) => { action(); }; return tm; } /// <summary> /// カウントダウン開始時間の設定 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void SetCountDownMenuItem_Click(object sender, RoutedEventArgs e) { EditCounterValue(EDIT_TIMER); } /// <summary> /// 現在のカウントダウン値の編集 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void EditCountDownMenuItem_Click(object sender, RoutedEventArgs e) { EditCounterValue(EDIT_COUNTER); } /// <summary> /// 編集モードの開始 /// </summary> /// <param name="targetName"></param> private void EditCounterValue(string targetName) { _target = targetName; if (targetName == EDIT_COUNTER) { uxEditPanel.Text = uxCounter.Text; } if(targetName == EDIT_TIMER) { uxEditPanel.Text = Timer.ToString(); } uxEditPanel.Visibility = Visibility.Visible; uxCounter.Visibility = Visibility.Collapsed; } /// <summary> /// 編集結果の確定 /// </summary> private void CommitCounterValue() { if (!TimeSpan.TryParse(uxEditPanel.Text, out TimeSpan value)) { MessageBox.Show("指定された時間が不正です", "エラー", MessageBoxButton.OK, MessageBoxImage.Error); } else { //Timer編集時 if(_target == EDIT_TIMER) { Timer = value; } //カウンター編集時 if(_target == EDIT_COUNTER) { uxCounter.Text = value.ToString(); _needle = value; } } _target = EDIT_NONE; uxEditPanel.Visibility = Visibility.Collapsed; uxCounter.Visibility = Visibility.Visible; } /// <summary> /// エンターキーの入力処理(エンターで編集結果を確定) /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void uxEditPanel_PreviewKeyDown(object sender, KeyEventArgs e) { if(e.Key == Key.Enter) { CommitCounterValue(); } } /// <summary> /// すべての色を取得するメソッド /// </summary> /// <returns></returns> private MyColor[] GetColorList() { return typeof(Colors).GetProperties(BindingFlags.Public | BindingFlags.Static) .Select(i => new MyColor() { Color = (Color)i.GetValue(null), Name = i.Name }).ToArray(); } /// <summary> /// 文字色ドロップダウンリスト選択時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void uxFontColor_SelectionChanged(object sender, SelectionChangedEventArgs e) { uxCounter.Foreground = new SolidColorBrush(((MyColor)uxFontColor.SelectedItem).Color); } } /// <summary> /// 色と色名を保持するクラス /// </summary> public class MyColor { public Color Color { get; set; } public string Name { get; set; } } } |
構成ファイル(app.config)について
画面から変更したフォントや文字色、ダウントダウン初期値、カウントダウン後の動作などの設定情報は、app.config ファイルに保存されています。
今回の場合、プログラムフォルダに存在するMyTimer.exe.config が該当しますが、実はここを読み書きしているのではなく、 次の場所に保管されている user.config が使われます。
1 |
C:\Users\ユーザー名\AppData\Local\MyTimer\実行ファイル名から始まるフォルダ自動採番されたフォルダ\1.0.0.0\user.config |
例えば、hoge というユーザーが MyTimerを実行した場合、以下のようになります。
1 |
C:\Users\hoge\AppData\Local\MyTimer\MyTimer.exe_Url_kftd2iywhtkgtzeowjfyfs0ts3g4o2i5\1.0.0.0\user.config |
では、実行ファイルが存在する MyTimer.config は削除してよいかというと、そうではありません。
これはMicrosoftの仕様のようで、プログラム起動時に MyTimer.config を必ず参照するようになっており、もし存在しなければプログラムはフリーズしたままになります。
MyTimer.config は、初回起動時に初期値を参照するだけかと思いきや、そうではありませんので、不要と判断して削除しないようにしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" > <section name="MyTimer.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" /> </sectionGroup> </configSections> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /> </startup> <userSettings> <MyTimer.Properties.Settings> <setting name="Timer" serializeAs="String"> <value /> </setting> <setting name="Mode" serializeAs="String"> <value /> </setting> <setting name="Command" serializeAs="String"> <value /> </setting> <setting name="IsCommand" serializeAs="String"> <value>False</value> </setting> <setting name="IsShutdown" serializeAs="String"> <value>False</value> </setting> <setting name="FontName" serializeAs="String"> <value>Harlow Solid Italic</value> </setting> <setting name="FontColorNo" serializeAs="String"> <value>0</value> </setting> </MyTimer.Properties.Settings> </userSettings> </configuration> |
まとめ
今回は「Timerでタイマーを作ってみました」の記事で紹介したTimerプログラムのソースコードについて紹介しました。
本来ならMVVMをつかうのが正統派なのかもしれませんが、メリットが全く感じられないので今回はC#でゴリゴリ作成しています。
私自身がWindowsFormに慣れているというものあるのですが、逆にMVVMでプログラミングされている技術者の数が圧倒的に少ないので、むしろ今回の書き方の方がシックリいく方が多いのではないかと思っています。
もし皆さんのプログラミングで何かのヒントになれば幸いです。