タイトルの通りですが、C#のコード側でボタンを作りたいなと思いました。
例えばマインスイーパーみたいにたくさんボタンがあって、しかもユーザー側がマス目(ボタン数)を決められる場合、
ボタンの数が動的に決まるので、どうしてもxamlだけで作るのは無理なのではと思いました。
コードビハインドとカスタムコントロールを使ってどうにかしていきたいと思います。
カスタムコントロールを使わずコードビハインドで完結するやり方をコメントで教えていただきました!ありがとうございます!この記事はカスタムコントロールを使うやり方のまま残しますが、同じことをやりたい方はぜひコメントも参考にしてみてください。
環境
- VisualStudio2022
- .NET 8.0
- prism 8.1.97
私が慣れているという理由でprismを使います。
VisualStudioの「新しいプロジェクトの作成」から、「Prism Blank App (WPF)」を選択してプロジェクトを作成します。
プロジェクト名は「ButtonListSample1」にしました。
その後、TargetFramework
をnet8.0-windows
に変更しています。
ボタンを置くパネルを作る
MainWindowにStackPanelを追加します。
<Window x:Class="ButtonListSample1.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
Title="{Binding Title}"
Width="525"
Height="350"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<!-- ボタンを追加するパネル -->
<StackPanel x:Name="myPanel" />
</Grid>
</Window>
x:Name="myPanel"
とすることでコードビハインドから参照可能にします。
コードビハインドも直していきます。
using System.Windows;
using System.Windows.Controls;
namespace ButtonListSample1.Views
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// ボタンを作る
for (int i = 0; i < 10; i++)
{
Button button = new()
{
Height = 30,
Width = 30
};
// StackPanelに追加
myPanel.Children.Add(button);
}
}
}
}
高さと幅が30のボタンコントロールを、先ほどxamlで名前を付けたStackPanel myPanel
に追加しています。
デバッグしてみるとこんな感じになりました。
いいですね!
ボタンクリックイベント
このままだとボタンを押してもなにも起こらないので、クリックイベントを処理します。
↓のようにイベントを処理するメソッドを追加して、
private void Button_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("ボタンがクリックされたよ!");
}
クリックイベントで呼ばれるようにします。
Button button = new()
{
Height = 30,
Width = 30
};
+ button.Click += Button_Click;
ボタンを押すと文字が出力されます。
ボタンがクリックされたよ!
どのボタンか判断したい
今のままだと、どのボタンが押されても同じ処理です。
いろいろ解決策はあると思うのですが、今回はボタンに番号を持たせ、その番号でどのボタンが押されたのか判断したいと思います。
番号を持ったボタンはカスタムコントロールで作りたいです。そのためにはまず、カスタムコントロールライブラリというプロジェクトをソリューションに追加します。
ソリューションエクスプローラーでソリューションを右クリック→追加→新しいプロジェクトと進み、「WPFカスタムコントロールライブラリ」を選択します。
プロジェクト名はそのまま「CustomControlLibrary」としました。
プロジェクトを追加すると、CustomControl1.cs
というファイルが自動で作成されていて、下記のようになっています。
using System.Windows;
using System.Windows.Controls;
namespace CustomControlLibrary
{
public class CustomControl1 : Control
{
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
}
}
クラス名をCustomControl1
からMyButtonControl
に変更し、継承元もControl
からButton
に変更します。
public class MyButtonControl : Button
{
static MyButtonControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyButtonControl), new FrameworkPropertyMetadata(typeof(MyButtonControl)));
}
}
今はただButtonを継承しただけで何もいじっていません。つまり、このカスタムコントロールは通常のButtonと同じように動くはずです。コードビハインド内のButtonを、新たに作ったMyButtonControlに差し替えて、同じ動作をするか確認します。
先ほどまでのプロジェクト(ButtonListSample1)に戻り、今作成した「CustomControlLibrary」へプロジェクト参照を追加します。
コードビハインドのButton
をMyButtonControl
に変更します。
// usingを追加
using CustomControlLibrary;
// ボタンを作る
for (int i = 0; i < 10; i++)
{
MyButtonControl button = new() // ButtonからMyButtonControlに変更
{
Height = 30,
Width = 30
};
button.Click += Button_Click;
// StackPanelに追加
myPanel.Children.Add(button);
}
良さそうですね!
出てこない…
このままだとデバッグしても何もないWindowが表示されます。ボタンが出てこないんです。なんでですか……?
おそらくですが、Buttonを継承こそしていますが、Styleは何もつけていないため、透明のままなのではと思います。
というわけでStyleをつけていきます。これも元のButtonと同じものをつけたいです。
カスタムコントロールライブラリに戻り、プロジェクト直下にThemesフォルダを追加します。さらにその中に、Generic.xamlというファイルを追加し、この中にコントロールのスタイルを書いていきます。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControlLibrary">
<Style BasedOn="{StaticResource {x:Type Button}}" TargetType="{x:Type local:MyButtonControl}" />
</ResourceDictionary>
元のButtonと同じがいいので、BaseOn
でButtonを指定しています。
これで実行すると、無事ボタンが出てきました。
先ほどまでと見た目もそっくりで、動作も同じです。今度こそ良さそうです。
番号を持つ
ここからカスタムコントロールをいじっていきます。
今回は縦に並べてボタンを表示しているので、縦方向の番号ということでRowNumber
を追加します。
下記のようにプロパティRowNumber
と、DependencyProperty
を追加します。
static MyButtonControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyButtonControl), new FrameworkPropertyMetadata(typeof(MyButtonControl)));
}
// 追加
public static readonly DependencyProperty RowNumberProperty =
DependencyProperty.Register(
nameof(RowNumber),
typeof(int),
typeof(MyButtonControl),
new UIPropertyMetadata(null)
);
public int RowNumber
{
get { return (int)GetValue(RowNumberProperty); }
set { SetValue(RowNumberProperty, value); }
}
コードビハインドからプロパティを指定します。
for (int i = 0; i < 10; i++)
{
MyButtonControl button = new()
{
Height = 30,
Width = 30,
RowNumber = i, // 追加
};
button.Click += Button_Click;
// StackPanelに追加
myPanel.Children.Add(button);
}
さらに、ボタンを押したときの処理を修正します。
private void Button_Click(object sender, RoutedEventArgs e)
{
// MyButtonControl にキャストする
if (sender is MyButtonControl myButtonControl)
{
Debug.WriteLine($"{myButtonControl.RowNumber + 1} 番目のボタンがクリックされたよ!");
}
}
連番は0から振っているので、表示するときには1足しておきます。
試しに1番上のボタンをクリックすると、
1 番目のボタンがクリックされたよ!
と表示されます。
これで無事、押されたボタンが何番目のボタンか分かるようになりました。
以上で最低限の実装はできたと思います。
感想
C#のコードでボタンを作りたいという当初の目的は達成できました。
横方向の番号が欲しくなった時も、RowNumber
と同じ要領でColumnNumber
を追加すれば良さそうです。
ボタンクリック時のイベントも、今はView(コードビハインド)にそのまま書いていますが、DataContextからViewModelを取得して処理を呼ぶ(さらにViewModelが持っているModelが呼ばれる)ようにできそうです。
あとはプロパティに応じて変わるStyleなどもやってみたいですね。RowNumber
が奇数の時は背景色を変える、とかやりたいです。
ただ、ボタンに番号を持って、その番号で処理を振り分けるのがスマートかと言うと、どうなんでしょうか。自分ではよく分からず、こういう実装になってしまいましたが、もっと良いやり方があればコメントいただけると嬉しいです。