在 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 視窗上方
以上程式碼放在 github 和 gitee 上,可以使用如下命令列拉取程式碼
先建立一個空資料夾,接著使用命令列 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 方法,否則關係設定無效
以上程式碼放在 github 和 gitee 上,可以使用如下命令列拉取程式碼
先建立一個空資料夾,接著使用命令列 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}");
}
}
}
配置了以上程式碼,執行專案,可以看到滑鼠在子視窗上時,只能收到子視窗的訊息,如下圖
以上程式碼有所忽略,全部的程式碼放在 github 和 gitee 上,可以使用如下命令列拉取程式碼
先建立一個空資料夾,接著使用命令列 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);
以上程式碼放在 github 和 gitee 上,可以使用如下命令列拉取程式碼
先建立一個空資料夾,接著使用命令列 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 基礎繪圖 建立和加工圖片 繪製的圖片。可以看到無論是主視窗還是子視窗都能透過去。但是子視窗將會遮擋主視窗的繪製,即讓子視窗直接顯示視窗之後的部分內容,但不會與主視窗合成,即主視窗被子視窗擋住的部分就沒有進行渲染
以上程式碼放在 github 和 gitee 上,可以使用如下命令列拉取程式碼
先建立一個空資料夾,接著使用命令列 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 資料夾,即可獲取到原始碼