为您的 C# OpenGL X11 应用添加剪贴板文本接收器功能





0/5 (0投票)
如何在 X11 上运行的基于 OpenTK 的 C# OpenGL 应用程序中通过进程间通信实现文本粘贴功能
引言
对于 X11 来说,实现剪贴板功能(提供复制粘贴或拖放)比 Windows 要困难得多。这是因为 X11 的设计比 Windows 的设计要古老得多,并且在其生命周期中只进行过少量扩展。
剪贴板功能必须根据 ICCM(客户端间通信约定手册)的 第 2 章:通过选择进行点对点通信 来实现。
我已经在我的 Roma Widget Set 中实现了复制粘贴,但这完全基于 Xlib,并且该方法无法 1:1 移植到基于 OpenTK 的 C# OpenGL 窗口。
解决方案是一个在 X11 上运行的客户端应用程序中常用的技巧:一个未映射(unmapped)的窗口负责进程间通信。一篇名为 “X11: “The” 剪贴板是如何工作的?” 的博客文章描述了通过剪贴板进行客户端间通信的基础知识 - 在 X11 中,它们被称为选择(selections)。
这是关于 X11 上 C# OpenGL 应用程序进行剪贴板文本交换的第二篇技巧。第一篇技巧是关于 剪贴板文本提供者功能,现在是关于 **剪贴板文本接收器功能**。
背景
对于剪贴板功能,我们只需要关注 XEventName.SelectionNotify
和 XEventName.SelectionRequest
事件类型。(实现细节 可以通过阅读 OpenTK 中 X11GLNative 类的源代码来确定。)
OpenTK.NativeWindow.ProcessEvents()
方法会评估XEventName.SelectionNotify
事件类型 - 但仅限于 freedesktop.org 对 ICCM 的 拖放 扩展。此实现仅专注于接收文件路径列表。这对于实现 **剪贴板文本提供者功能** 没有帮助。OpenTK.NativeWindow.ProcessEvents()
方法**不会**评估XEventName.SelectionRequest
事件类型。
基于 OpenTK 的 C# OpenGL 窗口通过 OpenTK.NativeWindow.ProcessEvents()
处理其所有事件。没有机会为 SelectionRequest
和 SelectionNotify
注册事件处理程序,也没有机会将代码注入 ProcessEvents()
方法。由于 OpenTK.NativeWindow
是一个 internal sealed class
,因此也无法从中派生。
X11GLNative 类使用 Xlib 方法 XCheckWindowEvent()
和 XCheckTypedWindowEvent()
来从事件队列中拾取特定事件。
但是,推断 X11GLNative 的方法 - 在调用 ProcessEvents()
方法之前,通过 XCheckWindowEvent()
和/或 XCheckTypedWindowEvent()
方法从事件队列中拾取 XEventName.SelectionNotify
和 XEventName.SelectionRequest
事件 - 会导致应用程序损坏。
引入一个负责进程间通信的未映射窗口解决了这个问题。
Using the Code
首先,我们需要一个用于 **进**程/客户**间** **通**信(Ipc)的未映射窗口 - 我称之为 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
类有一个非常小的实现,并且只添加了一些原子。所有原子都由构造函数初始化。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)
{
...
}
else if (e.SelectionEvent.type == X11.XEventName.SelectionNotify)
{
X11.XSelectionEvent selectionEventIn = e.SelectionEvent;
// Define the window property alias (atom),
// used to handle (requestor <--> owner)
// data transfer regarding supported IPC (clipboard) formats.
// QT uses: "_QT_SELECTION"
// GDK uses: "GDK_SELECTION"
// Since the window property alias (atom) is returned unchanged from
// X11lib.XSendEvent() after X11lib.XChangeProperty() has overridden the
// window property value (with the supported IPC (clipboard) formats),
// the exact value is not relevant - it just has to match the window
// property alias (atom) used for request of the window property value
// (supported IPC (clipboard) formats) by X11lib.XGetWindowProperty().
IntPtr dataProperty = ipcWindow.OTK_DATA_PROP;
// DnD or Cut/Copy and Paste - STAGE 1:
// Evaluate available clipboard formats and - if _XA_STRING
// format is supported -
// request the clipboard string.
// 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
if (selectionEventIn.target == ipcWindow.XA_TARGETS)
{
// Define the window property alias (atom),
// used to handle (requestor <--> owner)
// data transfer regarding supported IPC (clipboard) formats.
// QT uses: "_QT_SELECTION"
// GDK uses: "GDK_SELECTION"
// Since the window property alias (atom) is returned unchanged from
// X11lib.XSendEvent() after X11lib.XChangeProperty()
// has overridden the window property value
// (with the supported IPC (clipboard) formats),
// the exact value is not relevant - it just has to match the window
// property alias (atom) used for request of the window property value
// (supported IPC (clipboard) formats) by X11lib.XGetWindowProperty().
IntPtr property = ipcWindow.OTK_TARGETS_PROP;
IntPtr atomActType = IntPtr.Zero;
X11.TInt actFormat = 0;
X11.TUlong nItems = 0;
X11.TUlong nRemaining = 0;
X11.TLong nCapacity = (X11.TLong)4096;
IntPtr data = IntPtr.Zero;
X11.TInt result = X11.X11lib.XGetWindowProperty (
selectionEventIn.display, selectionEventIn.requestor,
property, (X11.TLong)0, nCapacity,
true, IpcWindow.AnyPropertyType,
ref atomActType, ref actFormat,
ref nItems, ref nRemaining, ref data);
if (result != 0)
{
ConsoleMessageSink.WriteError(null, this.GetType ().Name +
"::ProcessMessage () Failed to get
the property for SelectionNotify " +
"event containing the supported clipboard data formats!");
X11.X11lib.XFree(data);
return true;
}
bool supportsUTF8 = false;
bool supportsString = false;
if (atomActType == X11.XAtoms.XA_ATOM && nItems > 0)
{
IntPtr[] types = new IntPtr[(int)nItems];
for (int index = 0; index < (int)nItems; index++)
{
types[index] = Marshal.ReadIntPtr(data, index * IntPtr.Size);
if (types[index] == ipcWindow.XA_UTF8_STRING)
supportsUTF8 = true;
if (types[index] == ipcWindow.XA_STRING)
supportsString = true;
}
}
X11.X11lib.XFree(data);
// Signal the selection owner that we have successfully read the data.
X11.X11lib.XDeleteProperty(selectionEventIn.display,
selectionEventIn.requestor,
property);
if (supportsUTF8 == true)
Clipboard.RequestData (ipcWindow.XA_UTF8_STRING,
dataProperty,
selectionEventIn.time);
else if (supportsString == true)
Clipboard.RequestData (ipcWindow.XA_STRING,
dataProperty,
selectionEventIn.time);
}
// DnD or Cut/Copy and Paste - STAGE 2:
// Process the provided - _XA_STRING formatted - clipboard string.
// 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
if (selectionEventIn.property == dataProperty)
{
// Get the length of the string.
IntPtr atomActType = IntPtr.Zero;
X11.TInt actFormat = 0;
X11.TUlong nItems = 0;
X11.TUlong nRemaining = 0;
X11.TLong nCapacity = (X11.TLong)4096;
IntPtr data = IntPtr.Zero;
X11.TInt result = X11.X11lib.XGetWindowProperty(
selectionEventIn.display, selectionEventIn.requestor,
dataProperty, (X11.TLong)0, nCapacity, true,
IpcWindow.AnyPropertyType,
ref atomActType, ref actFormat,
ref nItems, ref nRemaining, ref data);
if (result != 0)
{
ConsoleMessageSink.WriteError(null, this.GetType ().Name +
"::XrwProcessSelectionNotify ()
Failed to get the property for " +
"SelectionNotify event containing the clipboard value!");
X11.X11lib.XFree(data);
return true;
}
if (atomActType == ipcWindow.XA_UTF8_STRING ||
atomActType == ipcWindow.XA_STRING)
{
// Provide the investigated clipboard data to the requestor.
string text;
if (atomActType == ipcWindow.XA_UTF8_STRING)
{
byte[] s = new byte[(int)nItems];
Marshal.Copy(data, s, 0, (int)nItems);
text = System.Text.Encoding.UTF8.GetString(s);
}
else
text = Marshal.PtrToStringAuto (data);
X11.ClipboardGetResultDelegate treatClipboardResult =
X11.X11Clipboard.TreatClipboardResultDelegate;
if (treatClipboardResult != null)
treatClipboardResult (text);
}
X11.X11lib.XFree(data);
// Signal the selection owner that we have successfully read the data.
X11.X11lib.XDeleteProperty(selectionEventIn.display,
selectionEventIn.requestor,
dataProperty);
}
}
}
}
}
// 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
类源代码可在技巧开头提供的下载内容中找到。
工作原理
假设我们有一个正在运行的文本编辑器,选中一些文本并通过“**编辑**”|“**复制**”菜单或 [Ctrl]+[c] 键提供文本,该文本编辑器会调用 XSetSelectionOwner()
将自身注册为复制粘贴内容的**选择所有者**,从而使其他应用程序能够查询复制粘贴内容。
另外,假设我们基于 OpenTK 的 C# OpenGL 窗口应用程序通过“**编辑**”|“**粘贴**”菜单或 [Ctrl]+[v] 键请求剪贴板文本,它将充当**选择请求者**,并调用 XConvertSelection()
来确定最佳数据交换格式。
**选择请求者**的 XConvertSelection()
调用会触发一个 XEventName.SelectionRequest
事件,以通知**选择所有者**其需求,并询问支持的 XA_TARGETS
。
- 来自**选择请求者**的每一个
XEventName.SelectionRequest
事件都指示一个窗口属性,该属性定义了一个由**选择请求者**和**选择所有者**共享的锚点,用于实现数据交换。因此,**选择所有者**被指示使用指示的窗口属性进行答复。 - 通常,
XA_CLIPBOARD
原子用于标识数据交换的窗口属性。很少使用的替代项是XA_PRIMARY
和XA_SECONDARY
原子。 XA_TARGETS
原子由**选择请求者**使用,以请求**选择所有者**支持哪些数据格式。
**选择所有者**收到一个询问支持的 XA_TARGETS
的 XEventName.SelectionRequest
事件,通过 XChangeProperty()
将其支持的 XA_TARGETS
公告给指示的窗口属性,并发送一个 XEventName.SelectionNotify
事件通知**选择请求者**其答复。
现在**选择请求者**将收到来自**选择所有者**的 XEventName.SelectionNotify
事件,其中包含在指示的窗口属性中公布的支持的 XA_TARGETS
。**选择请求者**应评估支持的 XA_TARGETS
,选择首选格式 - 标准文本交换格式是 XA_UTF8_STRING
,如 freedesktop.org 对 ICCM 的 拖放 扩展中所定义的 - 并调用 XConvertSelection()
以请求以 XA_UTF8_STRING
格式的复制粘贴内容。
**选择请求者**的 XConvertSelection()
调用会触发一个 XEventName.SelectionRequest
事件,以通知**选择所有者****选择请求者**希望数据以何种格式交付。
**选择所有者**将收到一个要求以 XA_UTF8_STRING
格式提供复制粘贴内容的 XEventName.SelectionRequest
事件,应通过 XChangeProperty()
将复制粘贴内容公告给指示的窗口属性,并发送一个 XEventName.SelectionNotify
事件通知**选择请求者**其答复。
最后,**选择所有者**从指示的窗口属性中拾取复制粘贴内容并粘贴文本。
此过程需要处理一系列在**选择所有者**端的 XEventName.SelectionRequest
事件和在**选择请求者**端的 XEventName.SelectionNotify
事件。这意味着,在**选择请求者**端,有一系列由 XConvertSelection()
调用触发的异步处理步骤(调用触发 XEventName.SelectionRequest
事件),以及一个用于接受请求的复制粘贴文本的最终异步处理步骤。或者换句话说:实现剪贴板功能并非易事,而是对事件的响应。
第三,我们需要为我们的基于 OpenTK 的 C# OpenGL 窗口应用程序请求复制粘贴文本并接受已交付的复制粘贴文本。我的应用程序使用 **[Ctrl]+[v]** 键组合来发起复制粘贴文本请求,并定义用于接受已交付复制粘贴文本的回调。
else if (e.Key == (uint)OpenTK.Input.Key.V)
{
Clipboard.GetText(delegate(object result)
{
...
});
}
我创建了一个静态 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>Ask the clipboard selection owner for supported formats.</summary>
/// <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="property">The <see cref="IntPtr"/> property atom,
/// that identifies the data
/// buffer for data transfer.</param>
/// <param name="time">The <see cref="TTime"/> time the event
/// (typical mouse or button) occurs,
/// that triggers this clipboard interaction.</param>
public static void RequestTypes (IntPtr selection, IntPtr property, X11.TTime time)
{
IpcWindow ipcWindow = XrwGL.Application.Current.IpcWindow;
if (ipcWindow == null)
return;
X11Clipboard.RequestClipboardTypes (ipcWindow.Display,
ipcWindow.Handle,
selection,
ipcWindow.XA_TARGETS,
property,
time);
}
/// <summary>Ask the clipboard selection owner for <c>target</c> data.</summary>
/// <param name="target">The <see cref="IntPtr"/> target atom, that defines the requested
/// format.</param>
/// <param name="property">The <see cref="IntPtr"/> property atom,
/// that identifies the data
/// buffer for data transfer.</param>
/// <param name="time">The <see cref="TTime"/> time the event
/// (typical mouse or button) occurs,
/// that triggers this clipboard interaction.</param>
public static void RequestData (IntPtr target, IntPtr property, X11.TTime time)
{
IpcWindow ipcWindow = XrwGL.Application.Current.IpcWindow;
if (ipcWindow == null)
return;
IntPtr[] possibleSelections = new IntPtr[]
{ ipcWindow.XA_CLIPBOARD,
ipcWindow.XA_PRIMARY,
ipcWindow.XA_SECONDARY
};
X11Clipboard.RequestClipboardData (ipcWindow.Display,
ipcWindow.Handle,
possibleSelections,
target,
property,
time);
}
}
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月18日:初始版本