2011年5月17日火曜日

WinTab.NET : ペンタブを縦置きして使うときの座標変換

ペンタブPTB-MT1を買って8ヶ月経ちました。 チマチマと絵を描く練習をしているんですが、やる気が続かないからか才能がないからか上達する気配がありません。 どうしたもんでしょう?

で、ペンタブの板の表面を見てみるとけっこうキズが付いてました。 ただし、キズが付いているのは左側の1部だけです。 絵の練習をするとき、ディスプレイの右側に資料を置いて、左側にペイントツール(AzPainter2)を置いているんですよね。 なのでペンが当たるのは板の左側だけです。 ただでさえ小さいペンタブなのに、その1部しか使っていないのは損した気分になります。

と、いうわけで「自分でペイントソフトを作ったら?」という空想のもと、縦置きしたペンタブとペイントソフトの描画領域とをマッピングする簡単なサンプルコードを作ってみました。 作った環境は、

  • Windwos XP home edition
  • Microsoft Visual C# 2005 Express Edition
  • .NET Framework 2
  • WinTab.NET 1.6.1

コードは、

// Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using WinTabDotnet;

namespace WinTabDotnetTest02
{
    public partial class Form1 : Form
    {
        private const int CLIENT_WIDTH = 450;
        private const int CLIENT_HEIGHT = 800;
        private const int WINTAB_RANGE = 65536;
        private const int CURSOR_SIZE = 32;
        private const string CURSOR_FILE_NAME = "cursor.png";

        private WinTabMessenger m_wtMessenger;
        private WinTabContext m_wtContext;
        private int m_cx;
        private int m_cy;
        private int m_cx_old;
        private int m_cy_old;
        private int m_tx;
        private int m_ty;
        private int m_pressure;

        private Image m_img;

        public Form1()
        {
            InitializeComponent();
            this.ClientSize = new System.Drawing.Size(CLIENT_WIDTH, CLIENT_HEIGHT);
            this.BackColor = System.Drawing.Color.White;
            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;

            m_img = new Bitmap(CURSOR_FILE_NAME);

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

            m_wtMessenger = new WinTabMessenger();
            m_wtContext = new WinTabContext();

            m_wtMessenger.CursorMove += Form1_CursorMove;
            m_wtMessenger.NPressureChange += Form1_NPressureChange;
            m_wtContext.Open(
                this.Handle,
                true,
                0,
                0,
                WINTAB_RANGE,
                WINTAB_RANGE,
                ContextOption.OFFMODE | ContextOption.SYSTEM,
                RelativeField.None);
        }

        private void Form1_CursorMove(PacketEventArgs e)
        {
            m_tx = e.pkts.pkX;
            m_ty = e.pkts.pkY;
            m_cx_old = m_cx;
            m_cy_old = m_cy;
            m_cx = TabY2ClientX(m_ty);
            m_cy = TabX2ClientY(m_tx);
            UpdateTitle();
            Region r = new Region();
            r.Union(new Rectangle(m_cx_old - CURSOR_SIZE / 2, m_cy_old - CURSOR_SIZE / 2, CURSOR_SIZE, CURSOR_SIZE));
            r.Union(new Rectangle(m_cx - CURSOR_SIZE / 2, m_cy - CURSOR_SIZE / 2, CURSOR_SIZE, CURSOR_SIZE));
            this.Invalidate(r);
        }

        private void Form1_NPressureChange(PacketEventArgs e)
        {
            m_pressure = e.pkts.pkNormalPressure;
            UpdateTitle();
        }

        private void Form1_Activated(object sender, EventArgs e)
        {
            m_wtContext.Overlap(true);
        }

        private void Form1_Deactivate(object sender, EventArgs e)
        {
            m_wtContext.Overlap(false);
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            Graphics g = e.Graphics;
            g.Clear(Color.White);
            g.DrawImage(m_img, m_cx - CURSOR_SIZE / 2, m_cy - CURSOR_SIZE / 2);
        }

        private int TabX2ClientY(int x)
        {
            return (WINTAB_RANGE - x) * this.ClientSize.Height / WINTAB_RANGE;
        }

        private int TabY2ClientX(int y)
        {
            return y * this.ClientSize.Width / WINTAB_RANGE;
        }

        private void UpdateTitle()
        {
            this.Text =
                "(" + m_tx + ", " + m_ty + ") → " +
                "(" + m_cx + ", " + m_cy + ") " +
                m_pressure;
        }

        protected override void WndProc(ref Message m)
        {
            if (!m_wtMessenger.WndProc(ref m) )
            {
                base.WndProc(ref m);
            }
        }
    }
}

コンパイルにはWinTabDotnet.dllのアセンブリが必要です。 SourceForge.JPから入手してください。 動作させるには32×32ピクセルの画像ファイルが必要です。 カーソル表示に使います。 「cursor.png」という名前で作業フォルダに入れて起動してください。

...前のサンプルにメソッド2つ(TabX2ClientYとTabY2ClientX)追加しただけのコードになってしまった。

起動すると、クライアントサイズが450×800で固定されたウィンドウが表示されます。 その中でペンを動かすと、タイトルに座標が表示されます。 表示される座標は、左のカッコの中がイベントで受け取った値をそのまま表示、右のカッコの中がペンタブを縦置き(左に90度回転)して使ったときの座標です。 縦置き座標はペンタブの左上の角とクライアント領域の左上の角、ペンタブの右下の角とクライアント領域の右下の角が合うようにマッピングされています。

クライアントサイズが固定ってのは、手抜きですね。 「いろんなサイズの画像を扱うときやウィンドウが画面外にはみ出ているときなど、どうマッピングさせるのか?」っていうのはちゃんと考えなくては。 本来は、画像の表示倍率や縦横比やその他のいろんな要素によってマッピングの率を変えなければなりません。 絵のサイズやウィンドウ位置によってマッピングの率が変わったら使いづらいかな? ディスプレイ解像度をマッピングの率に使った方がいいのかもしれません。 あと、漠然とintを使ったけど、画像が拡大縮小表示された状態で描くときのこととかペンタブの精度をより生かすためとかでdoubleの方が良かったかも?

マウスカーソルとペンの座標は連携させていません。 なので、このコードのままペイントソフトを作るとしたらペンでUIを操作する方法について考えるか、ペンでのUI操作は諦めてマウスやショートカットで操作してもらうかにしなければならないでしょう。

と、ここまで空想したけど、自分でペイントソフトを作るにはここからの道のりが長すぎですよね。 結局このコード書いただけでおしまいになりそうな予感がします。