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

如何处理剪贴板复制粘贴(底层)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2024 年 9 月 10 日

CPOL

4分钟阅读

viewsIcon

5724

一个解释如何在 X11、WinAPI 和 Cocoa 中设置和处理剪贴板复制粘贴的教程。

概述

  1. 剪贴板粘贴
  • X11(初始化原子、转换部分、获取数据)
  • Win32(打开剪贴板、获取数据、转换数据、关闭剪贴板)
  • Cocoa(设置数据类型、获取粘贴板、获取数据、转换数据)
  1. 剪贴板复制
  • X11(初始化原子、转换部分、处理请求、发送数据)
  • Win32(设置全局对象、转换数据、打开剪贴板、转换字符串、发送数据、关闭剪贴板)
  • Cocoa(创建数据类型数组、声明类型、转换字符串、发送数据)

剪贴板粘贴

X11

要处理剪贴板,您必须通过 XInternAtom 创建一些原子。 X 原子 用于通过 X11 请求或发送特定数据或属性。

您需要三个原子,

  1. UTF8_STRING:UTF-8 字符串的原子。
  2. CLIPBOARD:用于获取剪贴板数据的原子。
  3. XSEL_DATA:用于获取选择数据的原子。
const Atom UTF8_STRING = XInternAtom(display, "UTF8_STRING", True);
const Atom CLIPBOARD = XInternAtom(display, "CLIPBOARD", 0);
const Atom XSEL_DATA = XInternAtom(display, "XSEL_DATA", 0);

现在,要获取剪贴板数据,您必须使用 XConvertSelection 请求将剪贴板部分转换为 UTF8。

使用 XSync 将请求发送到服务器。

XConvertSelection(display, CLIPBOARD, UTF8_STRING, XSEL_DATA, window, CurrentTime);
XSync(display, 0);

选择将被转换并作为 XSelectionNotify 事件发送回客户端。您可以使用 XNextEvent 获取下一个事件,它应该是 SelectionNotify 事件。

XEvent event;
XNextEvent(display, &event);

检查事件是否为 SelectionNotify 事件,并使用 .selection 确保类型是 CLIPBOARD。此外,请确保 .property 不为 0 且可以检索。

if (event.type == SelectionNotify && event.xselection.selection == CLIPBOARD && event.xselection.property != 0) {

您可以通过使用选择属性的 XGetWindowProperty 获取转换后的数据。

    int format;
    unsigned long N, size;
    char* data, * s = NULL;
    Atom target;

    XGetWindowProperty(event.xselection.display, event.xselection.requestor,
	    event.xselection.property, 0L, (~0L), 0, AnyPropertyType, &target,
	    &format, &size, &N, (unsigned char**) &data);

通过检查 target 确保数据格式正确

    if (target == UTF8_STRING || target == XA_STRING) {

数据存储在 data 中,使用完后,请使用 XFree 释放它。

您还可以通过 XDeleteProperty 删除属性。

        XFree(data);
    }

    XDeleteProperty(event.xselection.display, event.xselection.requestor, event.xselection.property);
}

winapi

首先,打开剪贴板 OpenClipboard

if (OpenClipboard(NULL) == 0)
	return 0;

通过 GetClipboardData 以 utf16 字符串获取剪贴板数据

如果数据为 NULL,您应该使用 CloseClipboard 关闭剪贴板

HANDLE hData = GetClipboardData(CF_UNICODETEXT);
if (hData == NULL) {
	CloseClipboard();
	return 0;
}

接下来,您需要将 utf16 数据转换回 utf8。

首先通过 GlobalLock 锁定 utf8 数据的内存。

wchar_t* wstr = (wchar_t*) GlobalLock(hData);

使用 setlocale 确保数据格式为 utf8。

使用 wcstombs 获取 UTF-8 版本的长度。

setlocale(LC_ALL, "en_US.UTF-8");

size_t textLen = wcstombs(NULL, wstr, 0);

如果长度有效,则使用 wcstombs 转换数据。

if (textLen) {
	char* text = (char*) malloc((textLen * sizeof(char)) + 1);

	wcstombs(text, wstr, (textLen) + 1);
	text[textLen] = '\0';

    free(text);
}

请务必使用 GlobalUnlock 释放剩余的全局数据,并使用 CloseClipboard 关闭剪贴板。

GlobalUnlock(hData);
CloseClipboard();

cocoa

Cocoa 使用 NSPasteboardTypeString 请求字符串数据。如果您不使用 Objective-C,则必须自行定义此项。

NSPasteboardType const NSPasteboardTypeString = "public.utf8-plain-text";

尽管这是 C 字符串,Cocoa 使用 NSStrings,但您可以通过 stringWithUTF8String 将 C 字符串转换为 NSString。

NSString* dataType = objc_msgSend_class_char(objc_getClass("NSString"), sel_registerName("stringWithUTF8String:"), (char*)NSPasteboardTypeString);

现在我们将使用 generalPasteboard 获取默认的粘贴板对象。

NSPasteboard* pasteboard = objc_msgSend_id((id)objc_getClass("NSPasteboard"), sel_registerName("generalPasteboard")); 

然后您可以使用 stringForType 获取粘贴板的字符串数据,其中包含 dataType

但是,它会给您一个 NSString,可以使用 UTF8String 进行转换。

NSString* clip = ((id(*)(id, SEL, const char*))objc_msgSend)(pasteboard, sel_registerName("stringForType:"), dataType);
const char* str = ((const char* (*)(id, SEL)) objc_msgSend) (clip, sel_registerName("UTF8String"));

剪贴板复制

X11

要复制到剪贴板,您还需要一些原子。

  1. SAVE_TARGETS:请求转换为某个部分(用于复制)。
  2. TARGETS:处理一个请求的目标
  3. MULTIPLE:当有多个请求目标时
  4. ATOM_PAIR:获取支持的数据类型。
  5. CLIPBOARD_MANAGER:访问剪贴板管理器中的数据。
const Atom SAVE_TARGETS = XInternAtom((Display*) display, "SAVE_TARGETS", False);
const Atom TARGETS = XInternAtom((Display*) display, "TARGETS", False);
const Atom MULTIPLE = XInternAtom((Display*) display, "MULTIPLE", False);
const Atom ATOM_PAIR = XInternAtom((Display*) display, "ATOM_PAIR", False);
const Atom CLIPBOARD_MANAGER = XInternAtom((Display*) display, "CLIPBOARD_MANAGER", False);

我们可以请求一个剪贴板部分。首先,通过 XSetSelectionOwner 将该部分的所有者设置为客户端窗口。接下来使用 XConvertSelection 请求转换后的部分。

XSetSelectionOwner((Display*) display, CLIPBOARD, (Window) window, CurrentTime);

XConvertSelection((Display*) display, CLIPBOARD_MANAGER, SAVE_TARGETS, None, (Window) window, CurrentTime);

其余代码将存在于事件循环中。如果愿意,您可以从主事件循环创建一个外部事件循环,或将其添加到主事件循环中。

我们将处理 SelectionRequest,以便将剪贴板选择更新为字符串数据。

if (event.type == SelectionRequest) {
    const XSelectionRequestEvent* request = &event.xselectionrequest;

在 SelectionNotify 事件结束时,将向请求者发送响应。结构应该在这里创建并根据请求数据进行修改。

	XEvent reply = { SelectionNotify };
	reply.xselection.property = 0;

我们将处理的第一个目标是 TARGETS,当请求者想知道支持哪些目标时。

	if (request->target == TARGETS) {

我将创建一个支持目标的数组

        const Atom targets[] = { TARGETS,
								MULTIPLE,
								UTF8_STRING,
								XA_STRING };

此数组可以使用 XChangeProperty 传递。

我还会更改选择属性,以便请求者知道我们更改了哪些属性。

		XChangeProperty(display,
			request->requestor,
			request->property,
			4,
			32,
			PropModeReplace,
			(unsigned char*) targets,
			sizeof(targets) / sizeof(targets[0]));

		reply.xselection.property = request->property;
	}

接下来,我将处理 MULTIPLE 目标。

	if (request->target == MULTIPLE) {

我们将首先通过 XGetWindowProperty 获取支持的目标

		Atom* targets = NULL;

		Atom actualType = 0;
		int actualFormat = 0;
		unsigned long count = 0, bytesAfter = 0;

		XGetWindowProperty(display, request->requestor, request->property, 0, LONG_MAX, False, ATOM_PAIR, &actualType, &actualFormat, &count, &bytesAfter, (unsigned char **) &targets);

现在我们将遍历支持的目标。如果支持的目标与我们支持的目标之一匹配,我们可以使用 XChangeProperty 传递数据。

如果目标未使用,则第二个参数应设置为 None,将其标记为未使用。

		unsigned long i;
		for (i = 0; i < count; i += 2) {
			if (targets[i] == UTF8_STRING || targets[i] == XA_STRING) {
				XChangeProperty((Display*) display,
					request->requestor,
					targets[i + 1],
					targets[i],
					8,
					PropModeReplace,
					(unsigned char*) text,
					sizeof(text));
				XFlush(display);
			} else {
				targets[i + 1] = None;
			}
		}

您可以使用 XChangeProperty 将支持目标的最终数组传递给请求者。这告诉请求者要从它发送的原始列表中期望哪些目标。

当调用 XFlush 时,消息将尽快发送出去。

您可以使用 XFree 释放目标数组的副本。

		XChangeProperty((Display*) display,
			request->requestor,
			request->property,
			ATOM_PAIR,
			32,
			PropModeReplace,
			(unsigned char*) targets,
			count);

		XFlush(display);
		XFree(targets);

		reply.xselection.property = request->property;
	}

对于事件的最后一步,通过 XSendEvent 将选择发送回请求者。

然后使用 XFlush 刷新队列。

	reply.xselection.display = request->display;
	reply.xselection.requestor = request->requestor;
	reply.xselection.selection = request->selection;
	reply.xselection.target = request->target;
	reply.xselection.time = request->time;

	XSendEvent((Display*) display, request->requestor, False, 0, &reply);
	XFlush(display);
}

winapi

首先使用 GlobalAlloc 为您的数据和 utf-8 缓冲区分配全局内存

HANDLE object = GlobalAlloc(GMEM_MOVEABLE, (1 + textLen) * sizeof(WCHAR));
WCHAR*  buffer = (WCHAR*) GlobalLock(object);

接下来,您可以使用 MultiByteToWideChar 将字符串转换为宽字符串。

MultiByteToWideChar(CP_UTF8, 0, text, -1, buffer, textLen);

现在解锁全局对象并打开剪贴板

GlobalUnlock(object);
OpenClipboard(NULL);

要更新剪贴板数据,首先通过 EmptyClipboard 清除剪贴板上的内容,然后可以使用 SetClipboardData 将数据设置为 utf8 对象。

最后,使用 CloseClipboard 关闭剪贴板。

EmptyClipboard();
SetClipboardData(CF_UNICODETEXT, object);

CloseClipboard();

cocoa

首先创建一个您想要放入剪贴板的数据类型数组,并使用 initWithObjects 将其转换为 NSArray。

NSPasteboardType ntypes[] = { dataType };

NSArray* array = ((id (*)(id, SEL, void*, NSUInteger))objc_msgSend)
                    (NSAlloc(objc_getClass("NSArray")), sel_registerName("initWithObjects:count:"), ntypes, 1);

使用 declareTypes 将数组声明为支持的数据类型。

您还可以使用 NSRelease 释放 NSArray。

((NSInteger(*)(id, SEL, id, void*))objc_msgSend) (pasteboard, sel_registerName("declareTypes:owner:"), array, NULL);
NSRelease(array);

您可以将要复制的字符串通过 stringWithUTF8String 转换为 NSString,然后使用 setString 将剪贴板字符串设置为该 NSString。

NSString* nsstr = objc_msgSend_class_char(objc_getClass("NSString"), sel_registerName("stringWithUTF8String:"), text);

((bool (*)(id, SEL, id, NSPasteboardType))objc_msgSend) (pasteboard, sel_registerName("setString:forType:"), nsstr, dataType);	

完整示例

X11

// compile with:
// gcc x11.c -lX11

#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

#include <X11/Xatom.h>

int main(void) {
    Display* display = XOpenDisplay(NULL);
 
    Window window = XCreateSimpleWindow(display, RootWindow(display, DefaultScreen(display)), 10, 10, 200, 200, 1,
                                 BlackPixel(display, DefaultScreen(display)), WhitePixel(display, DefaultScreen(display)));
 
    XSelectInput(display, window, ExposureMask | KeyPressMask); 

	const Atom UTF8_STRING = XInternAtom(display, "UTF8_STRING", True);
	const Atom CLIPBOARD = XInternAtom(display, "CLIPBOARD", 0);
	const Atom XSEL_DATA = XInternAtom(display, "XSEL_DATA", 0);

	const Atom SAVE_TARGETS = XInternAtom((Display*) display, "SAVE_TARGETS", False);
	const Atom TARGETS = XInternAtom((Display*) display, "TARGETS", False);
	const Atom MULTIPLE = XInternAtom((Display*) display, "MULTIPLE", False);
	const Atom ATOM_PAIR = XInternAtom((Display*) display, "ATOM_PAIR", False);
	const Atom CLIPBOARD_MANAGER = XInternAtom((Display*) display, "CLIPBOARD_MANAGER", False);

	// input
	XConvertSelection(display, CLIPBOARD, UTF8_STRING, XSEL_DATA, window, CurrentTime);
	XSync(display, 0);

	XEvent event;
	XNextEvent(display, &event);

	if (event.type == SelectionNotify && event.xselection.selection == CLIPBOARD && event.xselection.property != 0) {

		int format;
		unsigned long N, size;
		char* data, * s = NULL;
		Atom target;

		XGetWindowProperty(event.xselection.display, event.xselection.requestor,
			event.xselection.property, 0L, (~0L), 0, AnyPropertyType, &target,
			&format, &size, &N, (unsigned char**) &data);

		if (target == UTF8_STRING || target == XA_STRING) {
			printf("paste: %s\n", data);
			XFree(data);
		}

		XDeleteProperty(event.xselection.display, event.xselection.requestor, event.xselection.property);
	}

	// output
	char text[] = "new string\0";

	XSetSelectionOwner((Display*) display, CLIPBOARD, (Window) window, CurrentTime);

	XConvertSelection((Display*) display, CLIPBOARD_MANAGER, SAVE_TARGETS, None, (Window) window, CurrentTime);
		
	Bool running = True;
	while (running) {
		XNextEvent(display, &event);
		if (event.type == SelectionRequest) {
			const XSelectionRequestEvent* request = &event.xselectionrequest;

			XEvent reply = { SelectionNotify };
			reply.xselection.property = 0;

			if (request->target == TARGETS) {
				const Atom targets[] = { TARGETS,
										MULTIPLE,
										UTF8_STRING,
										XA_STRING };

				XChangeProperty(display,
					request->requestor,
					request->property,
					4,
					32,
					PropModeReplace,
					(unsigned char*) targets,
					sizeof(targets) / sizeof(targets[0]));

				reply.xselection.property = request->property;
			}

			if (request->target == MULTIPLE) {	
				Atom* targets = NULL;

				Atom actualType = 0;
				int actualFormat = 0;
				unsigned long count = 0, bytesAfter = 0;

				XGetWindowProperty(display, request->requestor, request->property, 0, LONG_MAX, False, ATOM_PAIR, &actualType, &actualFormat, &count, &bytesAfter, (unsigned char **) &targets);

				unsigned long i;
				for (i = 0; i < count; i += 2) {
					Bool found = False; 

					if (targets[i] == UTF8_STRING || targets[i] == XA_STRING) {
						XChangeProperty((Display*) display,
							request->requestor,
							targets[i + 1],
							targets[i],
							8,
							PropModeReplace,
							(unsigned char*) text,
							sizeof(text));
						XFlush(display);
						running = False;
					} else {
						targets[i + 1] = None;
					}
				}

				XChangeProperty((Display*) display,
					request->requestor,
					request->property,
					ATOM_PAIR,
					32,
					PropModeReplace,
					(unsigned char*) targets,
					count);

				XFlush(display);
				XFree(targets);

				reply.xselection.property = request->property;
			}

			reply.xselection.display = request->display;
			reply.xselection.requestor = request->requestor;
			reply.xselection.selection = request->selection;
			reply.xselection.target = request->target;
			reply.xselection.time = request->time;

			XSendEvent((Display*) display, request->requestor, False, 0, &reply);
			XFlush(display);
		}
	}

    XCloseDisplay(display);
 }

Winapi

// compile with:
// gcc win32.c

#include <windows.h>
#include <locale.h>

#include <stdio.h>

int main() {
	// output
	if (OpenClipboard(NULL) == 0)
		return 0;

	HANDLE hData = GetClipboardData(CF_UNICODETEXT);
	if (hData == NULL) {
		CloseClipboard();
		return 0;
	}

	wchar_t* wstr = (wchar_t*) GlobalLock(hData);

	setlocale(LC_ALL, "en_US.UTF-8");

	size_t textLen = wcstombs(NULL, wstr, 0);

	if (textLen) {
		char* text = (char*) malloc((textLen * sizeof(char)) + 1);

		wcstombs(text, wstr, (textLen) + 1);
		text[textLen] = '\0';
		
		printf("paste: %s\n", text);
		free(text);
	}

	GlobalUnlock(hData);
	CloseClipboard();


	// input
		
	char text[] = "new text\0";

	HANDLE object = GlobalAlloc(GMEM_MOVEABLE, (sizeof(text) / sizeof(char))  * sizeof(WCHAR));

	WCHAR* buffer = (WCHAR*) GlobalLock(object);
	if (!buffer) {
		GlobalFree(object);
		return 0;
	}

	MultiByteToWideChar(CP_UTF8, 0, text, -1, buffer, (sizeof(text) / sizeof(char)));
	
	GlobalUnlock(object);
	if (OpenClipboard(NULL) == 0) {
		GlobalFree(object);
		return 0;
	}

	EmptyClipboard();
	SetClipboardData(CF_UNICODETEXT, object);
	CloseClipboard();
}

Cocoa

// compile with:
// gcc cocoa.c -framework Foundation -framework AppKit  


#include <objc/runtime.h>
#include <objc/message.h>
#include <CoreVideo/CVDisplayLink.h>
#include <ApplicationServices/ApplicationServices.h>

#ifdef __arm64__
/* ARM just uses objc_msgSend */
#define abi_objc_msgSend_stret objc_msgSend
#define abi_objc_msgSend_fpret objc_msgSend
#else /* __i386__ */
/* x86 just uses abi_objc_msgSend_fpret and (NSColor *)objc_msgSend_id respectively */
#define abi_objc_msgSend_stret objc_msgSend_stret
#define abi_objc_msgSend_fpret objc_msgSend_fpret
#endif

typedef void NSPasteboard;
typedef void NSString;
typedef void NSArray;
typedef void NSApplication;

typedef const char* NSPasteboardType;

typedef unsigned long NSUInteger;
typedef long NSInteger;

#define NSAlloc(nsclass) objc_msgSend_id((id)nsclass, sel_registerName("alloc"))

#define objc_msgSend_bool			((BOOL (*)(id, SEL))objc_msgSend)
#define objc_msgSend_void			((void (*)(id, SEL))objc_msgSend)
#define objc_msgSend_void_id		((void (*)(id, SEL, id))objc_msgSend)
#define objc_msgSend_uint			((NSUInteger (*)(id, SEL))objc_msgSend)
#define objc_msgSend_void_bool		((void (*)(id, SEL, BOOL))objc_msgSend)
#define objc_msgSend_void_int		((void (*)(id, SEL, int))objc_msgSend)
#define objc_msgSend_bool_void		((BOOL (*)(id, SEL))objc_msgSend)
#define objc_msgSend_void_SEL		((void (*)(id, SEL, SEL))objc_msgSend)
#define objc_msgSend_id				((id (*)(id, SEL))objc_msgSend)
#define objc_msgSend_id_id				((id (*)(id, SEL, id))objc_msgSend)
#define objc_msgSend_id_bool			((BOOL (*)(id, SEL, id))objc_msgSend)

#define objc_msgSend_class_char ((id (*)(Class, SEL, char*))objc_msgSend)

void NSRelease(id obj) {
	objc_msgSend_void(obj, sel_registerName("release"));
}

int main() {
	/* input */
	NSPasteboardType const NSPasteboardTypeString = "public.utf8-plain-text";

	NSString* dataType = objc_msgSend_class_char(objc_getClass("NSString"), sel_registerName("stringWithUTF8String:"), (char*)NSPasteboardTypeString);

	NSPasteboard* pasteboard = objc_msgSend_id((id)objc_getClass("NSPasteboard"), sel_registerName("generalPasteboard")); 
	
	NSString* clip = ((id(*)(id, SEL, const char*))objc_msgSend)(pasteboard, sel_registerName("stringForType:"), dataType);
	
	const char* str = ((const char* (*)(id, SEL)) objc_msgSend) (clip, sel_registerName("UTF8String"));

	printf("paste: %s\n", str);
	
	char text[] = "new string\0";

	NSPasteboardType ntypes[] = { dataType };

	NSArray* array = ((id (*)(id, SEL, void*, NSUInteger))objc_msgSend)
						(NSAlloc(objc_getClass("NSArray")), sel_registerName("initWithObjects:count:"), ntypes, 1);

	((NSInteger(*)(id, SEL, id, void*))objc_msgSend) (pasteboard, sel_registerName("declareTypes:owner:"), array, NULL);
	NSRelease(array);
	
	NSString* nsstr = objc_msgSend_class_char(objc_getClass("NSString"), sel_registerName("stringWithUTF8String:"), text);

	((bool (*)(id, SEL, id, NSPasteboardType))objc_msgSend) (pasteboard, sel_registerName("setString:forType:"), nsstr, dataType);	
}
© . All rights reserved.