[WinRT] Gridに貼り付けたコントロールの絶対座標を取得する

さて、手持ちの札を場に捨てるところのアニメーションを作成開始、ってことでちょっと悩んだのは、札の座標はどうやって取るのだろうか?ってことです。XAML では Grid か Canvas にコントロールをペタペタはりつけて位置決めをしていくのですが、この Margin の値は親の Grid からの相対座標なんですよね。移動するコントロールをルートの grid 上に張り付けられてば計算は簡単なのですが、Grid の中に Grid があって、その中にさらに Grid があって、その中に Image がある、っていう入れ子になっている場合には、ちょっと計算が大変。さらに言えば、花札の場合には、手札コントロールとか場コントロールとか、それぞれが別々のユーザーコントロールになっているので、複数のコントロール間の座標はどうやって取るの?ってのが、疑問でありました、って話です。

アニメーションとしては、

  1. 手札コントロールのクリックした札の絶対座標(pt1)を取得
  2. 場コントロールのマッチする札の絶対座標(pt2)を取得
  3. pt1 から pt2 に札をアニメーション

させたいわけです。手札と場のコントロールが別々になっているのは、UI として自由にデザインしたいからです。いやいや、花札ゲームコントロールとして全体をコントロールするという手もあるのですが、なんかごちゃごちゃしたので、コントロール毎に分けたんですよね。アニメーションがない場合は、これのほうが使いやすいのですが、相互に座標を参照するようなアニメーションの場合には結構手間 orz になってしまったということです。このあたり、XAML の場合は、リソースベースで別の XAML を読み込むってほうが素直なのかも。このあたりは後で検討してみます。

で、アニメーションをさせるのはいいけれど、手札の座標はどうやって取るのか?ってことです。

■指定したコントロールの絶対座標を取る。

var pt = pict.TransformToVisual(null).TransformPoint(new Point(0, 0));

tips っぽいですが、pict という Image コントロールの絶対座標は、TransformToVisual メソッドと TransformPoint メソッドの組み合わせで取れます。TransformToVisual メソッドはベースとなる座標軸を決めるってな具合で、null を指定すると画面全体が対象になります。普通は座標軸の原点を指定するために親の grid などを指定するのでしょう。TransformPoint メソッドは、現在の座標軸から変換先の座標軸へ移すために使います。この場合は、Image コントロール(pict)の左上の座標(0,0)を変換したいので、new Point(0,0) としています。
まあ、原理はさておき、こうやると grid の入れ子とか関係なく画面の左上からの座標が取れますね。

■コントロールを、指定した絶対位置に移動する。

左上からの絶対座標が取れたので、これで gird とか canvas とかの相対座標が関係なく移動できます…って訳にもいかず grid の margin に指定するのは grid に対しての「相対座標」なんですよね。なので、コントロールを任意の位置に移動させるためには、対 grid の相対座標値を計算し直さないといけません。この手の計算がいやな場合は、あらかじめ左上を原点として grid や canvas を配置しておくとよいです。たぶん、そっちのほうが簡単です。

アニメーションさせるためのコントロールを、適当な grid の中に適当な Image を貼り付けます。計算が面倒な場合には、左上に接するように作ればいいのですが、ここでは実験のために「あえて適当な」場所に配置しています。

<Grid x:Name="grid1" 
	HorizontalAlignment="Left" 
	VerticalAlignment="Top" Height="289" Width="335" Margin="343,200,0,0">
    <Image x:Name="pict1" 
	HorizontalAlignment="Left" Height="100" 
	Margin="56,44,0,0" 
	VerticalAlignment="Top" Width="64" 
	Source="/Images/FC001.png"/>
</Grid>

これを見ると、Margin=”56,44,0,0″ となっているので、Image は Grid に対して(56,44)という位置にありますね。正確に言うと Margin は外側の「余白」になるので、0,0 のところは、右と下の余白を意味しています…と言いますか、この 0,0 ってなんなんでしょうね?

で、いろいろやって試しに、Image を Grid からはみ出るように配置させます。

<Grid x:Name="grid1" 
	HorizontalAlignment="Left" 
	VerticalAlignment="Top" Height="289" Width="335" Margin="343,200,0,0">
    <Image x:Name="pict1" HorizontalAlignment="Left" Height="100" 
		Margin="356,135,-85,0" 
		VerticalAlignment="Top" Width="64" 
		Source="/Images/FC001.png" />
    </Image>
</Grid>

Image の Margin が “356,135,-85,0” になっています。356,135 は、x座標, y座標として計算できそうですが、-85 って何なんでしょうね?ってのをかなり悩みました。どうやら、Grid 内に含まれている間は、right と bottom は 0,0 なんですが、grid からはみ出た場合ははみ出た分の「余白」をマイナス値で設定しているようなのです。ためしに、ここを 0 にして「”356,135,0,0″」にすると、

な感じで Image が表示されなくなります。いろいろ試すと表示領域のクリッピングと関係があるようなので、このマイナス値の余白も真面目に計算しないとダメなんですね。

private void Button1Click(object sender, RoutedEventArgs e)
{
	// 移動対象の絶対座標
	var pt1 = pict1.TransformToVisual(null).TransformPoint(new Point(0, 0));
	// 開始位置の絶対座標
	var pt2 = pict2.TransformToVisual(null).TransformPoint(new Point(0, 0));

	// 開始位置に移動
	double dx = pt2.X - pt1.X;
	double dy = pt2.Y - pt1.Y;
	double l = pict1.Margin.Left + dx;
	double t = pict1.Margin.Top + dy;
	double r = grid1.Width - (l + pict1.Width );
	double b = grid1.Height - (t + pict1.Height);
	pict1.Margin = new Thickness(l,t,r,b);
}

移動元と移動先の絶対値をあらかじめ pt1, pt2 で取得しておいて、grid の対して相対値で margin を計算するってことをやります。margin の right と bottom の値は grid からはみ出た分を計算するのです。なんかややこしいですね。

で、実はこのコードではダメなんです。一度 margin を設定してしまうと、pict1.Margin.Left の値が変わってしまうので、実はもともとの margin の値を使わないダメというオチが(相対値を計算すればよいのかな?)

なので、最初の margin と pt1 の値を起動時にキープしておきます。

public MainPage()
{
    this.InitializeComponent();

	_ma1 = pict1.Margin;
	_pt1 = pict1.TransformToVisual(null).TransformPoint(new Point(0, 0));
}

Thickness _ma1;
Point _pt1;

そして、元の位置から計算しなおすというダサい方法が…

private void Button1Click(object sender, RoutedEventArgs e)
{
	// 移動対象の絶対座標
	var pt1 = _pt1;
	// 開始位置の絶対座標
	var pt2 = pict2.TransformToVisual(null).TransformPoint(new Point(0, 0));

	// 開始位置に移動
	double dx = pt2.X - pt1.X;
	double dy = pt2.Y - pt1.Y;
	double l = _ma1.Left + dx;
	double t = _ma1.Top + dy;
	double r  = grid1.Width - (l + pict1.Width );
	double b = grid1.Height - (t + pict1.Height);
	pict1.Margin = new Thickness(l,t,r,b);
}

まあ、これでも動くといえば動くのですが、カプセル化するにしても元の値を取っておくところがひどすぎるので、grid と image の位置を計算が簡単になるように左上の(0,0)にしておきます。
すると、次のように元の値を取っておく必要がなくなります。

private void Button1Click(object sender, RoutedEventArgs e)
{
	// 開始位置の絶対座標
	var pt2 = pict2.TransformToVisual(null).TransformPoint(new Point(0, 0));

	// 開始位置に移動
	double dx = pt2.X;
	double dy = pt2.Y;
	double l = dx;
	double t = dy;
	double r  = grid1.Width - (l + pict1.Width );
	double b = grid1.Height - (t + pict1.Height);
	pict1.Margin = new Thickness(l,t,r,b);
}

まあ、これはこれで動くから良いかな、と思っているわけで、さてこれをアニメーションに応用するのは…続きます。

カテゴリー: C#, WinRT パーマリンク