65.9K
CodeProject 正在变化。 阅读更多。
Home

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

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2019年2月18日

CPOL

7分钟阅读

viewsIcon

4267

downloadIcon

95

如何在 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.SelectionNotifyXEventName.SelectionRequest 事件类型。(实现细节 可以通过阅读 OpenTKX11GLNative 类的源代码来确定。)

  • OpenTK.NativeWindow.ProcessEvents() 方法会评估 XEventName.SelectionNotify 事件类型 - 但仅限于 freedesktop.orgICCM拖放 扩展。此实现仅专注于接收文件路径列表。这对于实现 **剪贴板文本提供者功能** 没有帮助。
  • OpenTK.NativeWindow.ProcessEvents() 方法**不会**评估 XEventName.SelectionRequest 事件类型。

基于 OpenTK 的 C# OpenGL 窗口通过 OpenTK.NativeWindow.ProcessEvents() 处理其所有事件。没有机会为 SelectionRequestSelectionNotify 注册事件处理程序,也没有机会将代码注入 ProcessEvents() 方法。由于 OpenTK.NativeWindow 是一个 internal sealed class,因此也无法从中派生。

X11GLNative 类使用 Xlib 方法 XCheckWindowEvent()XCheckTypedWindowEvent() 来从事件队列中拾取特定事件。

但是,推断 X11GLNative 的方法 - 在调用 ProcessEvents() 方法之前,通过 XCheckWindowEvent() 和/或 XCheckTypedWindowEvent() 方法从事件队列中拾取 XEventName.SelectionNotifyXEventName.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_PROPOTK_DATA_PROP,用于标识用于进程间通信的窗口属性。(X11 窗口之间的通信 - 特别是剪贴板所有者和剪贴板请求者之间的通信 - 使用窗口属性。)

OTK_TARGETS_PROP 原子负责与支持的剪贴板格式相关的通信。
OTK_DATA_PROP 原子负责剪贴板数据传输。
我经验上发现,支持的剪贴板格式的协商和剪贴板数据传输需要**两个独立的窗口属性**。
除了窗口属性之外,进程间通信还需要指示要交换的**数据类型**。这时,原子 XA_TARGETSXA_TIMESTAMPXA_UTF8_STRINGXA_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.orgICCM拖放 扩展中所定义的。

包含 ProcessMessage() 方法的 WindowBase 类源代码可在技巧开头提供的下载内容中找到。

工作原理

假设我们有一个正在运行的文本编辑器,选中一些文本并通过“**编辑**”|“**复制**”菜单或 [Ctrl]+[c] 键提供文本,该文本编辑器会调用 XSetSelectionOwner() 将自身注册为复制粘贴内容的**选择所有者**,从而使其他应用程序能够查询复制粘贴内容。

另外,假设我们基于 OpenTK 的 C# OpenGL 窗口应用程序通过“**编辑**”|“**粘贴**”菜单或 [Ctrl]+[v] 键请求剪贴板文本,它将充当**选择请求者**,并调用 XConvertSelection() 来确定最佳数据交换格式。

**选择请求者**的 XConvertSelection() 调用会触发一个 XEventName.SelectionRequest 事件,以通知**选择所有者**其需求,并询问支持的 XA_TARGETS

  • 来自**选择请求者**的每一个 XEventName.SelectionRequest 事件都指示一个窗口属性,该属性定义了一个由**选择请求者**和**选择所有者**共享的锚点,用于实现数据交换。因此,**选择所有者**被指示使用指示的窗口属性进行答复。
  • 通常,XA_CLIPBOARD 原子用于标识数据交换的窗口属性。很少使用的替代项是 XA_PRIMARYXA_SECONDARY 原子。
  • XA_TARGETS 原子由**选择请求者**使用,以请求**选择所有者**支持哪些数据格式。

**选择所有者**收到一个询问支持的 XA_TARGETSXEventName.SelectionRequest 事件,通过 XChangeProperty() 将其支持的 XA_TARGETS 公告给指示的窗口属性,并发送一个 XEventName.SelectionNotify 事件通知**选择请求者**其答复。

现在**选择请求者**将收到来自**选择所有者**的 XEventName.SelectionNotify 事件,其中包含在指示的窗口属性中公布的支持的 XA_TARGETS。**选择请求者**应评估支持的 XA_TARGETS,选择首选格式 - 标准文本交换格式是 XA_UTF8_STRING,如 freedesktop.orgICCM拖放 扩展中所定义的 - 并调用 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日:初始版本
© . All rights reserved.