WPFをWinFormsライクに使う:コードだけでシンプルなプログラミングスタイル

コンピュータ

WPFと聞くと「XAMLでUIを定義してMVVMで組み立てる」というイメージを持つ人が多いと思います。確かにそれは王道ですが、すべての場面で必要とは限りません。

実際には、WinFormsのようにコードだけで画面を構築し、シンプルにイベント処理を書いていくスタイルも可能です。さらに、ファクトリーやビルダーのヘルパークラスを用意すれば、冗長になりがちなUI生成コードもすっきりとまとめられます。

本記事では「NoXAML」でWPFをWinFormsライクに使う方法を紹介し、最小限のコードでGUIアプリの骨格を作るシンプルなプログラミングスタイルを提案します。

最小アプリのコード比較

ボタンを1つ配置し、クリックするとメッセージボックスを表示するGUIアプリを作ります。

WinForms

ファイル名:Form1.cs

namespace helloWinForms;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        this.Size = new Size(200, 130);

        var btn = new Button()
        {
            Text = "Click Me!!",
            Size = new Size(100, 30),
            Location = new Point (40, 30),
        };
        this.Controls.Add(btn);

        btn.Click += (s, e) =>
        {
            MessageBox.Show("Hello World!!", "メッセージ");
        };
    }
}

コードの流れ

  1. フォームを継承したコンストラクタで、ウィンドウサイズを変更
  2. ボタンを生成
  3. ウィンドウに配置
  4. ボタンのクリックイベントでメッセージボックスを表示

WPF+XAML(コードビハインド)

ファイル名:MainWindow.xaml

<Window x:Class="helloWPF.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:helloWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="130" Width="200">
    <Grid>
        <Button
            Content="Click Me!!"
            Height="30" Width="100" 
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Click="OnSayHello" />
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Windows;

namespace helloWPF;

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    private void OnSayHello(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Hello World!!");
    }    
}

XAMLを使う場合、GUIを構成するコントーロールの配置はXAMLに記述することになります。
スケルトンコードのグリッド内にボタンを追記しています。

    <Grid>
        <Button
            Content="Click Me!!"
            Height="30" Width="100" 
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Click="OnSayHello" />
    </Grid>

WinFormsではボタンの位置を座標指定していますが、WPFではHorizontalAlignmentVerticalAlignmentプロパティで、親要素から垂直水平方向で中央に配置するように設定してみました。
コードビハインドですので、ボタンをクリックした際呼び出されるメソッドをClick="OnSayHello"で定義しています。

MainWindow.xaml.csではコードビハインドのC#のコードを記述しており、先ほどXAMLで定義したOnSayHelloを実装しており、メッセージボックスを表示するコードを記述しています。

WPF+NoXAML

ファイル名:MainWindow.cs

// メインウィンドウ
using System.Windows;
using static UI;

public sealed class MainWindow : Window
{
    public MainWindow()
    {
        var grd = UI.Grd();
        var btn = Btn(
            "Click Me!!",
            () => MessageBox.Show("Hello World!!")
        ).PlaceIn(grd);
        
        btn.Height = 30;
        btn.Width = 100;
        btn.HorizontalAlignment = HorizontalAlignment.Center;
        btn.VerticalAlignment = VerticalAlignment.Center;

        this.Init("Hello NoXAML", 200, 130).Content(grd);
    }
}

こちらが、この記事のメインになります。前項のWPF+XAMLで記述したコードをC#だけで記述したプログラムに成ります。コード量を減らす為、ヘルパークラスを使っているので、厳密にはコード量の比較にはなりませんが、ヘルパークラスを使う側のコードだけを見ると、結構短めのコードになっています。

コードの流れは、

  1. グリッドを作成。
  2. ボタンを作成しクリック時のメッセージを表示するコードを記述⇒グリッドへ登録。
  3. ボタンの幅と高さ、垂直水平のレイアウトの定義。
  4. ウィンドウの初期化と、ウィンドウのコンテンツとしてグリッドを登録。

といった感じです。

ヘルパークラスについて

NoXAMLでそのままコントロールを生成して配置することもできますが、素直に書くとどうしても初期化コードが冗長になりがちです。
そこで本記事では、ヘルパークラスを用意し、次のような方針で設計しています。

  • ファクトリー / ビルダースタイル
    • よく使うコントロール(Button, Grid, TextBox など)の生成をメソッド化
    • デフォルト引数付きで「最小限の指定」で作れるようにする
    • 生成後は通常の WPF コントロールとして扱える(継承はしていない)
  • 定型処理の吸収
    • 「生成したコントロールを親に登録する」といった繰り返し処理を内部でまとめる
    • 使う側は「置く」ことだけを意識できる
  • 拡張メソッドでDSL風に
    • PlaceIn(parent) のように書けるようにし、メソッドチェーンで自然に書ける

コード例

例えばボタンを配置するコードは、素直に書くと:

var btn = new Button();
btn.Content = "Click Me!!";
btn.Click += (_, __) => MessageBox.Show("Hello World!!");
grid.Children.Add(btn);

となります。

XAMLを使わないWPFはWinFormsとよく似た雰囲気になります。

これをヘルパークラスを使うと:

Btn("Click Me!!", () => MessageBox.Show("Hello World!!"))
    .PlaceIn(grid);

と、1行にまで短縮できます。

DSL風の記述スタイル

さらに、複数のコントロールを組み合わせる場合でも、メソッドチェーンを使えば DSL 的に書けるようにしています。
たとえばラベルとテキストボックスを並べるコードも:

Lbl("名前:").PlaceIn(grid);
Txt().PlaceIn(grid);

のようにシンプルに書けます。

ヘルパークラスの利点

  • コード量の削減
    → UI構築部分が短く読みやすい
  • WinFormsライクな直感
    → 生成・配置・イベントを一連の流れで記述できる
  • 柔軟性
    → 継承していないため、通常の WPF コントロールとして全機能を利用可能
  • 可読性
    → DSL風で「GUIの構造が見やすいコード」になる

MVVM

テキストボックスに文字を入力しボタンを押すと、テキストブロックに文字が表示されます。

XAML

MVVMはアプリを構成するクラスを役割に応じてModelとViewとViewModelに分割します。
WPFでは一般的にViewをXAMLで記述し、ViewModelはINotifyPropertyChangedを実装する形になります。
INotifyPropertyChangedはプロパティの変更を通知する機能を持ち、データバインディングのソースとなれるプロパティを定義することが出来ます。ICommandは発生したイベントで処理するコマンドを記述し、こちらもデータバインディングソースになることが出来ます。

ファイル名:MainWindow.xaml

<Window x:Class="wpfMiniSample.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:wpfMiniSample.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="300">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <StackPanel Margin="20">
        <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
        <Button Content="Say Hello" Command="{Binding SayHelloCommand}" Margin="0,10,0,0"/>
        <TextBlock Text="{Binding Greeting}" FontSize="16" FontWeight="Bold"/>
    </StackPanel>
</Window>

ファイル名:Helpers\RelayCommand.cs


using System.Windows.Input;

public class RelayCommand : ICommand
{
    private readonly Action<object?> _execute;
    private readonly Predicate<object?>? _canExecute;

    public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public bool CanExecute(object? parameter) => _canExecute == null || _canExecute(parameter);
    public void Execute(object? parameter) => _execute(parameter);

    public event EventHandler? CanExecuteChanged
    {
        add    => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }
}

ファイル名:Models\Person.cs


namespace wpfMiniSample.Models;

public class Person
{
    public string Name { get; set; } = "";
}

ファイル名:ViewModels\MainViewModel.cs


using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace wpfMiniSample.ViewModels;

public class MainViewModel : INotifyPropertyChanged
{
    private string _name = "";
    private string _greeting = "";

    public string Name
    {
        get => _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged();
            }
        }
    }

    public string Greeting
    {
        get => _greeting;
        private set
        {
            if (_greeting != value)
            {
                _greeting = value;
                OnPropertyChanged();
            }
        }
    }

    public ICommand SayHelloCommand { get; }

    public MainViewModel()
    {
        SayHelloCommand = new RelayCommand(_ => Greeting = $"Hello, {Name}!");
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

MVVM+Rx

こちらはRxを使った例です。ViewModelでバインドするプロパティにReactiveProperty、同じくコマンドをReactiveCommandを使います。INotifyPropertyChangedやICommandを使ったコードと比べてViewModelがシンプルに記述することが出来るようになっていることが確認できます。

ファイル名:wpfMiniSample.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="ReactiveProperty.WPF" Version="9.7.0" />
  </ItemGroup>

</Project>

ファイル名:MainWindow.xaml

<Window x:Class="wpfMiniSample.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:wpfMiniSample.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="300">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <StackPanel Margin="20">
        <TextBox Text="{Binding Name.Value, UpdateSourceTrigger=PropertyChanged}" />
        <Button Content="Say Hello" Command="{Binding SayHelloCommand}" Margin="0,10,0,0"/>
        <TextBlock Text="{Binding Greeting.Value}" FontSize="16" FontWeight="Bold"/>
    </StackPanel>
</Window>

ファイル名:ViewModels\MainViewModel.cs

using Reactive.Bindings;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;

namespace wpfMiniSample.ViewModels;

public class MainViewModel : INotifyPropertyChanged
{
    // 入力値
    public ReactiveProperty<string> Name { get; }

    // 出力メッセージ
    public ReadOnlyReactiveProperty<string?> Greeting { get; }

    // コマンド
    public ReactiveCommand SayHelloCommand { get; }

    public MainViewModel()
    {
        // 初期値 = 空文字列
        Name = new ReactiveProperty<string>("");

        // コマンド:常に実行可能
        SayHelloCommand = new ReactiveCommand();

        // ボタン押下イベント → Greeting更新
        Greeting = SayHelloCommand
            .WithLatestFrom(Name, (_, name) => $"Hello, {name}!")
            .ToReadOnlyReactiveProperty();

    }

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

他のファイルはMVVMと同じ

NoXAML+MVVM

NoXAMLの場合、MainWindow.xamlの代わりに以下のMainWindow.csでビューを作成します。
それ以外のクラス(ソースファイルは)XAML+MVVMと同じ物がそのまま使えます。

ファイル名:MainWindow.cs

// メインウィンドウ
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using wpfMiniSample.ViewModels;

namespace wpfMiniSample;

public sealed class MainWindow : Window
{
    public MainWindow()
    {
        Title = "NoXAML + MVVM";
        Width = 300;
        Height = 200;

        // ViewModel を生成して DataContext に載せる
        var vm = new MainViewModel();
        DataContext = vm;

        // レイアウト
        var root = new StackPanel { Margin = new Thickness(20) };
        Content = root;

        // 入力 TextBox(Name <-> TwoWay、即時反映)
        var txtName = new TextBox();
        txtName.SetBinding(TextBox.TextProperty, new Binding(nameof(MainViewModel.Name))
        {
            Mode = BindingMode.TwoWay,
            UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
        });
        root.Children.Add(txtName);

        // ボタン(Command Binding)
        var btn = new Button { Content = "Say Hello", Margin = new Thickness(0, 10, 0, 0) };
        btn.SetBinding(Button.CommandProperty, new Binding(nameof(MainViewModel.SayHelloCommand)));
        root.Children.Add(btn);

        // 出力 TextBlock(OneWay)
        var lbl = new TextBlock { FontSize = 16, FontWeight = FontWeights.Bold };
        lbl.SetBinding(TextBlock.TextProperty, new Binding(nameof(MainViewModel.Greeting)));
        root.Children.Add(lbl);
    }
}

3パターンを試してみましたが、Model部分は共通で再利用できる点がMVVMの特徴だと言えます。
その他の部分も結構共通する部分が多く、MVVMとMVVM+NoXAMLでは、実質XAMLをcsで書き直すだけでビルドすることが出来ました。

XAMLを使わない理由

筆者が XAML を避けている大きな理由は、不具合がビルドをすり抜け、デバッグトレースが難しい点にあります。
C# コードだけで書けば型チェックが効くため、ミスはコンパイルエラーで即座に発見できます。
また、スタックトレースもシンプルに追えるため、デバッグ効率の面で大きな違いがあります。

まとめ

WPF にはさまざまなプログラミングスタイルがあります。

  • WinFormsライクにコードだけで組み立てる
  • XAML+コードビハインドで分離する
  • MVVM+バインディングで厳密に設計する
  • NoXAML+ヘルパー/DSLでシンプルに書く
  • Rx を組み合わせて宣言的に管理する

どのスタイルも一長一短で、状況に合わせて選ぶのが正解だと思います。
特に個人開発や小規模なツールであれば、シンプルに「自分が書きやすいスタイル」を選んで問題ありません。
前提として重要なのは、「プログラミングスタイルを自分で選択できる環境」であることです。

一方、多人数で開発するプロジェクトでは統一感が求められます。
その場合は、保守性や再利用性を重視して XAML+MVVM(+Rx) が選ばれることが多いでしょう。

WPF の面白さは、WinFormsライクにも、モダンなMVVM+Rxスタイルにも、どちらにも振れる柔軟性にあります。
自分の目的と環境に合わせて、最適なスタイルを探してみてください。

コメント