プログラミング演習Ⅲ(2023)

【WPF練習14】壁崩し(3/9)

プロジェクトタイプC# WPFアプリケーション※
プロジェクト名T10b
ソリューション名PET10
ターゲットフレームワーク.NET 6.0 (長期的なサポート)

※ 「WPFアプリ(.NET Framework)」ではないので注意せよ!

注意
  • 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.
    • 先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと.
    • プロジェクトの作成作業については準備を参照せよ.

14-3. 大枠の作成

まずはアプリケーションの大枠を作るため,MainWindow.xaml_に示す内容を追記しよう.

今回はCanvasViewbox(びゅーぼっくす)という部品に入れ子にして使用している. ViewBoxは内部に配置した部品を比率を保ったまま拡大縮小して表示してくれる部品である. 今回のCanvasは11行目で指定しているように幅600ピクセル,高さ800ピクセルと,ウィンドウサイズよりも大きいが, ViewBoxに入れることで常に全体を表示することができるようになる.

Canavsの中にはRectangle(名前:player)とEllipse(名前:ball)を一つずつ配置している. これらはそれぞれ壁崩しのプレイヤー(横棒)とボールとして使用する図形である. ちなみに24行目でballに指定しているPanel.ZIndexプロパティは,表示の優先順序を設定するプロパティである. このプロパティには整数値を設定するが,大きな値を設定するほど優先して表示される(≒他の部品で覆い隠されなくなる).

MainWindow.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
<Window x:Class="T10b.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:T10b"
        mc:Ignorable="d"
        Title="MainWindow" Height="400" Width="300" Background="#F0F0F0">
    <Grid>
        <Viewbox>
            <Canvas x:Name="canvas1" Width="600" Height="800"
                    Background="White" ClipToBounds="True">
                
                <!-- プレイヤー(横棒) -->
                <Rectangle x:Name="player" Width="100" Height="20"
                           Canvas.Left="250" Canvas.Top="700"
                           Stroke="Black" Fill="Orange"
                           Tag="Player"></Rectangle>

                <!-- ボール -->
                <Ellipse x:Name="ball" Width="10" Height="10"
                         Canvas.Left="295" Canvas.Top="685"
                         Stroke="Black" Fill="Red"
                         Panel.ZIndex="127"></Ellipse>

            </Canvas>
        </Viewbox>
    </Grid>
</Window>

ここまで書けたら起動してウィンドウの拡大を試みてみよう._のようにCanvasの描画内容が 拡大縮小されて表示されることが分かるはずである.

作業結果

次にマウスの移動によってプレイヤー(横棒)が左右に動くようにしてみよう.MainWindow.xamlを編集して,canvas1MouseMoveイベントを設定し イベントハンドラに_に示す内容を追記しよう.これはWPFのほとんどの部品が備えているイベントで,部品上でマウスが動くたびに発生するイベントである.

MainWindow.xaml.csの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private void canvas1_MouseMove(object sender, MouseEventArgs e)
{
    Point mousePos = e.GetPosition(canvas1); // マウスの位置を取得する.

    // プレイヤーの位置を計算する.
    double playerLeft = mousePos.X - player.Width / 2;
    playerLeft = Math.Clamp(playerLeft, 0, canvas1.Width - player.Width); // Canvasの外に出ないように制限

    Canvas.SetLeft(player, playerLeft); // プレイヤーの位置を設定する.
}

ここまで書けたら起動してCanvas上でマウスを動かしてみよう._に示すようにプレイヤー(横棒)が 動くことが分かるはずである.

作業結果

次に前回の様にDispatcherTimerによる 定期処理でボールを動かしてみよう.まずMainWindow.xaml.csファイルの冒頭に_に示すusingディレクティブを追記する.

追記するusingディレクティブ
1
using System.Windows.Threading;

そしてMainWindowクラスに_に示す内容を追記しよう.Timer_Tick()メソッドの定義は,17行目の+=を打ち込んだ後に タブキーを押せば生成されるので,その中に37~54行目の内容を追記しよう.

MainWindow.xaml.csの追記内容
 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
public partial class MainWindow : Window
{
    private double speedX;         // ボールの速度(X)
    private double speedY;         // ボールの速度(Y)
    private DispatcherTimer timer; // タイマー

    private Random rand;           // 乱数生成器

    public MainWindow()
    {
        InitializeComponent();

        rand = new Random();

        timer = new DispatcherTimer();                 // タイマーを生成する.
        timer.Interval = new TimeSpan(0, 0, 0, 0, 16); // 16ミリ秒ごとに作動させる.
        timer.Tick += Timer_Tick;                      // ※ Timer_Tickはサジェストを使って生成すること!

        ResetGame(); // ゲームを初期化する.
    }

    // ゲームの初期化処理のためのヘルパーメソッド
    private void ResetGame() 
    {
        Canvas.SetLeft(ball, 295);  // ボールの位置を
        Canvas.SetTop(ball, 685);   // 初期位置に戻す.

        speedX = -5.0 + 10.0 * rand.NextDouble();   // ボールの速度を
        speedY = -(8.0 + 16.0 * rand.NextDouble()); // ランダムに生成する.

        timer.Start(); // タイマー開始
    }

    // タイマー処理
    private void Timer_Tick(object sender, EventArgs e)
    {
        double ballX = Canvas.GetLeft(ball) + ball.Width / 2; // 現在のボールの位置を
        double ballY = Canvas.GetTop(ball) + ball.Height / 2; // 取得する.

        double nextBallX = ballX + speedX; // ボールの位置に speedX/Y を加算して
        double nextBallY = ballY + speedY; // 次のボール位置を計算する.

        if (nextBallX < 0 || canvas1.Width <= nextBallX)  //
        {                                                 //
            speedX *= -1;                                 //
        }// if                                            // 次のボール位置がキャンバスの外に
                                                          // 出てしまった場合に,速度を反転する.
        if (nextBallY < 0 || canvas1.Height <= nextBallY) //
        {                                                 //
            speedY *= -1;                                 //
        }// if                                            //

        Canvas.SetLeft(ball, nextBallX - ball.Width / 2); // ボールの位置を設定する.
        Canvas.SetTop(ball, nextBallY - ball.Height / 2); //
    }
    
    // (..以下略..)

ここまで書けたら起動してみよう._に示すようにボールがキャンバス内を動き回ることが分かるはずである.

作業結果

このままでもよいが,いまのTimer_Tick()メソッドの実装だと処理中にボールがキャンバスの外に出てしまう場合がある. 以降の処理ではそのままだと不都合なので修正しよう.Timer_Tick()メソッドに_に示す内容を追記する.

MainWindow.xaml.csの追記内容
 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
private void Timer_Tick(object sender, EventArgs e)
{
    const double EPSILON = 1 / 256.0; // 微小数定数

    double ballX = Canvas.GetLeft(ball) + ball.Width / 2;
    double ballY = Canvas.GetTop(ball) + ball.Height / 2;

    double nextBallX = ballX + speedX;
    double nextBallY = ballY + speedY;

    if (nextBallX < 0 || canvas1.Width <= nextBallX) 
    {
        speedX *= -1;

        // ボールの位置を壁から少し離れた位置に設定して
        // 次のボールの位置を壁から速度のぶん移動した位置にする.
        ballX = Math.Clamp(nextBallX, 0, canvas1.Width) + EPSILON * speedX; 
        nextBallX = ballX + speedX;                                         
    }// if

    if (nextBallY < 0 || canvas1.Height <= nextBallY) 
    {
        speedY *= -1;

        // (同上)
        ballY = Math.Clamp(nextBallY, 0, canvas1.Height) + EPSILON * speedY;
        nextBallY = ballY + speedY;
    }// if

    Canvas.SetLeft(ball, nextBallX - ball.Width / 2);
    Canvas.SetTop(ball, nextBallY - ball.Height / 2);
}
Last updated on 2024-01-09
Published on 2024-01-09

Powered by Hugo. Theme by TechDoc. Designed by Thingsym.