Windows Presentation Foundation (WPF) に入門してみる part 3 ComboBoxを使う


はじめに

xamlファイルを全く記述しないでC#のコードだけでGUIを構築するのが目標です。
今回も「XAML」「バインディング」などの知識ゼロのまま進めていきます。

前回までは記事はこちら。
Windows Presentation Foundation (WPF) に入門してみる part 1 - パソコン関連もろもろ
Windows Presentation Foundation (WPF) に入門してみる part 2 Imageを並べる - パソコン関連もろもろ

前回Imageを並べるところまで学習しました。今回はComboBoxを追加してその結果によってImageの並べ方を変えていきます。

左下にComboBoxを配置しました。

f:id:touch-sp:20210926132229p:plain:w500
「かんたん」を選択
f:id:touch-sp:20210926132315p:plain:w500
「ふつう」を選択
f:id:touch-sp:20210926132338p:plain:w500
「むずかしい」を選択
ついでにメモリーゲーム(神経衰弱)のゲームの部分のコードも完成させます。

本編

ComboBoxを追加する

ComboBoxを追加するのは非常に簡単です。「difficulty」という名前にします。
ComboBox difficulty = new ComboBox() {
    Margin = new Thickness(20),
    VerticalContentAlignment = VerticalAlignment.Center,
    HorizontalContentAlignment = HorizontalAlignment.Center };
difficulty.Items.Add("かんたん");
difficulty.Items.Add("ふつう");
difficulty.Items.Add("むずかしい");
difficulty.SelectedIndex = 1;

MainWindowにボタン類を配置する

ComboBoxを配置する方法がわかったのでMainWindowにボタンを配置するコードを書きます。 「MainWindow_Setting」というメソッド名にします。

アプリ起動時に一度だけ呼び出されるメソッドです。

新しいファイルをプロジェクトに加えてそのファイルに記述しました。
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace wpf_challenge
{
    public partial class MainWindow : Window
    {
        Button startButton = new Button() { Margin = new Thickness(20) };
        Button exitButton = new Button() { Margin = new Thickness(20) };

        ComboBox difficulty = new ComboBox() {
            Margin = new Thickness(20),
            VerticalContentAlignment = VerticalAlignment.Center,
            HorizontalContentAlignment = HorizontalAlignment.Center };

        Grid mainGrid = new Grid();
        Grid lowerGrid = new Grid();

        private void MainWindow_Setting()
        {
            WindowStyle = WindowStyle.None;
            WindowState = WindowState.Maximized;

            //mainGridの作成(2行)と配置
            Content = mainGrid;
            mainGrid.Background = new SolidColorBrush() { Color = Color.FromRgb(240, 240, 240) };
            mainGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(8, GridUnitType.Star) });
            mainGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });

            //lowerGridの作成(4列)と配置
            for (int i = 0; i < 4; i++)
            {
                lowerGrid.ColumnDefinitions.Add(new ColumnDefinition());
            }
            Grid.SetRow(lowerGrid, 1);
            mainGrid.Children.Add(lowerGrid);

            //ボタンの作成と配置
            startButton.Content = new Viewbox() { Child = new Label(){ Content = "Start" } };
            exitButton.Content = new Viewbox() { Child = new Label() { Content = "Exit" } };

            startButton.Click += PushstartButton;
            Grid.SetColumn(startButton, 1);
            lowerGrid.Children.Add(startButton);

            exitButton.Click += PushexitButton;
            Grid.SetColumn(exitButton, 2);
            lowerGrid.Children.Add(exitButton);

            //ComboBoxの作成と配置
            difficulty.Items.Add(new Viewbox() { Child = new Label() { Content = "かんたん" } });
            difficulty.Items.Add(new Viewbox() { Child = new Label() { Content = "ふつう" } });
            difficulty.Items.Add(new Viewbox() { Child = new Label() { Content = "むずかしい" } });
            difficulty.SelectedIndex = 1;
            difficulty.FontSize = 20;
            Grid.SetColumn(difficulty, 0);
            lowerGrid.Children.Add(difficulty);
        }
        private void PushexitButton(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}
ComboBoxのアイテム追加方法を少し変えています。こちらの方が少し見やすくなります。

MainWindowにImageを配置する

「Game_Setting」というメソッド名にします。

スタートボタンが押された時に呼び出されるメソッドです。

新しいファイルをプロジェクトに加えてそのファイルに記述しました。

ComboBoxで何が選択されているかを判定し、それによって並べるカードの枚数を変更しています。
using System.Windows;
using System.Windows.Controls;

namespace wpf_challenge
{
    public partial class MainWindow : Window
    {
        Grid upperGrid;
        Image[] cards;

        private void Game_Setting(int tate, int yoko)
        {
            //upperGridをいったんクリア
            mainGrid.Children.Remove(upperGrid);
            upperGrid = new Grid();

            //upperGirdの作成と配置
            for (int i = 0; i < tate; i++)
            {
                upperGrid.RowDefinitions.Add(new RowDefinition());
            }
            for (int i = 0; i < yoko; i++)
            {
                upperGrid.ColumnDefinitions.Add(new ColumnDefinition());
            }
            Grid.SetRow(upperGrid, 0);
            mainGrid.Children.Add(upperGrid);

            //cardsの作成
            cards = new Image[tate * yoko];
            for (int i = 0; i < tate * yoko; i++)
            {
                cards[i] = new Image() { Margin = new Thickness(10) };
                cards[i].Name = $"card_{i}";
                cards[i].MouseDown += Card_MouseDown;
            }

            for (int i = 0; i < tate; i++)
            {
                for (int j = 0; j < yoko; j++)
                {
                    Grid.SetRow(cards[yoko * i + j], i);
                    Grid.SetColumn(cards[yoko * i + j], j);
                    upperGrid.Children.Add(cards[yoko * i + j]);
                }
            }
        }
    }
}

ゲーム部分のコードを書く

using System;
using System.Media;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;

namespace wpf_challenge
{
    public partial class MainWindow : Window
    {
        int tate;
        int yoko;

        string[] all_images_path = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory + "/OriginalImages", "*.jpg");
        BitmapImage backside = new BitmapImage(new Uri(AppDomain.CurrentDomain.BaseDirectory + "/BacksideImage/0.jpg"));
        List<string> selected_images_path;
        Dictionary<string, BitmapImage> path_to_image = new Dictionary<string, BitmapImage>();
        int? first_choice = null;
        bool[] card_opened;

        SoundPlayer ok = new SoundPlayer(AppDomain.CurrentDomain.BaseDirectory + "/sounds/ok.wav");
        SoundPlayer finished = new SoundPlayer(AppDomain.CurrentDomain.BaseDirectory + "/sounds/yattane.wav");

        public MainWindow()
        {
            InitializeComponent();
            MainWindow_Setting();
        }
        private void gameStart()
        {
            startButton.IsEnabled = false;
            difficulty.IsEnabled = false;

            first_choice = null;

            //使用するカードを選ぶ
            selected_images_path = all_images_path.OrderBy(i => Guid.NewGuid()).Take(tate * yoko / 2).ToList();
            path_to_image.Clear();
            foreach(string each_path in selected_images_path)
            {
                path_to_image.Add(each_path, new BitmapImage(new Uri(each_path)));
            }
            selected_images_path.AddRange(selected_images_path);
            selected_images_path = selected_images_path.OrderBy(i => Guid.NewGuid()).ToList();
            card_opened = new bool[tate * yoko];
            for (int i = 0; i < tate * yoko; i++)
            {
                card_opened[i] = false;
                cards[i].Source = backside;
            }
        }
        private void PushstartButton(object sender, RoutedEventArgs e)
        {
            switch (difficulty.SelectedIndex)
            {
                case 0:
                    tate = 2;
                    yoko = 4;
                    break;
                case 1:
                    tate = 3;
                    yoko = 4;
                    break;
                case 2:
                    tate = 4;
                    yoko = 6;
                    break;
                default:
                    break;
            }
            Game_Setting(tate, yoko);
            gameStart();
        }
        private async void Card_MouseDown(object sender, RoutedEventArgs e)
        {
            string card_name = ((Image)sender).Name.ToString();
            int card_no = int.Parse(card_name.Replace("card_", ""));

            //すでにめくられていたら何もしない
            if (card_opened[card_no] == true) return;

            //すべてのカードのMouseDownをいったん無効にする(<=これ重要)
            foreach (Image each_card in cards) each_card.IsEnabled = false;

            //カードをめくる
            ((Image)sender).Source = path_to_image[selected_images_path.ElementAt(card_no)];
            card_opened[card_no] = true;

            if (first_choice == null) //1枚目なら
            {
                first_choice = card_no;
            }
            else //2枚目なら
            {
                if (selected_images_path[card_no] == selected_images_path[(int)first_choice]) //カード一致
                {
                    ok.Play();
                    //ゲーム終了の判定
                    if(card_opened.All(e => e == true))
                    {
                        await Task.Delay(500);
                        finished.Play();
                        startButton.IsEnabled = true;
                        difficulty.IsEnabled = true;
                    }
                }
                else //カード不一致
                {
                    await Task.Delay(700);
                    //カードを戻す
                    cards[(int)first_choice].Source = backside;
                    card_opened[(int)first_choice] = false;
                    ((Image)sender).Source = backside;
                    card_opened[card_no] = false;
                }
                first_choice = null;
            }
            //すべてのカードを有効にもどす
            foreach (Image each_card in cards) each_card.IsEnabled = true;
        }
    }
}

今回のポイント

Windowにコントロール(ButtonやImageなど)を配置する時にその座標や大きさを一度も指定していません。それをしないでもきれいに並べてくれます。

文字サイズも本当は指定したくなかったのですがComboBoxだけは「20」の指定をしました。どうもうまく文字サイズが調整できなかったからです。

Buttonの文字サイズはViewboxを使うと自動調整してくれるので指定せずに済みました。

最後に

間違いや改善点があればコメント頂けましたら幸いです。