C# 初級講座
VB2019 Visual Studio 2022

第12回 期間

2022/6/26

この記事が対象とする製品・バージョン

Visual Studio 2022 Visual Studio 2022 対象です。
Visual Studio 2019 Visual Studio 2019 対象です。
Visual Studio 2017 Visual Studio 2017 対象外ですが、参考になります。
Visual Studio 2015 Visual Studio 2015 対象外ですが、参考になります。
Visual Studio 2013 Visual Studio 2013 対象外ですが、参考になります。
Visual Studio 2012 Visual Studio 2012 対象外ですが、参考になります。
Visual Studio 2010 Visual Studio 2010 対象外ですが、参考になります。
Visual Studio 2008 Visual Studio 2008 対象外ですが、参考になります。
Visual Studio 2005 Visual Studio 2005 対象外ですが、参考になります。
Visual Studio.NET 2003 Visual Studio.NET 2003 × 対象外です。
Visual Studio.NET 2002 Visual Studio.NET (2002) × 対象外です。
  Visual Studio Code 対象外ですが、参考になります。

 

目次

 

1.今回作成するプログラム

2つの日時の差を何日とか何時間とか計算するプログラムを作ってみましょう。

 

今回作成するプログラムは、開始日時と終了日時を入力するとその差が何日間か、何時間か、何分間かを表示します。

日付に変換できない時間が入力された場合はテキストボックスが赤くなります。

 

まずは下記の設定で新規プロジェクトを作成してください。

プロジェクトテンプレート Windows フォーム アプリ
プロジェクト名 TimeClac
ソリューション名 TimeCalc
フレームワーク .NET 6.0

 

2.コントロールの配置

フォームには次のようにコントロールを配置します。

なお、TextBoxの高さは、既定ではフォントのサイズによって自動的に変わります。既定ではマウスで高さを変更しようとしてもできません。フォントのサイズは後でプロパティで設定します。

 

コントロールを配置したら、プロパティウィンドウでプロパティを次の通り設定してください。

コントロールの名前 種類 プロパティ 備考
イベント Form1 Form イベント FormBorderStyle Fixed3D フォームのサイズを変更できないようにします。
    イベント MaximizeBox false 最大化ボタンを押せなくします。
    イベント Text 経過時間  
イベント txtDate1 TextBox イベント Font サイズを20にする  
イベント txtDate2 TextBox イベント Font サイズを20にする  
イベント label1 Label イベント Font サイズを20にする  
    イベント Text 開始日時  
イベント label2 Label イベント Font サイズを20にする  
    イベント Text 終了日時  
イベント lblDays Label イベント Font サイズを20にする  
    イベント TextAlign MiddleRight 文字を右寄せで表示します。
イベント lblHours Label イベント Font サイズを20にする  
    イベント TextAlign MiddleRight 文字を右寄せで表示します。
イベント lblMinutes Label イベント Font サイズを20にする  
    イベント TextAlign MiddleRight 文字を右寄せで表示します。

 

 

3.プログラム

フォームの何もないところをダブルクリックして、Loadイベントハンドラーに次の通りプログラムします。

private void Form1_Load(object sender, EventArgs e)
{
    lblDays.Text = "";
    lblHours.Text = "";
    lblMinutes.Text = "";
    txtDate1.Text = DateTime.Now.ToString("g");
    txtDate2.Text = DateTime.Now.ToString("g");
}

Loadイベントは、プログラムを開始してフォームが表示される前に実行されます。このプログラムではlblDays, lblHours, lblMinutes を何も表示されていない状態にします。

そして、txtDate1 と txtDate2 には DateTime.Now を使ってシステム時刻を表示します。ToStringで指定している書式 g は標準の書式指定子で、これ一文字で 2028/06/26 14:23 のように年月日時分を文字列化します。

この時点で実行すると次のように両方のテキストボックスに同じシステム時刻が表示された状態になります。右側の3つのラベルは何も表示されません。

 

次に、Loadハンドラーの下に下記の通りプログラムを記述してください。

Loadハンドラーの最後の } より後です。

private void txtDate_TextChanged(object sender, EventArgs e)
{
    //▼コントロールを初期状態にする。
    lblDays.Text = "";
    lblHours.Text = "";
    lblMinutes.Text = "";
    txtDate1.BackColor = SystemColors.Window;
    txtDate2.BackColor = SystemColors.Window;

    //▼入力された文字列が日付であれば日付にする。
    if (!DateTime.TryParse(txtDate1.Text, out DateTime date1))
    {
        //txtDate1が日付と解釈できない場合、背景色を朱色にする。
        txtDate1.BackColor = Color.Salmon;
        return; //ここから下は実行しない。
    }

    if (!DateTime.TryParse(txtDate2.Text, out DateTime date2))
    {
        //txtDate2が日付と解釈できない場合、背景色を朱色にする。
        txtDate2.BackColor = Color.Salmon;
        return; //ここから下は実行しない。
    }

    //▼日付の差を計算し、表示する。
    TimeSpan span = date2 - date1;

    lblDays.Text = span.TotalDays.ToString("0.0 日");
    lblHours.Text = span.TotalHours.ToString("0.0 時間");
    lblMinutes.Text = span.TotalMinutes.ToString("0.0 分間");
}

このプログラムが今回のポイントです。この段階ではどのイベントとも関連付けられていないのでこのプログラムが実行されることはありません。

フォームデザイナーでtxtDate1 を選択し、プロパティウィンドウで、イベントを選択し、TextChangedイベント欄から txtDate_TextChangedイベントを選択してください。

同じことを txtDate2 でも実行してください。

これで txtDate_TextChanged は txtDate1 と txtDate2 で共通する TextChangedイベント(読み方:TextChanged=テキストチェンジド)のハンドラーになりました。

TextChangedイベントはテキストボックス内の文字列が変化した時に実行されるイベントです。

 

 

次からこのプログラムの内容とポイントを確認しておきましょう。

 

4.色

4-1.初期化のプログラム

//▼コントロールを初期状態にする。
lblDays.Text = "";
lblHours.Text = "";
lblMinutes.Text = "";
txtDate1.BackColor = SystemColors.Window;
txtDate2.BackColor = SystemColors.Window;

最初のこの部分で、日数・時間・分の表示をいったん空にします。後で計算した値を設定しますが、入力にエラーがあって処理できない場合は値を設定できないので空のままになります。

最後の2行はテキストボックスのBackColorプロパティ(読み方:BackColor=バックカラー)を使って背景色を設定しています。

このプログラムでは、この状態を初期状態として、後で値を表示したり色を変更したりしていきます。

このように初期状態を設定することを「初期化」と呼びます。

 

4-2.Color構造体による色の表現

このプログラムではエラーがある時にテキストボックスの背景色をサーモン色(薄い赤)にするので、それをリセットして、既定の色に戻すのが txtDate1.BackColor = SystemColors.Window という行です。

この色の設定について、ちょっと長くなりますが順を追って詳しくて説明しましょう。

BackColorプロパティはColor型(読み方:Color=カラー)です。Color型はその名の通り色を表現する型です。FromArgbという静的(読み方:FromArgc=フロムエーアールジービー)を使って自由に色を合成してColor構造体のインスタンスを作成できます。

たとえば、次のプログラムでは赤220, 緑10, 青180 の割合で色を合成したColor構造体のインスタンスを作成します。この割合で色を合成すると赤が強い紫になります。そしてそれをthis(フォームに記述した場合はフォームを表します)のBackColorプロパティに設定します。

Color mycolor = Color.FromArgb(220, 10, 180);
this.BackColor = mycolor;

結果としてフォームの背景色がピンクっぽい紫に代わります。

すべての色は赤・緑・青を0~255の割合で合成したものとして表現されます。すべて0だと黒になります。すべて255だと白になります。

ただ、これで毎回色を合成するのは面倒なので、赤、青、緑などのよく使う数十個の色は特別に簡単にインスタンスを生成できる静的プロパティが用意されています。たとえば、赤は Color.Red、 青は Color.Blue、緑は Color.Green (読み方:Red=レッド、Blue=ブルー、Green=グリーン)です。

Color mycolor = Color.Red; // Color.FromArgb(255, 0, 0)と同じ
this.BackColor = mycolor;

 

 

4-3.環境によって異なる色

それから、Windowsで設定されている色も簡単に生成できる特別な静的プロパティがあります。Windowsの色の設定はあまり変更しない人が多いかもしれませんが、個人用設定から変更できます。

この設定画面ではいくつかのポイントとなる色を選択できるだけですが、Windowsの内部ではさらに細かくこの部分はこの色、あの部分はあの色という設定が保存されています。

これらの色はユーザーの設定で変わるので、グレーとかブルーとか色の名前で表現することができません。そこで .NETのフレームワークでは、これらの色を表現するためにさきほどのColor構造体の静的プロパティとは別に、SystemColorsクラス(読み方:システムカラーズ)クラスの静的プロパティが用意されています。

SystemColors.Windowはウィンドウのクライアント領域(=ウィンドウ内の何もない部分)の色を表しています。多くのユーザーは白だと思いますが、違う色に設定されている場合もあるので、白を表すColor.White (読み方:White=ホワイト)ではなく、SystemColors.Windowを使ってテキストボックスの背景色を既定の色に設定しているというわけです。

思った以上に説明が長くなりました…。

ただ、Windowsの色の設定画面でこの色を変更することはできませんし、Windows フォーム アプリはダークモードにも自動では対応していないので、SystemColors.Windowsと書かないで Color.White と書いても実際のところほとんど問題ないことと思います。

 

5.入力された日付の取得

5-1.日付の取得

プログラムの次の部分ではテキストボックスに入力された値が日付であるか確認し、日付であるならば日付型(DateTime)の変数に格納します。

テキストボックスが2つあるので、同じ処理を2つ並べています。

//▼入力された文字列が日付であれば日付にする。
if (!DateTime.TryParse(txtDate1.Text, out DateTime date1))
{
    //txtDate1が日付と解釈できない場合、背景色を朱色にする。
    txtDate1.BackColor = Color.Salmon;
    return; //ここから下は実行しない。
}

if (!DateTime.TryParse(txtDate2.Text, out DateTime date2))
{
    //txtDate2が日付と解釈できない場合、背景色を朱色にする。
    txtDate2.BackColor = Color.Salmon;
    return; //ここから下は実行しない。
}

DateTime構造体の静的メソッド TryParse (読み方:TryParse=トライパース)は、ちょっと珍しいメソッドで、他ではあまり使われていない C# のレアな機能を使っています。

TryParseメソッドには2つの機能があります。

1つ目の機能は文字列型の値を日付型(DateTime)に変換できるかどうか確認し、変更可能なら true 、 変換できないのであれば false を返す機能です。対象の値は第1引数で指定します。

2つ目の機能は、日付型に変換できる場合、変換を実行し、結果を第2引数で宣言した変数に設定します。

 

見た目でわかりますが第2引数で変数を宣言sにて、その変数に値を設定するという機能がとても特殊です。こんなところで変数を宣言できるのも特殊ですし、その変数に値が代入されるのも特殊です。C#にはこのような機能が用意はされてはいるのですが、わかりにくい機能なのであまり使用されていません。この機能を使用するとき、対象の引数を指定する時にその前にキーワード out (読み方:out=アウト)を付けて目印にすることになっています。 out を省略するとエラーです。

なお、この宣言でも型推論を使用できるので、DateTime の代わりに var を記述することもできます。

 

今回のプログラムでは、この式の先頭に ! がついていて bool 型の値(つまり、trueまたはfalse)を反転させます。つまり、日付型に変換できない場合に、if の { } の中が実行されます。{ } の中のプログラムはテキストボックスの背景色を朱色にして、return (読み方:return=リターン)するというものです。return はコメントにも書いてあるように、これより下の処理を実行しないで処理を終了する機能があります。

 

この処理を txtDate1 と txtDate2 のそれぞれに記述しているので、この部分が実行されると txtDate1 または txtDate2 のどちらかに日付に変換できない値が入力されていればテキストボックスを朱色にして処理を終了する、両方が日付に変換できる場合、日付に変換した値をそれぞれ変数date1、date2に設定するという機能になります。

 

5-2.いろいろなTryParse

ちょっと話はそれますが、DateTime以外でも数値などの基本的な型には同じようにTryParse という静的メソッドが存在します。使い方も同じです。

if (int.TryParse("123",out int i1))
{
    // "123" は int に変換できるのでここが実行されます。
    MessageBox.Show(i1.ToString());
}

if (double.TryParse("1E2", out double d1))
{
    //驚くべきことに "1E2" はdoubleに変換できます。100になります。
    MessageBox.Show(d1.ToString());
}

if (long.TryParse("Hello!",out long l1))
{
    //さすがに "Hello!" は long に変換できないのでこれは実行されません。
    MessageBox.Show(l1.ToString());
}

この例では、"1E2"という文字列が double に変換可能であることも紹介しています。ここはかなり脇道のそれるのであまり気にしなくてよいと思います。

そもそも私は double と float は通常使うべきではないということを以前も説明しています。小数が必要なら decimal を使用しましょう。

それでも気になる人はdoubleやfloatが扱う「浮動小数点型」とは何なのかを見てみるとよいと思います。

(小難しいです。見ないことをお勧めします。)

指数表記 - Wikipedia

 

6.期間

6-1.日付の差を計算

プログラムの最後の部分で、ようやく2つの日付がどれだけ離れているか期間を計算します。この部分が今回一番説明したかったところです。

//▼日付の差を計算し、表示する。
TimeSpan span = date2 - date1;

lblDays.Text = span.TotalDays.ToString("0.0 日");
lblHours.Text = span.TotalHours.ToString("0.0 時間");
lblMinutes.Text = span.TotalMinutes.ToString("0.0 分間");

日付の差を計算するには、 - 演算子を使ってひき算します。

結果は TimeSpan型(読み方:TimeSpan=タイムスパン)になります。 TimeSpanは期間を表す型です。期間と日付は似ていますが違います。

たとえば、7月1日 から 7月5日 までは 4日間あります。7月1日 や 7月5日 は日付ですが、「4日間」というのは期間です。これは「1月4日」という意味ではないので、日付として扱うのは不適切です。

また、日付であれば、月は最大で12、日にちは31、時間は24、分は60までですが、期間であれば、最大値はありません。たとえば、あと100日で修学旅行だとか、この映画の時間は135分だとかのような表現は普通です。日付であれば「今2時135分です」などという表現はしません。(まぁ月末が締め切りの時に、「7月40日くらいに完成します」という冗談を言う人はいますが)

どちらにも共通するには「年」「月」「日」「時」「分」「秒」などで量を表すところです。

DateTimeの場合は、これらはYears. Month, Days, Hours, Monutes, Seconds というプロパティで表します。TimeSpanには2つの考え方があって、プロパティも2系統あります。

次のプログラムで確認してみましょう。

//2時間と20分と0秒を表す期間
TimeSpan span = new TimeSpan(2, 20, 0);

int hours = span.Hours; // 2
int minutes = span.Minutes; // 20
int seconds = span.Seconds; // 0

double totalHours = span.TotalHours; // 2.3333333333333335
double totalMinutes = span.TotalMinutes; // 140
double totalSeconds = span.TotalSeconds; // 8400

このプログラムでは2時間と20分と0秒を表すTimeSpanを生成しています。このようにTimeSpanはコンストラクターなどを使って生成することもできます。

Housrプロパティ、Minutesプロパティ、Secondsプロパティはこれをそのまま、2、20、0という値で返します。これが1系統目です。

ところで、2時間20分0秒というのは、つまり約2.3時間のことで、140分間のことです。あるいは、8400秒間のことです。このようなトータルで約2.3時間、140分、8400秒などの値を取得するのが2系統目で、TotalHoursプロパティ、TotalMinutesプロパティ、TotalSecondsプロパティで表します読み方:TotalHours=トータルアワーズ、TotalMinutes=トータルミニッツ、TotalSeconds=トータルセカンズ)。これらの値は約2.3時間のように小数になる場合があるので、型は double になっています。

年月日についても同様で、それぞれYears, Months, Daysプロパティと TotalYears, TotalMonths, TotalDaysプロパティがあります。

さて、もとのプログラムに戻ると、使っているのはTotalXXXプロパティの方ですので、このプログラムではトータルで差が何日間か、あるいは何時間か、あるいは何分間かを表示することになります。

LabelのTextプロパティは文字列型なので、double 型の値を代入するために ToString メソッドを使用してい文字列に変換しています。そのとき、単純に変換するのではなく書式を使って変換しています。

前回、日付型の書式を説明しましたが、doubleなどの数値型にも書式があります。「.」は小数点の記号に置き換えられます。日本ではそのまま「.」です。「.」より左側にある 「0」 は整数部分の数値に置き換えられます。「.」より右側にある「0」はその数だけ小数部分の桁の数値に置き換えられます。小数部分がない数値の場合はそのまま「0」に置き換えます。書式指定子以外の「日」「時間」「分間」はそのまま出力されます。

 

6-2.TimeSpanの活用

一定期間が経過すると何月何日の何時何分になるかという計算でも TimeSpan が使用できます。今回題材にしたプログラムでは登場しませんので、補足しておきます。

次の例はDateTime と TimeSpan をたし算して、期間経過後の日付を求めます。

//2024年5月1日8時という日付
DateTime date = new DateTime(2024, 5, 1, 8, 0, 0);

//60日と3時間という期間
TimeSpan span = new TimeSpan(60, 3, 0, 0);


//2024年6月30日11時という日付
DateTime result1 = date + span;

//これでも同じです。
DateTime result2 = date.AddDays(60).AddHours(3);

この場合は、TimeSpanを使わないで DateTime の AddDays や AddHours などのメソッドを組み合わせて同じ計算をすることもできます。

 

TimeSpanのインスタンスを生成するにはコンストラクターの他にも多様な静的メソッドが使えます。これを使うと、たとえば、1年間は時間でいうと何時間かという計算ができます。

次のプログラムはFromHoursメソッド(読み方:FromHours=フロムアワーズ)を使って1000時間を表すTimeSpanを生成し、それが約41.7日ということが確認できます。自分で÷24しても結果は同じです。

//1000時間を表すTimeSpanを生成
TimeSpan span = TimeSpan.FromHours(1000);

double totalDays = span.TotalDays;

MessageBox.Show($"1000時間はおよそ{totalDays:0.0}日間です。");

 

 

7.練習問題

問1.このプログラムを実行すると、どうなりそうですか?
DateTime.TryParse("2025/8/21", out var value);
System.Diagnostics.Debug.WriteLine(value);
2025/8/21が出力される
空行が出力される
エラーになる
問2.value の型は何になりますか?
DateTime dueDate = new DateTime(2028, 8, 31);
var value = dueDate - DateTime.Now;
DateTime
TimeSpan
int
問3.何が出力されますか?
DateTime date1 = new DateTime(2028, 2, 1, 8, 0, 0); //2028年2月1日 8時
DateTime date2 = new DateTime(2028, 2, 3, 9, 0, 0); //2028年2月3日 9時
TimeSpan span = date2 - date1;
System.Diagnostics.Debug.WriteLine(span.Hours);
0
1
49

 

8.さらなる探求

8-1.カレンダーを表示する

TextBoxの代わりに DateTimePicker(読み方:DateTimePicker=デイトタイムピッカー)を使用すると、カレンダーから日付を選択できるようになります。

また、DateTimePickerには必ず日付型に変換できる値しか入力できないため、プログラムで TryParse などする必要はなくなります。

 

8-2.入力チェック

TextBoxの場合、Validatingイベント(読み方:Validating=バリデイティング)を使って、日付型の値以外入力された場合、修正を必須にすることができます。こうしておくと、TextChangedイベントでは、必ず日付型の値が入っていることが保証されるのでTryParseなどの処理は不要になります。

private void txtDate1_Validating(object sender, System.ComponentModel.CancelEventArgs e)
{
    if (!DateTime.TryParse(txtDate1.Text, out _))
    {
        e.Cancel = true;
    }
}

TryParseの第2引数の _ は第2引数が不要であることを示す特別な記述方法です。この例では日付に変換できるかどうかチェックしたいだけ変換後の値は不要なのでここで変数を指定する代わりに _ を指定しています。

このイベント内で e.Cancel = true とすると、ユーザーの操作がキャンセルされます。

Validatingイベントにはいろいろと組み合わせ方や使い方があります。

Control.Validating イベント (System.Windows.Forms) | Microsoft Docs

 

 

 

 

次の回では、座標を表すPoint構造体を扱います。題材として簡単なグラフィックに挑戦します。

C#初級講座講座 次の回へ