プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

Javaで筆圧取得してみよう。集まれJPanJPenユーザー

 Javaで筆圧取得といえばJTabletである。文句は受け付けない。
 特に日本ではほぼJTabletが主流だ。日本製のJavaペイントソフトのしぃペインターやたかみんがJTabletに対応していることが大きな理由だろう。

 もっとも、これらで用いられたのは、一般人が簡単にappletで利用出来るようにインストーラーがついているからであり、その面倒見のよさ、簡単に扱えるAPIなどJTabletが一級品であることに疑いの余地はない。

 さぁ、みんなレッツJTablet!といいたいところだが、JTabletはさすがにもう古いAPIだけあって、APIから取得できる情報が少ない。


 というわけで、JTabletの後継バージョンJTablet2を使うことを推めたい。JTablet2は非常に良いライブラリーである。まず、管理すべき事、知るべき事が非常に少ない。実によい。さらに、Swingの様にイベントリスナ型になったのでSwingerにとって扱いが簡単なはずだ。口を開けていれば向こうから情報をくれるって言うんだから。さらにOS Xではジェスチャまで取得できるという完璧具合。ただ、Linuxではいまだに動かないのと、私の環境では時々イベントの順番が書き換わってるような不自然な挙動を見せるのが玉に傷といったところだ。



 というわけで、前置きが長くなりましたが今回紹介するのはJPen2というライブラリーです。ライセンスはLGPLで開発されています。
 Wacomなんかが新しいAPIを出したらたぶん、最も早くに対応するライブラリーです。実はJTablet2もJPenを元にして作られています。
 JPenはJTablet2に比べて扱いがやや難しいです。でも、まぁ、慣れたら何とかなりますよ。
 まぁ、私が作ってるブリッジライブラリを使っちゃえば簡単に使えるけど、多分に間違いあるし自分で作った方が楽しいしね!
 JTablet2もそのうち記事書きます。


JPenの基本

 JPenを利用するための手順を先に示しておきます。

  1. 筆圧を受けとるためのコンポーネントを用意する
  2. PenManagerを用意する。(JPenが用意してくれているのを使うのが良い。)
  3. PenManagerが保有するpenにイベントを受けとるためのPenListenerを作成、登録する。
  4. リスナーに渡されるイベントを処理する。



 最初のコンポーネントの用意はここで説明する必要はないですね。というわけで、2のPenManagerから説明していきましょう。

PenManagerの作成

 PenManagerはJPenを起動するための根幹となるクラスです。とりあえず、こいつがないと始まりません。
 PenManagerは自分で用意することもできますが、jpen.owner.multiAwt.AwtPenToolkitから取得するのが良いでしょう。

PenManager manager=AwtPenToolkit.getPenManager();

 なお、自作する場合は

new PenManager(AwtComponent)
new PenManager(PenOwner)

を利用します。
 ただし、一度作られたPenManagerはユーザーが終了することができません。つまり、一度作ったらずっとスレッドが走り続けます
 PenManagerに渡したオブジェクトの参照が消えることはないので、GCに回収されません。
 この問題を解決するために(?)作られたのがAwtPenToolkitです。AwtPenToolkitが持つPenManagerは、一つのPenManagerで複数のコンポーネントに対しイベントを送ることができます。私は長らく2-101219バージョンを使ってた(更新してなかった)ので、AwtPenToolkitなるものが導入されたことを知りませんでした。
 101219バージョン、すなわち2010年12月19日に出たバージョンですね。僅かの間で大きな変更や追加のあるライブラリーです
 あなたがこの記事を読んでる今現在、この記事の内容は相当古いかもしれないことに注意して下さい。この記事の段階で2-110926が最新バージョンです。

PenManager.getJPenFullVersion()

でライブラリーのバージョンが得られます。
 あるバージョン以上だったら使える、とするより、あるバージョンのみ使える、とした方がいいです。実際、私が使ってたmyライブラリーはバージョンアップしたら使えなくなりましたw


 なお、わざわざPenManagerを取得しましたが、AwtPenToolkitを使う場合はJPenの状態を調べたりする以外には、取得する必要ないです。(-_-)

イベントリスナーを登録

 タブレットの筆圧変化と言ったイベントはPenListenerインターフェースを登録することで受け取れるようになります。


 AwtPenToolkitを使う場合は

AwtPenToolkit.addPenListener(java.awt.Component, jpen.event.PenListener) 

で登録します。第一引数がイベントを発生させるコンポーネント、第二引数がイベントを受け取るPenListenerです。

 自分でPenManagerを用意した場合はPenManagerの保持するPenオブジェクトのpenにaddListenerでPenListenerを登録します。

manager.pen.addListener(PenListener)



PenListenerを実装

 PenListenerは5つのメソッドがあります。その中で重要なのはボタンイベントを受け取るメソッド(penButtonEvent(PButtonEvent ev) )とレベルイベントを受け取るメソッド(penLevelEvent(PLevelEvent ev) )です。
 基本的なイベントの流れは

  1. ボタンイベント(ボタンが押された)
  2. レベルイベント(移動した、筆圧が変化した、傾きが変化した、回転した)
  3. ボタンイベント(ボタンが放された)


という順に発生します。ちなみに、レベルイベントはボタンが押されていなくても移動や傾きの変化などを取得するので、正確に書けば

  1. レベルイベント(移動した、傾きが変化した、回転した)
  2. ボタンイベント(ボタンが押された)
  3. レベルイベント(移動した、筆圧が変化した、傾きが変化した、回転した)
  4. ボタンイベント(ボタンが放された)
  5. レベルイベント(移動した、傾きが変化した、回転した)

以下無限ループ。

 これらをマウスイベントに対応付けるならば、
レベルイベント→MOVED or DRAGGED
ボタンイベント→PRESSED or RELEASED

に対応します。

 EnterやExitはって?今のところないです。


PButtonEventを取得する

 ボタンイベントが発生すると通知されるPButtonEventを処理していきます。
 ボタンイベントはボタンがオンになった、またはオフになったときに発生します。


 イベントが発生するボタンは、

  1. LEFT(マウスの左ボタンとタブレットデバイスによるクリック
  2. CENTER(中ボタン (注意 ペンのサイドボタンに割り当てた場合も発生します))
  3. RIGHT(右ボタン (注意 ペンのサイドボタンに割り当てた場合も発生します))
  4. ON_PRESSURE(タブレット等の筆圧が取得できるボタン)
  5. CONTROL,SHIFT,ALTの修飾子キー



 注意すべきは、ペンタブレットのクリックイベントがLEFTとON_PRESSREの2回発生する事です。発生順序は決まっていません。これは、ペンのサイドボタンに左クリックを割り当てたときも同様です。どう見ても筆圧なんて発生しそうにないですが、ペンタブレットだったら左クリックすると筆圧が発生する仕様のようです。
 両方処理すると何が何だか分からないことになるので、ペンタブレットデバイスではLEFTは無視してしまうのが一番だと思っています(間違ってたらスマソ m(_ _)m)。
私の実装例はつぎのデバイス取得の後で載せておきます。
 どのボタンからイベントが発生したかは、PButtonEventが保持するButtonオブジェクトのタイプから取得できます。

pbuttonevent.pen.getType()




 さて、次の話題はイベントを発生させたポインターデバイスの取得についてです。処理の順序的にはボタンがどれかを調べるより先にデバイスの特定をすべきなのかもしれませんが、話の流れ的に2番目に書いてしまいました。
 ポインターを動かすデバイスがマウスなのか、タブレットのペン先なのか、消しゴムなのかを判断するにはPenオブジェクトからPKindを取得し、タイプを取得することで分かります。PKindについては後述しますが、現在ポインターを動かすデバイスを表します。PenオブジェクトはPenManagerから取得しても良いですし、PButtonEventからも取得できます。
>|java||
event.pen.getKind().getType()
|

 こんな感じに取得します。enumが返ってくるのでswitch文で分岐すると良いでしょう。種類は

  1. STYLUS(ペン先)
  2. ERASER(消しゴム)
  3. CURSOR(マウス)
  4. CUSTOM

 カスタムが何なのか正直よく分かってません。なので、私は無視しています。誰か分かる人教えて下さい。IGNOREってのもあるけど、その通り無視すりゃいいんじゃない?



 以下にデバイスによる分岐と、ボタンの取得の私の実装例を載せておきます。StateやTabletMouseEvent,CursorDeviceなどは私が用意している自前のクラスですが、特に説明しなくても何言っているかは分かると思います。

switch(event.pen.getKind().getType()){
case STYLUS:
	ctype = CursorDevice.TABLET;
	modify |=TabletMouseEvent.HEAD_MASK;
	state = pen.getButtonValue(event.button.getType())?
			State.PRESSED:State.RELEASED;
	switch(event.button.getType()){
	case LEFT://無視
		return;
	case ON_PRESSURE:
		modify |=BUTTON1_DOWN_MASK|BUTTON1_MASK;
		break;
	case RIGHT:
		modify |=BUTTON3_DOWN_MASK|BUTTON3_MASK;
		break;
	case CENTER:
		modify |=BUTTON2_DOWN_MASK|BUTTON2_MASK;
		break;
	}
break;
//以下略

 さて、次に座標情報、筆圧情報、傾き情報、回転情報等を取得します
 傾き情報は、ペンの倒れ具合を表します。-π/2〜π/2の範囲です。
 回転情報は上方向を0ラジアンとして、ペンがどの角度に向いているかを表します。回転方向は時計回りです。値は0〜2πの範囲です。
 回転情報はデバイスによっては取得できず常に0です。(傾きが分かれば、回転方向は手動で計算できます。) 
 これらはイベントに保持されているのではなく、必要になる度にPenオブジェクトに聞きに行きます。その辺低レベルインターフェースというか、抽象度が低いというか、不便だと思います。

pen.getLevelValue(PLevel.Type)



 Penは先ほども言ったようにPButtonEventやPenManagerから取得します。
 PLevel.Typeには以下の種類があります。

  1. X  (x座標)
  2. Y  (y座標)
  3. PRESSURE (筆圧)
  4. TILT_X (x軸方向の傾き)
  5. TILT_Y (y軸方向の傾き)
  6. ROTATION (回転角度)
  7. SIDE_PRESSURE (エアブラシとかのサイドボタンっぽいです。)
  8. CUSTOM (相変わらず意味不明)

 なお、JPen開発チームにペンマーカーまたは4Dマウス、それとエアブラシを持ってる人がいなく、ROTATIONとSIDE_PRESSUREのテストができてないそうです。フィードバックを求めていますので、ぜひ持っている方は協力して下さいここのフォーラムで報告して下さい。私?もってないお。(^_^;)






 スルーしてきましたが、どうも最近なぜかCONTROL,SHIFT,ALTの修飾子キーもイベントが発生するようになりました。
 これのおかげで一年前に私が作ってそのまま使ってたライブラリーが使えなくなりましたw ぶっちゃけ全てのキーボードイベントを奪取してキーモディファイは取得してたから別にこれいらないんだけどなぁ………
 イベントの発生元がこれらの修飾子キーかどうかは、PButtonEvent.button.getType().getGroup()で得られるTypeGroup列挙型がMODIFIERかどうかで判別できます。
 適当に修飾子だけ得て、イベント自体は無視してしまうのが一番かと思います。
 私の実装例です。

	public void penButtonEvent(PButtonEvent e) {
		//version 2-110623から追加された。
		if(e.button.getType().getGroup()==TypeGroup.MODIFIER){
			int m=0;
			switch (e.button.getType()) {
			case ALT:
				m = ALT_DOWN_MASK|ALT_MASK;
				break;
			case CONTROL:
				m = CTRL_DOWN_MASK|CTRL_MASK;
				break;
			case SHIFT:
				m = SHIFT_DOWN_MASK|SHIFT_MASK;
				break;
			default:
				return;
			}
			if(e.pen.getButtonValue(e.button.getType())){
				keymodify|=m;
			}else{
				keymodify &= ~m;
			}
			return;
		}
		以下略

 getButtonValueはもう一つ定義されていて、ここでつかったのは渡されたタイプのボタンが押されているかどうかを返します。






 次回レベルイベントの処理についてお話しします。
 先は長い(-_-)