■はじめに
今回はアラームを作りながら正規表現やファイル読み書きなどを学びます。
入力チェックなどもして今までよりも少しまともなアプリケーションを作ります。
キーワード:正規表現, ファイル入出力, ファイル保存ダイアログ, タイマー, Dictionary, スタイル, Null条件演算子
[注意]
これまでの回で説明済みの操作方法等は、説明を省略したり簡略化している場合があります。
■開発環境
- Windows 10
- Visual Studio Community 2017
- .NET Framework 4.x
■作ってみる
◇画面作成
新しいプロジェクトで Visual C#
- Windows デスクトップ
- WPF アプリ
で任意の名前でプロジェクトを作成します。ここでは PoorAlarm と入力しました。
そして以下のように画面を作成します。
Gridを横3×縦2に分割し、上段にLabel, TextBox, Button, ListBoxを、下段にRadioButtonを配置します。Gridの分割は 連載4回目 や 連載5回目 でやりましたね。
GridのFocusManager.FocusedElement
は初期フォーカスを時刻のテキストボックスに設定しています。
<Grid FocusManager.FocusedElement="{Binding ElementName=timeText}" Background="LightCyan">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel>
<Label Content="時刻"/>
<TextBox Width="60" x:Name="timeText" MaxLength="5"/>
<Label Content="メッセージ"/>
<TextBox Height="60" Width="200" x:Name="msgText" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"/>
</StackPanel>
<Button Grid.Column="1" Content="追加 >" IsDefault="True" Height="30" VerticalAlignment="Top" Margin="5,50" Padding="5"/>
<ListBox Grid.Column="2" Margin="10" x:Name="listBox"/>
<StackPanel Orientation="Horizontal" Grid.Row="1" Grid.ColumnSpan="3" Background="PaleTurquoise">
<RadioButton x:Name="alarmOn" Content="アラームON"/>
<RadioButton x:Name="alarmOff" Content="アラームOFF" IsChecked="True"/>
</StackPanel>
</Grid>
WindowのResizeModeプロパティを変更してサイズ変更グリップを表示するようにしています。
Window.Resourcesの中はLabel, TextBox, RadioButtonの既定のスタイルを設定しています。
連載10回目 ではスタイルに任意の名前を付けて、各コントロールのStyleプロパティでその名前を指定して適用していました。
<Window x:Class="PoorAlarm.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:PoorAlarm"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" ResizeMode="CanResizeWithGrip">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="Margin" Value="5,10,5,2"/>
<Setter Property="Padding" Value="0"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Margin" Value="5,0"/>
</Style>
<Style TargetType="RadioButton">
<Setter Property="Margin" Value="5"/>
</Style>
</Window.Resources>
<Grid FocusManager.FocusedElement="{Binding ElementName=timeText}" Background="LightCyan">
:
:
いったん実行してみます。
こんな感じ。
時刻とメッセージを入力して追加ボタンを押すと右側の一覧に登録され、
指定した時刻が来たら(アラームがONになっている場合)メッセージが表示される。
というものにします。
追加した時刻とメッセージの一覧は終了時にテキストファイルに保存し、次回起動時に読み込むようにします。
◇設定項目作成
アラームON/OFFラジオボタン選択値と、時刻・メッセージ一覧ファイルのパスを記録しておく設定項目を用意します。
これは 連載10回目 でやりました。
ソリューションエクスプローラーでSettings.settings
をダブルクリックし、
AlarmEnabled
というbool型の設定項目と、SaveConfigPath
というstring型の設定項目を用意します。
◇設定ファイル存在確認メソッド作成
設定のSaveConfigPath
に値が設定されているか、設定されている場合はそのファイルが存在するか確認するメソッドを作成します。
このメソッドは後で使います。
/// <summary>
/// 一覧設定ファイルパス存在チェック
/// </summary>
/// <returns>設定が有効ならtrue</returns>
private bool IsExistsSaveConfigPath()
{
if (string.IsNullOrWhiteSpace(Properties.Settings.Default.SaveConfigPath) ||
System.IO.File.Exists(Properties.Settings.Default.SaveConfigPath) == false)
{
// パスの設定が無い、または指定した場所にファイルが存在しない
return false;
}
else
{
// パスが設定されていてファイルが実在する
return true;
}
}
◇文字列リソース作成
アプリのタイトルを文字列リソースとして登録します。
ソリューションエクスプローラーでResources.resx
をダブルクリックし、
「アクセス修飾子」をPublic
にし、
「名前」にAppTitle
、
「値」にこのアプリの名前、
「コメント」にこの文字列リソースをどこで使うのか等の説明を入力します。コメントは後で見た時に分かりやすくするためのものなので空欄でも構いません。
文字列リソースは英語版や日本語版など、多言語対応時に活きてくるものです。
本来は画面のラベルやメッセージボックスのメッセージ内容なども文字列リソースとして登録しますが、今回は使い方の紹介なのでタイトルだけ登録します。
ここまでで一度ビルドしておきましょう。
◇文字列リソースの利用
Xamlにxmlns:prop="clr-namespace:PoorAlarm.Properties"
を追加します。
これでPropertiesがprop
という識別子でアクセス可能になったので、
Title
プロパティの値を {x:Static prop:Resources.AppTitle}
に書き換えます。
<Window x:Class="PoorAlarm.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:PoorAlarm"
xmlns:prop="clr-namespace:PoorAlarm.Properties"
mc:Ignorable="d"
Title="{x:Static prop:Resources.AppTitle}" Height="450" Width="800" ResizeMode="CanResizeWithGrip">
◇メッセージボックス表示メソッド作成
メッセージボックスを表示するためのメソッドを作成します。
連載1回目 のメッセージボックスと違い、メッセージボックスタイトルとアイコンも指定しています。
ここでもタイトル部分にリソース文字列を使用しています。
/// <summary>
/// 情報メッセージ表示
/// </summary>
/// <param name="msg"></param>
private void InfoMsg(string msg)
{
MessageBox.Show(msg, Properties.Resources.AppTitle,
MessageBoxButton.OK, MessageBoxImage.Information);
}
/// <summary>
/// エラーメッセージ表示
/// </summary>
/// <param name="msg"></param>
private void ErrMsg(string msg)
{
MessageBox.Show(msg, Properties.Resources.AppTitle,
MessageBoxButton.OK, MessageBoxImage.Error);
}
◇追加ボタンの処理1
追加ボタンのイベントハンドラを記述します。
<Button Grid.Column="1" Content="追加 >" IsDefault="True" Height="30" VerticalAlignment="Top" Margin="5,50" Padding="5" Click="Button_Click"/>
/// <summary>
/// 追加ボタンクリック時の処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Button_Click(object sender, RoutedEventArgs e)
{
DateTime d;
// 時刻チェック
//(数字2桁:数字2桁か簡易チェックしてOKならDateTime型に変換できるかチェックする)
if (System.Text.RegularExpressions.Regex.IsMatch(timeText.Text, "^[0-9]{2}:[0-9]{2}$") &&
DateTime.TryParse("2000/01/01 " + timeText.Text, out d))
{
// 時刻として正しい
// メッセージが入力されているか?
if (string.IsNullOrWhiteSpace(msgText.Text))
{
// メッセージ未入力(または空白しか入力されていない)
ErrMsg("メッセージを入力してください。");
msgText.Focus();
}
else
{
// 時刻もメッセージも入力されている
// 一覧に追加
listBox.Items.Add(timeText.Text + "\t" + msgText.Text);
timeText.Clear();
msgText.Clear();
timeText.Focus();
}
}
else
{
// 時刻として正しくない
ErrMsg("時刻を正しく入力してください。例)12:34");
timeText.Focus();
}
}
時刻チェック1
まず、正規表現というものを使って入力された時刻が「数字2桁」 + 「:」 + 「数字2桁」になっているかチェックしています。
条件に合致していればRegexクラスのIsMatchメソッドはtrueを返します。
"^[0-9]{2}:[0-9]{2}$"
の^
は行頭、[0-9]
は0~9のいずれか、{2}
は直前(左)の文字を2回繰り返す、$
は行末を意味します。
"^あいう"
という正規表現は、「あいう」から始まる"あいうえお"
にはヒットしますが"ああいうえお"
にはヒットしません。
"^いよ$"
という正規表現は"いよ"
にはヒットしますが、"いいよ"
、"いよかん"
、"たいよう"
にはヒットしません。
時刻チェック2
しかしこのままでは25:99
なんてものもチェックを通ってしまいます。
そこで次にDateTimeクラスのTryParseメソッドを使って日付型に変換できるか確認しています。
日付型への変換が成功すれば戻り値にtrueが返り、2番目の引数に変換後の日付オブジェクトが設定されます。が、今回は日付に変換できるか知りたいだけなのでd
変数はこの先使いません。
時刻とメッセージの一覧への追加
メッセージが入力されていることを確認したらリストボックスに時間とメッセージをタブ区切りで追加します。\t
はタブ文字です。
途中のチェックでエラーがあればエラーメッセージを表示します。
◇動作確認
ここまでで一度実行してみましょう。
正しくない時刻を入力して追加ボタンを押してみると、エラーメッセージが表示されます。
正しい時刻とメッセージを入力して追加ボタンを押すと右の一覧に追加されます。
◇ウィンドウ起動時と終了時の処理
ウィンドウ起動時と終了時のイベントハンドラを用意します。
<Window x:Class="PoorAlarm.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:PoorAlarm"
xmlns:prop="clr-namespace:PoorAlarm.Properties"
mc:Ignorable="d"
Title="{x:Static prop:Resources.AppTitle}" Height="450" Width="800" ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded" Closing="Window_Closing">
/// <summary>
/// ウィンドウ起動時
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Loaded(object sender, RoutedEventArgs e)
{
}
/// <summary>
/// ウィンドウ終了時
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
}
終了時の処理
アプリケーション終了時に呼び出す、一覧に追加した内容をファイルに保存するメソッドを作成します。
using { }
で囲った部分が終了すれば開かれたファイルは自動的に閉じられます。
StreamWriterクラスを生成する部分は、コードを見ただけでパラメータの意味が分かるよう、パラメータ名も書いています。
ここは Properties.Settings.Default.SaveConfigPath, false, Encoding.UTF8
と記述することもできます。
2番目の引数false
は追記しない、つまり毎回ファイルを作り直すという意味です。
/// <summary>
/// 一覧ファイル保存
/// </summary>
/// <remarks>
/// 前提:SaveConfigPathに実在するパスが設定されていること
/// </remarks>
private void SaveConfigFile()
{
using (var sw = new System.IO.StreamWriter(
Properties.Settings.Default.SaveConfigPath, append: false, encoding: Encoding.UTF8))
{
// リストボックスの内容をファイル出力
foreach (string item in listBox.Items)
{
// 一行書き込み
sw.WriteLine(item);
}
}
}
保存するファイルパスを指定するためのダイアログを表示する処理です。
ファイルが指定されたら上で作成したファイル保存の処理を呼んでいます。
/// <summary>
/// ファイル保存ダイアログを表示してファイル保存
/// </summary>
private void ShowSaveDialogToConfigFile()
{
// ダイアログ生成
var dlg = new Microsoft.Win32.SaveFileDialog();
// ダイアログタイトル設定
dlg.Title = "設定を保存";
// 保存する一覧ファイルの既定ファイル名を設定
dlg.FileName = Properties.Resources.AppTitle + "_設定.dat";
// フィルタ設定
dlg.Filter = "設定ファイル|*.dat|全てのファイル|*.*";
// 保存ダイアログ表示
if (dlg.ShowDialog() == true)
{
// ダイアログでOKされたらファイルパス設定
Properties.Settings.Default.SaveConfigPath = dlg.FileName;
// ファイル保存
SaveConfigFile();
}
}
ウィンドウ終了イベントに処理を記述します。
Properties.Settings.Default.AlarmEnabled = alarmOn.IsChecked == true;
の部分は、アラームONのIsCheckedプロパティがtrueの場合にAlarmEnabled
にtrueを、その他(nullかfalse)の場合にAlarmEnabled
にfalseを設定しています。
Settingsの変更をファイル保存するにはSaveメソッドを呼びます。
/// <summary>
/// ウィンドウ終了時
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
try
{
if (listBox.Items.Count > 0)
{
// 一覧の保存先パスチェック
if (IsExistsSaveConfigPath())
{
// ファイル保存
SaveConfigFile();
}
else
{
// 一覧ファイル保存先パスが未設定か、指定した場所にファイルが無い場合
// ファイル保存ダイアログを表示してファイル保存
ShowSaveDialogToConfigFile();
}
}
// 画面のアラームOn/OFF設定値をSettingsに反映
Properties.Settings.Default.AlarmEnabled = alarmOn.IsChecked == true;
// 設定ファイル保存
Properties.Settings.Default.Save();
}
catch (Exception ex)
{
ErrMsg(ex.Message);
}
}
初期処理、タイマー処理
色々と変数を用意します。
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
/// <summary>
/// 前回メッセージ表示時間
/// </summary>
/// <remarks>
/// 同じ時間に何度もメッセージが出ないようにするためのもの
/// </remarks>
string showTime;
/// <summary>
/// 時間とメッセージ一覧
/// </summary>
Dictionary<string, string> timeAndMsgs;
/// <summary>
/// タイマー
/// </summary>
private System.Windows.Threading.DispatcherTimer timer;
初期化用のメソッドを作成します。
Dictionaryクラスはキーと値のセットを格納できます。
Dictionaryの後ろの<>
の意味は<キーの型, 値の型>
です。
今回はキーに時刻の文字列、値にメッセージの文字列を格納します。
タイマーは 連載2回目 でも使用しましたが、タイマーイベントの登録を別パターンで書いています。
/// <summary>
/// 初期化処理
/// </summary>
private void InitProc()
{
// 登録時間一覧初期化
timeAndMsgs = new Dictionary<string, string>();
showTime = "";
// タイマー初期化
timer = new System.Windows.Threading.DispatcherTimer();
// イベント発生間隔を200ミリ秒に設定
timer.Interval = TimeSpan.FromMilliseconds(200);
// タイマーイベント設定
timer.Tick += new EventHandler(timer_Tick);
}
こちらがタイマーイベントの処理です。
timeAndMsgs
というのは先ほど用意したDictionaryです。
現在時刻の文字列を取得し、それがtimeAndMsgsの中にあればメッセージを表示します。
200ミリ秒ごとにこのイベントが発生するようにしているので、そのままだと分が変わるまで何度もメッセージが出てしまいます。
その対策でメッセージを表示したらshowTime
変数に時刻を保存し、同じ分に連続してメッセージが出ないようにしています。
メッセージ表示部分の timeAndMsgs[time]
は、Dictionary変数にキーを渡すことで対応する値を取得できます。
つまり時間文字列を渡して対応するメッセージを取得しています。
/// <summary>
/// タイマーイベント
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void timer_Tick(object sender, EventArgs e)
{
// 現在時刻取得
string time = DateTime.Now.ToString("HH:mm");
// 表示済みの時間ではないか?登録されている時刻か?
if (time != showTime &&
timeAndMsgs.ContainsKey(time))
{
// メッセージ表示時間を保存
showTime = time;
// 対応するメッセージ表示
InfoMsg(timeAndMsgs[time]);
}
}
そして初期化処理をコンストラクタで呼ぶようにします。
/// <summary>
/// コンストラクタ
/// </summary>
public MainWindow()
{
InitializeComponent();
// 初期化処理
InitProc();
}
追加ボタンの処理2
追加ボタンを押したときに、同じ時刻を登録できないようにDictionaryを使って重複チェックをし、
リストボックスへの追加と同時にDictionaryへも追加するようにします。
:
else
{
// 時刻もメッセージも入力されている
// まだ追加されてない時刻かチェック
if (timeAndMsgs.ContainsKey(timeText.Text))
{
ErrMsg("この時刻は追加済みです。");
}
else
{
// 一覧に追加
listBox.Items.Add(timeText.Text + "\t" + msgText.Text);
timeAndMsgs.Add(timeText.Text, msgText.Text);
timeText.Clear();
msgText.Clear();
}
timeText.Focus();
}
:
起動時の処理
ウィンドウ起動時の処理を作成します。
ファイルの読み込みはStreamReader
を使います。
/// <summary>
/// ウィンドウ起動時
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Loaded(object sender, RoutedEventArgs e)
{
try
{
// 一覧設定あり?
if (IsExistsSaveConfigPath())
{
using (var sr = new System.IO.StreamReader(
Properties.Settings.Default.SaveConfigPath, Encoding.UTF8))
{
// ファイルの終わりまで繰り返し
while (sr.EndOfStream == false)
{
// 一行読み込み
string line = sr.ReadLine();
// 分解
string[] lineAry = line.Split('\t');
if (lineAry.Length == 2)
{
// 追加
listBox.Items.Add(line);
timeAndMsgs.Add(lineAry[0], lineAry[1]);
}
}
}
}
// アラームON/OFF設定値読み込み
alarmOn.IsChecked = Properties.Settings.Default.AlarmEnabled;
if (alarmOn.IsChecked == true)
{
// アラームONならタイマー開始
timer.Start();
}
}
catch (Exception ex)
{
ErrMsg(ex.Message);
}
}
◇ラジオボタンの動作
ラジオボタンのCheckedイベントを作成します。
2つともalarmOnOff_Checked
を呼ぶようにします。
複数のコントロールに同じ処理を設定するのは 連載5回目 でやりました。
<StackPanel Orientation="Horizontal" Grid.Row="1" Grid.ColumnSpan="3" Background="PaleTurquoise">
<RadioButton x:Name="alarmOn" Content="アラームON"
Checked="alarmOnOff_Checked"/>
<RadioButton x:Name="alarmOff" Content="アラームOFF" IsChecked="True"
Checked="alarmOnOff_Checked"/>
</StackPanel>
アラームONがチェックされたらタイマーを起動し、OFFがチェックされたらタイマーを停止します。
/// <summary>
/// アラームON/OFFチェック時
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void alarmOnOff_Checked(object sender, RoutedEventArgs e)
{
if (timer == null)
{
return;
}
if (alarmOn.IsChecked == true)
{
timer.Start();
}
else
{
timer.Stop();
}
}
■動かしてみる
アラームONのラジオボタンが選択されているときに、右の一覧に登録されている時間が来たらメッセージが表示されるようになりました。
時間が来てもアラームOFFが選択されている場合はメッセージは出ません。
アプリケーションを閉じると、ファイル保存ダイアログが表示され、一覧ファイルの保存先を聞かれます。
次回起動時はそのファイルを読み込み、一覧を画面に復元します。
■おまけ(ソース整理)
追加ボタンを押したときの処理が、if文が入り組んでいて分かりづらいので少し整理します。
エラーチェックしてエラーになったらreturnですぐ処理を抜けるようにし、if文のネストが深くならないようにしました。
/// <summary>
/// 追加ボタンクリック時の処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Button_Click(object sender, RoutedEventArgs e)
{
DateTime d;
// 時刻の形式が正しいか?
//(数字2桁:数字2桁か簡易チェックしてOKならDateTime型に変換できるかチェックする)
if (System.Text.RegularExpressions.Regex.IsMatch(
timeText.Text, "^[0-9]{2}:[0-9]{2}$") == false ||
DateTime.TryParse("2000/01/01 " + timeText.Text, out d) == false)
{
// 時刻として正しくない
ErrMsg("時刻を正しく入力してください。例)12:34");
timeText.Focus();
return;
}
// メッセージが入力されているか?
if (string.IsNullOrWhiteSpace(msgText.Text))
{
// メッセージ未入力(または空白しか入力されていない)
ErrMsg("メッセージを入力してください。");
msgText.Focus();
return;
}
// 時刻もメッセージも入力されている
// まだ追加されていない時刻かチェック
if (timeAndMsgs.ContainsKey(timeText.Text))
{
ErrMsg("この時刻は追加済みです。");
}
else
{
// 一覧に追加
listBox.Items.Add(timeText.Text + "\t" + msgText.Text);
timeAndMsgs.Add(timeText.Text, msgText.Text);
timeText.Clear();
msgText.Clear();
}
timeText.Focus();
}
ラジオボタン選択時の処理を修正します。
timer変数が初期化される前にイベントが動いてしまうとStartやStopメソッドを呼ぶときにエラーになってしまうため、nullチェックをしていました。
これをNull条件演算子「?.」を使うやり方に変えます。
nullチェックしていたif文を削除し、timer.Start()
を timer?.Start()
に変えます。Stopも同様です。
?.
の左側のオブジェクトがnullの場合は右側は参照されず、nullを返します。今回の例で言えばStartやStopメソッドは実行されません。
?.
の左側がnull以外の時は右側が実行されます。
Null条件演算子は連続して使うこともできます。
A?.B?.C()
は、オブジェクトAとBがnullではないときに、メソッドCが実行されます。
/// <summary>
/// アラームON/OFFチェック時
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void alarmOnOff_Checked(object sender, RoutedEventArgs e)
{
if (alarmOn.IsChecked == true)
{
timer?.Start();
}
else
{
timer?.Stop();
}
}
おしまい