C# 初級講座 |
![]() ![]() |
2022/7/24
この記事が対象とする製品・バージョン
![]() |
Visual Studio 2022 | ◎ | 対象です。 |
![]() |
Visual Studio 2019 | ◎ | 対象です。 |
![]() |
Visual Studio 2017 | △ | 対象外ですが、参考になります。 |
![]() |
Visual Studio 2015 | △ | 対象外ですが、参考になります。 |
![]() |
Visual Studio 2013 | △ | 対象外ですが、参考になります。 |
![]() |
Visual Studio 2012 | △ | 対象外ですが、参考になります。 |
![]() |
Visual Studio 2010 | △ | 対象外ですが、参考になります。 |
![]() |
Visual Studio 2008 | △ | 対象外ですが、参考になります。 |
![]() |
Visual Studio 2005 | △ | 対象外ですが、参考になります。 |
![]() |
Visual Studio.NET 2003 | × | 対象外です。 |
![]() |
Visual Studio.NET (2002) | × | 対象外です。 |
Visual Studio Code | △ | 対象外ですが、参考になります。 |
目次
今回は前回の続きです。
プログラムを改造して、ボールが反射する動きをプログラムし、反射するときに音を鳴らすようにします。
前回作成したプログラム全体はここで確認できます。
前回も紹介しましたが、完成版は次のようになります。この動画には音はありませんが完成版ではボールが反射するときに音が鳴ります。
さて、端っこまで移動したらボールが反射するようにプログラムを改造してみましょう。
このプログラムでは、ボールの移動は MoveBall メソッド で行っていますから、改造するのはこのメソッドです。
このようにプログラム内で役割分担が明確になっているのと機能を変更したり、バグを修正したりという作業がやりやすくなります。この目的のために、大きなメソッドを1つ作成するよりも、役割ごとに小分けにされたメソッドを複数作成する方が良いとされています。
MoveBallメソッドを次のように書き換えてください。
/// <summary>ボールを位置を更新します。</summary>
private void MoveBall()
{
//次の移動でボールがフォームの右端か左端を超えるかチェックします。
if (circleLocation.X + xStep + circleSize > this.ClientRectangle.Width
|| circleLocation.X + xStep - circleSize < 0)
{
//横方向の反射。つまり、右端か左端を超える場合、移動量を反転(マイナス)させます。
xStep = -xStep;
Play(); //サウンドを鳴らします。
}
circleLocation.X += xStep; //ボールのX座標を更新します。
//同様に次の移動でボールがフォームの上端か下端を超えるかチェックします。
if (circleLocation.Y + yStep + circleSize > this.ClientRectangle.Height
|| circleLocation.Y + yStep - circleSize < 0)
{
//縦方向の反射
yStep = -yStep;
Play();
}
circleLocation.Y += yStep; //ボールのY座標を更新します。
}
これで実行するとフォームの端っこまで移動したボールが跳ね返ってフォームの中を反射していくアニメーションになります。音が出ない以外は完成品と同じ動きです。
どういうプログラムなのか見てみましょう。
このプログラムでは横方向のX軸と縦方向のY軸とでそれぞれ計算・判定を行っています。
X軸の部分だけ抜き出してみます。
//次の移動でボールがフォームの右端か左端を超えるかチェックします。
if (circleLocation.X + xStep + circleSize > this.ClientRectangle.Width
|| circleLocation.X + xStep - circleSize < 0)
{
//横方向の反射。つまり、右端か左端を超える場合、移動量を反転(マイナス)させます。
xStep = -xStep;
Play(); //サウンドを鳴らします。
}
circleLocation.X += xStep; //ボールのX座標を更新します。
まず、if 文でボールがフォームの右端を超える位置にあるか、または、左端を超える位置にあるかを判定します。
この if 文では「右端を超えるか」と「左端を超えるか」の2つの条件を判断しています。このようなとき if 文を2つ書いたり、else if を使用することもできますが、今回のサンプルでは1つの if 文で2つの条件を判断しています。それをやるには、if の条件判断の中で || または && を使います。
&& で条件を2つつなぐと、2つの条件が成立しているか判断することができます。
//条件1 と 条件2 の両方が成立しているか判断する場合
if (条件1 && 条件2)
{
…
}
|| で条件を2つつなぐと、2つの条件の少なくとも1つが成立しているか判断することができます。
//条件1 と 条件2 の少なくとも1つが成立しているか判断する場合
if (条件1 || 条件2)
{
…
}
&& と || を使って 3つ以上の条件をつなぐことも可能です。
今回は || を使って、「右端を超えるか」と「左端を超えるか」の少なくとも1つが成立するかを判断しています。
条件をつなぐと1行が長くなって見にくくなってしまうので、この例では || の直前で改行しました。 C# はキーワードや記号の切れ目での改行は自由なのでお好みで見やすくなるように改行するのが一般的です。
改行した時に || の位置をどこにするかもスペースやタブなどを入力して自分で調整できます。
それでは右端を超えるのかの判断をどのように行うのか考えてみましょう。
みんさんも今後のプログラム人生でいろいろな条件判断を書くことになると思います。そのときのポイントは、判断したい状態を式で表すことです。今回の例で具体的に見てみましょう。
まず、MoveBallメソッドが呼ばれた段階ではまだボールの座標は更新されていません。現在のボールのX座標は circleLocation.X です。
移動を実行した時のX軸の座標は circleLocation.X + xStep です。
ボールの半径は circleSize ですから、移動実行後のボールの右端のざっひょうは circleLocation.X + xStep + circleSize です。
一方、フォームの右端の X座標は this.ClientRectangle.Width です。フォームは自由に描画できる「クライアント領域」と呼ばれる領域と最大化ボタンや最小化ボタンや薄い枠があるフレームから構成されます。今回はクライアント領域のサイズが問題で、ClientRectangleプロパティ(読み方:ClientRectangle=クライアントレクトアングル)で取得できます。
つまり、右端をオーバーする状態とは式で表すと次の状態です。
circleLocation.X + xStep + circleSize > this.ClientRectangle.Width
既定では xStep の値は 10 なので、この状態になる前に xStep の値を -10 にすることで、逆方向に移動することになりますから、この条件が成立する場合、やるべきことは xStep をマイナスにすることで。プログラムで書くと xStep = -xStep ということになります。
この式だと、まだ右側に xStep + circleSize 未満の隙間が合った場合でも反射する判定になってしまいますが、今回はこれでOKとします。実際のところ、この隙間は小さいのとボールの移動スピードが速いことから人間の目には多分気が付かない程度の誤差です。
次に、左端を超えるかの判断です。左端を目指しているということは既に1回以上、右端で反射している場合で、xStep は -10 になっています。
このとき、現在のボールの中心のX座標は circleLocation.X です。
移動後のボールの中心のX座標は circleLocation + xStep です。逆方向への移動なので circleLocation - xStep と思ってしまうかもしれませんが、このプログラムでは、xStep の値自体が -10 というようにマイナス付きになるので式は circleLocation + xStep が正解です。
移動後のボールの左端の座標は circleLocation + xStep - circleSize となり、これが 0 を下回っているかで左端を超えるか判断できます。
整理すると、次の式でこの状態を判断できます。
circleLocation.X + xStep - circleSize < 0
この条件が成立する場合、xStep の値を -10 から 10 に変更すべきなので、やるべきことは xStep = -xStep です。
なんと、見事な数学の理屈なのですが、つまり、右端を超える場合も、左端を超える場合もやるべきことは xStep = -xStep で同じということです。
それから、反射するときに音を鳴らしたいので Play メソッドも呼ぶことにします。今のところ Play メソッドの中身は空なので音はなりませんが、これは後でプログラムすることにしましょう。
というわけで、さきほど引用した部分をもう一度引用します。
この if の意味はもうすべて説明しました。理解していただけますでしょうか?
//次の移動でボールがフォームの右端か左端を超えるかチェックします。
if (circleLocation.X + xStep + circleSize > this.ClientRectangle.Width
|| circleLocation.X + xStep - circleSize < 0)
{
//横方向の反射。つまり、右端か左端を超える場合、移動量を反転(マイナス)させます。
xStep = -xStep;
Play(); //サウンドを鳴らします。
}
circleLocation.X += xStep; //ボールのX座標を更新します。
MoveBallメソッドでは反射する場合も反射しない場合も、座標を更新する必要がありますので、if 文の後で実際に X座標の更新を行っています。
縦方向の Y軸についても、これとまったく同じ理屈でプログラムしています。
最後にサウンドを実装します。
.NETのフレームワークにはサウンドを簡単に再生できる SoundPlayer というクラス(読み方:SoundPlayer=サウンドプレイヤー)があります。System.Media名前空間に属しています。
次のように簡単に使用できます。
var player = new System.Media.SoundPlayer(@"C:\temp\sound.wav");
player.Play();
しかし、wavファイルしか再生できないうえ、既定では複数のサウンドを同時に再生できないなど使い勝手が少し悪いです。
これ以上のサウンド再生機能が欲しい場合、フレームワークの外の機能を利用するのが一般的です。
フレームワークの外に目を向けるとサウンド再生機能はいろいろあります。
今回は、Windowsのもつマルチメディア機能 mciSendString (読み方:mciSendString=エムシーアイセンドストリング)を呼び出すようにします。C# には一定の条件でC#以外の機能を呼び出せる機能があり、この mciSendString は呼び出し可能です。これはもはや C# の機能ではないので、普通のC#のプログラムとは考え方が全然違います。
今回は8個の wav ファイルをあらかじめ準備し、ランダムで再生することにします。
ここからダウンロードしてください。
私は音楽が趣味で、このサウンドファイルは Cubase という音楽のソフトを使って自分で作りました。自分でサウンドを作れない方は、インターネットでフリーで公開されているサウンドをいろいろ探すという手もあります。探してみると結構いろいろありますが、著作権や利用条件にはご注意ください。
今回のこのサウンドファイルはどこかに公開・配布しない限り自由に個人利用していただいて構いません。
このファイルをダウンロードして解凍すると8個の wav ファイルが確認できます。これをVisual Studioのプロジェクトに取り込む方法は次の通りです。
まず、プロジェクトに Sound フォルダーを作ります。
その方法は、Visual Studio のソリューションエクスプローラーでBoundBall プロジェクトを右クリックして、[追加] - [新しいフォルダー] を選択して、Sound という名前を付けることです。
右クリックするのは一番上にあるソリューションのBoundBallではなく、その下にあるC#のアイコンがついているプロジェクトのBoundBallです。フォルダーの名前は Sound でなくてもよいのですが、サンプルプログラムをそのまま動かすためには Sound である必要があります。
次に、プロジェクトのSoundフォルダー配下に8個のwavファイルをコピーします。
その方法は、Windowsのエクスプローラーでダウンロードした8個の wav ファイルを選択して「コピー」してから、Visual Studioのソリューションエクスプローラーで Sound フォルダーを右クリックして「貼り付け」することです。
ここまでできるとソリューションエクスプローラーは次のような表示になります。
これでこの8個の wav ファイルはプロジェクトに含まれました。
このようにプロジェクトにはプログラム以外の任意のファイルを含むことができます。このようなプログラムではないけれど、プログラムから使用するファイルを「リソース」と呼びます。リソースという言葉は使用可能メモリを指す場合などいろいろな意味があるので、リソースという言葉の1つの意味として覚えておいてください。
プログラム以外のファイルをプロジェクトに含めた場合、そのファイルをどのように扱うか設定が必要です。既定では、これらのファイルはただ単にここに存在しているだけです。ちょっとしたテキストファイルのメモ書きを追加するなら既定の設定で良いのですが、この wav ファイルをプログラムから利用するためにもう少しやることがあります。
wavファイルや画像ファイルなどのリソースをプログラムから利用するためには、これらのリソースをビルドして生成されるexeファイルやdllファイルに含めるという埋め込み方式と、あくまで独立したファイルとしてビルドした成果物の中に含めるという独立方式があります(この方式には名前がないので、ここで便宜上「独立方式」と呼ぶことにします)。他にもインターネットからオンデマンドでダウンロードするとか、データベースから取得するとかやりようはあります。
独立方式が直感的なので今回は独立方式を採用します。
そのために、ソリューションエクスプローラーで8個のwavファイルを選択状態にしてください。Ctrlキーを押しながら、8個をそれぞれクリックするか、一番上のwavファイルを選択してから Shiftキーを押しながら一番下のwavファイルをクリックすると8個が同時に選択状態になります。
この状態で、プロパティウィンドウで「出力ディレクトリにコピー」を「新しい場合はコピーする」にしてください。
これで、プログラムをビルドとこの8個のwavファイルは成果物の出力先のフォルダーにコピーされることになります。
出力先のフォルダーはVisual Studio 2022 + .NET 6.0 でWindows フォーム アプリをデバッグ実行する場合、既定では下記です。
BoundBall\BoundBall\bin\Debug\net6.0-windows
この場所に アプリケーションを開始するためのexe ファイルも出力されています。
準備が整ったところでプログラムしましょう。
▼ここから下はサウンド再生機能です。という目印のコメントの下に次の通りプログラムします。
//▼ここから下はサウンド再生機能です。
//Windowsが持っているメディア制御機能(mciSendString)の呼び出し口を定義
[System.Runtime.InteropServices.DllImport("winmm.dll")]
private static extern int mciSendString(string lpstrCommand, string lpstrReturnString, int uReturnLength, IntPtr hwndCallback);
/// <summary>ランダムにサウンドを再生します。</summary>
private void Play()
{
//再生するサウンドファイルをランダムに決定します。
//サイコロをふってでた目に応じてファイル名を設定するようなロジック
string fileName = Random.Shared.Next(1, 9) switch {
1 => "C2.wav",
2 => "F2.wav",
3 => "G2.wav",
4 => "C3.wav",
5 => "EF2.wav",
6 => "AF2.wav",
7 => "BF2.wav",
_ => "EF3.wav" };
//サウンドファイル名をフルパスにします。アプリ本体のある場所のSoundフォルダー以下です。
fileName = Application.StartupPath + @"Sound\" + fileName;
//このサウンド再生に一意な名前を付けます。ここではシステム時刻とランダムを利用します。
string aliasName = $"Alias{DateTime.Now.Ticks}{Random.Shared.Next(1, 100000)}";
//サウンドを再生します。
mciSendString($"open \"{fileName}\" alias {aliasName}", "", 0, IntPtr.Zero);
mciSendString($"play {aliasName}", "", 0, IntPtr.Zero);
}
これで完成です。実行するとボールが反射するタイミングでランダムなwavファイルが再生されます。
(ただ、時々ノイズのような妙な音になることがあるような…。本題ではないので気にしないことにします。)
このプログラムの上の部分に見慣れない妙な定義があるのが確認できます、
これがC#の外部で定義されている mciSendString を呼び出せるようにするための特殊な構文です。
C#の外部というのはmciSendStringの場合、winmm.dll というファイルです。このファイルはWindowsのsystem32フォルダー内にあります。
//Windowsが持っているメディア制御機能(mciSendString)の呼び出し口を定義
[System.Runtime.InteropServices.DllImport("winmm.dll")]
private static extern int mciSendString(string lpstrCommand, string lpstrReturnString, int uReturnLength, IntPtr hwndCallback);
目印は DllImport と extern です。extern キーワードは、この機能がフレームワークの外部に存在することを示すキーワードです。DllInportの部分の [ ] で囲まれた記述はC#の「属性」という機能で、プログラムにいろいろな付加情報を与えることができます。ここでは DllImport属性を使って、mciSendStringの実体はwinmm.dllファイル内にあることを示しています。
この定義をすることで mciSendString を C# のメソッドのように呼び出すことができるようになります。
図にすると上のようになります。mciSendStringの本体があるwinmm.dllファイルはC++でプログラムされています。
C#のプログラムでは DllImport属性を使って、この本体を呼び出せる口だけを定義します。この定義によりC#の通常のプログラムから mciSendString を呼び出せるようになりますが、実際に作動するのはあくまでwinmm.dll内のプログラムです。
このようなWindows自身が持つ機能で外部のプログラムから呼び出せるようになっているものを「Windows API」と呼びます。
この部分は奥が深く、かつ難しく、かつC#ではない機能と連動するため、説明は割愛させてください。
深く知りたい方は DllImport や Windows API というキーワードで検索するとよいと思います。
Playメソッド内では8個あるwavファイルのうちどれを再生するかランダムに決定することにします。
それが次の部分です。
//再生するサウンドファイルをランダムに決定します。
//サイコロをふってでた目に応じてファイル名を設定するようなロジック
string fileName = Random.Shared.Next(1, 9) switch {
1 => "C2.wav",
2 => "F2.wav",
3 => "G2.wav",
4 => "C3.wav",
5 => "EF2.wav",
6 => "AF2.wav",
7 => "BF2.wav",
_ => "EF3.wav" };
このプログラムではRandomクラスのNextメソッドを使って1~8までのランダムな数字を生成します。(Nextメソッドは第1引数以上、第2引数未満の整数を生成するので、1,9 という指定で 1~8 が生成されます。)
次に switch式 (読み方:switch=スイッチ)を使って、この8個の数字をそれぞれファイルを表す文字列を割り当てて返します。結果として 変数fileNameには "C2.wav"とか"F2.wav"のような文字列が代入されます。
switch式は初級講座ではじめて登場ですが、見れば意味はわかると思います。
1 なら "C2.wav"、2 なら "F2.wav"、…という感じですね。最後の _ は その他を意味します。
理解を深めるために別の例も紹介します。この例では文字列で分岐しています。変数nameが"クジラ"なので、変数categoryは"哺乳類"になります。
string name = "クジラ";
string category = name switch
{
"サル" => "哺乳類",
"クジラ" => "哺乳類",
"トカゲ" => "爬虫類",
"カエル" => "両生類",
_ => name + "の分類は不明です。"
};
この例では最後の _ のところで + を使って簡単な式を記述しています。switch式ではもっとこったこともできます。詳しくはリファレンスを参照してください。
switch 式 - C# リファレンス | Microsoft Docs
ところで、今回のプログラムではRandomクラスが1~8のランダムな数字を生成するので、この場合、その他に該当するのは 8 しかありません。ならば、最後は 8 => と書いても意味は同じなのですが、8 => と書くと、賢い Visual Studio がswitch のところに緑の波線で次の警告を表示します。
CS8509 この switch 式では入力型の可能な値がすべて扱われるわけではありません (すべてが網羅されているわけではありません)。たとえば、パターン '0' がカバーされていません。
私は1~8を生成するようにしているので大丈夫だとわかっているのですが、Visual Studioはそこまでは検知できないようで、「1~8以外の数値がカバーされないけど大丈夫?」と警告しているわけです。switch式は該当する項目がない場合エラーになるので特に心配してくれているという事情もあります。今回のプログラムでは大丈夫なので無視しても良いのですが、私は緑の波線と警告が気になるたちなのでVisual Studioに安心してもらうために最後を _ => にしました。
このあと、サウンドを再生するときにmciSendStringにファイル名のフルパスを渡す必要があるので、変数 fileName をフルパスにします。
//サウンドファイル名をフルパスにします。アプリ本体のある場所のSoundフォルダー以下です。
fileName = Application.StartupPath + @"Sound\" + fileName;
アプリケーションの本体がある場所は Windows フォーム アプリ であれば、Application.StartupPath で取得できます。(読み方:Application=アプリケーション、StartupPath=スタートアップパス)。
ソリューションエクスプローラーでは Soundフォルダーの配下にwavファイルを格納して、出力ディレクトリーにコピーするように設定しているので、実行時もこの Soundフォルダーごとコピーされています。
再生するファイルが決まったのでいよいよ再生を命令します。
前述したようにここでは C# ではない mciSendString を使いますので、C#の勉強が目的であればとりあえず無視してもよいです。発想が C# とは違うのです。参考になりません。
でも、気になる人もいると思うので簡単に説明しておきます。
//このサウンド再生に一意な名前を付けます。ここではシステム時刻とランダムを利用します。
string aliasName = $"Alias{DateTime.Now.Ticks}{Random.Shared.Next(1, 100000)}";
//サウンドを再生します。
mciSendString($"open \"{fileName}\" alias {aliasName}", "", 0, IntPtr.Zero);
mciSendString($"play {aliasName}", "", 0, IntPtr.Zero);
mciSendString でサウンドを再生するためには play から始まる文字列でサウンドのエイリアスを指定します。
ファイル名ではなく「エイリアス」というところがミソです。エイリアスとは便宜的に付ける別名のことです。
また、play を命令する前に open を命令する必要があるルールになっていて、エイリアスはオープン時に自分で好きな名前を付けます。
ということで、open "C:\xxxxxxx\Sound\C2.wav" alias "myAlias" のような文字列を mciSendString に渡せばよいことになります。
ところが、エイリアスは、再生中のサウンドと重複指定はいけないことになっているので、システム時刻のTicksプロパティと1~99999までのランダムな数字を組み合わせて、自動的にユニーク(=他と重複しない)値を生成してそれを指定するようにしています。
最後に完成したプログラム全体を掲載しておきます。
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()
{
//次の移動でボールがフォームの右端か左端を超えるかチェックします。
if (circleLocation.X + xStep + circleSize > this.ClientRectangle.Width
|| circleLocation.X + xStep - circleSize < 0)
{
//横方向の反射。つまり、右端か左端を超える場合、移動量を反転(マイナス)させます。
xStep = -xStep;
Play(); //サウンドを鳴らします。
}
circleLocation.X += xStep; //ボールのX座標を更新します。
//同様に次の移動でボールがフォームの上端か下端を超えるかチェックします。
if (circleLocation.Y + yStep + circleSize > this.ClientRectangle.Height
|| circleLocation.Y + yStep - circleSize < 0)
{
//縦方向の反射
yStep = -yStep;
Play();
}
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);
}
//▼ここから下はサウンド再生機能です。
//Windowsが持っているメディア制御機能(mciSendString)の呼び出し口を定義
[System.Runtime.InteropServices.DllImport("winmm.dll")]
private static extern int mciSendString(string lpstrCommand, string lpstrReturnString, int uReturnLength, IntPtr hwndCallback);
/// <summary>ランダムにサウンドを再生します。</summary>
private void Play()
{
//再生するサウンドファイルをランダムに決定します。
//サイコロをふってでた目に応じてファイル名を設定するようなロジック
string fileName = Random.Shared.Next(1, 9) switch {
1 => "C2.wav",
2 => "F2.wav",
3 => "G2.wav",
4 => "C3.wav",
5 => "EF2.wav",
6 => "AF2.wav",
7 => "BF2.wav",
_ => "EF3.wav" };
//サウンドファイル名をフルパスにします。アプリ本体のある場所のSoundフォルダー以下です。
fileName = Application.StartupPath + @"Sound\" + fileName;
//このサウンド再生に一意な名前を付けます。ここではシステム時刻とランダムを利用します。
string aliasName = $"Alias{DateTime.Now.Ticks}{Random.Shared.Next(1, 100000)}";
//サウンドを再生します。
mciSendString($"open \"{fileName}\" alias {aliasName}", "", 0, IntPtr.Zero);
mciSendString($"play {aliasName}", "", 0, IntPtr.Zero);
}
if (x == 2 ■ y == 3) {
MessageBox.Show("OK");
}
string status = SystemInformation.Network ■ {true => "オンライン", false => "オフライン" };
MessageBox.Show(status);
次の回では、もうちょっとグラフィックで遊びながら、フレームワークの機能の使い方に慣れてもらいます。