2011年8月7日日曜日

wpfでWinTab.NETを使う

前にWindowsXPでWinTab.NETを使うサンプルを書きました。

今回はそれと同じことをwpfでやってみました。 後者の「ペンタブを縦置き...」と同等のコードです。 環境は、

  • Windows7 Home Premium 64bit
  • Visual C# 2010 Express
  • WinTab.NET 1.6.1

xamlのコードは、

<Window x:Class="WinTabTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Height="640"
        Width="360"
        ResizeMode="NoResize"
        SourceInitialized="OnSourceInitialized"
        Activated="OnActivated"
        Deactivated="OnDeactivated">
    <Canvas Name="mainCanvas">
        <Ellipse Name="penCursor"
                 Width="16"
                 Height="16"
                 StrokeThickness="2"
                 Stroke="Black"/>
    </Canvas>
</Window>

csのコードは、

// MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using WinTabDotnet;

namespace WinTabTest
{
    public partial class MainWindow : Window
    {
        private const int WINTAB_RANGE = 65536;

        private WinTabMessenger _wtMessenger;
        private WinTabContext _wtContext;
        private int _cx;
        private int _cy;
        private int _tx;
        private int _ty;
        private int _pressure;
        private System.Windows.Forms.Message _wndProcMessage;

        public MainWindow()
        {
            InitializeComponent();
            _wndProcMessage = new System.Windows.Forms.Message();
        }

        private void OnSourceInitialized(object sender, EventArgs e)
        {
            if (!WinTab.LoadWinTab())
            {
                MessageBox.Show("ペンタブレットが見つかりません(WinTab32.dllが見つかりません)。", "WinTabTest");
                throw new WinTabException("WinTab.NETの初期化に失敗しました。");
            }

            _wtMessenger = new WinTabMessenger();
            _wtContext = new WinTabContext();

            _wtMessenger.CursorMove += OnTabletCursorMove;
            _wtMessenger.NPressureChange += OnTabletNPressureChange;

            HwndSource source = (HwndSource)HwndSource.FromVisual(this);
            _wtContext.Open(
                source.Handle,
                true,
                0,
                0,
                WINTAB_RANGE,
                WINTAB_RANGE,
                ContextOption.OFFMODE | ContextOption.SYSTEM,
                RelativeField.None);

            source.AddHook(new HwndSourceHook(WndProc));
        }

        private void OnTabletCursorMove(PacketEventArgs e)
        {
            _tx = e.pkts.pkX;
            _ty = e.pkts.pkY;
            _cx = TabY2ClientX(_ty);
            _cy = TabX2ClientY(_tx);
            UpdateTitle();

            Canvas.SetLeft(penCursor, _cx - penCursor.Width / 2);
            Canvas.SetTop(penCursor, _cy - penCursor.Height / 2);
        }

        private void OnTabletNPressureChange(PacketEventArgs e)
        {
            _pressure = e.pkts.pkNormalPressure;
            UpdateTitle();

            penCursor.Width = 16 + _pressure / 8;
            penCursor.Height = 16 + _pressure / 8;
        }

        private void OnActivated(object sender, System.EventArgs e)
        {
            _wtContext.Overlap(true);
        }

        private void OnDeactivated(object sender, System.EventArgs e)
        {
            _wtContext.Overlap(false);
        }

        private int TabX2ClientY(int x)
        {
            return (int)((WINTAB_RANGE - x) * mainCanvas.ActualHeight / WINTAB_RANGE);
        }

        private int TabY2ClientX(int y)
        {
            return (int)(y * mainCanvas.ActualWidth / WINTAB_RANGE);
        }

        private void UpdateTitle()
        {
            this.Title =
                "(" + _tx + ", " + _ty + ") → " +
                "(" + _cx + ", " + _cy + ") " +
                _pressure;
        }

        private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            _wndProcMessage.HWnd = hwnd;
            _wndProcMessage.Msg = msg;
            _wndProcMessage.WParam = wParam;
            _wndProcMessage.LParam = lParam;
            _wndProcMessage.Result = IntPtr.Zero;

            _wtMessenger.WndProc(ref _wndProcMessage);
            handled = false;
            return IntPtr.Zero;
        }
    }
}

変わったところは、まずはWinTabの初期化のタイミングですね。 wpfではhwndをWindowクラスのコンストラクタで取得できません。 なので、WinTabの初期化はWindow.SourceInitializedイベントで行っています。 hwndを得るには、コードの通りHwndSourceを使います。

WndProcの扱いも変わっています。 overrideではなく、HwndSource.AddHookで登録しなければなりません。 base.WndProcにあたるものは呼ばなくてもいいようです。 handled = falseを設定するのはその代わりなのかな?

WinTabMessenger.WndProcは引数にSystem.Windows.Forms.Message構造体をとります。 これは参照の追加をしなければ使用できません。 参照の追加ダイアログ → .NETタグでSystem.Windows.Formsのコンポーネントを追加しましょう。 ただし、このコンポーネントにはwpfとクラス名がかぶっているものが多いです。 Message構造体以外に使う要素もないでしょうし、usingを書かずにフルパスで構造体を指定しましょう。

このサンプルではコンストラクタでMessage構造体をnewして使いまわしています。 「イベントのたびにnewすると重いかも?」と思ってのコーディングですけど、微々たる重さなので分かりやすいコードにした方がよかったかも?

あと、カーソルをxamlのEllipseに変更しています。 筆圧で大きさが変わるようにもしてるんですが、ちょっとチラつきますね。 ちゃんとしたアプリケーションではこういう方法はダメっぽいです。

ペンタブのカーソル座標はmainCanvasの幅と高さを元に計算しています。 しかし、mainCanvasのサイズはxamlで明記していません。 こういう場合、ActualWidthとActualHeightでサイズを調べます。