ArrangeOverride と MeasureOverride

Last Updated 2011/09/21


カスタムコントロールを作成する場合、ArrangeOverride と MeasureOverride の 2 つのメソッドをオーバーライドしなければならないケースがあります。しかし、これらのメソッドはその実態がよく分かりません。そこで、実際にカスタムコントロールを作りながら研究してみました。


子要素を持つカスタムコントロールを作成する場合、子要素をどのようにレイアウトするかを決めなければなりません。ArrangeOverride と MeasureOverride の 2 つのメソッドはそのためのメソッドです。

Note これらのメソッドのほかに、Arrange メソッドや Measure メソッド、DesiredSize プロパティなどが関係する場合もあります。

これら 2 つのメソッドは FrameworkElement クラスに与えられていますが、メソッドに対する FrameworkElement クラスとしての実装は実質的な意味を持ちません。派生クラスである Control クラスで具体的な実装をしています。

WPF SDK では Control クラスにおけるこれらのメソッドの機能の説明の中で、ArrangeOverride メソッドに対しては「Control オブジェクトのコンテンツを配置し、そのサイズを設定するために呼び出されます。」、MeasureOverride メソッドに対しては「コントロールを再測定するために呼び出されます。」とあります。しかし、これでは何のことかサッパリ分かりません。

解説文をダラダラとならべるだけでは直感的な理解は得にくいので、具体的なカスタムコントロールの作成手順の中でテストしながら説明文を書くことにしました。下図は、例として取り上げる吹き出し形(バルーン形)のコントロール(以後、便宜上、BalloonControl と表記します)の実行時の状態です。WPF ではない通常の .Net Framework のツールチップと見た目は同じものです。

BalloonControl

まず、このコントロールをどのクラスから派生するかについて悩みました。子要素を持つことになるので、Child プロパティとか Content プロパティを持つクラスでなければなりません。作成しようとしているコントロールにもっとも近いと考えたコントロールは Border クラスです。このクラスは Decorator クラスから派生していることに違和感を感じましたが、Child プロパティを持つことに気付きました。このプロパティには UIElement オブジェクトを設定できますので、よさげに思えましたが、Decorator クラスから派生するクラスのうち、Border クラスのほかはどうも違うように感じました。

次に、Content プロパティをもつ ContentControl クラスから派生することを検討しました。Content プロパティには Object 型の子要素を設定できますが、Button、Expander、Label、ListBoxItem などのクラスが ContentControl クラスから派生しており、こちらのほうが適しているように思えました。

Note 子要素として、UIElement 型と Object 型との優劣はよく分かりませんが、Object 型であれば何でも受け入れると考えていいのだろうと思います。ただし、子要素として追加する可能性のある UIElement 型でないオブジェクトとはどんなものでしょうね。すぐには思いつきません。

そこで、BalloonControl は ContentControl クラスから派生することにしました。具体的なコードは挙げませんが、いくつかのプロパティの設定と コントロールの形状を規定する OnRender メソッドを実装しただけの段階で、正常に動作することを確認しました。上図がその状態です。

さて、BalloonControl をフォームに配置したときの状態(フォームデザイナにおける状態)をテストするため、子要素として TextBlock コントロールを配置してみました。

<ctrl:BalloonTool Width="150" Height="80">
  <TextBlock Background="#50FF0000">あいうえお</TextBlock>
</ctrl:BalloonTool>

結果はこうです。つまり、TextBlock は BalloonControl と同じサイズになっていることが分かりますね。

BalloonControl3

最初に示した図のように、子要素の Margin プロパティを適切に設定すれば問題ありませんが、設定の内容によっては下図のようになる欠点があります。

BalloonControl2

子要素は矩形部分だけにしか配置できないようにするにはどうするか。ここで、ArrangeOverride と MeasureOverride の 2 つのメソッドの登場です。テストすると、MeasureOverride メソッドはコントロールを作成するときに一度だけ呼ばれますが、ArrangeOverride メソッドは複数回呼ばれているようです。

さて、ここで Control クラスにおけるこれら 2 つのメソッドの実装内容を検討してみようと思います。ちなみに、ContentControl クラスに独自の実装はありません。つまり、Control クラスと同じ内容です。

【注 意】
子要素を配置すると、親要素のサイズのほかに、厳密には BorderThickness プロパティや Padding プロパティも算入しなければならないことに注意してください。ここではこれらの要素を勘案していませんが、コントロールの性質に深く関係する場合は算入しなければなりません。


MeasureOverride メソッド

Control クラスの MeasureOverride メソッドのプロトタイプは以下のとおりです。

protected override Size MeasureOverride(
  Size constraint //(戻り値)子要素を配置する現在の要素のサイズ
);

constraint の「現在の要素」とは作ろうとしているコントロールの最大サイズです。constraint の英語としての意味は、「制限」です。つまり、コントロールはこれ以上大きなサイズには設定できないことを意味します。

さて、次のコードを実行すると TextBlock は StackPanel の幅一杯になりますが、そのときの TextBlock の幅は Infinity、つまり、double 型の最大値になります。

<StackPanel>
  <TextBlock Background="AliceBlue">textBlock</TextBlock>
</StackPanel>

このように子要素のサイズを指定しない場合、constraint には (Infinity, Infinity) が戻ります。一方、以下のコードでは (200, Infinity) が戻ります。

<StackPanel>
  <TextBlock Background="AliceBlue" Width="200">textBlock</TextBlock>
</StackPanel>

つまり、引数の constraint はユーザー(アプリケーション開発者)が望む最大サイズであることが分かります。

次に、MeasureOverride メソッドは戻り値として何を返すのか。Control クラスの実装では子要素の Measure メソッドを呼び出し、その DesiredSize プロパティ値を返しています。また、子要素がない場合は幅と高さがともに 0 のサイズを返しています。要するに、constraint の制限の中で、子要素をレイアウトするために必要な最小サイズを返すと理解してよさそうです。しかし、コントロールの Width または Height プロパティを設定する場合はユーザーがそのサイズを望んでいるのですから戻り値は constraint と同じ値を返さなければなりません。

なお、MeasureOverride メソッドはカスタムコントロールのインスタンスを作成するときに最初に呼び出され、そのあと、ArrangeOverride メソッドが呼び出されます。もし問題なければ MeasureOverride メソッドは二度と呼ばれません。

ちなみに、BalloonControl ではクラスの中でもデフォルトのサイズを設定していますので、MeasureOverride メソッドのオーバーライドはしていません。


ArrangeOverride メソッド

Control クラスの ArrangeOverride メソッドのプロトタイプは以下のとおりです。

protected override Size ArrangeOverride(
  Size arrangeBounds //(戻り値)親要素のサイズ
);

arrangeBounds は子要素のレイアウトに実際に使えるサイズです。MeasureOverride メソッドの項のように Infinity が戻ることはありません。

このメソッドに対する Control クラスとしての実装は各子要素の Arrange メソッドを呼び出して子要素を配置しています。

さて、このメソッドの戻り値として何を返すべきか。簡単に言えば、子要素を配置しおわった状態のコントロールに必要なサイズと考えられます。もしこれが arrangeBounds と異なる値の場合は再び MeasureOverride メソッドが呼び出され、同じ手順を繰り返すことになるようです。

コントロールの Width / Height プロパティを指定した場合、コントロールのサイズを変更したくないのですから、この場合は arrangeBounds と同じ値を返します。

BalloonControl の実装をどうしたかについて説明しましょう。目的は先に説明したように、子要素を矩形部内に配置することです。

protected override Size ArrangeOverride(Size arrangeBounds)
{
  // 矩形部の有効なサイズ
  // MAXRADIUS   : コーナーの丸み(半径)
  // ARROWLENGTH : 吹き出し部の高さ
  Size size = new Size(this.Width - MAXRADIUS, this.Height - ARROWLENGTH - MAXRADIUS);

  UIElement element = null;

  if (this.VisualChildrenCount > 0)
  {
    element = this.GetVisualChild(0) as UIElement; // Visual ツリー内の最上位の要素
  }

  if (element != null)
  {
    // 子要素を配置する位置とサイズを指定する
    element.Arrange(new Rect(MAXRADIUS / 2, MAXRADIUS / 2, size.Width, size.Height));
  }

  return arrangeBounds; // コントロールのサイズは変える必要がないので、そのままの値を返す
}

以上の結果を下図に示します。目的どおり、子要素を所定の矩形内に納めることができました。

BalloonControl4

<ctrl:BalloonControl Margin="20">
  <TextBlock Background="#55FF0000">あいうえお</TextBlock>
</ctrl:BalloonControl>

−以上−