在 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; }
}
可见在构造函数里面创建了窗口,但是没有经过 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 文件夹,即可获取到源代码
更多技术博客,请参阅 博客导航
本文会经常更新,请阅读原文: https://blog.lindexi.com/post/X11-%E8%AE%BE%E7%BD%AE%E5%A4%9A%E5%B1%8F%E4%B8%8B%E7%AA%97%E5%8F%A3%E5%9C%A8%E5%93%AA%E4%B8%AA%E5%B1%8F%E5%B9%95%E4%B8%8A%E5%85%A8%E5%B1%8F.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航
本作品采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接:
https://blog.lindexi.com
),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请
与我联系
。
无盈利,不卖课,做纯粹的技术博客
以下是广告时间
推荐关注 Edi.Wang 的公众号
欢迎进入 Eleven 老师组建的 .NET 社区
以上广告全是友情推广,无盈利