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で真面目にやるほうがいいのか、それすら分からないのが困ったものだ…
