X11: ICCCM (4) - ウィンドウ構成

ウィンドウ構成
クライアントが、XConfigureWindow() などを使用して、ウィンドウの位置・サイズ・境界線の幅・兄弟ウィンドウの重ね順を変更した場合、イベントマスク SubstructreRedirectMask を選択しているウィンドウマネージャが、ConfigureRequest イベントを受け取ります。

その後、ウィンドウマネージャが、それぞれの値を独自に解釈した上で、実際にウィンドウの状態が変更された場合、本来のクライアントに ConfigureNotify イベントが来ます。

ウィンドウの状態を変更する場合、実際はウィンドウマネージャによる介入が行われていることに注意してください。
ウィンドウ位置
ウィンドウマネージャによって装飾フレームウィンドウが作成されている場合、クライアントのウィンドウは、フレームウィンドウの子になるため、XMoveWindow() などでクライアントウィンドウの位置を変更した場合、本来は、親であるフレームウィンドウを基準とした相対位置を移動することになります。

ただし、ConfigureRequest イベントを受け取ったウィンドウマネージャは、対象のウィンドウが、自身で管理しているウィンドウの場合は、要求された位置をルート座標 (ルートウィンドウ) 上の位置として扱い、クライアントウィンドウを移動する代わりに、フレームウィンドウの位置を移動させます。

これにより、クライアントウィンドウに対して位置移動を行うと、フレームウィンドウのルート位置を変更することができます。

ウィンドウ位置の移動時は、WM_NORMAL_HINTS プロパティの win_gravity 値によって、どの位置が基準になるかが異なります。
クライアントが受け取るイベント
ウィンドウマネージャが、実際にウィンドウのサイズなどのいずれかの状態を変更した場合、X サーバーによる通常の ConfigureNotify イベントが発生します。

その後、ウィンドウマネージャによる人工的な ConfigureNotify イベントが送られてくる場合があります (XSendEvnet 関数により)。

ウィンドウ位置だけを変更した場合、実際に位置が移動されるのはフレームウィンドウなので、クライアントの実際の相対位置は変化しないため、X サーバーによる ConfigureNotify イベントは発生しません。
その代わりとして、ウィンドウマネージャは、ウィンドウのルート位置が変更されたことを通知するための ConfigureNotify イベントを送信します。

X サーバーによって生成された通常の ConfigureNotify イベントの場合、x,y 座標は親 (フレームウィンドウ) を基準とした相対位置となりますが、ウィンドウマネージャから送られたイベントの場合は、クライアントウィンドウの左上のルート座標になります。

send_event が True なら、ウィンドウマネージャから送られたイベントとなります。

人工的な ConfigureNotify イベントで送られてくるルート座標は、WM_NORMAL_HINTS プロパティの win_gravity 値に関係なく、常にクライアントウィンドウの左上の位置になります。
win_gravity
ウィンドウマネージャが、MapRequest および ConfigureRequest イベントを受け取った後、ウィンドウの位置を変更する必要がある場合、クライアントウィンドウに設定されている WM_NORMAL_HINTS プロパティの、win_gravity で指定された基準位置を元に、フレームウィンドウの位置を移動させます。

この値は、ウィンドウ属性の win_gravity で指定する値と同じです。

通常は、デフォルトで NorthWestGravity (フレームウィンドウの左上) です。
この状態で、XMoveWindow() などによってクライアントウィンドウの位置を移動させた場合、指定されたルート座標は、フレームウィンドウの左上位置となります。

StaticGravity が指定されている場合、指定された位置 = クライアントウィンドウの左上となります。

SouthEastGravity (フレームウィンドウの右下) の場合、指定位置にクライアントウィンドウのサイズを追加した位置が、フレームウィンドウの右下に来るように調整されます。

StaticGravityクライアントウィンドウの左上
NorthWestGravityフレームウィンドウの左上
NorthGravityフレームウィンドウの上辺の中央
NorthEastGravityフレームウィンドウの右上
EastGravityフレームウィンドウの右辺の中央
SouthEastGravityフレームウィンドウの右下
SouthGravityフレームウィンドウの下辺の中央
SouthWestGravityフレームウィンドウの左下
WestGravityフレームウィンドウの左辺の中央
CenterGravityフレームウィンドウの中央
ウィンドウ位置の取得
なお、任意のタイミングで、現在のウィンドウのルート座標位置を取得したい場合は、XTranslateCoordinates 関数を使います。

Bool XTranslateCoordinates(Display *display, Window src_w, Window dest_w,
    int src_x, int src_y,
    int *dest_x_return, int *dest_y_return, Window *child_return);

src_w ウィンドウ上の指定位置を、dest_w ウィンドウ上の位置に変換して返します。
True で変換に成功。False で、src_w と dest_w は異なるスクリーン上にあります。

child_return は、指定位置が、dest_w の直下の子ウィンドウ上にある場合、その子ウィンドウが返ります (なければ None)。

dest_w にルートウィンドウを指定して、src_x, src_y を 0 にすると、src_w の左上位置のルート座標が取得できます。
これは、ウィンドウマネージャから送られてきた ConfigureNotify イベントの x, y 座標と同じになります。
プログラム
WM_NORMAL_HINTS プロパティの win_gravity 値を変更して、ウィンドウ位置を移動した時、ウィンドウの位置がどう変化するかを確認します。

左ボタンを押すごとに、win_gravity を NorthWestGravity, SouthEastGravity, StaticGravity に変更した後、ウィンドウ位置を (300, 300) に移動します。

それ以外のボタンを押すと、終了します。

$ cc -o run d05-configure.c util.c -lX11

<d05-configure.c>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include "util.h"

int g_gravity[3] = {NorthWestGravity, SouthEastGravity, StaticGravity};
const char *g_names[3] = {"NorthWest", "SouthEast", "Static"};

static void _set_wm_hints(Display *disp,Window win,int gravity)
{
    XSizeHints sh;

    sh.flags = PWinGravity;
    sh.win_gravity = gravity;

    XSetWMNormalHints(disp, win, &sh);
}

static void _get_winpos(Display *disp,Window win)
{
    int x,y;
    Window child;

    if(XTranslateCoordinates(disp, win, DefaultRootWindow(disp),
        0, 0, &x, &y, &child))
    {
        printf("* position: x(%d) y(%d)\n", x, y);
    }
}

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

    win = create_test_window(disp,
        ButtonPressMask | StructureNotifyMask);

    //イベント

    XMapWindow(disp, win);

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

        switch(ev.type)
        {
            case ButtonPress:
                if(ev.xbutton.button != Button1) goto END;

                cur++;
                if(cur > 2) cur = 0;

                printf("* gravity: %s\n", g_names[cur]);

                _set_wm_hints(disp, win, g_gravity[cur]);

                XMoveWindow(disp, win, 0, 0);

                XMoveWindow(disp, win, 300, 300);

                _get_winpos(disp, win);
                break;
            case ConfigureNotify:
                printf("[Configure] send_event(%d) x(%d) y(%d)\n",
                    ev.xconfigure.send_event, ev.xconfigure.x, ev.xconfigure.y);
                break;
        }
    }

END:
    XCloseDisplay(disp);

    return 0;
}
解説
位置移動の注意点
WM_NORMAL_HINTS プロパティの win_gravity が変更された場合、現在のウィンドウ位置を維持するために、ウィンドウマネージャは自動的にウィンドウ位置を調整します。
(PropertyNotify イベントによって、WM_NORMAL_HINTS プロパティの変更を監視している)

そのため、WM_NORMAL_HINTS プロパティを変更した後に、現在のウィンドウ位置と同じ座標 (前回の XMoveWindow で指定した値と同じ座標) に位置を移動させると、位置の変更がないものとして扱われるので、ウィンドウ位置は移動しません。

それを回避するために、WM_NORMAL_HINTS プロパティを変更した後は、一度別の位置に移動させています。
結果
[Configure] send_event(0) x(1) y(20)
[Configure] send_event(1) x(1) y(850)

* gravity: NorthWest
* position: x(1) y(850)
[Configure] send_event(1) x(1) y(20)
[Configure] send_event(1) x(301) y(320)

* gravity: SouthEast
* position: x(301) y(320)
[Configure] send_event(1) x(-1) y(-4)
[Configure] send_event(1) x(299) y(296)

* gravity: Static
* position: x(299) y(296)
[Configure] send_event(1) x(0) y(0)
[Configure] send_event(1) x(300) y(300)

※ウィンドウの装飾フレームの幅によって、実際の値は変わります。
上記の場合、フレームの幅は、それぞれ left = 1, right = 1, top = 20, bottom = 4 です。

1つ目の ConfigureNotify は、XMoveWindow(disp, win, 0, 0) 時のイベントです。
ウィンドウ位置の取得
XTranslateCoordinates() で取得したルート位置は、移動前のウィンドウ位置であることに注意してください。
つまり、XTranslateCoordinates() を実行した時点で、ウィンドウの位置移動はまだ行われていないことになります。

XMoveWindow 関数は、実際は出力バッファにリクエストを追加するだけなので、この直後はまだ、X サーバーによって何も行われていない状態となります。
ウィンドウマネージャが実際に位置移動を行うためには、以下の手順が必要になります。

  • 出力バッファがクライアントによってフラッシュされ、リクエストが X サーバーに送信される。
  • X サーバーが、位置移動のリクエストを処理する。
  • ウィンドウマネージャに ConfigureRequest イベントが送られる。
  • ウィンドウマネージャがイベントを受信して、実際に位置移動を行う。

実際に位置移動を行うのが X サーバーであれば、XSync(disp, False) を行って、出力バッファをフラッシュ&サーバーがリクエストを処理するまで待機すれば問題ありませんが、この場合、実際に位置移動を行うのはウィンドウマネージャのクライアントなので、ウィンドウマネージャが ConfigureRequest イベントを受信して、その処理を行った後、クライアントに ConfigureNotify イベントが送られるまで、移動後のウィンドウ位置は取得できません。

X の処理は非同期であることと、実際の処理が X サーバーで行われているのか、ウィンドウマネージャによって行われているのかによっても動作が異なるため、ウィンドウの操作が、実際にどういう手順で動作しているかを認識しておく必要があります。
win_gravity の違い
NorthWest (フレームウィンドウの左上) の場合、(300,300) はフレームウィンドウの左上位置となるので、クライアントウィンドウの左上は、(x + left, y + right) になります。

SouthEast (フレームウィンドウの右下) の場合、(300,300) は、クライアントウィンドウの左上にサイズを追加した位置 = フレームウィンドウの右下となるので、クライアントウィンドウの左上は、(x - right, y - bottom) となります 。

Static の場合は、指定位置が、そのままクライアントウィンドウの左上になります。