X11 使用灵活的多缓冲多格式异步应用端剪贴板协议。
大多数工具包都实现了它(GTK 的gtk_clipboard_get()、Qt 的QApplication::clipboard()、Tk 的clipboard_get)。但是您可以使用 X11 API 手动执行此操作,例如,如果您不使用工具包,或者如果您必须通过剪贴板缓冲区传递大量数据而不同时将其全部保存在内存中。
理论
缓冲区可能有很多,但你只需要知道两个:
-
CLIPBOARD 是通常的显式缓冲区:您可以使用 Edit/Copy 菜单将内容复制到那里,然后使用 Edit/Paste 菜单将其粘贴。
-
PRIMARY selection 是一种隐式的鼠标选择功能:当用鼠标光标选择文本时,文本会进入其中,并在文本输入字段中单击鼠标中键时从其中粘贴。
主选择不需要按键,因此对于在相邻的窗口之间复制小片段很有用。此功能主要是特定于 unix 的,但我已经看到 putty、trillian 和一些 gtk 应用程序在 Windows 操作系统上模拟它。当中键单击页面的空白非交互空间时,Firefox 还具有“粘贴 & Go”功能。
为了优化那些是应用程序端缓冲区的东西:应用程序不会在每次更改时将整个剪贴板/选择推送到服务器,而是告诉服务器“我拥有它”。要获得缓冲区,您要求所有者给您其内容。这样,即使是大缓冲区,在实际请求之前也不会占用任何资源。
请求缓冲区时,您需要向所有者询问您需要的特定格式。例如,从 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;
}
这适用于许多简单的情况。这里缺少的一件事是对大缓冲区的增量读取的支持。让我们添加它!
大缓冲区
某些应用可能需要复制/粘贴 100 GB 的文本日志。 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 选择内容。
参考文献