LECTURE 9 マウス入力処理の方法

○ はじめに

アプリケーションを作成する場合、ユーザからの入力を受け取って処理を行いたいことが良くあります。ここではマウスでクリックするなどの、マウス入力を読みとるための方法と、それに関連した Tips について説明します。

マウス入力に関するイベントには、ユーザがマウスボタンを押した場合( ButtonPress イベント)、離した場合( ButtonRelease イベント)、マウスを動かした場合( MotionNotify イベント)の 3 種類があります。その他に、マウスがウィンドウから出た場合に発生する LeaveNotify イベント、ウィンドウに入ってきた場合に発生する EnterNotify イベントがあります。

LeaveNotify / EnterNotify については対応するイベントマスクを設定してイベントを検出するだけなので、ここではマウス入力に関する 3 つのイベント

について説明します。また、イベントとは関係ありませんが

についても説明します。

○ ButtonPress / ButtonRelease イベント

ユーザがマウスボタンを押すと ButtonPress イベントが、離すと ButtonRelease イベントが発生します。これらのイベントはボタンイベントと呼ばれます。X サーバから送られて来るボタンイベント情報は下図のような構造体の形式で送られて来ます。

typedef struct
{
  int           type;           /* イベントの種類 */
  unsigned long serial;         /* サーバが処理した最後のリクエストの番号 */
  Bool          send_event;     /* クライアントから送られたイベントならTrue */
  Display*      display;        /* イベントが読み込まれたdisplay */
  Window        window;         /* イベントが発生したウィンドウのID */
  Window        root;           /* イベントが起こったウィンドウのID */
  Window        subwindow;      /* windowの子ウィンドウID */
  Time          time;           /* イベント発生時刻 */
  int           x, y;           /* イベントが起こった座標(window内) */
  int           x_root, y_root; /* イベントが起こった座標(root内) */
  unsigned int  state;          /* 押されていたボタンや修飾キー */
  unsigned int  button;         /* pressあるいはreleaseされたボタン */
  Bool          same_screen;    /* マウスカーソルがwindowと同スクリーンにあればTrue */
} XButtonEvent;

typedef XButtonEvent XButtonPressedEvent;
typedef XButtonEvent XButtonReleaseEvent;

type にはイベントの種類( ButtonPress、ButtonRelease )が入り、x、yにはイベントが発生したときのマウスカーソルの座標、x_root, y_root にはルートウィンドウから見た場合のマウスカーソルの座標、window にはイベントが発生したウィンドウ ID が入ります。また、button にはマウスボタンに対応した定数が入ります(下表参照、通常はこの通りですが、X ウィンドウシステムの設定によって割り当てを変更することもできます)。

左ボタン真中ボタン右ボタン
Button1 Button2 Button3

state には、ボタンイベントが発生したときに同時に押されているモディファイアキー ( Shift や Ctrl などのキー )とマウスボタンのマスクが論理和で格納されます(下表参照、なお下表が全てではありません)。

モディファイアキーまたはボタンマスク
シフトキー ShiftMask
ロックキー LockMask
コントロールキー ControlMask
メタキー Mod1Mask
左ボタン Button1Mask
真中ボタン Button2Mask
右ボタン Button3Mask

○ MotionNotify イベント

マウスが移動した時には MotionNotify イベントが送られて来ます。XSelectInput() の引数に与えるイベントマスクに PointerMotionMask や Button1MotionMask などの MotionMask を設定しておくと、マウスが移動するたびにイベントが送られて来ます。

想像できるように、MotionNotify イベントは大量のイベントを発生します。動いているということが分かれば充分であって、全ての移動を知ることが必ずしも必要なわけではありません。このような場合には、イベントマスクに MotionMask と PointerMotionHintMask の論理和を指定すると、MotionNotify イベントが圧縮されて連続したマウスカーソルの動きに対して 1 個の MotionNotify イベントが送られて来ます。この圧縮されたイベントをヒントイベントといいます。

送られてきた MotionNotify イベントがヒントイベントかどうかを知りたい時には、MotionNotify イベント構造体の is_hint メンバを調べます。圧縮されたヒントイベントの場合には、is_hint メンバに NotifyHint が指定されています(圧縮されていない場合は NotifyNormal )。なお、ヒントイベントを受け取った場合は 現在のマウスカーソルの座標を自分で調べる必要があります(どの位置の場合のイベントが来たか分からないから)が、これには XQueryPointer() を使います。

Bool XQueryPointer( Display*      display,
                    Window        w, 
                    Window*       root_return, 
                    Window*       child_return, 
                    Window*       root_x_return, 
                    Window*       root_y_return, 
                    int*          win_x_return, 
                    int*          win_y_return, 
                    unsigned int* mask_return 
                  );

root_return                 : マウスカーソルを含むルートウィンドウ
child_return                : マウスカーソルを含む子ウィンドウ
root_x_return,root_y_return : ルートウィンドウ座標系でのマウスの位置
win_x_return, win_y_return  : 指定ウィンドウ座標系でのマウスの位置
mask_return                 : モディファイアキーとマウスボタンの現在状態

指定したウィンドウ win と同じスクリーンにマウスカーソルが無い場合に、XQueryPointer() は False を返します。

PointerMotionHintMask ではなく Button1MotionMask 〜 Button5MotionMask を使うと、対応するマウスボタンが押されている場合にのみ MotionNotify イベントが送られて来ます。

○ マウスカーソルのグラブ

通常、マウスに関するイベントはマウスカーソルがあるウィンドウに送られます。ボタンイベントのみを扱う場合であれば特に問題ありませんが、マウスボタンを押したままの MotionNotify イベントを扱うような場合には問題が生じる場合があります。

例えば、マウスボタンを押すとメニューウィンドウが表示され、押したままメニュー項目を選択して離すとその項目が選択される、という UI を考えます。この場合、マウスボタンを押したままメニューウィンドウの外にマウスを移動してボタンを離すと、ButtonRelease イベントがメニューウィンドウに送られずにメニュー項目が選択されないばかりかマウスボタンが押されたままという状態の不整合が起こってしまいます( LeaveNotify イベントを使って、ウィンドウからマウスカーソルが出た瞬間にメニューウィンドウを閉じるという UI にすれば問題無さそうですけどね)。

このような場合、マウスカーソルがどこにあってもそのイベントが特定のウィンドウに送られるように、X サーバに要求することができます。これをグラブするといいます。ただし、グラブは特定のウィンドウがイベントを占有するため、必要なときにグラブを開始し、処理が終ったら速やかに解除しなくてはなりません。

グラブを開始するには XGrabPointer() を使います。

int XGrabPointer( Display*     display, 
                  Window       grab_window, 
                  Bool         owner_events, 
                  unsigned int event_mask,
                  int          pointer_mode, 
                  int          keyboard_mode, 
                  Window       confine_to, 
                  Cursor       cursor, 
                  Time         time 
                );

grab_window   : グラブ中イベントが通知されるウィンドウ
owner_events  : グラブ中に必要のないイベントの扱い
event_mask    : グラブ中に通知するイベント
pointer_mode  : グラブ後のポインタイベントの制御
keyboard_mode : グラブ後のキーボードイベントの制御 
confine_to    : マウスカーソルをその中に制限したいウィンドウ
cursor        : グラブ中に表示するマウスカーソル
time          : タイムスタンプ

cursor にはグラブ中に使用するマウスカーソルをリソース ID で指定します。これについては、次のフォントカーソルの使用法を参照してください。time はグラブを開始した時刻を指定します。現在の時刻を指定したい場合には、定数の CurrentTime を指定します。confine_to は、マウスカーソルをある特定のウィンドウから外に出したくない場合にそのウィンドウID を指定します。pointer_mode と keyboard_mode はグラブ中のマウスイベントやキーボードの処理を制御します。通常は GrabModeAsync を指定します。これはグラブ中にマウスイベントやキーボードイベントを正常に処理することを意味しています。owner_events には False か True を指定します。False を指定するとマウスイベントの通知は grab_window に関してしか行われません。True を指定すると、既にアプリケーションで選択していたイベントはグラブの影響を受けずに通知されます。

グラブに成功した場合、GrabSuccess が返ります。

グラブの必要がなくなったら速やかにグラブを解除します。次のいずれかを行うことによってグラブは解除されます。

  1. grab_window か confine_window をアンマップする
  2. XUngrabPointer() を実行する
XUngrabPointer( Display* display, Time time );
time; /* タイムスタンプ */

グラブ操作には、マウスカーソル以外にもマウスボタンをグラブする XGrabButton()、キーボードをグラブする XGrabKeyboard()、特定のキーをグラブする XGrabKey() があります。

○ フォントカーソルの使用法

Xlib ではフォントカーソルというものが標準で多数用意されていて、マウスカーソルにこのフォントカーソルの形状を設定することができます。フォントカーソルは <X11/cursorfont.h> において定義されています(正確には、フォントカーソルを指定するための定数が定義されており、フォントカーソルの本体は別に定義されています)。

このフォントカーソルをマウスカーソルに設定するには、まずフォントカーソルをロードして Cursor 型のリソースに変換しなければなりません。これには XCreateFontCursor() を使います。

Cursor XCreateFontCursor( Display* display, unsigned int shape );
shape : <X11/cursorfont.h> 内に定義されたカーソル形状名を指定する

(例)
  Cursor cursor;
  cursor = XCreateFontCursor( dpy, XC_arrow );

このようにして作成した Cursor をウィンドウに適用するには、XDefineCursor() を使います。また、それを解除するには XUndefineCursor() を使います。XUndefineCursor() を実行すると、以後は親ウィンドウのカーソルが使用されます。

XDefineCursor( Display* display, Window w, Cursor cursor );
XUndefineCursor( Display* display, Window w );

なお、前述の XGrabPointer() の引数である cursor にこのようにして作成した Cursor を指定すれば、グラブ時にはこのマウスカーソルが使用されます。