読者です 読者をやめる 読者になる 読者になる

かたちづくり

つれづれに、だらだらと、おきらくに

初めての WPF 事始め

この文章は先日中途で入社されたSさんに向けて書いています。SさんはC++Javaの経験はあるが、C#WPFの経験はないそうです。

私は一般向けに解説記事が書けるほどWPFに詳しいわけでは全くありませんが、そうは言ってもSさんには業務が回せる程度の知識を伝えなければならないわけで、ならばブログとして説明記事を公開してしまってあわよくば誤りを訂正して頂いたり補足を頂ければラッキー、などと思ったのが書き始めた経緯です。といっても、ほんの導入部までしか書けませんでした。「口頭で伝えるほうが手っ取り早いんじゃないか」とか思い始めちゃうとなかなかモチベーションが続かないですね(^^; とりあえず今後の学習の取っ掛かりになればいいなあという程度の浅~い内容ということで、簡単な Binding までを書きました。

WPFのメリット

WPFのメリットは次の2点です。

  • ビューとロジックを分離した綺麗な設計ができる(MVVMパターン
  • ビューのデザインの自由度が高い

ここでは前者の特長を伝えたいと思います。理由は2つあって、まずウチの業務では凝ったGUIデザインを追求することよりもメンテナンス性の高い綺麗な設計を実現するほうが重要度が高いこと、次にGUIデザインを追求するにしてもまず土台となる設計がきちんとできることが前提であると思うことです。

XAMLとは

Visual StudioWPF アプリケーションを作成すると、次のようなXMLが自動生成されます。これがXAML(eXtensible Application Markup Language)というヤツです。XAMLと書いて「ザムル」と発音するようです。

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        
    </Grid>
</Window>

このXAMLを使ってGUIをデザインするのですが、ここではGUIのデザインで使用するXAMLの要素についていちいち説明しません。それはググるなり本を読むなり既存コードを読むなりすれば分かることだと思います。それよりも、XAMLとはなんぞや、という大枠をザックリと掴んでおきましょう。

XAMLというのはGUIデザイン専用言語ではありません。他の目的にも使用しうるものです。それを理解するために、まず次のようなクラスを定義してみます。

  public class Piyo
  {
    public int Foo { get; set; }
  }

  public class Hoge
  {
    public Piyo Piyo { get; set; }
  }

そして次のようなXAMLファイル(Hoge.xaml とします)を作成します。

<Hoge xmlns="clr-namespace:WpfApplication1">
  <Hoge.Piyo>
    <Piyo Foo="123"/>
  </Hoge.Piyo>
</Hoge>

そうしますと、次のように HogeインスタンスXAML から生成することが出来るのです。

// hoge.Piyo.Foo は 123 に初期化されている
var hoge = (Hoge)Application.LoadComponent(
  new Uri( "/WpfApplication1;component/Hoge.xaml", UriKind.Relative ) );

つまり、XAMLというのはオブジェクトのプロパティを設定する初期化処理をXML形式に則って宣言的に書けるもの、ということになります。決してGUIデザインに特化した言語ではないということが分かるかと思います。

DataContextプロパティ

WPFのライブラリは巨大です。膨大なクラス、膨大なメソッド、膨大なプロパティ。その中でも最初に覚えて欲しいのがこの DataContext プロパティです。大げさな言い方ですが、膨大なプロパティの中でこの DataContext プロパティは燦然とひときわ明るく輝いているのです。
さあ、次のようなクラスを用意して MainWindow.xaml の DataContext プロパティに設定してみましょう。

class MainWindowModel {...}
<Window x:Class="WpfApplication1.MainWindow"
        ...
        xmlns:a="clr-namespace:WpfApplication1">
  <Window.DataContext>
    <a:MainWindowModel/>
  </Window.DataConext>
  ...
</Window>

図で表すと次のようになります。
f:id:u_1roh:20140302150334p:plain
これがWPFプログラミングの出発点です。ビューのデザイン(見た目)は MainWindow (XAML) で、ビューの状態管理は MainWindowModel で、と役割分担するのがWPFプログラミングの基本となります。

Data Binding

Binding という機能を使うと、DataContext(ここでは MainWindowModel)に定義されているプロパティをビューに「バインド」することが出来ます。まずは単純な例として MainWindowModel に Message プロパティを定義し、これを MainWindow 上に Label として表示してみましょう。

class MainWindowModel {
  public string Message { get { return "この記事には誤りが含まれている可能性があります。"; } }
<Window ...>
  <Window.DataContext>
    <a:MainWindowModel/>
  </Window.DataConext>
  <StackPanel>
    <Label Content="{Binding Path=Message}"/>
  </StackPanel>
</Window>

こうなります。
http://gyazo.com/abdb66e1ca66b08885401145fe352969.png
この例ははじめの一歩としては悪くありませんが、ラベル文字列は固定で何の変化もありませんから面白みに欠けますね。ではユーザーに「同意」を促すチェックボックスを追加してみましょう。

class MainWindowModel {
  ...
  bool isAgreed;
  public bool IsAgreed {
    get { return isAgreed; }
    set { isAgreed = value; } // ← ここに break point を仕掛けてみよう!
  }
}
<Window ...>
  ...
    <Label Content="{Binding Path=Message}"/>
    <CheckBox IsChecked="{Binding Path=IsAgreed}">同意します</CheckBox>
  ...
</Window>

http://gyazo.com/1ce7404d6af26ddd54d1e2dbbf032e21.png
IsAgreed プロパティにブレークポイントを仕掛けてチェックボックスをON/OFFしてみてください。チェック状態の変化が MainWindowModel に伝達されることが分かるはずです。これが Binding の威力です。この機能のお陰で、ビューの状態管理を簡単に別のクラス(ここでは MainWindowModel)に分離することができるようになるのです。
なお、Binding 出来るのは「依存関係プロパティ(Dependency Property)」というちょっと特殊なプロパティだけです。しかしWPFコントロールのプロパティはほとんど全て Dependency Property として定義されていますので最初はあまり意識する必要はないと思います。

INotifyPropertyChanged インターフェイス

実は今までの MainWindowModel には問題が残っていて、これを解決するのが INotifyPropertyChanged インターフェイスです。
まずは問題点をあぶり出すサンプルを作っていきましょう。画面に「次へ」ボタンを追加し、「同意します」にチェックが入っている場合のみ「次へ」ボタンが表示されるようにしたいと思います。
「次へ」ボタンの可視性(Visibility)は次のようなプロパティで定義できるでしょう。

class MainWindowModel {
  ...
  public Visibility NextButtonVisibility
  {
    get { return this.IsAgreed ? Visibility.Visible : Visibility.Collapsed; }
  }
}

XAMLに「次へ」ボタンを追加し、その可視性を NextButtonVisibility プロパティにバインドします。

...
<Label Content="{Binding Path=Message}"/>
<CheckBox IsChecked="{Binding Path=IsAgreed}">同意します</CheckBox>
<Button Visibility="{Binding Path=NextButtonVisibility }">次へ</Button>
  ...

さあ起動してみましょう。残念!チェックボックスをONにしてもボタンは表示されません。
この例ですと IsAgreed プロパティの値が変化すると同時に NextButtonVisibility プロパティも連動して変化するわけですが、その変化をビュー側(つまり MainWindow)が知る手段がありません。ですので NextButtonVisibility の値が変わっていることに MainWindow が気づかないのです。この問題を解消するためには、MainWindow にプロパティの変化を通知してあげる必要があります。そのためのインターフェイスが INotifyPropertyChanged なのです。次の図のように MainWindowModel がこのインターフェイスを実装する必要があるのです。
f:id:u_1roh:20140302215427p:plain
INotifyPropetyChanged インターフェイスの定義は下記のとおりです。

namespace System.ComponentModel
{
  // 概要:
  //     プロパティ値が変更されたことをクライアントに通知します。
  public interface INotifyPropertyChanged
  {
    // 概要:
    //     プロパティ値が変更するときに発生します。
    event PropertyChangedEventHandler PropertyChanged;
  }

これを MainWindowModel に実装しましょう。

class MainWindowModel : INotifyPropertyChanged
{
  ...
  public event PropertyChangedEventHandler PropertyChanged;

  void RasePropertyChanged( params string[] propertyNames )
  {
    if ( this.PropertyChanged != null ) {
      foreach ( string name in propertyNames )
        this.PropertyChanged( this, new PropertyChangedEventArgs( name ) );
    }
  }

  public bool IsAgreed
  {
    get { return isAgreed; }
    set
    {
      isAgreed = value;
      // ↓↓↓変更通知↓↓↓
      this.RasePropertyChanged( "IsAgreed", "NextButtonVisibility" );
    }
  }
}

IsAgreed プロパティの set 関数でプロパティの変更通知を行っているのがポイントです。isAgreed への代入により IsAgreed プロパティと NextButtonVisibility プロパティの2つが変化しうるので、その2つのプロパティの変更通知を行っています。
この状態で起動してみると、チェックボックスのON/OFFにボタンの表示/非表示が連動することが確認できるはずです。
http://gyazo.com/33ae5f28e5f20088cdabbeaddb824bde.png
http://gyazo.com/0838a9083b142a761cc270844779a49b.png

以降のトピックス

ここまで書いて息切れしたので、今後必要となる項目を箇条書きにして逃げます(^^;

(そもそも自分も理解しきれてないので説明出来ない項目が含まれている気が…)
(それにしても、いざこういう記事を自分で書いてみると、多くの解説記事をアップされている方々がいかにスゴイか痛感させられますね…)