X11 使用 XSetWMNormalHints 固定窗口所在的屏幕
我的需求是雙屏雙窗口,即一個屏幕顯示一個窗口。我的是 KWin 窗口管理器,默認情況下,我的正常窗口會被顯示到鼠標最后一次命中的屏幕上,無論當前在 XCreateWindow 中傳入的 X 和 Y 坐標是多少
本文的測試是在 UOS 上進行的,系統(tǒng)信息如下
$ cat /etc/os-release
PRETTY_NAME="UnionTech OS Desktop 20 E"
NAME="uos"
VERSION_ID="20"
VERSION="20"
ID=uos
HOME_URL="https://www.chinauos.com/"
BUG_REPORT_URL="http://bbs.chinauos.com"
VERSION_CODENAME=uranus
$ cat /etc/os-version
[Version]
SystemName=UnionTech OS Desktop
SystemName[zh_CN]=統(tǒng)信桌面操作系統(tǒng)
ProductType=Desktop
ProductType[zh_CN]=桌面
EditionName=E
EditionName[zh_CN]=E
MajorVersion=20
MinorVersion=1050
OsBuild=11068.102
處理器 CPU 信息如下
$ cat /proc/cpuinfo
processor : 0
vendor_id : CentaurHauls
cpu family : 7
model : 59
model name : ZHAOXIN KaiXian KX-U6780A@2.7GHz
...
在 X 里面,通過 XDisplayWidth 和 XDisplayHeight 拿到的是虛擬屏幕的尺寸,即多個物理屏幕拼接的外接矩形虛擬范圍。我的兩個屏幕排放如下

可通過以下調(diào)用 XRRGetMonitors 的代碼獲取兩個屏幕的信息
// Copy from https://github.com/AvaloniaUI/Avalonia \src\Avalonia.X11\Screens\X11Screen.Providers.cs
public class Randr15ScreensImpl
{
public Randr15ScreensImpl(nint display, nint rootWindow)
{
_display = display;
var eventWindow = CreateEventWindow(display, rootWindow);
_window = eventWindow;
XRRSelectInput(display, _window, RandrEventMask.RRScreenChangeNotify);
}
public unsafe MonitorInfo[] GetMonitorInfos()
{
XRRMonitorInfo* monitors = XRRGetMonitors(_display, _window, true, out var count);
var screens = new MonitorInfo[count];
for (var c = 0; c < count; c++)
{
var mon = monitors[c];
var outputs = new nint[mon.NOutput];
for (int i = 0; i < outputs.Length; i++)
{
outputs[i] = mon.Outputs[i];
}
screens[c] = new MonitorInfo()
{
Name = mon.Name,
IsPrimary = mon.Primary != 0,
X = mon.X,
Y = mon.Y,
Width = mon.Width,
Height = mon.Height,
Outputs = outputs,
Display = _display,
};
}
return screens;
}
private readonly IntPtr _display;
private readonly IntPtr _window;
}
public unsafe struct MonitorInfo
{
public IntPtr Name;
public bool IsPrimary;
public int X;
public int Y;
public int Width;
public int Height;
public IntPtr[] Outputs;
public IntPtr Display { get; init; }
public override string ToString()
{
var namePtr = XGetAtomName(Display, Name);
var name = Marshal.PtrToStringAnsi(namePtr);
XFree(namePtr);
return $"{name}({Name}) IsPrimary={IsPrimary} XY={X},{Y} WH={Width},{Height}";
}
}
調(diào)用的方法如下
var randr15ScreensImpl = new Randr15ScreensImpl(display, rootWindow);
var monitorInfos = randr15ScreensImpl.GetMonitorInfos();
for (var i = 0; i < monitorInfos.Length; i++)
{
Console.WriteLine($"屏幕{i} {monitorInfos[i]}");
}
var xDisplayWidth = XDisplayWidth(display, screen);
var xDisplayHeight = XDisplayHeight(display, screen);
Console.WriteLine($"XDisplayWidth={xDisplayWidth}");
Console.WriteLine($"XDisplayHeight={xDisplayHeight}");
嘗試運行程序,可見控制臺輸出如下
屏幕0 DisplayPort-1(343) IsPrimary=True XY=1920,309 WH=1920,1080
屏幕1 DisplayPort-0(626) IsPrimary=False XY=0,0 WH=1920,1080
XDisplayWidth=3840
XDisplayHeight=1389
兩個屏幕都是 1920x1080 的,水平擺放,于是 XDisplayWidth 寬度就是 1920+1920=3840 的尺寸。高度因為存在一定的高度差,通過 XY=1920,309 可知道,主屏 DisplayPort-1 低了 309 大小,于是高度為 1080+309=1389 的尺寸。這就意味著 X 的行為上 XDisplayWidth 和 XDisplayHeight 為多個屏幕的外接矩形尺寸
和 Windows 上不同的是,在 X 上沒有使用主屏當成 0,0 點坐標,意味著不會存在負數(shù)坐標系。在 X 中將最左邊的顯示器屏幕當成 X 坐標的 0 點,將最上方的顯示器屏幕當成 Y 坐標的 0 點。這一點差異會在一些計算中坑到我,預計坑到的時候,我已經(jīng)忘記我寫了這篇博客
了解了基礎信息,我接下來嘗試為雙屏創(chuàng)建雙窗口。簡單起見,我將固定寫魔數(shù),而不是真的根據(jù)屏幕而來
我將設置第 1 個窗口,顯示在 0,0 坐標。設置第 2 個窗口,顯示在 1920,0 坐標。預期行為就是第 1 個窗口顯示在副屏 DisplayPort-0 上,第 2 個窗口顯示在主屏 DisplayPort-1 上
其實現(xiàn)代碼如下
public TestX11Window(int x, int y, int width, int height, nint display,nint rootWindow,int screen)
{
Display = 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 handle = XCreateWindow(display, rootWindow, x, y, width, height, 5,
32,
(int) CreateWindowArgs.InputOutput,
visual,
(nuint) valueMask, ref xSetWindowAttributes);
// 在 XMapWindow 之前固定在某個屏幕上
var hints = new XSizeHints
{
min_width = width,
min_height = height,
max_width = width,
max_height = height,
x = x,
y = y,
};
var flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PResizeInc | XSizeHintsFlags.PPosition | XSizeHintsFlags.USPosition;
hints.flags = (IntPtr) flags;
XSetWMNormalHints(display, handle, ref hints);
X11Window = handle;
XEventMask ignoredMask = XEventMask.SubstructureRedirectMask | XEventMask.ResizeRedirectMask |
XEventMask.PointerMotionHintMask;
var mask = new IntPtr(0xffffff ^ (int) ignoredMask);
XSelectInput(display, handle, mask);
...
}
調(diào)用代碼如下
var testX11Window1 = new TestX11Window(0, 0, width, height, display, rootWindow, screen);
testX11Window1.MapWindow();
testX11Window1.Draw();
Console.WriteLine($"X11Window1={testX11Window1.X11Window}");
var testX11Window2 = new TestX11Window(1920, 0, width, height, display, rootWindow, screen);
testX11Window2.MapWindow();
testX11Window2.Draw();
Console.WriteLine($"X11Window2={testX11Window2.X11Window}");
以上的核心代碼就是調(diào)用 XSetWMNormalHints 設置進去 XSizeHints 參數(shù)。詳細請參閱 XSetWMProperties
嘗試通過 XNextEvent 獲取消息,可見輸出控制臺如下
屏幕0 DisplayPort-1(343) IsPrimary=True XY=1920,309 WH=1920,1080
屏幕1 DisplayPort-0(626) IsPrimary=False XY=0,0 WH=1920,1080
XDisplayWidth=3840
XDisplayHeight=1389
X11Window1=134217731
X11Window2=134217734
ConfigureNotify XConfigureEvent (type=ConfigureNotify, serial=20, send_event=False, display=94542337201216, xevent=134217731, window=134217731, x=0, y=0, width=1920, height=694, border_width=0, above=134217729, override_redirect=False)
ConfigureNotify XConfigureEvent (type=ConfigureNotify, serial=20, send_event=True, display=94542337201216, xevent=134217731, window=134217731, x=0, y=40, width=1920, height=694, border_width=0, above=0, override_redirect=False)
ConfigureNotify XConfigureEvent (type=ConfigureNotify, serial=68, send_event=False, display=94542337201216, xevent=134217734, window=134217734, x=1920, y=0, width=1920, height=694, border_width=0, above=134217729, override_redirect=False)
ConfigureNotify XConfigureEvent (type=ConfigureNotify, serial=96, send_event=True, display=94542337201216, xevent=134217734, window=134217734, x=1920, y=349, width=1920, height=694, border_width=0, above=0, override_redirect=False)
通過 ConfigureNotify 消息可見 window=134217734 的 X11Window2 窗口的 X 坐標確實被設置到 1920 上,且通過實際的屏幕顯示內(nèi)容也可以看到兩個窗口被分別顯示到兩個顯示器屏幕上
如果沒有調(diào)用 XSetWMNormalHints 設置,則窗口顯示過程中,收到的 ConfigureNotify 會根據(jù)鼠標最后停留在哪個屏幕上,選擇對應的屏幕設置給到窗口坐標。如以下的去掉了 XSetWMNormalHints 的代碼跑出來的控制臺效果
X11Window1=134217731
X11Window2=134217734
ConfigureNotify XConfigureEvent (type=ConfigureNotify, serial=19, send_event=False, display=94583148290624, xevent=134217731, window=134217731, x=0, y=0, width=1920, height=694, border_width=0, above=134217729, override_redirect=False)
ConfigureNotify XConfigureEvent (type=ConfigureNotify, serial=19, send_event=True, display=94583148290624, xevent=134217731, window=134217731, x=1920, y=349, width=1920, height=694, border_width=0, above=0, override_redirect=False)
ConfigureNotify XConfigureEvent (type=ConfigureNotify, serial=51, send_event=False, display=94583148290624, xevent=134217734, window=134217734, x=1920, y=0, width=1920, height=694, border_width=0, above=134217729, override_redirect=False)
ConfigureNotify XConfigureEvent (type=ConfigureNotify, serial=103, send_event=True, display=94583148290624, xevent=134217734, window=134217734, x=1920, y=655, width=1920, height=694, border_width=0, above=0, override_redirect=False)
此時可見 window=134217731 的 X11Window1 將和 X11Window2 一樣,被設置 x=1920 到主屏上
本文代碼放在 github 和 gitee 上,可以使用如下命令行拉取代碼。我整個代碼倉庫比較龐大,使用以下命令行可以進行部分拉取,拉取速度比較快
先創(chuàng)建一個空文件夾,接著使用命令行 cd 命令進入此空文件夾,在命令行里面輸入以下代碼,即可獲取到本文的代碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin e4780e542104ca07e16735506736bb16030e47fe
以上使用的是國內(nèi)的 gitee 的源,如果 gitee 不能訪問,請?zhí)鎿Q為 github 的源。請在命令行繼續(xù)輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼。如果依然拉取不到代碼,可以發(fā)郵件向我要代碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin e4780e542104ca07e16735506736bb16030e47fe
獲取代碼之后,進入 X11/CeejemwhucemwaileeRerallbefe 文件夾,即可獲取到源代碼
更多 X11 技術博客,請參閱 博客導航
博客園博客只做備份,博客發(fā)布就不再更新,如果想看最新博客,請訪問 https://blog.lindexi.com/
如圖片看不見,請在瀏覽器開啟不安全http內(nèi)容兼容

本作品采用知識共享署名-非商業(yè)性使用-相同方式共享 4.0 國際許可協(xié)議進行許可。歡迎轉(zhuǎn)載、使用、重新發(fā)布,但務必保留文章署名[林德熙](http://www.rzrgm.cn/lindexi)(包含鏈接:http://www.rzrgm.cn/lindexi ),不得用于商業(yè)目的,基于本文修改后的作品務必以相同的許可發(fā)布。如有任何疑問,請與我[聯(lián)系](mailto:lindexi_gd@163.com)。

浙公網(wǎng)安備 33010602011771號