X11: Xcursor - カーソル形状

カーソル形状
ここでは、Xlib の関数も含めて、ポインタのカーソル形状について、総合的に解説していきます。

X のカーソル形状 (Cursor 型の XID) は、主に以下の関数を使って、作成することができます。

  • [Xlib] XCreateFontCursor() で、X 標準で定義されているカーソルから、指定形状のカーソルを作成。
  • [Xlib] XCreatePixmapCursor() で、1bit の Pixmap イメージを元に、2色+透明のカーソルを作成。
  • [Render 拡張機能] XRenderCreateCursor() で、Picture イメージからカーソルを作成 (RGBA のイメージを使用可能)。
  • [Render 拡張機能] XRenderCreateAnimCursor() で、複数の Cursor から、アニメーションカーソルを作成。
  • Xcursor ライブラリを使って、X カーソルファイルやカーソルテーマから、カーソルを読み込む (アニメーションカーソルも含む)。
    また、ARGB イメージのバッファから Cursor を作成することもできる。

Xlib の関数では、2色+透明のカーソルしか作れません。
フルカラーにしたり、アルファ値を使いたい場合などは、Render 拡張機能を使う必要があります。
Xcursor ライブラリ
Xcursor 自体は拡張機能ではなく、内部で拡張機能 (Render) を使用した、ライブラリです。

システムにインストールされているカーソルテーマから、カーソルを読み込むことができるので、あらかじめ定義された一般的なカーソルを使いたい場合は、主にこちらを使います。

ARGB イメージを元に、任意のカーソルを作成することもできます。
カーソル関連の関数 (Xlib)
//カーソルの解放
void XFreeCursor(Display *display, Cursor cursor);

//ウィンドウ上のカーソルを変更
void XDefineCursor(Display *display, Window w, Cursor cursor);

//デフォルトのカーソルに戻す
void XUndefineCursor(Display *display, Window w);

XDefineCursor() でカーソルを設定すると、指定ウィンドウ上にポインタが来た時、自動でそのカーソル形状に変更されます。

XDefineCursor() で cursor を None にするか、XUndefineCursor() を行うと、デフォルトのカーソル形状 (ルートウィンドウのカーソル) に戻すことができます。
2色のカーソル
Pixmap からカーソル作成
2色+透明で、独自のカーソルを作りたい場合は、XCreatePixmapCursor() を使います。

Cursor XCreatePixmapCursor(Display *display, Pixmap source, Pixmap mask,
    XColor *foreground_color, XColor *background_color, unsigned int x, unsigned int y);

typedef struct {
    unsigned long pixel;
    unsigned short red, green, blue;
    char flags; //DoRed, DoGreen, DoBlue
    char pad;
} XColor;

この場合、depth = 1bit の Pixmap で、色のイメージ (前景色か背景色か) と、マスクイメージ (透明か不透明か) の、2つのイメージが必要になります。
ビットが ON で、前景色または不透明として扱われます。

前景色と背景色は、それぞれ XColor 構造体で指定することができます。
pixel 値は使われないので、red, green, blue で色を指定する必要があります。
※値は常に 0〜0xffff の範囲で指定する必要があるので、注意してください。

x, y は、ホットスポット位置です。
カーソルイメージ内で、どの位置を基準位置とするかを指定します。
ポインタの位置 = イメージ内のホットスポット位置となります。
1bit イメージから Pixmap 作成
なお、1bit のイメージデータを元に Pixmap を作成したい時は、XCreateBitmapFromData 関数が使えます。

Pixmap XCreateBitmapFromData(Display *display, Drawable d,
    char *data, unsigned int width, unsigned int height);

d は、Pixmap を作成するスクリーンを指定することになります。ルートウィンドウなどで構いません。

data は、1bit のイメージデータです。
Y1列は 8bit 単位にします。余分なバイト余白はありません。

左上から順に、最下位ビットから上位ビットへの順で、各ピクセルが並びます。
※先頭のビットは最下位ビットであることに注意してください。(0,0) のビットが ON であれば、0x01 となります。
Xcursor
Xcursor は、カーソルに関する X ライブラリです。

<X11/Xcursor/Xcursor.h> のインクルードと、-lXcursor のリンクが必要です。

関数について詳しくは、Xcursor をご覧ください。
ARGB カーソル
ARGB イメージで独自のカーソル (非アニメーション) を作りたい場合は、XcursorImageCreate() で XcursorImage 構造体を作成した後、pixels メンバのバッファにイメージデータをセットし、XcursorImageLoadCursor() でそれを Cursor に変換します。

//XcursorImage 構造体を確保
XcursorImage *XcursorImageCreate(int width, int height);

//XcursorImage から Cursor を作成
Cursor XcursorImageLoadCursor(Display *dpy, const XcursorImage *image);

//XcursorImage の破棄
void XcursorImageDestroy(XcursorImage *image);
XcursorImage 構造体
typedef struct _XcursorImage {
    XcursorUInt  version;
    XcursorDim   size;
    XcursorDim   width;
    XcursorDim   height;
    XcursorDim   xhot;
    XcursorDim   yhot;
    XcursorUInt  delay;
    XcursorPixel *pixels;
} XcursorImage;

versionイメージデータのバージョン (1)
sizeマッチさせるカーソルサイズ
width,heightイメージの幅と高さ
xhot,yhotホットスポット位置
delayアニメーション時、次のフレームまでの時間 (ms)
pixelsイメージデータ (uint32_t *)。
0xAARRGGBB の形で、左上から順にピクセルを並べる。
カーソルテーマ
カーソルテーマから X カーソルファイルを読み込んで、Cursor を作成したい場合は、XcursorLibraryLoadCursor() を使います。

Cursor XcursorLibraryLoadCursor(Display *dpy, const char *file);

現在のテーマから、指定カーソルファイルを読み込んで、Cursor を返します。
アニメーションカーソルの場合は、Cursor に複数のカーソルイメージが含まれます。

現在のテーマは、XcursorGetTheme(display) で取得できます。
戻り値が NULL の場合、デフォルトのテーマ (default) が読み込まれます。

テーマの検索
カーソルテーマのデフォルトの検索パスは、以下のディレクトリです。

~/.local/share/icons
~/.icons
/usr/share/icons
/usr/share/pixmaps

検索パスは XcursorLibraryPath() で取得できます。複数パスは ':' で区切られています。

カーソルテーマは、各検索パスのディレクトリ上に、指定テーマ名のディレクトリが存在するかどうかで検索されます。
ディレクトリが存在する場合、そのディレクトリ下の cursors ディレクトリ内に、カーソルファイルが置かれています。

なお、カーソルテーマのディレクトリ内に index.theme がある場合、そこで指定されたテーマを継承することができるので、追加として、そのテーマ内のカーソルファイルも検索されます。

カーソルファイル名
テーマ内にあるカーソルファイルは、拡張子なしで、arrow や hand などの、各形状を指定する名前になっているので、file 引数には、パスを含まないファイル名を指定します。

複数の形状で同じファイルを使用する場合は、リンクファイルになっている場合もあります。

標準的なカーソルファイル名については、以下を参考にしてください。
>> https://www.freedesktop.org/wiki/Specifications/cursor-spec/
プログラム
左ボタンを押すごとに、カーソル形状を変更します。
(2色の十字カーソル → 青のグラデーションカーソル → デフォルトテーマの "wait" カーソル)

閉じるボタンで終了します。

$ cc -o run e06-cursor.c util.c -lX11 -lXcursor

<e06-cursor.c>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xcursor/Xcursor.h>
#include "util.h"

const unsigned char g_curimg_1bit[] = {
15,15, //width,height
7,7,   //xhot, yhot
//image
0xc0,0x01,0x40,0x01,0x40,0x01,0x40,0x01,0x40,0x01,0x40,0x01,0x3f,0x7e,0x01,0x40,
0x3f,0x7e,0x40,0x01,0x40,0x01,0x40,0x01,0x40,0x01,0x40,0x01,0xc0,0x01,
//mask
0xc0,0x01,0xc0,0x01,0xc0,0x01,0xc0,0x01,0xc0,0x01,0xc0,0x01,0xff,0x7f,0xff,0x7f,
0xff,0x7f,0xc0,0x01,0xc0,0x01,0xc0,0x01,0xc0,0x01,0xc0,0x01,0xc0,0x01 };

/* 2色カーソル */

static Cursor _create_cursor2(Display *disp,const unsigned char *buf)
{
    Cursor cur;
    Pixmap img,mask;
    XColor colf,colb;
    int w,h;

    w = buf[0];
    h = buf[1];

    //Pixmap 作成

    img = XCreateBitmapFromData(disp, g_disp.root, (char *)buf + 4, w, h);

    mask = XCreateBitmapFromData(disp, g_disp.root,
            (char *)buf + 4 + ((w + 7) / 8) * h, w, h);

    //Cursor 作成

    colf.red = colf.green = colf.blue = 0;
    colf.flags = DoRed | DoGreen | DoBlue;

    colb.red = 0xffff;
    colb.green = colb.blue = 0;
    colb.flags = DoRed | DoGreen | DoBlue;

    cur = XCreatePixmapCursor(disp, img, mask, &colf, &colb, buf[2], buf[3]);

    XFreePixmap(disp, img);
    XFreePixmap(disp, mask);

    return cur;
}

/* ARGB カーソル */

static Cursor _create_cursorARGB(Display *disp)
{
    XcursorImage *img;
    XcursorPixel *pd;
    Cursor cur;
    int ix,iy,a;

    img = XcursorImageCreate(16, 8);
    if(!img) return 0;

    img->xhot = 0;
    img->yhot = 0;

    //イメージ

    pd = img->pixels;

    for(iy = 0; iy < 8; iy++)
    {
        for(ix = 0; ix < 16; ix++)
        {
            a = 255 - ix * 255 / 15;
            *(pd++) = ((uint32_t)a << 24) | 0x0000ff;
        }
    }

    //カーソルに変換

    cur = XcursorImageLoadCursor(disp, img);

    XcursorImageDestroy(img);

    return cur;
}

int main(int argc,char **argv)
{
    Display *disp;
    Window win;
    XEvent ev;
    Cursor cur[3];
    int current = 0;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    set_display(disp);

    printf("[path] %s\n", XcursorLibraryPath());

    cur[0] = _create_cursor2(disp, g_curimg_1bit);
    cur[1] = _create_cursorARGB(disp);
    cur[2] = XcursorLibraryLoadCursor(disp, "wait");

    win = create_test_window2(disp, 200, 200, rgb_to_pixel(0xffffff), ButtonPress);

    XDefineCursor(disp, win, cur[0]);

    //イベント

    XMapWindow(disp, win);

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

        if(event_quit(&ev)) break;

        if(ev.type == ButtonPress && ev.xbutton.button == Button1)
        {
            current++;
            if(current >= 3) current = 0;

            XDefineCursor(disp, win, cur[current]);
        }
    }

    XCloseDisplay(disp);

    return 0;
}
XFIXES 拡張機能
クリップボードの説明時に XFIXES 拡張機能を使いましたが (セレクションの所有者変更時の通知イベント)、この拡張機能を使うと、ウィンドウでカーソル形状が変更された時の通知を受け取ることができます。

また、現在表示されているカーソルのイメージを取得することも出来るので、スクリーンショットを撮った後に、カーソルイメージを合成させることも出来ます。

さらに、カーソルを非表示にしたり、表示させたりすることも出来ます。
(通常の関数でも、すべて透明なイメージでカーソルを作成すれば、非表示のような状態にすることは可能です)

詳しくは、XFixes をご覧ください。