X11: ポインタイベント

ポインタイベント
マウスなどのポインタデバイスのボタンが押された時は ButtonPress イベント、ボタンが離された時は ButtonRelease イベント、ポインタの位置が移動した時は MotionNotify イベントが送られてきます。

また、ポインタが、他のウィンドウ上からこのウィンドウ上に表示される時は EnterNotify イベント、ポインタが他のウィンドウ上に表示される時は LeaveNotify イベントが送られてきます。
ButtonPress/ButtonRelease
ボタンが押された時は ButtonPress イベントが来ます (ButtonPressMask のイベントマスクで選択)。
ボタンが離された時は ButtonRelease イベントが来ます (ButtonReleaseMask のイベントマスクで選択)。

XButtonEvent 構造体 (XEvent の xbutton から参照可能) を使います。

typedef struct {
 int            type;
 unsigned long  serial;
 Bool           send_event;
 Display        *display;
 Window         window;
 Window         root;
 Window         subwindow;
 Time           time;
 int            x, y;
 int            x_root, y_root;
 unsigned int   state;
 unsigned int   button;
 Bool           same_screen;
} XButtonEvent;

typedef XButtonEvent XButtonPressedEvent;
typedef XButtonEvent XButtonReleasedEvent;

rootイベントが起きたルートウィンドウ
subwindow自身のウィンドウで起きたイベントの場合、None。

子ウィンドウが ButtonPressMask を選択していなかったために、イベントが処理されなかった場合、親を辿って上位のウィンドウにイベントを送ることができます。
そのようにして親がイベントを受け取った場合、実際にイベントが起きた、子ウィンドウの ID となります。
timeイベントが生成された時刻 (ミリ秒単位)。
通常は、X サーバーが起動/リセットされた時からの経過時刻。
(Time = unsigned long)
x,ywindow の原点位置を基準とした、ポインタの相対座標。
window が root と同じスクリーン上にない場合は 0。
x_root
y_root
root のウィンドウの原点を基準とした位置
stateこのイベントの直前に押されていたポインタボタンと修飾キー。
※このイベントで押されたボタンは含まない。

Button1Mask、Button2Mask、Button3Mask、Button4Mask、Button5Mask、
ShiftMask、LockMask、ControlMask、
Mod1Mask、Mod2Mask、Mod3Mask、Mod4Mask、Mod5Mask
button状態が変化したボタンの番号。(1〜)
Button1 (1)、Button2、Button3、Button4、Button5 が定義されていますが、それ以上の値も使用できます。

一般的に、1 は左ボタン、2 は中ボタン、3 は右ボタン、4 はホイールの上操作、5 はホイールの下操作、6 はホイールの左操作、7 はホイールの右操作となっています。
デバイスによっては、更にボタンがある場合、8〜 の値が使われます。
same_screenwindow が root と同じスクリーン上にあるか
修飾キーについて
日本語キーボードの場合、基本的に以下のようになります。
Lock = CapsLock, Mod1 = Alt, Mod2 = NumLock, Mod4 = Windows キー

Mod1〜Mod5 については、キーボードのセットごとに自由に割り当てられるため、どれがどのキーになるかは明確に定まっていません。

コアの X のみで、Mod1〜Mod5 に対応するキーを調べるためには、X のキーマッピング情報を調べる必要があります。
XKB 拡張機能を使うと、キーの名前から、対応する Mod1〜Mod5 を調べることができます。
MotionNotify
ポインタの位置が移動した時に来るイベントです。

ボタンが押されていない時のイベントは PointerMotionMask で、何らかのボタンが押されている間のイベントは ButtonMotionMask のイベントマスクで選択します。

XMotionEvent 構造体を使います (XEvent の xmotion)

※どのくらいの距離間隔で送られてくるかは保証されていませんが、ポインタが移動した後に動きを止めた時は、少なくとも1回は送られてきます。

typedef struct {
 int            type;
 unsigned long  serial;
 Bool           send_event;
 Display        *display;
 Window         window;
 Window         root;
 Window         subwindow;
 Time           time;
 int            x, y;
 int            x_root, y_root;
 unsigned int   state;
 char           is_hint;
 Bool           same_screen;
} XMotionEvent;

typedef XMotionEvent XPointerMovedEvent;

基本的に XButtonEvent と同じです。

is_hint は、イベントマスクで PointerMotionHintMask が選択されている時の情報です。以下のいずれかの値です。

NotifyNormal通常時
NotifyHintPointerMotionHintMask が選択されていて、特定のタイミングで送られてくる時に設定される。
EnterNotify/LeaveNotify
ポインタの移動・ウィンドウの表示/非表示・ウィンドウの表示順の変更などによって、ポインタ (マウスカーソル) が以前と異なるウィンドウ上に表示される時に来ます。
ポインタのグラブ・アングラブ時にも生成されます。

EnterWindowMask または LeaveWindowMask のイベントマスクで選択します。

XCrossingEvent 構造体を使います (XEvent の xcrossing)

typedef struct {
 int           type;
 unsigned long serial;
 Bool          send_event;
 Display       *display;
 Window        window;
 Window        root;
 Window        subwindow;
 Time          time;
 int           x, y;
 int           x_root, y_root;
 int           mode;
 int           detail;
 Bool          same_screen;
 Bool          focus;
 unsigned int  state;
} XCrossingEvent;

typedef XCrossingEvent XEnterWindowEvent;
typedef XCrossingEvent XLeaveWindowEvent;

このイベントの原因となった、各 UnmapNotify、MapNotify、ConfigureNotify、GravityNotify、CirculateNotify イベントの後に生成されます。

※タイトルバーなどのウィンドウ装飾の部分は、ウィンドウマネージャが作成した別のウィンドウとなるため、その間を行き来した場合も EnterNotify/LeaveNotify イベントが来ます。

subwindowイベントウィンドウのいずれかの子の上にポインタがある場合、その子ウィンドウ。
なければ None。
x,yポインタの相対位置。
LeaveNotify の場合、直前の位置ではなく、ポインタがウィンドウ上にない状態の現在の位置。
そのため、負の値など、ウィンドウの領域外の座標になる。
modeイベントが発生した原因。

NotifyNormal (0) : 通常のイベント
NotifyGrab (1) : グラブがアクティブになったときのイベント
NotifyUngrab (2) : グラブが非アクティブになったときのイベント
detail変化したウィンドウの関係。

NotifyAncestor (0): 先祖
NotifyVirtual (1): 仮想
NotifyInferior (2): 下位
NotifyNonlinear (3): 親や子の関係にない
NotifyNonlinearVirtual (4)

トップレベルウィンドウ間で移動した場合、NotifyNonlinear になります。
focusTrue の場合、window は、入力フォーカスがあるウィンドウ、または、フォーカスウィンドウの下位ウィンドウである。
ノート
例えば、アイコンが並んだバー上で、ポインタが移動した時に、ポインタ下のアイコンを強調表示したいというような場合があります。

この場合は、MotionNotify イベントが来た時に、各アイコン上にカーソルがあるかどうかを判定しますが、他のウィンドウ上にカーソルが表示されている状態で、ウィンドウの表示順が変わるなどの要因によって、ポインタの位置は動いていないのに、アイコンバー上に突然カーソルが表示された時は、EnterNotify イベントがきます。

この場合、ポインタの位置自体は動いていないので、EnterNotify の後に MotionNotify は来ません。

そのため、ポインタの下に来ているものの状態を変えるような場合は、EnterNotify によって、突然ウィンドウ上にカーソルが表示された時も、ポインタの移動として扱う必要があります。
プログラム
ポインタイベントの情報を端末に出力します。
Enter キーを押すと終了します。

$ cc -o run 07-pointer.c util.c -lX11

<07-pointer.c>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include "util.h"

int main(int argc,char **argv)
{
    Display *disp;
    XSetWindowAttributes attr;
    Window win;
    XEvent ev;
    KeySym key;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    //ウィンドウ作成

    attr.background_pixel = 0;
    attr.event_mask = ButtonPressMask | ButtonReleaseMask
        | PointerMotionMask | ButtonMotionMask
        | EnterWindowMask | LeaveWindowMask | KeyPressMask
        //| PointerMotionHintMask
        ;

    win = XCreateWindow(disp, DefaultRootWindow(disp),
        0, 0, 200, 200, 0,
        CopyFromParent, CopyFromParent, CopyFromParent,
        CWBackPixel | CWEventMask, &attr);

    XMapWindow(disp, win);

    //イベント

    while(1)
    {
        XNextEvent(disp, &ev);

        switch(ev.type)
        {
            case ButtonPress:
            case ButtonRelease:
                printf("* [%s] serial(%lu) time(%lu) x(%d) y(%d) x_root(%d) y_root(%d)\n",
                    (ev.type == ButtonPress)? "Press": "Release",
                    ev.xbutton.serial, ev.xbutton.time, ev.xbutton.x, ev.xbutton.y,
                    ev.xbutton.x_root, ev.xbutton.y_root);

                put_state(ev.xbutton.state);
                
                printf("  button(%u)\n", ev.xbutton.button);
                fflush(stdout);
                break;
            case MotionNotify:
                printf("- [Motion] serial(%lu) time(%lu) x(%d) y(%d) x_root(%d) y_root(%d)\n",
                    ev.xmotion.serial, ev.xmotion.time, ev.xmotion.x, ev.xmotion.y,
                    ev.xmotion.x_root, ev.xmotion.y_root);

                put_state(ev.xmotion.state);

                printf("  is_hint(%d)\n", ev.xmotion.is_hint);
                fflush(stdout);
                break;
            case EnterNotify:
            case LeaveNotify:
                printf("$ [%s] serial(%lu) time(%lu) x(%d) y(%d) x_root(%d) y_root(%d)\n",
                    (ev.type == EnterNotify)? "Enter": "Leave",
                    ev.xcrossing.serial, ev.xcrossing.time, ev.xcrossing.x, ev.xcrossing.y,
                    ev.xcrossing.x_root, ev.xcrossing.y_root);

                printf("  mode(%d) detail(%d) focus(%d)\n",
                    ev.xcrossing.mode, ev.xcrossing.detail, ev.xcrossing.focus);

                put_state(ev.xcrossing.state);
                                
                fflush(stdout);
                break;

            case KeyPress:
                key = XLookupKeysym((XKeyEvent *)&ev, 0);
                if(key == XK_Return || key == XK_KP_Enter) goto END;
                break;
        }
    }

END:
    XCloseDisplay(disp);

    return 0;
}
確認すること
  • ボタンが押された時の button の番号、また、ホイールを操作した時のボタン番号。
  • ボタンが押された時・移動した時の state の状態。
    Mod2 は NumLock のため、常に ON になっている場合があります。
    CapsLock を ON にする (Shift + CapsLock) と、LockMask が ON になります。
    Shift, Ctrl, Alt, Windows キーなどを押した状態で、ポインタを動かしてみてください。
  • ボタンが押された時、押されたボタンの state のマスクは ON になっていません。
  • LeaveNotify の場合、x,y は直前の位置ではなく現在の位置なので、ウィンドウ領域外の座標になっています。
  • ウィンドウの表示状態の変更によって、EnterNotify/LeaveNotify が来ることを確認。
    まず、プログラムウィンドウの上に別のウィンドウを重ねた後、(隠れているが) プログラムウィンドウの上に来るような位置にポインタを移動します。
    その後、Alt+Tab などで、プログラムウィンドウを選択し、前面に表示します。
    プログラムウィンドウの上にポインタが表示された瞬間、EnterNotify イベントが来ます。
  • PointerMotionHintMask のイベントマスクを ON にした状態で、試してみてください。
    特定のタイミングの時のみ MotionNotify イベントが来ると思います。
PointerMotionHintMask
PointerMotionMask, ButtonMotionMask などと共に、PointerMotionHintMask イベントマスクが選択されている場合、以下のタイミングの時のみ、MotionNotify イベントが送られてきます。

ポインタの座標は常に必要ではないが、あるタイミングのときだけ欲しいという時に使います。

送られてくるのは、以下に当てはまる時だけです。

  • キーまたはボタンの押し状態が変化した時 (その後に来る)。
  • ポインタがウィンドウ上に表示された時 (EnterNotify の後)。
  • クライアントが XQueryPointer() を呼び出して、ポインタの位置を参照した時。

この場合、XMotionEvent 構造体の is_hint の値が NotifyHint (1) になっています。