dotnet 設置 X11 建立窗口之間的父子關係

在 X11 裏面有和 Win32 類似的窗口之間的關係機制,如 Owner-Owned 關係,以及 Parent-Child 關係。本文將告訴大家如何進行設置以及其行爲

本文將大量使用到 new bing 提供的回答內容,感謝 new bing 人工智能提供的內容

Owner-Owned 關係

  • 在這種關係中,一個窗口可以被另一個窗口擁有(owner)。
  • 被擁有的窗口永遠顯示在擁有它的那個窗口的前面。
  • 當所有者窗口最小化時,它所擁有的窗口也會被隱藏。
  • 當所有者窗口被銷燬時,它所擁有的窗口也會被銷燬。
  • 當子窗口最小化時,不會影響到所有者窗口
  • 子窗口可以超過所有者窗口的範圍

被擁有的窗口 = 子窗口

所有者窗口 = “在擁有它的那個窗口”

即與 WPF 的 ChildWindow.Owner = MainWindow 的效果類似。以上的 ChildWindow 爲子窗口,而 MainWindow 爲 所有者窗口

核心 C# 代碼如下

        // 我們使用XSetTransientForHint函數將窗口a設置爲窗口b的子窗口。這將確保窗口a始終在窗口b的上方
        XSetTransientForHint(Display, a, b);

通過關係的描述可以瞭解到,使用上面代碼即可設置 a 窗口一定在 b 窗口上方

以上代碼放在 githubgitee 上,可以使用如下命令行拉取代碼

先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行裏面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 0331c5dd6057106df5cb179e45d34966a3eafd1b

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換爲 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 0331c5dd6057106df5cb179e45d34966a3eafd1b

獲取代碼之後,進入 GececurbaiduhaldiFokeejukolu 文件夾,即可獲取到源代碼

Parent-Child 關係

  • 在這種關係中,一個窗口是另一個窗口的父窗口。
  • 子窗口只能顯示在父窗口的客戶區內。
  • 當父窗口被隱藏時,它的所有子窗口也會被隱藏。
  • 當父窗口被銷燬時,它所擁有的子窗口也會被銷燬。

核心 C# 代碼如下

        // 設置父子關係
        XReparentWindow(display, childWindowHandle, mainWindowHandle, 0, 0);
        XMapWindow(display, childWindowHandle);

需要記住在 XMapWindow 之前調用 XReparentWindow 方法,否則關係設置無效

以上代碼放在 githubgitee 上,可以使用如下命令行拉取代碼

先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行裏面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin bcfc938d44460c3f055957910ac1082525501c29

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換爲 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin bcfc938d44460c3f055957910ac1082525501c29

獲取代碼之後,進入 DikalehebeekaJaqunicobo 文件夾,即可獲取到源代碼

建立 Parent-Child 關係之後,如果子窗口沒有調用 XSelectInput 方法時,那所有在子窗口上的消息都能被所有者窗口收到,如果調用了 XSelectInput 則子窗口收到子窗口的消息,即所有者窗口被子窗口遮擋的部分將不能收到消息,被子窗口遮擋的部分的觸摸或鼠標消息會被子窗口接收

簡單的測試代碼邏輯如下

var xDisplayWidth = XDisplayWidth(display, screen) / 2;
var xDisplayHeight = XDisplayHeight(display, screen) / 2;
var handle = XCreateWindow(display, rootWindow, 0, 0, xDisplayWidth, xDisplayHeight, 5,
    32,
    (int) CreateWindowArgs.InputOutput,
    visual,
    (nuint) valueMask, ref xSetWindowAttributes);


XEventMask ignoredMask = XEventMask.SubstructureRedirectMask | XEventMask.ResizeRedirectMask |
                         XEventMask.PointerMotionHintMask;
var mask = new IntPtr(0xffffff ^ (int) ignoredMask);
XSelectInput(display, handle, mask);

var mainWindowHandle = handle;

        // 再創建另一個窗口設置 Owner-Owned 關係
        var childWindowHandle = XCreateSimpleWindow(display, rootWindow, 0, 0, 300, 300, 5, white, black);

        XSelectInput(display, childWindowHandle, mask);

        // 設置父子關係
        XReparentWindow(display, childWindowHandle, mainWindowHandle, 50,50);
        XMapWindow(display, childWindowHandle);

while (true)
{
    var xNextEvent = XNextEvent(display, out var @event);

    if(@event.type == XEventName.MotionNotify)
    {
        if (@event.MotionEvent.window == handle)
        {
            Console.WriteLine($"Window1 {DateTime.Now:HH:mm:ss}");
        }
        else
        {
            Console.WriteLine($"Window2 {DateTime.Now:HH:mm:ss}");
        }
    }
}

配置了以上代碼,運行項目,可以看到鼠標在子窗口上時,只能收到子窗口的消息,如下圖

以上代碼有所忽略,全部的代碼放在 githubgitee 上,可以使用如下命令行拉取代碼

先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行裏面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 07fa8637c7c744935419e5a122b38718d8bc87e3

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換爲 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 07fa8637c7c744935419e5a122b38718d8bc87e3

獲取代碼之後,進入 DikalehebeekaJaqunicobo 文件夾,即可獲取到源代碼

設置 Parent-Child 關係之後,將限制子窗口只能在主窗口的客戶區範圍內,即子窗口不能超過主窗口範圍,如下圖所示

以上代碼是在 XReparentWindow 方法裏面設置了子窗口的座標,讓子窗口超過主窗口的範圍,代碼如下

        var mainWindowHandle = handle;

        // 再創建另一個窗口設置 Owner-Owned 關係
        var childWindowHandle = XCreateSimpleWindow(display, rootWindow, 0, 0, 300, 300, 5, white, black);

        XSelectInput(display, childWindowHandle, mask);

        // 設置父子關係
        XReparentWindow(display, childWindowHandle, mainWindowHandle, 300, 50);
        XMapWindow(display, childWindowHandle);

以上代碼放在 githubgitee 上,可以使用如下命令行拉取代碼

先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行裏面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin fbc6151abcbeba9b54028a849f06a8796db0adf7

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換爲 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin fbc6151abcbeba9b54028a849f06a8796db0adf7

獲取代碼之後,進入 DikalehebeekaJaqunicobo 文件夾,即可獲取到源代碼

以下是 new bing 給出的 XReparentWindow 函數的更多信息

XReparentWindow 函數的作用是將一個窗口重新設置其父窗口。具體來說,如果指定的窗口已經被映射到屏幕上,XReparentWindow 會自動執行 UnmapWindow 請求,將其從當前層次結構中移除,並將其插入到指定父窗口的子級中。這個窗口會在兄弟窗口中的堆疊順序中置於頂部。¹²

如果原始窗口已經被映射,XReparentWindow 還會導致 X 服務器生成一個 ReparentNotify 事件。在此事件中,override_redirect 成員被設置爲窗口的相應屬性。通常情況下,窗口管理器客戶端應該忽略此窗口,如果此成員設置爲 True。最後,如果原始窗口已經被映射,X 服務器會自動對其執行 MapWindow 請求。對於原先被遮擋的窗口,X 服務器會執行正常的曝光處理。但是,由於最終的 MapWindow 請求會立即遮擋初始 UnmapWindow 請求的某些區域,因此 X 服務器可能不會爲這些區域生成 Expose 事件。¹

以下情況會導致 BadMatch 錯誤:

  • 新的父窗口不在與舊的父窗口相同的屏幕上。
  • 新的父窗口是指定窗口本身或指定窗口的下級。
  • 新的父窗口是 InputOnly 類型,而窗口不是。
  • 指定窗口具有 ParentRelative 背景,而新的父窗口與指定窗口的深度不同。

總之,XReparentWindow 允許您在 X 窗口系統中重新組織窗口的層次結構。

使用 XReparentWindow 設置的窗口關係時,子窗口將會擋住主窗口的渲染部分,即在子窗口範圍內將看不到主窗口的繪製內容

其測試代碼如下,先在主窗口和子窗口繪製內容

    if (@event.type == XEventName.Expose)
    {
        if (@event.ExposeEvent.window == handle)
        {
            XDrawLine(display, handle, gc, 2, 2, xDisplayWidth - 2, xDisplayHeight - 2);
            XDrawLine(display, handle, gc, 2, xDisplayHeight - 2, xDisplayWidth - 2, 2);
        }
        else if (childWindowHandle != 0 && @event.ExposeEvent.window == childWindowHandle)
        {
            XDrawLine(display, childWindowHandle, gc, 1, 1, xDisplayWidth - 2, 1);
            XDrawLine(display, childWindowHandle, gc, 1, xDisplayHeight - 2, xDisplayWidth - 2, xDisplayHeight - 2);
            XDrawLine(display, childWindowHandle, gc, 1, 1, 1, xDisplayHeight - 2);
            XDrawLine(display, childWindowHandle, gc, xDisplayWidth - 2, xDisplayHeight - 2, xDisplayWidth - 2, xDisplayHeight - 2);
        }
    }

接着使用 XMoveWindow 設置子窗口座標,此時可見子窗口所在地方將不可見主窗口繪製的內容

    while (true)
    {
        await Task.Delay(TimeSpan.FromSeconds(1));

        await InvokeAsync(() =>
        {
            XMoveWindow(display, childWindowHandle, Random.Shared.Next(200), Random.Shared.Next(100));
        });
    }

全部的測試代碼如下

// See https://aka.ms/new-console-template for more information

using CPF.Linux;

using System;
using System.Diagnostics;
using System.Runtime;

using static CPF.Linux.XLib;

XInitThreads();
var display = XOpenDisplay(IntPtr.Zero);
var screen = XDefaultScreen(display);

var rootWindow = XDefaultRootWindow(display);

XMatchVisualInfo(display, screen, 32, 4, out var info);
var visual = info.visual;

var valueMask =
        //SetWindowValuemask.BackPixmap
        0
        | SetWindowValuemask.BackPixel
        | SetWindowValuemask.BorderPixel
        | SetWindowValuemask.BitGravity
        | SetWindowValuemask.WinGravity
        | SetWindowValuemask.BackingStore
        | SetWindowValuemask.ColorMap
    //| SetWindowValuemask.OverrideRedirect
    ;
var xSetWindowAttributes = new XSetWindowAttributes
{
    backing_store = 1,
    bit_gravity = Gravity.NorthWestGravity,
    win_gravity = Gravity.NorthWestGravity,
    //override_redirect = true, // 設置窗口的override_redirect屬性爲True,以避免窗口管理器的干預
    colormap = XCreateColormap(display, rootWindow, visual, 0),
    border_pixel = 0,
    background_pixel = 0,
};

var xDisplayWidth = XDisplayWidth(display, screen) / 2;
var xDisplayHeight = XDisplayHeight(display, screen) / 2;
var handle = XCreateWindow(display, rootWindow, 0, 0, xDisplayWidth, xDisplayHeight, 5,
    32,
    (int) CreateWindowArgs.InputOutput,
    visual,
    (nuint) valueMask, ref xSetWindowAttributes);


XEventMask ignoredMask = XEventMask.SubstructureRedirectMask | XEventMask.ResizeRedirectMask |
                         XEventMask.PointerMotionHintMask;
var mask = new IntPtr(0xffffff ^ (int) ignoredMask);
XSelectInput(display, handle, mask);

XMapWindow(display, handle);
XFlush(display);

var white = XWhitePixel(display, screen);
var black = XBlackPixel(display, screen);

var gc = XCreateGC(display, handle, 0, 0);
XSetForeground(display, gc, white);
XSync(display, false);

var invokeList = new List<Action>();
var invokeMessageId = new IntPtr(123123123);

async Task InvokeAsync(Action action)
{
    var taskCompletionSource = new TaskCompletionSource();
    lock (invokeList)
    {
        invokeList.Add(() =>
        {
            action();
            taskCompletionSource.SetResult();
        });
    }

    // 在 Avalonia 裏面,是通過循環讀取的方式,通過 XPending 判斷是否有消息
    // 如果沒有消息就進入自旋判斷是否有業務消息和判斷是否有 XPending 消息
    // 核心使用 epoll_wait 進行等待
    // 整個邏輯比較複雜
    // 這裏簡單處理,只通過發送 ClientMessage 的方式,告訴消息循環需要處理業務邏輯
    // 發送 ClientMessage 是一個合理的方式,根據官方文檔說明,可以看到這是沒有明確定義的
    // https://www.x.org/releases/X11R7.5/doc/man/man3/XClientMessageEvent.3.html
    // https://tronche.com/gui/x/xlib/events/client-communication/client-message.html
    // The X server places no interpretation on the values in the window, message_type, or data members.
    // 在 cpf 裏面,和 Avalonia 實現差不多,也是在判斷 XPending 是否有消息,沒消息則判斷是否有業務邏輯
    // 最後再進入等待邏輯。似乎 CPF 這樣的方式會導致 CPU 佔用略微提升
    var @event = new XEvent
    {
        ClientMessageEvent =
        {
            type = XEventName.ClientMessage,
            send_event = true,
            window = handle,
            message_type = 0,
            format = 32,
            ptr1 = invokeMessageId,
            ptr2 = 0,
            ptr3 = 0,
            ptr4 = 0,
        }
    };
    XSendEvent(display, handle, false, 0, ref @event);

    XFlush(display);

    await taskCompletionSource.Task;
}

IntPtr childWindowHandle = 0;

_ = Task.Run(async () =>
{
    await InvokeAsync(() =>
    {
        var mainWindowHandle = handle;

        // 再創建另一個窗口設置 Owner-Owned 關係
        // 創建無邊框窗口
        valueMask =
            //SetWindowValuemask.BackPixmap
            0
            | SetWindowValuemask.BackPixel
            | SetWindowValuemask.BorderPixel
            | SetWindowValuemask.BitGravity
            | SetWindowValuemask.WinGravity
            | SetWindowValuemask.BackingStore
            | SetWindowValuemask.ColorMap
            | SetWindowValuemask.OverrideRedirect // [dotnet C# X11 開發筆記](https://blog.lindexi.com/post/dotnet-C-X11-%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0.html )
            ;
        xSetWindowAttributes = new XSetWindowAttributes
        {
            backing_store = 1,
            bit_gravity = Gravity.NorthWestGravity,
            win_gravity = Gravity.NorthWestGravity,
            override_redirect = true,
            colormap = XCreateColormap(display, rootWindow, visual, 0),
            border_pixel = 0,
            background_pixel = 0,
        };

        childWindowHandle = XCreateWindow(display, rootWindow, 0, 0, xDisplayWidth, xDisplayHeight, 5,
            32,
            (int) CreateWindowArgs.InputOutput,
            visual,
            (nuint) valueMask, ref xSetWindowAttributes);

        XSelectInput(display, childWindowHandle, mask);

        // 設置父子關係
        XReparentWindow(display, childWindowHandle, mainWindowHandle, 300, 50);
        XMapWindow(display, childWindowHandle);
    });

    while (true)
    {
        await Task.Delay(TimeSpan.FromSeconds(1));

        await InvokeAsync(() =>
        {
            XMoveWindow(display, childWindowHandle, Random.Shared.Next(200), Random.Shared.Next(100));
        });
    }
});

Thread.CurrentThread.Name = "主線程";

while (true)
{
    var xNextEvent = XNextEvent(display, out var @event);
    if (xNextEvent != 0)
    {
        Console.WriteLine($"xNextEvent {xNextEvent}");
        break;
    }

    if (@event.type == XEventName.Expose)
    {
        if (@event.ExposeEvent.window == handle)
        {
            XDrawLine(display, handle, gc, 2, 2, xDisplayWidth - 2, xDisplayHeight - 2);
            XDrawLine(display, handle, gc, 2, xDisplayHeight - 2, xDisplayWidth - 2, 2);
        }
        else if (childWindowHandle != 0 && @event.ExposeEvent.window == childWindowHandle)
        {
            XDrawLine(display, childWindowHandle, gc, 1, 1, xDisplayWidth - 2, 1);
            XDrawLine(display, childWindowHandle, gc, 1, xDisplayHeight - 2, xDisplayWidth - 2, xDisplayHeight - 2);
            XDrawLine(display, childWindowHandle, gc, 1, 1, 1, xDisplayHeight - 2);
            XDrawLine(display, childWindowHandle, gc, xDisplayWidth - 2, xDisplayHeight - 2, xDisplayWidth - 2, xDisplayHeight - 2);
        }
    }
    else if (@event.type == XEventName.ClientMessage)
    {
        var clientMessageEvent = @event.ClientMessageEvent;
        if (clientMessageEvent.message_type == 0 && clientMessageEvent.ptr1 == invokeMessageId)
        {
            List<Action> tempList;
            lock (invokeList)
            {
                tempList = invokeList.ToList();
                invokeList.Clear();
            }

            foreach (var action in tempList)
            {
                action();
            }
        }
    }
    else if (@event.type == XEventName.MotionNotify)
    {
        if (@event.MotionEvent.window == handle)
        {
            Console.WriteLine($"Window1 {DateTime.Now:HH:mm:ss}");
        }
        else
        {
            Console.WriteLine($"Window2 {DateTime.Now:HH:mm:ss}");
        }
    }
}

Console.WriteLine("Hello, World!");

運行代碼之後的效果如下圖

如上圖,應用是透明窗口,可以看到背後的圖片應用顯示的內容。上述圖片是使用 WPF 基礎繪圖 創建和加工圖片 繪製的圖片。可以看到無論是主窗口還是子窗口都能透過去。但是子窗口將會遮擋主窗口的繪製,即讓子窗口直接顯示窗口之後的部分內容,但不會與主窗口合成,即主窗口被子窗口擋住的部分就沒有進行渲染

以上代碼放在 githubgitee 上,可以使用如下命令行拉取代碼

先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行裏面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin bd9f8b2c8f3f42bea639677bf4ac69602b521fc0

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換爲 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin bd9f8b2c8f3f42bea639677bf4ac69602b521fc0

獲取代碼之後,進入 DikalehebeekaJaqunicobo 文件夾,即可獲取到源代碼

更多 X11 相關,請參閱 博客導航

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章