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

    Прочитав заголовок, Вы, наверное, очень удивились.
    Ведь казалось бы, все предельно просто — есть объект 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>
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 15
    • –4
      Форматирование — топовая беда данного блога.
      http://superhabr.ru/ — ждем =)
    • +2
      Какой-то ужас в
      MakeLink()
      - магические числа, дикая смесь
      Encoding
      и
      StringBuffer
      - если считаете байты, то и выводие результат в байтах, а не в абстрактной строке, привязанной к определенной кодировке, непереносимые переводы строк - правильнее
      Environment.NewLine
      ...
      • 0
        Сори, минус вместо плюса нажал)
        • 0
          На самом деле MakeLink() не должно быть отдельной функцией вообще, т.к. больше нигде не нужна в принципе.
          Но хотелось для наглядности все-таки разделить формирование буфера и запихивание его в буфер.
          • 0
            А касательно Environment.NewLine — мне кажется, как-то неуместно рассуждать о том, что правильнее, когда в описании четко сказано:
            End of lines in the clipboard format header could be CR or CR/LF or LF.
            • 0
              Это, скорее, из разряда "читабельности". У меня в VS2008 не видны переносы в Watch, потому как переносы не те.
          • 0
            Если вы пишите на .NET - старайтесь избегать импорта DLL функций и обходиться внутренними.
            Я считаю, что в данном случае импорт функций лишний и некрасивый.
            Вот интересная статья по этой теме
            • 0
              Насчет кодировки - если погуглите - найдете ответ.
              • 0
                Угу. Вот что по этому вопросу пишет любезный Джеффри Рихтер:

                "Microsoft .NET Framework позволяет разработчикам в гораздо большей степени задействовать готовые технологии, чем предыдущие платформы разработки от Microsoft. В частности, .NET Framework предоставляет реальные возможности повторного использования кода, управления ресурсами, многоязыковой разработки, защиты, развертывания и администрирования. При проектировании этой новой платформы Microsoft учла недостатки существующих Windows-платформ. Вот далеко не полный список преимуществ CLR и FCL.

                • Единая программная модель В отличие от существующего подхода, когда одни функции ОС доступны через процедуры динамически подключаемых библиотек (DLL), а другие — через СОМ-объекты, весь прикладной сервис представлен общей объектно-ориентированной программной моделью.

                • Упрощенная модель программирования CLR избавляет от работы с разными потаенными структурами, как это было с Win32 и СОМ. Так, разработчику не нужно разбираться с реестром, глобально-уникальными идентификаторами (QUID), lUnknown, AddRef, Release, HRESUIT и т. д. CLR не просто позволяет разработчику абстрагироваться от этих концепций — их просто нет в CLR в каком бы то ни было виде. Конечно, если вы хотите написать приложение .NET Framework, которое взаимодействует с существующим не-.NET кодом, вам нужно разбираться во всех этих концепциях.

                • Отсутствие проблем с версиями Все Windows-разработчики знают о проблемах совместимости версий, известных под названием «ад DLL». Этот «ад» возникает, когда компоненты, устанавливаемые для нового приложения, заменяют компоненты старого приложения, и в итоге последнее начинает вести себя странно или перестает работать. Архитектура .NET Framework позволяет изолировать прикладные компоненты, так что приложение всегда загружает компоненты, с которыми оно строилось и тестировалось. Если приложение работает после начальной установки, оно будет работать всегда. Врата «ада DLL» закрыты."
                • 0
                  Конкретно эта "интересная статья" ничего нового, в принципе, не рассказала.
                  Зато глубинное гугление намекнуло, что в отличие от строки и массива, Stream (и его производные, типа MemoryStream) сериализируется в буфер обмена правильно.

                  Так что в целом — спасибо за наводку. В следующий раз надо лениться чуть меньше :)
                • 0
                  Хорошая статья.
                  Хотел бы обратить внимание по этой теме на еще один вариант (более тяжелый, но и более полный) решения проблемы:

                  http://blogs.msdn.com/jmstall/pages/samp…

                  Здесь тоже самое реализовано без импорта DLL.
                  • 0
                    Не проверял, но на первый взгляд у этого решения тоже будут косяки с кодировкой.
                    Да и нашлось уже, в принципе, полное решение без импорта.
                    • 0
                      Может следующий код поможет победить кодировку?

                      byte[] data = Encoding.UTF8.GetBytes(text);
                      text = Encoding.Default.GetString(data);
                  • 0
                    когда начал читать статью, вспомнил, как работал с буфером обмана на Borland Pascal for Windows больше десяти лет назад.
                    а когда вы в статье о .NET решили обратиться к WinAPI, понял, что за десять лет в программировании под Windows мало что изменилось :)

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.