気楽なソフト工房

プログラミングについていろいろな記事を書いています。



mykonos2008

Author:mykonos2008
システムエンジニアとして働いている30代の会社員です。
仕事や趣味でプログラムを書いている方の役に立つ記事を書いていきたいと思っています。
ご意見、ご感想はこちらまで
If you are an english speaker,Please visit my english blog.

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
Silverlightのアプリケーションを開発していると、デザイン時にWidthやHeightを指定しなかったコントロールの
描画時のサイズが必要になるケースがよくあると思います。

例えば、ウィンドウの「閉じる」ボタンのようにCanvasの右上のコーナーにボタンを配置する場合、
Canvasの幅を取得して、ボタンの左端の位置(Canvas.Left)を指定する必要があります。

このようなケースに使用できるプロパティとして「ActualWidth」と「ActualHeight」があります。
「ActualWidth」と「ActualHeight」にはレイアウトシステムによって算出された結果の値が
格納されます。

しかし、これがちょっと曲者なのです。コンストラクタやLoadedイベントのハンドラなどで、値を取得しようと
すると、「0」が返されてしまいます。(Loadedイベントのハンドラの場合、何度かやってるとたまに取得できたり
しますが。)

「ActualWidth」、「ActualHeight」が「0」を返す理由

この現象の原因は、レイアウトシステムの処理が非同期で行われているためなのです。
例として以下のコードを見てください。

  public Page()
  {
      InitializeComponent();

      Canvas.SetTop(imageButton, 5);
      Canvas.SetLeft(imageButton, LayoutRoot.ActualWidth - 25);
  }

上記はUserControlクラスを継承するクラスのコードサンプルです。

InitializeComponent()はVisual Studioが自動的に挿入するメソッドですが、その内部では
XAMLの内容を解析して、XAMLに定義されているコンポーネントを生成するという処理を行っています。

そして、サンプルではXAMLに定義されている「imageButton」のCanvas上の位置を指定する処理を
続けて行っています。

しかし、残念ながらこのコードは上手くいきません。コンストラクタからLoadedイベントのハンドラに
この処理を移しても結果は同じです。

上手くいかない理由は、InitialzeComponent()の処理が終了した時点では、レイアウトシステムがまだ処理を行っていないからです。
InitialzeComponent()を抜けた時点で、レイアウト処理はコントロールに関連づけらているスレッドの待ち行列に、
処理されるべき処理として登録されていると考えられますが、この時点では、まだコンストラクタのコードブロック内に
いるため、レイアウト処理は未実行のままなのです。

「ActualWidth」、「ActualHeight」を確実に取る方法

この問題に対処するためには、先ほどのサンプルを以下のようなコードに変更します。

  public Page()
  {
      InitializeComponent();

      Dispatcher.BeginInvoke(
         () =>
         {
            Canvas.SetTop(imageButton, 5);
            Canvas.SetLeft(imageButton, LayoutRoot.ActualWidth - 25);
         }
      );

  }

「ActualWidth」を取得する処理を非同期処理に変更しています。Dispatcherはコントロールに関連付けられた
スレッドが処理するべき処理の待ち行列を管理するクラスで、BeginInvoke()は、待ち行列に処理を登録する
ためのメソッドです。

レイアウトシステムによる計算処理は、InitializeComponent()のいずれかの場所で、Dispatcherに登録されていると
考えられます。ActualWidthの取得の処理はその後にDispatcherに登録しているための、レイアウト処理よりも
待ち行列内の後ろの位置に登録されます。

したがって、ActualWidthを取得するタイミングでは、レイアウトシステムによる処理が完了していることが
保証されるのです。

レイアウトシステムによる処理は、Childrenに子要素が追加された場合や、Width、Hegihtなどの値が変更された場合などに起動されますが、
上記のコードを応用することでいずれのケースにおいても「ActualWidth」や「ActualHeight」を確実に取得することが出来ます。

Dispatcherの処理順に関する補足

SilverlightのDispatcher.BeginInvoke()には優先順位を指定する引数が有りません(WPF版にあります。)ので、単純にキューへの登録順に
処理が実行されると考えられます。実際、それを確認するために以下のようなコードを書き、検証して
みましたが、順番が変わることはありませんでした。

  Dispatcher.BeginInvoke(
      () =>
      {
          Debug.WriteLine("1");
      }
  );

  Dispatcher.BeginInvoke(
      () =>
      {
          Debug.WriteLine("2");
      }
  );

  Dispatcher.BeginInvoke(
      () =>
      {
          Debug.WriteLine("3");
      }
  );

何度か実行してみましたが、1,2,3の順番で表示されました。

コメント

はじめまして。夢曲と申します。
WPFをしている関係でよく拝見させて頂いております。
ありがとうございます。
質問なのですが、僕もActualWidthが直後に
取得できなくてこのページにたどり着きました。
早速ためさせていただきました。

InitializeComponent();
の直後ではなく普通のメソッド内なのですが

label1のWidthがAutoの状態で

label1.Content = "あああああ";
Canvas.SetLeft(label1, 500+label1.ActualWidth);

としたところ
label1.ActualWidthが0になっているようなので
(ちょっと時間を置くと取得できるのですが)
やらせていただきました。

Dispatcher.BeginInvoke(
() =>
{
label1.Content = "あああああ";
Canvas.SetLeft(label1, 500+label1.ActualWidth);
}
);

こうしてみたところ

ラムダ式 はデリゲート型ではないため、型 'System.Delegate' に変換できません。

とエラーが返ってきました。
これは何ででしょうか?
お忙しいところ申し訳ございませんが
お時間がございましたらお返事いただけましたらありがたいです。
よろしくお願いいたします。

Re: タイトルなし

夢曲さん、こんにちは。
mykonosと申します。

私も気になって、WPFのソースに貼り付けたところコンパイルエラーになりました。
WPFとSilverlightでは少し違うみたいですね。。

こんな風に変えてみたところ、上手くいきました。

delegate void Test(); //これはクラスのメンバーとして宣言します。

//以下はメソッド内

Dispatcher.BeginInvoke(new Test(delegate
{
label1.Content = "あああああ";
Canvas.SetLeft(label1, 500+label1.ActualWidth);
}), null);

あらかじめデリゲートを宣言して、匿名メソッドをそのコンストラクタに渡すようにしました。
C#の仕様を見返したところこれが本来は正しいみたいです。なぜ、SilverlightではOKなのか不思議ですね。。。

BeginInvoke()の引数も違っていました。少なくとも、2つは指定しないといけないみたいです。

後、WPF版のBeginInvoke()は処理の優先順位を指定できるみたいなので、
この指定の仕方によってはなお、ActualWidthがとれないかもです。

だめな時は、優先順位を変えてみてください。

ではでは、今後ともよろしくお願いします。

返信いただきありがとうございます。
やらせていただきました。
エラーは出なかったです。
ただ、0になってしまいました。
優先順位をつけて試行錯誤してみようと思いましたが
検索してみたところ意外に情報が見つけられず
優先順位のつけ方はどうしたらよいものか
困っているところです。

すいません。あれから色々やってみたら
DispatcherPriority.SystemIdle
にしたところできました。
大変助かりました。
ありがとうございました。

Re: タイトルなし

夢曲さん、解決してよかったです!

やはり優先度の問題がありましたか。
私も試せていなかったので、勉強になりました。

ではでは、今後ともよろしくお願いします。

コメントの投稿

管理者にだけ表示を許可する

トラックバック

http://csfun.blog49.fc2.com/tb.php/41-f810208a

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。