C# 初級講座
VB2019 Visual Studio 2022

第14回 反射するボール1

2022/7/19

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

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.今回の狙いと作成するプログラム

1-1.狙い

今回は反射するボールのアニメーションを作成します。完成版は100行ほどのプログラムですが、説明することが多いので2回に分けます。

私が説明したいのはグラフィックやアニメーションではありません。これらはあくまで題材です。

この題材を通してクラスやメソッドをどのように使って機能を実現するのかという実体験をするとともに、プログラムの構造や変数のスコープ・複数の条件を指定するif文・switch式・リソースとしてファイルを扱う方法 などC#の汎用的な知識を身に着けることを目指します。

 

1-2.サンプルプログラム

今回と次回で完成させるプログラムは次のようなものです。

反射するときには音がなりますので、放っておいてしばらく見ているいろいろな音がなります。

※完成版では音がなりますが、この動画には音声はありません。

 

このあたりからちょっと工夫しだすと、簡単なゲームに発展したりもしますが、初級講座の知識量ではまだ難しいですね。でも、サンプルを少し自分流にいじってみるのも良い経験です。

 

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

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

 

2.枠組みを作ってみる

2-1.コントロールの配置と設定

Windows フォーム アプリで自動的に進行していくプログラムを作成するには Timerコンポーネント(読み方:Timer=タイマー)を使うのがお勧めです。

Timerコンポーネントは一定時間ごとにTickイベント(読み方:Tick=ティック)を発生させます。プログラマーはこのTickイベントハンドラーにプログラムを書くことで時間が経過するたびに実行されることになります。

ツールボックスの中でTimerは「コンポーネント」のカテゴリーにあります。ツールボックスからTimerコンポーネントを選んでフォームに配置してください。Timerはユーザーが使用するタイプの「コントロール」ではなく、プログラムの部品として動作する「コンポーネント」なので、フォームに配置しても、勝手に下の領域に表示されます。コンポーネントは実行時には目に見えません。

 

 

フォームとタイマーのプロパティは次の通り設定してください。

コントロールの名前 種類 プロパティ/イベント 備考
イベント Form1 Form イベント DoubleBuffered true グラフィックのちらつきを防止します。
    イベント Paint   ハンドラーを生成してください。
イベント timer1 Timer イベント Enabled true タイマーを有効にします。
    イベント Interval 10 タイマーがTickイベントを発生させる間隔です。
10 は 0.01秒 を意味します。
    イベント Tick   ハンドラーを生成してください。

タイマーがTickイベントを発生させる間隔は Intervalプロパティ(読み方:Interval=インターバル)で指定します。このプロパティの単位はミリ秒です。つまり、1000が1秒を表します。1秒ごとにTickイベントを発生させたい場合は 1000 にします。今回は 10 を指定しているので、約0.01秒ごとにTickイベントが発生します。

タイマーは精密に動作するようにはできていないので、この機能を利用して何かとても細かい時間を測定したり制御するプログラムは作れません。

Form1のPaintイベントハンドラーの生成と、timer1 の Tickイベントハンドラーの生成も忘れずに行ってください。timer1 の Tick イベントハンドラーを生成する一番簡単な方法は配置された timer1 をダブルクリックすることです。)

 

2-2.第1段階のプログラム

まずは第1段階で次の通りプログラムしましょう。

できればここからコピペするのではなく、自分でキーボードから打ち込んでみてください。きっといろいろな発見があると思います。

namespace BoundBall
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        const int circleSize = 50; //円の半径(=外接四角形の一辺の長さ÷2)
        Point circleLocation = new Point(60,110); //ボールの中心の位置。
        int xStep = 10; //横方向の1フレーム当たりの移動量
        int yStep = 10; //縦方向の1フレーム当たりの移動量

        private void timer1_Tick(object sender, EventArgs e)
        {
            MoveBall(); //ボールの座標を更新します。
            this.Invalidate(); //ボールを描画(するようWindowsに依頼)します。
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            Draw(e.Graphics);
        }

        /// <summary>ボールを位置を更新します。</summary>
        private void MoveBall()
        {  
            circleLocation.X += xStep; //ボールのX座標を更新します。
            circleLocation.Y += yStep; //ボールのY座標を更新します。
        }

        /// <summary>ボールを描画します。</summary>
        private void Draw(Graphics g)
        {
            g.Clear(Color.Black);

            //円の外接四角形の位置と大きさを定義
            Rectangle rect = new Rectangle(
                circleLocation.X - circleSize,
                circleLocation.Y - circleSize,
                circleSize * 2,
                circleSize * 2);

            //外接四角形に内接する位置にベージュの円を描画。
            g.FillEllipse(Brushes.Beige, rect);
        }

        //▼ここから下はサウンド再生機能です。

        /// <summary>ランダムにサウンドを再生します。</summary>
        private void Play()
        {
            //この段階では何もしません。
        }
    }
}

サウンド機能はまだありません。

これで実行すると、実行直後、ボールが右下に移動してすぐに消えてしまう様子が確認できます。

さて、何が起こっているのでしょうか?

 

3.プログラムの仕組み

3-1.描画

まずは描画の部分を抜粋してよく見てみます。

Paintイベントハンドラーは Drawメソッドを呼んでいるだけです。

private void Form1_Paint(object sender, PaintEventArgs e)
{
    Draw(e.Graphics);
}

PaintイベントとGraphicsについては前回の説明を参照してください。

 

DrawメソッドはClearメソッドで背景色を黒にした後、変数rectで外接四角形の位置をサイズを定義して、FillEllipseメソッド(読み方:FillEllipse=フィルイリプス)そこに収まるように塗りつぶした円を描画しています。

/// <summary>ボールを描画します。</summary>
private void Draw(Graphics g)
{
    g.Clear(Color.Black);

    //円の外接四角形の位置と大きさを定義
    Rectangle rect = new Rectangle(
        circleLocation.X - circleSize,
        circleLocation.Y - circleSize,
        circleSize * 2,
        circleSize * 2);

    //外接四角形に内接する位置にベージュの円を描画。
    g.FillEllipse(Brushes.Beige, rect);
}

座標の指定は Point型の変数 circleLocation と 定数 circleSize で行っています。

ちなみに circle とは英語で「丸」という意味です。

ここでは circleLocation が円の中心になるように、circleSize が円の半径になるように外接四角形の位置とサイズを決めています。前回の初級講座を読んだ方は少し注意してください。前回は、直径を扱っていましたが今回は半径を扱っています。

FillEllipseメソッドは塗りつぶした円を描画するメソッドで、第1引数は塗りつぶしに使うブラシ(筆)、第2引数で位置と大きさを指定するために外接四角形を指定します。

塗りつぶしのブラシは単色のブラシやグラデーションのブラシなどいくつか種類があります。単色のブラシはあらかじめBrushesクラス(読み方:Brushes=ブラッシェズ)の静的プロパティから取得できて楽なので、このプログラムではベージュ色の単色ブラシを指定しています。Beigeプロパティ(読み方:Beige=ベージュ)がそれです。

 

以上で理解していただけたと思いますが、描画のプログラムではボールの移動は一切行いません。変数で指定された位置と大きさでボールを描画するだけです。

 

3-2.座標の更新

勘のいい方はもうわかったかもしれませんが、このような仕組みなので、円の中心を表す変数 circleLocation の値を次々と変えていくことでボールが動いているように見せることができます。

まず、各種変数と定数を確認しておきましょう。

このプログラムでは重要な4つの変数を定義しています。

const int circleSize = 50; //円の半径(=外接四角形の一辺の長さ÷2)
Point circleLocation = new Point(60,110); //ボールの中心の位置。
int xStep = 10; //横方向の1フレーム当たりの移動量
int yStep = 10; //縦方向の1フレーム当たりの移動量   

円の半径を表す circleSize と 座標を表す circleLocation は既に登場しました。

このプログラムでは circleSize は値が変更されないので const を付けて定数にしています。const をはずして変数にしても機能は変わりません。const を付けておくと、少しだけ実行速度が速いのと、何かの勘違いで値を変更するプログラムを書いてしまうという間違いを防止できます。この程度の行数のプログラムならそのような間違いはあまり起こらないと思いますが、実用レベルのプログラムは数人から数十人でチームで開発するうえに、何万行・何十万行もあることがあるので、やって欲しくないことはそもそもきないようにしておいた方が安心なのです。

circleLocationはXとYの組み合わせで座標を表現できる Point型にしています。初期値は座標(60, 100)に設定しています。

xStep と yStep は1回あたりのボールの移動量です。このあとボールの座標を更新するところで使います。

座標の更新は MoveBall メソッドで行っています。

このMoveBallメソッドは Timer の Tick イベントから呼ばれるようにしています。

/// <summary>ボールを位置を更新します。</summary>
private void MoveBall()
{  
    circleLocation.X += xStep; //ボールのX座標を更新します。
    circleLocation.Y += yStep; //ボールのY座標を更新します。
}

このバージョンではcircleLocationのXプロパティとYプロパティにそれぞれに移動量 xStep と yStep をたし算しているだけです。

つまり、circleLocationの初期値が座標(60, 100) で、 xStep と yStep は 10 なので、1回MoveBallメソッドを呼ぶと circleLocationは座標(70, 110)になります。もう1回呼ぶと座標(80, 120)になります。

どんどん座標の値が大きくなっていくので、やがてボールの位置はフォームからはみ出ます。(結果としてボールはすぐに見えなくなります。)

この部分のプログラムは次回変更して、ボールがはじっこまでいったら反射するように変更します。

 

3-3.タイマー

最後に Timer の Tickイベントで実行されるプログラムを確認しておきましょう。

ここでは MoveBallメソッドを読んで、ボールの座標を更新し、次にInvalidateメソッドを使って、Paintイベントを発生させています。

private void timer1_Tick(object sender, EventArgs e)
{
    MoveBall(); //ボールの座標を更新します。
    this.Invalidate(); //ボールを描画(するようWindowsに依頼)します。
}      

timer1 の Intervalプロパティを 10 にしてあるので、このプログラムは 0.1秒ごとにくりかえし実行されることを思い出してください。

つまり、座標更新 → 描画 という処理を0.1秒ごとに繰り返しているので、座標更新 → 描画 → 座標更新 → 描画 → … とプログラムが終了するまで永遠に続きます。

 

この通りの状況ですので、MoveBallメソッドを実行するたびに座標がどんどん増えていき、再描画のたびにボールの位置は右下にずれていくようになります。人間にはアニメーションの原理によりボールが右下方向に動いているように見えます。

この現時点では、はじめのうちはフォームに表示できる範囲内の座標だからボールが表示されますが、すぐにフォームの範囲外の座標になってボールは表示されなくなります。ボールが表示されなくなってもプログラムは健気に約0.1秒ごとに座標を更新します。

 

4.変数のスコープ

4-1.プログラムの構造

これまで、細かいプログラムをその都度説明してきましたが、全体的なプログラムの構造についてはほとんど説明していませんでした。

私たちが記述するプログラムは大きな観点でどのようになっているのか一度整理してみます。

 

このプログラムの全体的な構造は次の通りです。

このプログラム全体は namespace BoundBall { } で囲まれています。

これは、BoundBall 名前空間 の中に機能をプログラムしていることを表しています。

その中はさらに Public partial calss Form1 : From { }  で囲まれています。

これは、あなたは今 Form1 クラスをプログラムしているということを表しています。クラスをプログラムしている自覚はなかったかもしれませんが、C# では機能はクラス・構造体のメンバーとして存在することになるので、テンプレートとしてこの定義が自動生成されています。

そして、今 Form1 クラスの中にコンストラクターが1個とフィールドが4つと、メソッドが5つ存在する状態になっています。

コンストラクターはテンプレートが自動生成したものです。今回のテーマではないので今回はここにふれないでおきます。

4つのフィールドというのは、つまり変数/定数のことで、circleSize, circleLocation, xStep, yStep のことです。

5つのメソッドは次のものです。

MoveBall、Draw、Play は private void … { … } という形で自分で定義したメソッドなのでわかりやすいと思いますが、自動生成された timer1_Tick と Form1_Paint も同じ構文で宣言されており、実はメソッドであるということがわかります。標準的なイベントハンドラーはメソッドの一種なのです。

 

4-2.変数の適用範囲

ここで私が説明したいのは「変数のスコープ」です。これは初歩的な知識かつ重要な知識です。

「変数/定数」と書くのはわずらわしいので、以下では「変数」とだけ書いて両方を指すことにします。

変数は宣言されているブロックの範囲内でのみ有効です。

「ブロック」と言っているのは主に { } で囲まれた範囲です。これを変数の「適用範囲」まはた「スコープ」と呼びます。

たとえば、Drawメソッド内で rect という変数を宣言しています。この変数 rect は Drawメソッドの { } の中で宣言しているので、Drawメソッド内でのみ有効な変数です。

他方、circleLocation や circleSize, xStep, yStep を見るとこれよりはるかに広いスコープであることがわかります。public partial class Form1 のスコープですね。

クラスを定義しているスコープなので、こういうものをクラスレベルの変数と呼ぶこともあります。

また、クラスレベルの変数は、もはや単なる変数ではなくクラス自身のデータの一部なので、その観点でこれを「フィールド」とも呼びます。

 

というわけで、これら4つの変数はクラスのスコープなので、このクラス内のどこでも読み書きできるのに対し、変数 rect は Drawメソッド内でしか使用できないということになります。

このルールを知らないと宣言してあるはずの変数が使えないというのようなことで困惑してしまうこともあります。

 

ここで重要になる「位置」というのは、どのブロック内になるかということです。行の順番は関係ありません。

たとえば、このプログラムではtimer_Tickメソッドの上で4つの変数/定数を宣言していますが、たとえば、Playメソッドの下に移動しても構いません。(Playメソッドの「中」はダメですよ。)

 

このプログラムでは登場していませんが、条件判断を行う if や 繰り返して実行を行う while などでも { } を使った構文が登場します。これも事情は同じです。if の { } の中で変数を宣言すると、その変数はその if の { } 内でのみ有効になります。

たとえば、次のプログラムはエラーです。(意味不明なプログラムで恐縮です)

private void MyMethod()
{
    if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
    {
        int count = 5;
        System.Diagnostics.Debug.WriteLine($"日曜日なので{count}個必要");
    }

    count += 2; //←エラー。このスコープでは count という変数が存在しない。
    System.Diagnostics.Debug.WriteLine($"結論として{count}個必要");
}

変数 count が if ブロックで宣言されているのに、ifブロックの外でもこの変数を使おうとしているからです。

この例の場合、if の外でも count を使いたいのであれば、MyMethodメソッドのスコープで変数を宣言するべきです。たとえば、次のようにします。(プログラムの内容は適当なので気にしないでください。もっと意味のある例で説明できれば良いのですが…)

private void MyMethod()
{
    int count = 0;

    if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
    {
        count = 5;
        System.Diagnostics.Debug.WriteLine($"日曜日なので{count}個必要");
    }

    count += 2;
    System.Diagnostics.Debug.WriteLine($"結論として{count}個必要");
}

 

4-3.同じ名前の変数

宣言されているスコープが違う変数はまったく別物です。同じ名前の変数を別のスコープで宣言することもできます。

スコープが重複するケースもあります。次のようなケースです。

int count = 3; //広いスコープの変数
private void MyMethod()
{
    int count = 5; //狭いスコープの変数

    System.Diagnostics.Debug.WriteLine($"{count}個必要"); //この count はどちら?
}

この例では count という変数が2つ宣言されていますがエラーではありません。広いスコープの方の変数はフィールドです。これら2つは別々の変数として存在できます。

最後の Debug.WriteLine の位置だと どちらの count もスコープに入っています。どちらの count が出力されるでしょうか?

答えは狭い方のスコープです。つまり、「5」が出力されます。

同じスコープ内に同じ名前の変数が存在する場合、狭いスコープの変数が優先されます。近くにある方を採用するということですね。これは変数に限ったルールではありません。

 

5.練習問題

問1.FormのInvalidateメソッドの直接的または間接的な効果として間違っていることはどれですか?
フォームの再描画
Paintイベント発生
フォームの背景が黒になる
問2.button1,button2,button3それぞれのClickイベントハンドラーを作成し、次の通りプログラムしました。これを実行して、button1をクリックしてからbutton3をクリックしようと思います。どうなるでしょうか?
private void button1_Click(object sender, EventArgs e)
{
    string message = "button1クリック";
}

private void button2_Click(object sender, EventArgs e)
{
    string message = "button2クリック";
}

private void button3_Click(object sender, EventArgs e)
{
    MessageBox.Show(message);
}
button1クリックと表示される
button2クリックと表示される
空のメッセージボックスが表示される
エラー
問3.Windows フォーム アプリで一定時間ごと(たとえば1秒ごと)にプログラムを実行したい場合、適している方法は何でしょうか?
Timerを使う
ループを使う
Invalidateを使う

 

次の回では、反射機能とサウンド機能を付けてプログラムを完成させます。

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