X11 設置多屏下窗口在哪個屏幕上全屏
在 X11 里面,根據 Window Manager Protocols - Extended Window Manager Hints 文檔說明,可使用 _NET_WM_FULLSCREEN_MONITORS 設置窗口應該在哪個屏幕上進行全屏顯示
其使用方法如下:
- 在窗口 XMapWindow 之后調用
- 配合
_NET_WM_STATE_FULLSCREEN使用 - 通過 ClientMessage 發送
_NET_WM_FULLSCREEN_MONITORS給到 RootWindow 設置全屏所在屏幕,其中參數信息如下
要求傳入 4 個參數,分別是上下、左右四個邊角所在的屏幕的索引號。標重點,這里的上下左右不是要像素值,而是顯示器屏幕的索引號
根據官方文檔說明,屏幕的索引號可通過 Xinerama extension 獲取到。然而 Xinerama 十分古老,現在可以使用 XRRGetMonitors 來獲取
設計上,可以給上下、左右四個邊角傳入相同的顯示器屏幕索引,從而讓窗口在指定的顯示器屏幕上全屏顯示。也可以給上下、左右四個邊角傳入不同的顯示器屏幕索引,從而實現跨多個屏幕全屏顯示
核心調用示例代碼如下,以下代碼需要在窗口 XMapWindow 之后調用
public void SetFullScreenMonitor(int monitorIndex)
{
// [Window Manager Protocols | Extended Window Manager Hints](https://specifications.freedesktop.org/wm-spec/1.5/ar01s06.html )
// 6.3 _NET_WM_FULLSCREEN_MONITORS
// A read-only list of 4 monitor indices indicating the top, bottom, left, and right edges of the window when the fullscreen state is enabled. The indices are from the set returned by the Xinerama extension.
// Windows transient for the window with _NET_WM_FULLSCREEN_MONITORS set, such as those with type _NEW_WM_WINDOW_TYPE_DIALOG, are generally expected to be positioned (e.g. centered) with respect to only one of the monitors. This might be the monitor containing the mouse pointer or the monitor containing the non-full-screen window.
// A Client wishing to change this list MUST send a _NET_WM_FULLSCREEN_MONITORS client message to the root window. The Window Manager MUST keep this list updated to reflect the current state of the window.
var wmState = XInternAtom(Display, "_NET_WM_FULLSCREEN_MONITORS", true);
Console.WriteLine($"_NET_WM_FULLSCREEN_MONITORS={wmState}");
// 如 https://github.com/underdoeg/ofxFenster/blob/6ecd5bd9b8412f98e1c4e73433e2aade2b5902c4/src/ofxFenster.cpp#L691 的代碼所示。這里傳入的 Left、Top、Right、Bottom 不是像素的值,而是屏幕的索引值
// _NET_WM_FULLSCREEN_MONITORS, CARDINAL[4]/32
/*
data.l[0] = the monitor whose top edge defines the top edge of the fullscreen window
data.l[1] = the monitor whose bottom edge defines the bottom edge of the fullscreen window
data.l[2] = the monitor whose left edge defines the left edge of the fullscreen window
data.l[3] = the monitor whose right edge defines the right edge of the fullscreen window
*/
// 這里的 Left、Top、Right、Bottom 是屏幕的索引值,而不是像素的值
var left = monitorIndex;
var top = monitorIndex;
var right = monitorIndex;
var bottom = monitorIndex;
Console.WriteLine($"Left={left} Top={top} Right={right} Bottom={bottom}");
//int[] monitorEdges = [top, bottom, left, right];
//XChangeProperty(Display, X11Window, wmState, (IntPtr) Atom.XA_CARDINAL, format: 32, PropertyMode.Replace,
// monitorEdges, monitorEdges.Length);
// A Client wishing to change this list MUST send a _NET_WM_FULLSCREEN_MONITORS client message to the root window. The Window Manager MUST keep this list updated to reflect the current state of the window.
var xev = new XEvent
{
ClientMessageEvent =
{
type = XEventName.ClientMessage,
send_event = true,
window = X11Window,
message_type = wmState,
format = 32,
ptr1 = top,
ptr2 = bottom,
ptr3 = left,
ptr4 = right,
}
};
XSendEvent(Display, RootWindow, false,
new IntPtr((int) (EventMask.SubstructureRedirectMask | EventMask.SubstructureNotifyMask)), ref xev);
}
如上文所述,單獨通過 ClientMessage 發送 _NET_WM_FULLSCREEN_MONITORS 給到 RootWindow 是沒有效果的,需要配合 _NET_WM_STATE_FULLSCREEN 使用。發送 _NET_WM_STATE_FULLSCREEN 的示例代碼如下,同樣以下代碼也應該在窗口 XMapWindow 之后調用
public void SetFullScreen()
{
var hiddenAtom = XInternAtom(Display, "_NET_WM_STATE_HIDDEN", true);
var fullScreenAtom = XInternAtom(Display, "_NET_WM_STATE_FULLSCREEN", true);
ChangeWMAtoms(false, hiddenAtom);
ChangeWMAtoms(true, fullScreenAtom);
}
private void ChangeWMAtoms(bool enable, params IntPtr[] atoms)
{
if (atoms.Length != 1 && atoms.Length != 2)
{
throw new ArgumentException();
}
var wmState = XInternAtom(Display, "_NET_WM_STATE", true);
SendNetWMMessage(wmState,
(IntPtr) (enable ? 1 : 0),
atoms[0],
atoms.Length > 1 ? atoms[1] : IntPtr.Zero,
atoms.Length > 2 ? atoms[2] : IntPtr.Zero,
atoms.Length > 3 ? atoms[3] : IntPtr.Zero
);
}
private void SendNetWMMessage(IntPtr message_type, IntPtr l0,
IntPtr? l1 = null, IntPtr? l2 = null, IntPtr? l3 = null, IntPtr? l4 = null)
{
var xev = new XEvent
{
ClientMessageEvent =
{
type = XEventName.ClientMessage,
send_event = true,
window = X11Window,
message_type = message_type,
format = 32,
ptr1 = l0,
ptr2 = l1 ?? IntPtr.Zero,
ptr3 = l2 ?? IntPtr.Zero,
ptr4 = l3 ?? IntPtr.Zero
}
};
xev.ClientMessageEvent.ptr4 = l4 ?? IntPtr.Zero;
XSendEvent(Display, RootWindow, false,
new IntPtr((int) (EventMask.SubstructureRedirectMask | EventMask.SubstructureNotifyMask)), ref xev);
}
本文接下來將使用實際的代碼給大家演示 _NET_WM_FULLSCREEN_MONITORS 的調用方法,需求是在一個包含雙屏的設備上,每個屏幕分別顯示一個全屏的窗口
本文的演示是在 UOS 上進行的,系統信息如下
$ 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]=統信桌面操作系統
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
...
使用 xrandr 命令可查看到的雙屏信息如下
$ xrandr --listmonitors
Monitors: 2
0: +*DisplayPort-1 1920/708x1080/398+1920+0 DisplayPort-1
1: +DisplayPort-0 1920/708x1080/398+0+0 DisplayPort-0
先使用 XRRGetMonitors 獲取多個屏幕的信息,本文這里直接抄 Avalonia 的 Randr15ScreensImpl 類,代碼如下
// 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}";
}
}
獲取到的屏幕順序十分重要,因為接下來調用 _NET_WM_FULLSCREEN_MONITORS 將傳遞參數為顯示器序號
為了方便地創建 X11 窗口,本文這里封裝了名為 TestX11Window 的窗口輔助類,其構造函數和成員屬性如下
internal class TestX11Window
{
public TestX11Window(string name, int x, int y, int width, int height, nint display, nint rootWindow, int screen)
{
Name = name;
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);
X11Window = handle;
XEventMask ignoredMask = XEventMask.SubstructureRedirectMask | XEventMask.ResizeRedirectMask |
XEventMask.PointerMotionHintMask;
var mask = new IntPtr(0xffffff ^ (int)ignoredMask);
XSelectInput(display, handle, mask);
var gc = XCreateGC(display, handle, 0, 0);
GC = gc;
X = x;
Y = y;
Width = width;
Height = height;
RootWindow = rootWindow;
}
public string Name { get; }
public IntPtr X11Window { get; }
public IntPtr Display { get; }
public IntPtr GC { get; }
public int X { get; }
public int Y { get; }
public int Width { get; }
public int Height { get; }
public IntPtr RootWindow { get; }
}
以上代碼中創建窗口的部分是比較標準的做法,沒有什么特殊處理。如大家對以上創建窗口細節感興趣,還請參閱 了解 X11 窗口和消息基礎知識 博客
通過以上代碼可見在構造函數里面創建了窗口,但是沒有經過 XMapWindow 顯示出來。再添加一個名為 MapWindow 的方法,用于顯示出窗口
internal class TestX11Window
{
public void MapWindow()
{
XMapWindow(Display, X11Window);
XFlush(Display);
}
}
以上代碼里面默認引用了 XLib 的 PInvoke 方法,這部分代碼的方法定義和 XLib 相同,就不在本文列舉出來,如需要全部的項目文件,可到本文末尾找到本文所有代碼的下載方法
為了能夠在窗口上顯示點東西,咱繼續添加一個名為 Draw 的方法,刪減之后的代碼如下
internal class TestX11Window
{
public void Draw()
{
XImage xImage = CreateImage();
XPutImage(Display, X11Window, GC, ref xImage, 0, 0, 0, 0, (uint) Width,
(uint) Height);
}
}
創建圖片的 CreateImage 不屬于本文關注的內容,還請忽略,只需要了解到有一個方法能夠創建出 XImage 即可
準備工作完成之后,回到 Program.cs 主函數里面,接下來咱將為每個顯示器屏幕創建一個窗口,其代碼如下
XInitThreads();
var display = XOpenDisplay(IntPtr.Zero);
var screen = XDefaultScreen(display);
var rootWindow = XDefaultRootWindow(display);
var dictionary = new Dictionary<IntPtr, TestX11Window>();
var randr15ScreensImpl = new Randr15ScreensImpl(display, rootWindow);
var monitorInfos = randr15ScreensImpl.GetMonitorInfos();
for (var i = 0; i < monitorInfos.Length; i++)
{
// 屏幕0 DisplayPort-1(343) IsPrimary=True XY=1920,309 WH=1920,1080
// 屏幕1 DisplayPort-0(626) IsPrimary=False XY=0,0 WH=1920,1080
MonitorInfo monitorInfo = monitorInfos[i];
Console.WriteLine($"屏幕{i} {monitorInfo}");
var x = monitorInfo.X;
var y = monitorInfo.Y;
var width = monitorInfo.Width;
var height = monitorInfo.Height;
var testX11Window = new TestX11Window($"Window{i}", x, y, width, height, display, rootWindow, screen);
testX11Window.MapWindow();
testX11Window.Draw();
dictionary[testX11Window.X11Window] = testX11Window;
Console.WriteLine($"X11Window{i}={testX11Window.X11Window}");
}
現在如果嘗試跑起來應用,則會發現窗口似乎隨機顯示到某個屏幕上。如果更細心一點,會發現窗口將會顯示到鼠標最后一次落下的屏幕上。或觸摸最后點擊到的屏幕上
在 上一篇博客 中,采用 XSetWMNormalHints 固定窗口所在的屏幕,此方法可以決定窗口應該在哪個屏幕上顯示
在本文里面,將不使用 XSetWMNormalHints 的方法,而是只采用 _NET_WM_FULLSCREEN_MONITORS 進行設置
如本文一開始所述,單獨設置 _NET_WM_FULLSCREEN_MONITORS 是沒有效果的,需要配合 _NET_WM_STATE_FULLSCREEN 使用
給 TestX11Window 再添加 SetFullScreen 和 SetFullScreenMonitor 方法,分別用于設置全屏和控制在哪個窗口全屏。具體實現在上文已經列舉出來了,這里就不再重復說明
添加之后,繼續修改 Program.cs 函數,在創建完成窗口之后,先調用 MapWindow 再分別設置窗口全屏
var testX11Window = new TestX11Window($"Window{i}", x, y, width, height, display, rootWindow, screen);
testX11Window.MapWindow();
testX11Window.SetFullScreen();
testX11Window.SetFullScreenMonitor(i);
嘗試運行代碼,可見在雙屏設備上,每個屏幕分別顯示一個全屏的窗口
本文代碼放在 github 和 gitee 上,可以使用如下命令行拉取代碼。我整個代碼倉庫比較龐大,使用以下命令行可以進行部分拉取,拉取速度比較快
先創建一個空文件夾,接著使用命令行 cd 命令進入此空文件夾,在命令行里面輸入以下代碼,即可獲取到本文的代碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 951e0cc432ee948c71bb4365d56e1ae8eb43d502
以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼。如果依然拉取不到代碼,可以發郵件向我要代碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 951e0cc432ee948c71bb4365d56e1ae8eb43d502
獲取代碼之后,進入 X11/HairkallberciderqewallReedeegewhige 文件夾,即可獲取到源代碼
更多 X11 技術博客,請參閱 博客導航
博客園博客只做備份,博客發布就不再更新,如果想看最新博客,請訪問 https://blog.lindexi.com/
如圖片看不見,請在瀏覽器開啟不安全http內容兼容

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

浙公網安備 33010602011771號