为您的 X11 上的 C# OpenGL 应用添加剪贴板文本提供程序功能





5.00/5 (3投票s)
如何在 X11 上运行的基于 OpenTK 的 C# OpenGL 应用程序中通过进程间通信实现文本复制功能
- 下载某些应用程序类的源代码
- 下载 X11Wrapper(原始版本)
- 下载更新的 X11Wrapper(更新版本)
引言
剪贴板功能(提供复制粘贴或拖放)在 X11 上实现比在 Windows 上要困难得多。这是因为 X11 的设计比 Windows 的设计要老得多,并且在其生命周期中只有少量扩展。
剪贴板功能必须按照 ICCM(客户端间通信约定手册)第 2 章:通过 Selection 进行的点对点通信来实现。
我已经为我的 Roma Widget Set 实现的复制粘贴功能,但这完全基于 Xlib,并且该方法无法 1:1 移植到基于 OpenTK 的 C# OpenGL 窗口。
解决方案是一个通常用于在 X11 上运行的客户端应用程序的技巧:一个未映射的窗口负责进程间通信。“X11:剪贴板是如何工作的?”博文描述了通过剪贴板进行客户端间通信的基础——在 X11 中,这被称为selections(选择)。
这是关于 X11 上 C# OpenGL 应用程序剪贴板文本交换的第一个技巧。第二个技巧将围绕剪贴板文本使用者功能,而这一个技巧则围绕剪贴板文本提供程序功能。
背景
对于剪贴板功能,我们只需要关注类型为 XEventName.SelectionNotify
和 XEventName.SelectionRequest
的事件。(实现细节可以通过阅读OpenTK 源代码中的X11GLNative类来确定。)
XEventName.SelectionNotify
事件类型由OpenTK.NativeWindow.ProcessEvents()
进行评估——但仅限于freedesktop.org 对ICCM 的 拖放扩展。此实现仅专注于接收文件路径列表。这对于实现剪贴板文本提供程序功能没有帮助。XEventName.SelectionRequest
事件类型不会被OpenTK.NativeWindow.ProcessEvents()
进行评估。
基于 OpenTK 的 C# OpenGL 窗口通过 OpenTK.NativeWindow.ProcessEvents()
处理其所有事件。没有办法为 SelectionRequest
和 SelectionNotify
注册事件处理程序,也没有办法将代码注入 ProcessEvents()
方法。也不能从 OpenTK.NativeWindow
派生,因为它是一个internal sealed class
。
X11GLNative
类使用 Xlib 方法 XCheckWindowEvent()
和 XCheckTypedWindowEvent()
从事件队列中选择特定事件。
但是,将 X11GLNative 的方法外推——通过 XCheckWindowEvent()
和/或 XCheckTypedWindowEvent()
方法从事件队列中选择 XEventName.SelectionNotify
和 XEventName.SelectionRequest
事件——在调用 ProcessEvents()
方法之前,会导致应用程序损坏。
引入一个未映射的窗口,负责进程间通信,解决了这个问题。
Using the Code
首先,我们需要一个未映射的窗口用于进程间/客户端间通信——我称之为 IpcWindow
。这个窗口位于 Application
类中,并在 Application
主窗口(OpenTK 原生窗口)注册/注销期间创建/销毁。
从此时起,IpcWindow
实例已准备好管理客户端间通信。
/// <summary>Get or set the main window of the application.</summary>
public Window MainWindow
{ get { return _mainWindow; }
internal set
{
if (_mainWindow != null && value != null)
throw new InvalidOperationException (
"Unable to change main window after first initialization.");
_mainWindow = value;
if (_mainWindow != null)
{
var attributeMask = X11.XWindowAttributeMask.CWEventMask;
var attributes = new X11.XSetWindowAttributes();
attributes.event_mask = (X11.TLong)X11.EventMask.PropertyChangeMask;
IntPtr handle = X11.X11lib.XCreateWindow(_mainWindow.Display,
_mainWindow.RootHandle,
(X11.TInt)10, (X11.TInt)10,
(X11.TUint)10, (X11.TUint)10,
(X11.TUint)0,
/* CopyFromParent */ (X11.TInt)0,
X11.XWindowClass.CopyFromParent,
/* CopyFromParent */ IntPtr.Zero,
attributeMask, ref attributes);
if (_ipcWindow == null)
_ipcWindow = new IpcWindow(_mainWindow.Display, _mainWindow.Screen,
_mainWindow.RootHandle, handle);
}
else
{
if (_ipcWindow != null)
{
if (_ipcWindow.Display != IntPtr.Zero && _ipcWindow.Handle != IntPtr.Zero)
X11.X11lib.XDestroyWindow (_ipcWindow.Display, _ipcWindow.Handle);
_ipcWindow = null;
}
}
}
}
IpcWindow
类实现非常简洁,只添加了一些原子(atoms)。所有原子都在构造函数中初始化。
更新
IpcWindow
类的源代码现已在文章顶部提供下载。- 我引入了两个非标准原子:
OTK_TARGETS_PROP
和OTK_DATA_PROP
,用于标识用于客户端间通信的窗口属性。(X11 窗口之间的通信——特别是剪贴板所有者和请求者之间的通信——使用窗口属性。) OTK_TARGETS_PROP
原子负责支持的剪贴板格式的通信。OTK_DATA_PROP
原子负责剪贴板数据传输。- 我的经验是,支持的剪贴板格式协商和剪贴板数据传输需要两个独立的窗口属性。
- 除了窗口属性之外,客户端间通信还需要指示要交换的数据类型。这就是原子
XA_TARGETS
、XA_TIMESTAMP
、XA_UTF8_STRING
和XA_STRING
发挥作用的地方。
这是 IpcWindow
的原子初始化。
/// <summary>Initialize a new instance of the <see cref="OpenFW.Platform.X11.IpcWindow"/>
/// class with display, screen and rootHandle.</summary>
/// <param name="display">Display.</param>
/// <param name="screen">Screen.</param>
/// <param name="rootHandle">Root handle.</param>
public IpcWindow (IntPtr display, int screen, IntPtr rootHandle, IntPtr handle)
{
if (display == IntPtr.Zero)
throw new ArgumentNullException("display");
if (screen < 0)
throw new ArgumentNullException("screen");
if (rootHandle == IntPtr.Zero)
throw new ArgumentNullException("rootHandle");
if (handle == IntPtr.Zero)
throw new ArgumentNullException("handle");
Display = display;
Screen = screen;
RootHandle = rootHandle;
Handle = handle;
WM_GENERIC = X11lib.XInternAtom(display, "WM_GENERIC_CLIENT_MESSAGE", false);
XA_CLIPBOARD = X11lib.XInternAtom(display, "CLIPBOARD", false);
XA_PRIMARY = X11lib.XInternAtom(display, "PRIMARY", false);
XA_SECONDARY = X11lib.XInternAtom(display, "SECONDARY", false);
XA_TARGETS = X11lib.XInternAtom(display, "TARGETS", false);
XA_TIMESTAMP = X11lib.XInternAtom(display, "TIMESTAMP", false);
XA_UTF8_STRING = X11lib.XInternAtom(display, "UTF8_STRING", false);
XA_STRING = X11lib.XInternAtom(display, "STRING", false);
OTK_TARGETS_PROP = X11lib.XInternAtom(display, "OTK_TARGETS_PROP", false);
OTK_DATA_PROP = X11lib.XInternAtom(display, "OTK_DATAP_ROP", false);
}
其次,我们需要为 IpcWindow
评估 XEventName.SelectionRequest
事件。我在调用 Application
主窗口(OpenTK 原生窗口)的 ProcessEvents()
方法之前直接实现了它。
/// <summary>Processes one GL window message.</summary>
/// <returns>Returns <c>true</c>, if display has been validated, or <c>false</c>
/// otherwise.</returns>
/// <remarks>While a typical X11 message loop (utilizing XNextEvent()) blocks until
/// the next event arrives (and saves CPU power), a typical GL message loop
/// (utilizing ProcessEvents()) doesn't block (and wastes CPU power).</remarks>
public bool ProcessMessage ()
{
if (!_glWindow.Exists)
return false;
// Can be called from child windows (with own GL content) as well.
// Thus we have to ensure the right GL context.
if (!_context.IsCurrent)
{
// ConsoleMessageSink.WriteInfo (null, typeof(WindowBase).Name +
// "::ProcessMessage() Reactivating GL context={0} ...",
// _context.GetHashCode());
_context.MakeCurrent(_glWindow.WindowInfo);
}
IpcWindow ipcWindow = XrwGL.Application.Current.IpcWindow;
if (ipcWindow != null)
{
X11.XEvent e = new X11.XEvent();
//using (new XLock(ipcWindow.Display))
{
if (X11.X11lib.XCheckTypedWindowEvent(ipcWindow.Display, ipcWindow.Handle,
X11.XEventName.SelectionNotify, ref e) != (X11.TBoolean)0 ||
X11.X11lib.XCheckTypedWindowEvent(ipcWindow.Display, ipcWindow.Handle,
X11.XEventName.SelectionRequest, ref e) != (X11.TBoolean)0 )
{
if (e.SelectionEvent.type == X11.XEventName.SelectionRequest)
{
X11.XSelectionRequestEvent selectionEventIn = e.SelectionRequestEvent;
X11.XEvent selectionEventOut = new X11.XEvent();
selectionEventOut.SelectionEvent.type =
X11.XEventName.SelectionNotify;
selectionEventOut.SelectionEvent.display = selectionEventIn.display;
selectionEventOut.SelectionEvent.requestor = selectionEventIn.requestor;
selectionEventOut.SelectionEvent.selection = selectionEventIn.selection;
selectionEventOut.SelectionEvent.target = selectionEventIn.target;
selectionEventOut.SelectionEvent.property = selectionEventIn.property;
selectionEventOut.SelectionEvent.time = selectionEventIn.time;
if (selectionEventIn.target == ipcWindow.XA_TARGETS)
{
ConsoleMessageSink.WriteInfo
(null, "SelectionRequest: XA_TARGETS", null);
// DnD or Cut/Copy and Paste - SUPPORT:
// Provide available clipboard formats.
// For DnD see also:
// https://github.com/dfelinto/blender/tree/master/extern/xdnd
// and https://www.uninformativ.de/blog/postings/
// 2017-04-02/0/POSTING-de.html
IntPtr data = Marshal.AllocHGlobal (3 * IntPtr.Size);
Marshal.WriteIntPtr (data, ipcWindow.XA_TARGETS);
Marshal.WriteIntPtr (data, IntPtr.Size, ipcWindow.XA_STRING);
Marshal.WriteIntPtr (data, IntPtr.Size * 2, ipcWindow.XA_UTF8_STRING);
X11.X11lib.XChangeProperty(selectionEventIn.display,
selectionEventIn.requestor,
selectionEventIn.property,
X11.XAtoms.XA_ATOM,
(X11.TInt)32,
(X11.TInt)X11.XChangePropertyMode.
PropModeReplace,
data, (X11.TInt)2);
Marshal.FreeHGlobal(data);
}
else if (selectionEventIn.target == ipcWindow.XA_TIMESTAMP)
{
ConsoleMessageSink.WriteInfo
(null, "SelectionRequest: XA_TIMESTAMP", null);
ConsoleMessageSink.WriteInfo(null, this.GetType ().Name +
"::ProcessMessage () Provide requested format '{0}', " +
"that is acquired by a 'SelectionRequest' event.", "TIMESTAMP");
int datasize = sizeof (X11.TUlong);
IntPtr data = Marshal.AllocHGlobal (datasize);
if (datasize == 4)
Marshal.WriteInt32 (data, (int)X11.X11lib.CurrentTime);
else // (datasize == 8)
Marshal.WriteInt64 (data, (long)X11.X11lib.CurrentTime);
X11.X11lib.XChangeProperty (selectionEventIn.display,
selectionEventIn.requestor,
selectionEventIn.property,
selectionEventIn.target,
(X11.TInt)32,
(X11.TInt)X11.
XChangePropertyMode.PropModeReplace,
data, (X11.TInt)(datasize == 4 ? 1 : 2));
Marshal.FreeHGlobal(data);
}
else if (selectionEventIn.target == ipcWindow.XA_UTF8_STRING)
{
ConsoleMessageSink.WriteInfo (null, this.GetType ().Name +
"::ProcessMessage () Provide requested format '{0}', " +
"that is acquired by a 'SelectionRequest' event.", "UTF8_STRING");
byte[] s = System.Text.Encoding.UTF8.GetBytes(Clipboard.Text);
IntPtr data = Marshal.AllocHGlobal(Marshal.SizeOf
(typeof(byte)) * s.Length);
Marshal.Copy(s, 0, data, s.Length);
X11.X11lib.XChangeProperty(selectionEventIn.display,
selectionEventIn.requestor,
selectionEventIn.property,
ipcWindow.XA_UTF8_STRING,
(X11.TInt)8,
(X11.TInt)X11.XChangePropertyMode.
PropModeReplace,
data, (X11.TInt)s.Length);
Marshal.FreeHGlobal(data);
}
else if (selectionEventIn.target == ipcWindow.XA_STRING)
{
ConsoleMessageSink.WriteInfo (null, this.GetType ().Name +
"::ProcessMessage () Provide requested format '{0}', " +
"that is acquired by a 'SelectionRequest' event.", "STRING");
string s = Clipboard.Text;
IntPtr data = Marshal.StringToHGlobalAuto (s);
X11.X11lib.XChangeProperty(selectionEventIn.display,
selectionEventIn.requestor,
selectionEventIn.property,
ipcWindow.XA_STRING,
(X11.TInt)8,
(X11.TInt)X11.XChangePropertyMode.
PropModeReplace,
data, (X11.TInt)s.Length);
Marshal.FreeHGlobal(data);
}
else
{
IntPtr pFormatName = X11.X11lib.XGetAtomName (selectionEventIn.display,
selectionEventIn.target);
string sFormatName = Marshal.PtrToStringAuto (pFormatName);
ConsoleMessageSink.WriteWarning (null, this.GetType ().Name +
"::ProcessMessage () Unhandled requested format '{0}', " +
"that is acquired by a 'SelectionRequest' event.", sFormatName);
}
X11.X11lib.XSendEvent (selectionEventIn.display, selectionEventIn.requestor,
(X11.TBoolean)1, (X11.TLong)X11.EventMask.NoEventMask,
ref selectionEventOut);
}
else if (e.SelectionEvent.type == X11.XEventName.SelectionNotify)
{
...
}
}
}
}
// ProcessEvents() source code:
// https://github.com/opentk/opentk/blob/master/src/OpenTK/Platform/X11/X11GLNative.cs
_glWindow.ProcessEvents ();
// Calls: implementation.ProcessEvents (): Just delegates complete
// processing to implementation.
// Calls LinuxNativeWindow.ProcessEvents (): Just calls ProcessKeyboard()
// and ProcessMouse().
// Calls NativeWindowBase.ProcessEvents (): Just clears keyboard,
// to prevent confusion on missing KeyUp.
...
}
标准的文本交换格式是 XA_UTF8_STRING
,正如 freedesktop.org 对 ICCM 的 拖放扩展中定义的。
更新
- 包含
ProcessMessage()
方法的WindowBase
类源代码现已在文章顶部提供下载。
工作原理
假设我们在基于 OpenTK 的 C# OpenGL 窗口应用程序中选择了某些文本,并通过“编辑”|“复制”菜单或 [Ctrl]+[c] 键提供该文本,我们的应用程序将通过调用 XSetSelectionOwner()
将自身注册为复制粘贴内容的选择所有者——这使得其他应用程序可以请求复制粘贴内容。
另外,假设我们运行任何文本编辑器,并通过“编辑”|“粘贴”菜单或 [Ctrl]+[v] 键请求剪贴板文本,它将充当选择请求者,并发送 XEventName.SelectionRequest
事件通知选择所有者其需求,并请求支持的 XA_TARGETS
。
- 选择请求者发送的每一个
XEventName.SelectionRequest
事件都指示了一个窗口属性,该属性定义了选择请求者和选择所有者之间共享的一个锚点,用于实现数据交换。因此,选择所有者被指示使用指示的窗口属性来响应。 - 通常,
XA_CLIPBOARD
原子用于标识数据交换的窗口属性。很少使用的替代方案是XA_PRIMARY
和XA_SECONDARY
原子。 XA_TARGETS
原子由选择请求者使用,以请求选择所有者支持哪些数据格式。
选择所有者将收到 XEventName.SelectionRequest
事件,该事件要求支持的 XA_TARGETS
,以便将它们宣告到指示的窗口属性。选择所有者应通过 XChangeProperty()
将其支持的 XA_TARGETS
宣告到指示的窗口属性,并发送 XEventName.SelectionNotify
事件通知选择请求者其响应。
现在,选择请求者可以评估支持的 XA_TARGETS
,选择一种合适的格式——标准的文本交换格式是 XA_UTF8_STRING
,正如 freedesktop.org 对 ICCM 的 拖放扩展中定义的——并向选择所有者请求以 XA_UTF8_STRING
格式的复制粘贴内容,发送 XEventName.SelectionRequest
事件。
选择所有者将收到 XEventName.SelectionRequest
事件,该事件要求以 XA_UTF8_STRING
格式的复制粘贴内容,并将其宣告到指示的窗口属性。选择所有者应通过 XChangeProperty()
将复制粘贴内容宣告到指示的窗口属性,并发送 XEventName.SelectionNotify
事件通知选择请求者其响应。
最后,选择请求者可以从指示的窗口属性中获取复制粘贴内容并粘贴文本。
此过程需要在选择所有者端处理一系列 XEventName.SelectionRequest
事件,在选择请求者端处理一系列 XEventName.SelectionNotify
事件。这意味着,在选择所有者端,我们有一系列异步处理步骤——由 XEventName.SelectionRequest
事件触发。换句话说:实现剪贴板功能并非易事,而是对事件的响应。
第三,我们需要提供复制粘贴文本/需要将我们的基于 OpenTK 的 C# OpenGL 窗口应用程序注册为剪贴板文本提供程序。我的应用程序使用[Ctrl]+[c] 键组合来启动此操作。
...
else if (e.Key == (uint)OpenTK.Input.Key.C)
{
int anchorOffset = _selection.AnchorPosition.GetSymbolOffset();
int movingOffset = _selection.MovingPosition.GetSymbolOffset();
int selLength = -(movingOffset - anchorOffset);
// No selected range.
if (selLength == 0)
return;
if (e.KeyboardDevice == null)
return;
if (!e.KeyboardDevice.IsKeyDown(System.Windows.Input.Key.LeftCtrl) &&
!e.KeyboardDevice.IsKeyDown(System.Windows.Input.Key.RightCtrl))
return;
string content;
if (selLength < 0)
content = (this.TextContainer as TextContainer).TextInternal
(movingOffset+selLength, -selLength);
else
content = (this.TextContainer as TextContainer).TextInternal(movingOffset, selLength);
Clipboard.ProvideText(content, X11.X11lib.CurrentTime);
}
...
我创建了一个static
Clipboard
类来实现一些可重用的复制粘贴客户端间通信辅助方法。与选择所有者相关的方法是
public static class Clipboard
{
#region Static Properties
/// <summary>Get or set the last registered clipboard text.</summary>
/// <value>The last registered clipboard text.</value>
public static string Text { get; private set; }
#endregion Static Properties
#region Static Methods
/// <summary>Register as data provider for clipboard interaction.</summary>
/// <param name="text">The <see cref="Object"/> text to provide to the clipboard.</param>
/// <param name="selection">The <see cref="IntPtr"/> selection atom, that defines the
/// selection buffer to use. Predefined are CLIPBOARD, PRIMARY and SECONDARY.</param>
/// <param name="time">The <see cref="TTime"/> time the event
/// (typical mouse or button) occurs,
/// that triggers this clipboard interaction.</param>
/// <returns>Returns <see cref="Boolean"/> <c>true</c> on success, or <c>false</c>
/// otherwise.</returns>
public static bool ProvideText (string text, IntPtr selection, X11.TTime time)
{
if (string.IsNullOrEmpty (text))
return false;
IpcWindow ipcWindow = XrwGL.Application.Current.IpcWindow;
if (ipcWindow == null)
return false;
if (selection == IntPtr.Zero)
selection = ipcWindow.XA_CLIPBOARD;
return X11Clipboard.ProvideClipboardText (ipcWindow.Display,
ipcWindow.Handle,
selection,
time, text);
}
...
}
更新
Clipboard
类的源代码现已在文章顶部提供下载。X11Clipboard
类的源代码是可下载的X11Wrapper
程序集项目的一部分。
其他先决条件
所有用于原生代码调用的 Xlib
函数调用、结构、数据类型和常量(以及更多)都由 X11Wrapper_V1.1_Preview
项目进行原型定义或定义——这是我的 Roma Widget Set 的一个分支。我已添加完整的库项目(包括源代码)供下载。
要为自己的解决方案包含并编译库项目,必须通过编译器符号指定目标平台。使用 x86
进行 32 位平台构建,使用 x64
进行 64 位平台构建。
限制
本技巧仅提供选择所有者端的解决方案。
请参阅我的技巧 剪贴板文本使用者功能,了解选择请求者端的解决方案。
它还忽略了在复制粘贴内容很大,需要在多个片段中传输的情况下(因为指示的窗口属性容量有限)的额外工作。
本技巧使用我操作系统安装包中的 OpenTK 版本 1.1.0 编译和测试——只需对 GL.BlendFunc()
参数进行少量调整,即可启用与 NuGet 上的 OpenTK 版本 3.0.1 的编译。
历史
- 2019 年 2 月 14 日:初始版本
- 2019 年 2 月 18 日:第一次更新