在Linux上如何从剪贴板获取字符串(C++)

10

你好,我正在编写一个c++程序,需要将剪贴板中的内容获取到一个字符串变量中。我找到了很多解决方案,但它们都是为Windows编写的。有没有不使用QT库的方法?我找到了一些与X11相关的东西,但也不是非常明确。

非常感谢!


可能是 https://dev59.com/E1jUa4cB1Zd3GeqPNQIn 的重复问题。 - user3159253
嗯,我在寻找可以编写和理解的代码,而不是整个理论背后的知识。例如,关于剪贴板的Windows问题可以使用简单的Windows函数轻松处理,我听说在Linux上可能会更复杂,但我找不到任何代码示例。 顺便说一句,我已经在Windows端完成了这个(可行的)任务。 - RubenC
那么您应该更加精确地指定您想要处理的抽象级别。从技术上讲,我提供的答案给出了最低可能级别的信息。然而,在实践中,所有这些令人厌倦和繁琐的细节并不直接使用,而是通过各种包装程序/工具包(如Qt或Gtk +)间接使用。 - user3159253
顺便提一下,以下是 Gtk.Clipboard 的示例。 - user3159253
2个回答

38
X11使用灵活的多缓冲区、多格式、异步应用程序剪贴板协议。
大多数工具包都已经实现了它(例如GTK的gtk_clipboard_get(),Qt的QApplication::clipboard(),Tk的clipboard_get)。但是,如果您没有使用工具包,或者必须通过剪贴板缓冲区传递大量数据而不将其全部保存在内存中,则可以使用X11 API手动完成此操作。
理论上可能会有许多缓冲区,但您只需要知道两个:
- CLIPBOARD是通常的显式缓冲区:您可以使用编辑/复制菜单将内容复制到其中,并使用编辑/粘贴菜单将其粘贴出来。 - PRIMARY选择是一种隐式鼠标选择功能:当使用鼠标光标进行选择时,文本会进入其中,并在文本输入字段上进行中键单击时从其中粘贴。
Primary selection不需要按键,因此它对于在相邻窗口之间复制小片段非常有用。这个功能主要是针对Unix的,但我已经看到putty、trillian和一些gtk应用程序在Windows操作系统上模拟它。另外,Firefox在点击页面上的空白非交互区域时具有“粘贴并转到”功能。
为了优化这些东西,这些都是应用程序端的缓冲区:而不是每次更改时将整个剪贴板/选择推送到服务器,应用程序只需告诉服务器“我拥有它”。要获取缓冲区,您可以请求所有者提供其内容。这样,即使是大型缓冲区也不会占用任何资源,直到实际请求。
当请求缓冲区时,您会向所有者请求所需的特定格式。例如,从seamonkey浏览器中复制的图像(右键单击图像并按“复制图像”)可以表示为不同的格式。如果您将其粘贴到终端中,它将显示为图像URL。如果您将其粘贴到libreoffice writer中,则会变成从该URL加载的图片。如果在gimp中粘贴,它将是图像本身。这起作用是因为seamonkey很聪明,并为每个应用程序提供它要求的格式:对于终端而言是文本字符串,对于libreoffice是HTML,对于gimp是图像数据。要请求文本格式,您需要请求UTF8_STRING格式,并退回到STRING
由于您要求另一个应用程序准备缓冲区,这可能需要一些时间,因此请求是异步的:所有者准备缓冲区,在指定位置保存它(窗口属性用作临时存储),并在完成后通过SelectionNotify事件通知您。
因此,要获取缓冲区:
  • 选择缓冲区名称 (CLIPBOARD, PRIMARY),格式 (UTF8_STRING, STRING)和一个窗口属性来存储结果
  • 调用XConvertSelection()请求缓冲区
  • 等待SelectionNotify事件
  • 从窗口属性中读取缓冲区内容

天真的实现

// gcc -o xclipget xclipget.c -lX11
#include <stdio.h>
#include <limits.h>
#include <X11/Xlib.h>

Bool PrintSelection(Display *display, Window window, const char *bufname, const char *fmtname)
{
  char *result;
  unsigned long ressize, restail;
  int resbits;
  Atom bufid = XInternAtom(display, bufname, False),
       fmtid = XInternAtom(display, fmtname, False),
       propid = XInternAtom(display, "XSEL_DATA", False),
       incrid = XInternAtom(display, "INCR", False);
  XEvent event;

  XConvertSelection(display, bufid, fmtid, propid, window, CurrentTime);
  do {
    XNextEvent(display, &event);
  } while (event.type != SelectionNotify || event.xselection.selection != bufid);

  if (event.xselection.property)
  {
    XGetWindowProperty(display, window, propid, 0, LONG_MAX/4, False, AnyPropertyType,
      &fmtid, &resbits, &ressize, &restail, (unsigned char**)&result);

    if (fmtid == incrid)
      printf("Buffer is too large and INCR reading is not implemented yet.\n");
    else
      printf("%.*s", (int)ressize, result);

    XFree(result);
    return True;
  }
  else // request failed, e.g. owner can't convert to the target format
    return False;
}

int main()
{
  Display *display = XOpenDisplay(NULL);
  unsigned long color = BlackPixel(display, DefaultScreen(display));
  Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 0,0, 1,1, 0, color, color);
  Bool result = PrintSelection(display, window, "CLIPBOARD", "UTF8_STRING") ||
                PrintSelection(display, window, "CLIPBOARD", "STRING");
  XDestroyWindow(display, window);
  XCloseDisplay(display);
  return !result;
}

这在许多简单情况下都可以使用。这里缺少的一件事是支持对大缓冲区进行增量读取。让我们添加它!
大缓冲区
某些应用程序可能希望复制/粘贴100GB的文本日志。而X11允许这样做!但数据必须被递增地传递,分成块。
如果请求的缓冲区太大,所有者将设置格式为INCR的属性,而不是将其存储到窗口属性中。如果您删除它,所有者会认为您已经阅读了它,并在同一属性中放置下一个块。这将继续进行,直到读取并删除了最后一个块。最后,所有者设置大小为0的属性以标记数据的结尾。
因此,要读取大缓冲区,您需要删除INCR属性并等待属性再次出现(PropertyNotify事件,状态== PropertyNewValue),然后读取并删除它,等待它再次出现,依此类推,直到它以零大小出现。
// gcc -o xclipget xclipget.c -lX11
#include <stdio.h>
#include <limits.h>
#include <X11/Xlib.h>

Bool PrintSelection(Display *display, Window window, const char *bufname, const char *fmtname)
{
  char *result;
  unsigned long ressize, restail;
  int resbits;
  Atom bufid = XInternAtom(display, bufname, False),
       fmtid = XInternAtom(display, fmtname, False),
       propid = XInternAtom(display, "XSEL_DATA", False),
       incrid = XInternAtom(display, "INCR", False);
  XEvent event;

  XSelectInput (display, window, PropertyChangeMask);
  XConvertSelection(display, bufid, fmtid, propid, window, CurrentTime);
  do {
    XNextEvent(display, &event);
  } while (event.type != SelectionNotify || event.xselection.selection != bufid);

  if (event.xselection.property)
  {
    XGetWindowProperty(display, window, propid, 0, LONG_MAX/4, True, AnyPropertyType,
      &fmtid, &resbits, &ressize, &restail, (unsigned char**)&result);
    if (fmtid != incrid)
      printf("%.*s", (int)ressize, result);
    XFree(result);

    if (fmtid == incrid)
      do {
        do {
          XNextEvent(display, &event);
        } while (event.type != PropertyNotify || event.xproperty.atom != propid || event.xproperty.state != PropertyNewValue);

        XGetWindowProperty(display, window, propid, 0, LONG_MAX/4, True, AnyPropertyType,
          &fmtid, &resbits, &ressize, &restail, (unsigned char**)&result);
        printf("%.*s", (int)ressize, result);
        XFree(result);
      } while (ressize > 0);

    return True;
  }
  else // request failed, e.g. owner can't convert to the target format
    return False;
}

int main()
{
  Display *display = XOpenDisplay(NULL);
  unsigned long color = BlackPixel(display, DefaultScreen(display));
  Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 0,0, 1,1, 0, color, color);
  Bool result = PrintSelection(display, window, "CLIPBOARD", "UTF8_STRING") ||
                PrintSelection(display, window, "CLIPBOARD", "STRING");
  XDestroyWindow(display, window);
  XCloseDisplay(display);
  return !result;
}

例如,xsel 工具对于大于 4000 的缓冲区使用 INCR 传输。根据 ICCCM,选择合理的大小限制取决于应用程序。
相同的代码适用于 PRIMARY 选择。将 "CLIPBOARD" 替换为 "PRIMARY",以打印 PRIMARY 选择内容。

参考资料


10
这个答案太棒了。X11大师如此罕见。你是谁? - étale-cohomology
1
从这里开始,我从未感谢过你的努力!谢谢! - RubenC
是的,这是对这些信息漫长搜索的终结,谢谢。 - Scott Franco

2
你是否尝试过寻找一个带有实现的程序而不是仅仅代码?我已经为您找到了很多使用直接 X11 调用的实现。我认为最有价值的是这个,但您也可以阅读这个。只需找到任何程序并查找源代码。尝试在维基百科上查找哪些应用程序使用 x11 剪贴板/选择系统。
以下是专门操作数据传输机制的程序:
- `xcutsel` 将数据从选择中转移到剪切缓冲区或反之。 - `xclipboard`、`glipper`(Gnome)、`parcellite`(LXDE) 和 `klipper`(KDE) 是剪贴板管理器,也许 `wmcliphist` 也是。`xcb` 显示剪贴板的内容,并允许用户对其进行操作。 - `xselection`、`xclip`、`xsel` 和 `xcopy` 是命令行程序,可将数据复制到或从 X 选择中复制。`xcopy` 具有帮助调试 X 选择问题的详细选项。`parcellite` 还具有从命令行读取和写入特定 X 选择的功能。 - `synergy` 是一种跨平台工具,允许您在运行多个操作系统的多台计算机之间共享剪贴板。 - `xfce4-clipman-plugin` 是 Xfce4 面板的“剪贴板历史记录插件”,也是剪贴板管理器。`xtranslate` 在多语言词典中查找 Xselection 中的单词。`autocutsel` 同步剪贴板和选择缓冲区。
简而言之,在理论上,X11 有两个“剪贴板”:实际上一个键盘,用于选择 - 您立即选择的文本可以通过按下中间鼠标按钮在任何地方粘贴,而实际的“键盘”则用于不同类型对象之间的主/默认剪贴板交换。
P.S. 我已经不再使用 x11 了,基于我的经验。祝您好运 :)

是的,我确实寻找了一些实现,并且正在尝试一些,但目前还没有成功。 - RubenC
这就是为什么我说“我不再使用X11了” :) Qt和一些库可能有针对已知和未修复的x11-/xlibs错误的解决方法,因此您可能需要尝试并花费一些夜晚才能使某些东西正常工作。这就是我说“享受 :)” 的原因。 - Victor Polevoy
啊哈,我明白你的意思了 :P - RubenC
如果这个答案对你有帮助,能否请标记为“答案”? :) - Victor Polevoy
我会将另一篇帖子标记为答案,因为这篇只有其他代码的链接,没有解释或内部结构。@x11user的帖子真的很好。 - sdd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接