Windows Phoneで遊び始めている。
Windows Phone自体もそうだが、WPFも殆ど触ったことがないので、とても苦しいw
取り合えず、簡単なアプリを実際に公開してみて感触をつかみつつ、WPF理解への足掛かりにしたいなと思っている。
で、早速製作中なのだが、Metro UI(名称がぽしゃったので、何と呼べばいいのか困るなぁ)に使用するユーザーインターフェイスのボタン、
のマウスカーソルのあたりをタップした時に、ボタンの隅が押されて変形したような挙動でフィードバックがある、アレをやりたいと思ったのだが、どうも簡単に出来ないようだ。
で、WPFの変形の基礎とか、コントロールのカスタマイズの方法など、色々調べて以下のコードを書いた。
(ここまで紆余曲折の末、約2日 orz 直前にGeometry/Path/RenderTargetBitmapだけいじっていたのが幸いした。でないと、座標がdoubleというだけでも悶絶していたかもしれない…)
public sealed class TiltBehavior : Behavior<UIElement> { private PlaneProjection projection_; <pre><code>public TiltBehavior() { this.Depth = 30.0; this.Tracking = true; } public double Depth { get; set; } public bool Tracking { get; set; } protected override void OnAttached() { this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown; this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp; this.AssociatedObject.LostMouseCapture += AssociatedObject_LostMouseCapture; if (this.Tracking == true) { this.AssociatedObject.MouseMove += AssociatedObject_MouseMove; } } protected override void OnDetaching() { this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown; this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp; this.AssociatedObject.LostMouseCapture -= AssociatedObject_LostMouseCapture; if (this.Tracking == true) { this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove; } } private static void Apply(Size size, Point point, PlaneProjection projection, double depth) { // コントロールのサイズからノーマライズした割合を得る var normalizePoint = new Point( point.X / size.Width, point.Y / size.Height); // 0~1の範囲外を切り捨てる var satulatePoint = new Point( (normalizePoint.X &gt; 1.0) ? 1.0 : ((normalizePoint.X &lt; 0.0) ? 0.0 : normalizePoint.X), (normalizePoint.Y &gt; 1.0) ? 1.0 : ((normalizePoint.Y &lt; 0.0) ? 0.0 : normalizePoint.Y)); // 中心位置からの割合を得る var originPoint = new Point( satulatePoint.X * 2.0 - 1.0, satulatePoint.Y * 2.0 - 1.0); // 絶対位置 var absolutePoint = new Point( Math.Abs(originPoint.X), Math.Abs(originPoint.Y)); // 中心からの位置関係 var directionX = originPoint.X &gt;= 0.0; var directionY = originPoint.Y &gt;= 0.0; // タップされた位置に応じて、回転軸位置を固定する(0又は1) projection.CenterOfRotationX = directionX ? 0.0 : 1.0; projection.CenterOfRotationY = directionY ? 0.0 : 1.0; // 辺ではなく、中心をタップした場合にも、フィードバックを得る // (辺をタップした場合は0に近づく事で影響を避ける) var distance = (absolutePoint.X &gt; absolutePoint.Y) ? absolutePoint.X : absolutePoint.Y; projection.GlobalOffsetZ = (1.0 - distance) * 0.5 * // 中心位置でのZ座標 (-depth); // Rotationは角度なので、計算して算出 projection.RotationY = Math.Atan2(depth * (0.0 - originPoint.X) * 0.5, size.Width) / // 0.5はGlobalOffsetZに含まれているので (Math.PI / 180.0); projection.RotationX = Math.Atan2(depth * originPoint.Y * 0.5, size.Height) / // 0.5はGlobalOffsetZに含まれているので (Math.PI / 180.0); } private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (this.AssociatedObject.Projection == null) { this.AssociatedObject.CaptureMouse(); projection_ = new PlaneProjection(); this.AssociatedObject.Projection = projection_; var size = this.AssociatedObject.RenderSize; if ((size.Width * size.Height) &gt; 0) { var point = e.GetPosition(this.AssociatedObject); Apply(size, point, projection_, this.Depth); } } } private void AssociatedObject_MouseMove(object sender, MouseEventArgs e) { if (projection_ != null) { var size = this.AssociatedObject.RenderSize; if ((size.Width * size.Height) &gt; 0) { var point = e.GetPosition(this.AssociatedObject); Apply(size, point, projection_, this.Depth); } } } private void Uncapture() { if (object.ReferenceEquals(this.AssociatedObject.Projection, projection_) == true) { this.AssociatedObject.Projection = null; projection_ = null; this.AssociatedObject.ReleaseMouseCapture(); } } private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { Uncapture(); } private void AssociatedObject_LostMouseCapture(object sender, MouseEventArgs e) { Uncapture(); } </code></pre> }
何しろWPFは完全に初心者なので、変なことをやっていたり、思想から外れる設計なのかもしれないのであしからず。これはUIElementクラスに適用できるビヘイビアクラスで、プロジェクトに入れておいて、ページのXAMLで以下のような感じで使う。
<ListBox x:Name="MainListBox" Margin="0,0,0,0" Padding="0,0,0,0"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Margin="8,0,8,8"> <TextBlock Margin="0,0,0,0" Padding="0,0,0,0" Text="{Binding Name}" TextWrapping="Wrap" Style="{StaticResource PhoneTextSubtleStyle}"/> <TextBlock Margin="8,8,0,8" Padding="0,0,0,0" Text="{Binding Description}" TextWrapping="Wrap" Style="{StaticResource PhoneTextSubtleStyle}"/> <i:Interaction.Behaviors> <!-- ココ --> <Behaviors:TiltBehavior /> </i:Interaction.Behaviors> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
ListBoxにコレクションをバインディングし、その要素毎にStackPanelで表示する。そのStackPanelにビヘイビアの指定を行うと、対応するクラスのビヘイビアが呼び出される。名前空間「i」は、System.Windows.Interactivityで、これはWindows Phone以外ではアセンブリが違うかもしれないが存在すると思う。最初にxmlns:iで宣言しておくこと。
なお、Buttonに適用するとうまく動かない。何故かは、これから悩むところ (^^;; StackPanelのクリックを検出する方向で逃げたほうがいいのか、Buttonで真面目にやるほうがいいのか、それすら分からないのが困ったものだ…