Pull to refresh

Работа с буфером обмена

Reading time 4 min
Views 17K
Прочитав заголовок, Вы, наверное, очень удивились.
Ведь казалось бы, все предельно просто — есть объект Clipboard, есть его статические методы (вроде SetText/SetData и GetText/GetData), чего еще для счастья нужно?

Однако, на практике все просто лишь до тех пор, пока Вы копируете или вставляете только базовые объекты, вроде текста или bitmap-картинки. Что же случается, когда нужно оперировать более сложной структурой?

Лично я недавно столкнулся с необходимостью копировать «гиперссылки», которые потом должны легко вставляться в Word/Outlook/любую другую программу. Причем, не полагаясь на то, что программа-получатель сама определит во вставленном тексте ссылку и не преобразует в нужный формат. Поэтому и рассмотрим работу на примере гиперссылки (алгоритм действий для любого другого формата будет аналогичным).

Итак, с чего же начать?

Для начала, необходимо выяснить, в каком формате должны быть те или иные данные. Выяснить это можно разными способами, но самый наглядный, пожалуй, — это небольшая утилита ClipSpy. Достаточно просто скопировать откуда угодно нужный объект и определить, какие форматы при этом создаются в буфере обмена и что они в себе содержат.

Как показывает эксперимент, для гиперссылки обычно создаются форматы TEXT, UNICODETEXT и HTML. Первый и второй содержат в себе текстовое представление гиперссылки (то, что будет вставлено, например, в блокнот). Формат HTML же представляет для нас наибольший интерес — в нем содержится html-фрагмент, который и будет вставлен в целевую программу как гиперссылка. Выглядит он примерно так:
Version:1.0
StartHTML:XXXXXXXX
EndHTML:YYYYYYYY
StartFragment:ZZZZZZZZ
EndFragment:TTTTTTTT
<html ....
<a href="http://some.site.com/target">Some target</a>
... </html>
где XXXXXXXX — смещение начала html-содержимого (фактически, длина заголовка),
YYYYYYYY — соответственно, смещение конца html-содержимого (фактически, длина всего контента),
ZZZZZZZZ и TTTTTTTT — начало и конец фрагмента с гиперссылкой.

Что ж, с форматом определились. Теперь сформировать нужный контент — дело техники. А мы пока что рассмотрим, как отправить нужное содержимое (в нескольких форматах) в буфер обмена.

Гугл (любимый всеми советчик) дает, как правило, следующее решение:
void CopyLink(Uri target, string title)
{
	var htmlContent = MakeLink(target, title);
	var data = new DataObject();
	data.SetData(DataFormats.Text, true, target.ToString());
	data.SetData(DataFormats.Unicode, true, html_content);
	data.SetData(DataFormats.Html, true, formatted_buffer);
	Clipboard.SetDataObject(data, true);
}

И все бы было хорошо, если бы не одно «но» — такое решение совершенно не позволяет управлять кодировкой при задании html-содержимого. Поэтому ссылки, например, с русским текстом в названии вставляются совершенно неправильно, что приводит к разным интересным эффектам, вплоть до «вылетания» программы-получателя.

На этой ноте идея написания чисто управляемого кода накрылась медным тазом, посему пришлось обращаться к WinAPI. В итоге вышло не так красиво, зато весьма работоспособно:
[DllImport("user32.dll")]
private static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);
[DllImport("user32.dll")]
private static extern bool OpenClipboard(IntPtr hWndNewOwner);
[DllImport("user32.dll")]
private static extern bool EmptyClipboard();
[DllImport("user32.dll")]
private static extern bool CloseClipboard();
[DllImport("user32.dll", SetLastError = true)]
private static extern uint RegisterClipboardFormat(string lpszFormat);

void CopyLink(Uri target, string title)
{
	var htmlContent = MakeLink(target, title, Encoding.UTF8);

	if (!OpenClipboard(IntPtr.Zero))
		throw new Exception("Failed to open clipboard");
	EmptyClipboard();
        
	var pText = IntPtr.Zero;
	var pHtml = IntPtr.Zero;
	try
	{
		pText = Marshal.StringToHGlobalAnsi(target.ToString());
		SetClipboardData(1 /* CF_TEXT */, pText); // Для TEXT и UNICODETEXT

		var bytes = Encoding.UTF8.GetBytes(htmlContent);
		pHtml = Marshal.AllocHGlobal(bytes.Length);
		Marshal.Copy(bytes, 0, pHtml, bytes.Length);
		SetClipboardData(RegisterClipboardFormat(DataFormats.Html), pHtml);
	}
	finally 
	{
		CloseClipboard();
		if (pText != IntPtr.Zero)
			Marshal.FreeHGlobal(pText);
		if (pHtml != IntPtr.Zero)
			Marshal.FreeHGlobal(pHtml);
	}
}

Ну, и для полноты картины исходный текст MakeLink:
string MakeLink(Uri target, string title, Encoding encoding)
{
	const int numberLengthWithCr = 11;
	var htmlIntro = "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=" 
		+ encoding.WebName + "\" />\n</head>\n<body>\n<!--StartFragment-->";
	var htmlOutro = "<!--EndFragment-->\n</body>\n</html>";
	var htmlLink = string.Format("<a href=\"{0}\">{1}</a>", target, title);

	var startHtmlIndex = 57 + 4 * numberLengthWithCr;
	var startFragmentIndex = startHtmlIndex + encoding.GetByteCount(htmlIntro);
	var endFragmentIndex = startFragmentIndex + encoding.GetByteCount(htmlLink);
	var endHtmlIndex = endFragmentIndex + encoding.GetByteCount(htmlOutro);

	var buff = new StringBuilder();
	buff.AppendFormat("Version:1.0\n");
	buff.AppendFormat("StartHTML:{0:0000000000}\n", startHtmlIndex);
	buff.AppendFormat("EndHTML:{0:0000000000}\n", endHtmlIndex);
	buff.AppendFormat("StartFragment:{0:0000000000}\n", startFragmentIndex);
	buff.AppendFormat("EndFragment:{0:0000000000}\n", endFragmentIndex);
	buff.Append(htmlIntro).Append(htmlLink).Append(htmlOutro);
	return buff.ToString();
}


P.S. Кстати, вопрос: есть ли возможность писать код нормально, без отключения автоформатирования и ручной расстановки разрывов строк? А то с автоформатированием в
 интервал между строками просто убийственный.
</habracut>
Tags:
Hubs:
+18
Comments 15
Comments Comments 15

Articles