2011年11月11日金曜日

wpf : スクリーン座標はデバイス座標?

昨日の投稿では、マウスのグローバルフックとwin32sdkのWindowFromPointを使っています。 そのときちょっと気になってたのが座標変換です。

win32sdk上の座標は当然ながらデバイスピクセル単位、wpf上の座標は論理ピクセル単位になっています。 マウスフックの座標やWindowFromPointの座標はデバイスピクセル⇔論理ピクセルの座標変換をしないとズレてしまうはずなんですよね? ですが、昨日投稿したコードは座標変換なしで動いています。

「なにやら勘違いしていたらしい」ってことで、座標がどうなっているのか簡単に調べてみました。

とりあえずこんなxamlを用意。

<Window x:Class="ScreenPointTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        WindowStyle="None"
        AllowsTransparency="True"
        Top="0"
        Left="0"
        Width="600"
        Height="600"
        Title="MainWindow"
        MouseUp="Window_MouseUp"
        Loaded="Window_Loaded">
    <Canvas>
        <Canvas Name="markA">
            <Line X1="-10" Y1="-10" X2="10" Y2="10" Stroke="Blue"/>
            <Line X1="-10" Y1="10" X2="10" Y2="-10" Stroke="Blue"/>
        </Canvas>
        <Canvas Name="markB">
            <Line X1="-10" Y1="0" X2="10" Y2="0" Stroke="Red"/>
            <Line X1="0" Y1="-10" X2="0" Y2="10" Stroke="Red"/>
        </Canvas>
        <Canvas Name="markC">
            <Line X1="-8" Y1="-8" X2="8" Y2="8" Stroke="LightGray"/>
            <Line X1="-8" Y1="8" X2="8" Y2="-8" Stroke="LightGray"/>
            <Line X1="-8" Y1="0" X2="8" Y2="0" Stroke="LightGray"/>
            <Line X1="0" Y1="-8" X2="0" Y2="8" Stroke="LightGray"/>
        </Canvas>
    </Canvas>
</Window>

Windowの枠がない白無地のウィンドウを用意して、スクリーン左上に配置します。 そして、markA~Cを次の方法で座標変換してどこに表示されるかを確認します。

  1. markA : 青い×印。CompositionTarget.TransformFromDeviceを使ったデバイス座標→論理座標の変換。
  2. markB : 赤い+印。PointFromScreenを使ったスクリーン座標→コントロール座標の変換。
  3. markC : 灰色米印。変換なし。

MainWindow.xaml.csはこうなりました。

using System.Windows;
using System.Windows.Controls;

namespace ScreenPointTest
{
    public partial class MainWindow : Window
    {
        private PixelScaleTransformer _trans;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            _trans = new PixelScaleTransformer(this);

            // 座標変換1
            Point devicePoint = new Point(100, 100);
            Point clientPointA = _trans.DeviceToWpf(devicePoint);
            Canvas.SetLeft(markA, clientPointA.X);
            Canvas.SetTop(markA, clientPointA.Y);

            // 座標変換2
            Point screenPoint = new Point(100, 100);
            Point clientPointB = PointFromScreen(screenPoint);
            Canvas.SetLeft(markB, clientPointB.X);
            Canvas.SetTop(markB, clientPointB.Y);

            // 座標変換3
            Canvas.SetLeft(markC, 100);
            Canvas.SetTop(markC, 100);

            Window w = new Window();
            w.Left = 100;
            w.Top = 100;
            w.Title = "座標テスト";
            w.Width = 200;
            w.Height = 50;
            w.Show();
        }

        private void Window_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            Close();
        }
    }
}

markAの座標変換には以前作ったクラスを使用。 ついでにWindowを作って、どの位置に表示されるか確認しています。

ディスプレイの表示倍率が大(150%)の状態で実行すると、結果はこうなりました。 (左上の確認したい部分だけキャプチャー)

markAとmarkBが重なってますね。 つまり、PointFromScreenはデバイス座標→論理座標の変換と同じということです。 スクリーン座標=デバイス座標。 それでマウスフックやWindowFromPointでずれなかったんですね。

markCはコントロール上の論理座標、無変換です。 markCと座標テストウィンドウの左上が同じ位置になってますね。 スクリーン座標=デバイス座標ですが、WindowのTopとLeftは論理座標。 同じ単位の座標だと思っていたけど、実際は違うようです。 WidthやHeightも論理座標っぽい。 この辺が私の勘違いの原因なのかも。

まぁとりあえず、このテストプログラムで座標変換について少し理解できました。 wpfだけでコーディングしていると、PointToScreenとPointFromScreenで出てくるスクリーン座標だけがデバイス座標で、あとはだいたい論理座標ということですね。 デバイス座標が出てくるのは、

  • 自前のコーディングでCompositionTarget.TransformFromDevice/TransformToDeviceを使ったとき。
  • win32sdkを使ったとき。
  • .net frameworkのwpf以外の部分を使ったとき。

と考えておきましょう。 それ以外にもあるかもしれないけど、そういうのは出くわしてから考えればいいかな?

ただ、スクリーン座標=デバイス座標っていうのは確定しない方がいいかも。 msdnライブラリとかの公式資料で明記してあるのを見つけたわけじゃないですしね。 一応、デバイス座標⇔論理座標の変換が必要になったら、メソッド名にデバイス座標と明記されているCompositionTarget.TransformFromDeviceで変換する方がバグの芽にならないような気がします。