LINEで送る

今回は、Wintabを利用し、ペンの位置や筆圧を取得する方法を紹介します。
取得した情報を利用し、アプリケーション側では画面上に線を描画するプログラムを組みます。

1.Wintabの関数追加
前回は、WTInfoAという関数をWintabFunctionsに追加しましたが、ペンの位置を取得するために必要な関数を追加します。

class WintabFunctions
{
    // タブレット情報の取得
    [DllImport("Wintab32.dll", CharSet = CharSet.Auto)]
    public static extern UInt32 WTInfoA(UInt32 wCategory, UInt32 nIndex, IntPtr lpOutput);

    // タブレットのオープン
    [DllImport("Wintab32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr WTOpenA(IntPtr hWnd, ref WintabLogContext logContext, bool enable);

    // タブレットのクローズ
    [DllImport("Wintab32.dll", CharSet = CharSet.Auto)]
    public static extern bool WTClose(IntPtr hctx);

    // パケットの取得
    [DllImport("Wintab32.dll", CharSet = CharSet.Auto)]
    public static extern bool WTPacket(IntPtr hctx, UInt32 pktSerialNum, IntPtr pktBuf);
}

2.定数・構造体の追加
コード量が多くなってしまうため、WTOpenA/WTPacketに必要な構造体のみ載せておきます。
詳細なコードを確認したい方は、サンプルコードを参照してください。

まずは、WTOpenAで利用するWintabLogContextという構造体です。
この構造体に指定する内容によって、取得できる情報を変更することが可能です。
詳細な内容については、WDNを参照してください。

/// <summary>
/// コンテキストデータ構造体
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct WintabLogContext
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 40)]
    public string lcName;
    public UInt32 lcOptions;
    public UInt32 lcStatus;
    public UInt32 lcLocks;
    public UInt32 lcMsgBase;
    public UInt32 lcDevice;
    public UInt32 lcPktRate;
    public UInt32 lcPktData;
    public UInt32 lcPktMode;
    public UInt32 lcMoveMask;
    public UInt32 lcBtnDnMask;
    public UInt32 lcBtnUpMask;
    public Int32 lcInOrgX;
    public Int32 lcInOrgY;
    public Int32 lcInOrgZ;
    public Int32 lcInExtX;
    public Int32 lcInExtY;
    public Int32 lcInExtZ;
    public Int32 lcOutOrgX;
    public Int32 lcOutOrgY;
    public Int32 lcOutOrgZ;
    public Int32 lcOutExtX;
    public Int32 lcOutExtY;
    public Int32 lcOutExtZ;
    public UInt32 lcSensX;
    public UInt32 lcSensY;
    public UInt32 lcSensZ;
    public bool lcSysMode;
    public Int32 lcSysOrgX;
    public Int32 lcSysOrgY;
    public Int32 lcSysExtX;
    public Int32 lcSysExtY;
    public UInt32 lcSysSensX;
    public UInt32 lcSysSensY;
}

次に、WTPacketに指定するWintabPakcet構造体です。
本来は、WTOpenに設定した内容によってパケットのパラメータは増減します。
C#では手間になってしまうので、取得するパラメータを固定にし、構造体でパケットを読み込むようにしています。

/// <summary>
/// パケット構造体
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct WintabPacket
{
    public UInt32 pkContext;                    // PK_CONTEXT
    public UInt32 pkStatus;                     // PK_STATUS
    public UInt32 pkTime;                       // PK_TIME
    public UInt32 pkChanged;                    // PK_CHANGED
    public UInt32 pkSerialNumber;               // PK_SERIAL_NUMBER
    public UInt32 pkCursor;                     // PK_CURSOR
    public UInt32 pkButtons;                    // PK_BUTTONS
    public Int32 pkX;                           // PK_X
    public Int32 pkY;                           // PK_Y
    public Int32 pkZ;                           // PK_Z
    public WintabPressure pkNormalPressure;     // PK_NORMAL_PRESSURE
    public WintabPressure pkTangentPressure;    // PK_TANGENT_PRESSURE
    public WintabOrientation pkOrientation;     // ORIENTATION
    public WintabRotation pkRotation;           // ROTATION
}

3.デフォルトのコンテキスト情報取得
WTOpenAのコンテキスト情報は、WTInfoAから取得できるデフォルトのコンテキスト情報をカスタマイズして設定します。
WintabManagerに、デフォルトコンテキストを取得するメソッドを追加します。

// デフォルトのコンテキスト情報を取得する
public static WintabLogContext GetDefaultSystemContext(ECTXOptionValues options)
{
    // パケットのサイズは、指定したビットによって可変となる。
    // 全てのビットを指定し、パケットのサイズを固定にする。
    uint pktData = (uint)EWintabPacketBit.PK_PKTBITS_ALL;
    uint pktMode = (uint)EWintabPacketBit.PK_BUTTONS;

    // デフォルトのコンテキストの取得
    WintabLogContext context = new WintabLogContext();

    // メモリの確保
    IntPtr pContext = WintabMemoryUtil.AllocUnmanagedBuffer(context);

    try
    {
        uint sz = WintabFunctions.WTInfoA((uint)EWTICategoryIndex.WTI_DEFSYSCTX, 0, pContext);
        context = WintabMemoryUtil.MarshalUnmanagedBuffer<WintabLogContext>(pContext, sz);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine("GetDefaultSystemContext : " + ex.Message);
        throw ex;
    }
    finally
    {
        // メモリの開放を忘れないこと
        WintabMemoryUtil.FreeUnmanagedBuffer(pContext);
    }

    // パケットメッセージを受信するために指定
    context.lcOptions |= (uint)ECTXOptionValues.CXO_MESSAGES;
    // 指定されたオプション
    context.lcOptions |= (uint)options;

    // コンテキストのビット設定
    context.lcPktData = pktData;
    context.lcPktMode = pktMode;
    context.lcMoveMask = pktData;
    context.lcBtnUpMask = context.lcBtnDnMask;

    return context;
}

4.タブレットのXYZ座標の範囲取得
コンテキスト情報には、タブレット側の入力領域を指定する必要があります。
WTInfoAを利用することで、XYZ座標の範囲を取得することができるので、WintabManagerにメソッドを追加します。

// タブレットのXYZ座標の範囲取得
public static WintabAxis GetTabletAxis(EAxisDimension dimention)
{
    WintabAxis axis = new WintabAxis();
    IntPtr pAxis = WintabMemoryUtil.AllocUnmanagedBuffer(axis);

    try
    {
        uint sz = WintabFunctions.WTInfoA(
            (uint)EWTICategoryIndex.WTI_DEVICES,
            (uint)dimention,
            pAxis);
        axis = WintabMemoryUtil.MarshalUnmanagedBuffer<WintabAxis>(pAxis, sz);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine("GetTabletAxis : " + ex.Message);
        throw ex;
    }
    finally
    {
        WintabMemoryUtil.FreeUnmanagedBuffer(pAxis);
    }

    return axis;
}

5.タブレットのオープン・クローズ処理の追加
パケットの受信を開始するためのオープン処理と、停止するためのクローズ処理をWintabManagerに追加します。

// タブレットのオープン
public static IntPtr Open(IntPtr hWnd, WintabLogContext context)
{
    IntPtr hCtx = IntPtr.Zero;

    try
    {
        hCtx = WintabFunctions.WTOpenA(hWnd, ref context, true);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine("Open : " + ex.Message);
    }

    return hCtx;
}

// タブレットのクローズ
public static void Close(IntPtr hCtx)
{
    if (hCtx != IntPtr.Zero)
    {
        WintabFunctions.WTClose(hCtx);
        hCtx = IntPtr.Zero;
    }
}

6.パケット取得処理の追加
Wintabからパケットを受信すると、ウインドウメッセージにWT_PACKETというメッセージが送信されます。
メッセージを受信した際に、パケット情報を読み出すことで、ペンの位置や筆圧を取得することができます。
WintabManagerには、WTPacketを利用してパケットを取得するメソッドを追加します。

// パケットの取得
public static WintabPacket GetPacket(IntPtr hCtx, UInt32 pktID)
{
    WintabPacket packet = new WintabPacket();
    IntPtr pPacket = WintabMemoryUtil.AllocUnmanagedBuffer(packet);

    try
    {
        bool result = WintabFunctions.WTPacket(hCtx, pktID, pPacket);
        packet = WintabMemoryUtil.MarshalUnmanagedBuffer<WintabPacket>(
            pPacket,
            (uint)WintabMemoryUtil.SizeOf(typeof(WintabPacket)));
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine("GetPacket : " + ex.Message);
        throw ex;
    }
    finally
    {
        WintabMemoryUtil.FreeUnmanagedBuffer(pPacket);
    }

    return packet;
}

7.描画処理の実装
ここからはWPFのプログラムとなります。
まずは、XAML上に線を描画するCanvasを設置します。

<Window x:Class="WpfWintabSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBlock x:Name="screenText"/>
        <Canvas x:Name="screenCanvas" />
    </Grid>
</Window>

次に、追加したメソッドを利用し、タブレットの情報の取得を開始する処理をLoadedイベント内に追加します。

//////////////////////////////////////////////////////////////////////////////
// タブレットのオープン
//////////////////////////////////////////////////////////////////////////////

// Wintabの初期化
WintabLogContext context = WintabManager.GetDefaultSystemContext(ECTXOptionValues.CXO_SYSTEM);

context.lcName = "Wintab sample";

// タブレットのXY座標の範囲取得
WintabAxis tabletX = WintabManager.GetTabletAxis(EAxisDimension.AXIS_X);
WintabAxis tabletY = WintabManager.GetTabletAxis(EAxisDimension.AXIS_Y);

// 入力側の範囲指定
context.lcInOrgX = 0;
context.lcInOrgY = 0;
context.lcInExtX = tabletX.axMax;
context.lcInExtY = tabletY.axMax;

// 出力側の範囲指定
// 実際にはマルチモニタに対応する処理を実装する必要があります
//context.lcOutOrgX = context.lcOutOrgY = 0;
context.lcOutExtY = -context.lcOutExtY;

// ウインドウハンドルの取得
WindowInteropHelper helper = new WindowInteropHelper(this);
IntPtr hWnd = helper.Handle;

// パケットの受信開始
m_hCtx = WintabManager.Open(hWnd, context);
if (m_hCtx != IntPtr.Zero)
{
    // ウインドウプロシージャをフックする
    HwndSource source = HwndSource.FromHwnd(helper.Handle);
    source.AddHook(new HwndSourceHook(WndProc));
}

上記の処理の最後で、ウインドウプロシージャをフックしています。
WndProcというメソッドに、ウインドウメッセージが送信されるようになるので、処理を追加します。
この処理では、WT_PACKETを受信した際に、筆圧が0以外だった場合にaddPathというメソッドで、画面上に線を追加しています。
線の太さは、事前の取得した筆圧の最大値と、現在の筆圧を比較して設定しています。

.NETでは、基本的にウインドウメッセージやウインドウプロシージャを処理することはほとんどありませんが、詳しく知りたい方は、MSDNを参照してください。

// ウインドウプロシージャ
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) 
{
    if (msg == (int)EWintabEventMessage.WT_PACKET)
    {
        WintabPacket packet = WintabManager.GetPacket(lParam, (UInt32)wParam.ToInt32());

        if (packet.pkNormalPressure.pkAbsolutePressure != 0)
        {
            // アプリケーション内の位置を取得
            Point screenPoint = PointToScreen(new Point(0, 0));
            int x = (int)(packet.pkX - screenPoint.X);
            int y = (int)(packet.pkY - screenPoint.Y);

            if (m_Point.X == -1)
            {
                // 始点なので座標を保存する
                m_Point.X = x;
                m_Point.Y = y;
            }
            else
            {
                // 線の追加を行う
                addPath((int)m_Point.X, (int)m_Point.Y, x, y, (int)packet.pkNormalPressure.pkAbsolutePressure);

                // 位置の保存
                m_Point.X = x;
                m_Point.Y = y;
            }
        }
        else
        {
            m_Point.X = -1;
        }
    }
    return IntPtr.Zero;
}

// 線をCanvasに追加する
private void addPath(int x, int y, int x2, int y2, int pressure)
{
    Line l = new Line();

    // 始点と終点の設定
    l.X1 = x;
    l.Y1 = y;
    l.X2 = x2;
    l.Y2 = y2;

    // 線のスタイル設定
    l.Stroke = Brushes.Black;
    l.StrokeStartLineCap = PenLineCap.Round;

    // 線の太さは筆圧によって変更する
    l.StrokeThickness = (double)pressure / (double)m_MaxPressure * 10.0;

    // Canvasへ線を追加
    screenCanvas.Children.Add(l);
}

以上で、アプリケーション上に線を描画するための実装が完了しました。
実際にアプリケーションを実行し、タブレットで線を書いてみましょう。
スクリーンショットのように、線が描画され、筆圧によって線の太さが変わるかと思います。

wintab_2_01

サンプルソースはこちらにアップしてありますので、興味のある方は実際に試して頂ければと思います。
次回は、Wintabで位置や筆圧以外に取得できる情報いつて説明したいと思います。

Top