Showing posts with label clipboard. Show all posts
Showing posts with label clipboard. Show all posts

Monday, June 7, 2010

Странные ошибки и ExternalException при работе с буфером обмена

В комментарии к предыдущей записи Омен подбросил задачку:
… предлагаю для решения простую задачку: посмотреть, как работает пример из этого поста при запущенной вот этой маленькой программке. Выяснить, что и почему происходит, и как с этим бороться.
Чур без рефлектора:)
Ну что же, давайте разбираться. Грузим архив, открываем приложение. Видим обычную форму без каких-либо компонентов. Что же с ней не так? В этот момент я отвлекся на сообщение в скайпе, а когда попытался скопировать оттуда ссылку, вот что получилось:
Получается, наш TestClipboard.exe каким-то образом запорол доступ к буферу обмена (причём, не только для скайпа — чтобы сделать скриншот выше, тест-приложение пришлось закрыть). Как он это сделал? Можно, конечно, посмотреть рефлектором, но нас же просили этого не делать — поэтому пойдем слегка обходным путём:
  1. У меня уже была открыта Visual Studio с каким-то проектом, и я банально нажал F11, чтобы перейти в режим отладки. Открытый проект совершенно не важен, потому как нам будет нужен Immediate Window. Давайте попробуем из него получить текст из буфера обмена:
    Clipboard.GetText()
  2. Студия, как и скайп раньше, не может получить доступ к буферу обмена, только в этот раз у нас куда больше информации:
  3. Параметр ErrorCode — как раз то, что нам нужно. Поскольку под классом Clipboard находятся нативные функции Windows, мы будем смотреть, что не так у них. Конвертируем -2147221040 в hex, получаем 0x800401d0. Поискав число в MSDN или заглянув в winerror.h, узнаём, что числу соответствует константа CLIPBRD_E_CANT_OPEN.
  4. Давайте посмотрим теперь, какая функция WinAPI вернула нам такой результат. В описании константы в MSDN это указано:
    0x800401D0 CLIPBRD_E_CANT_OPEN
    OpenClipboard failed.
    Итак, «виновница» торжества — функция WinAPI OpenClipboard.
В процессе расследования мы спустились с вершин дотнета в старое доброе WinAPI. И вот как в нём устроена работа с буфером обмена:
  1. Приложение вызывает функцию OpenClipboard для получения доступа к буферу обмена. С этого момента доступ к буферу закрыт для всех остальных приложений.
  2. Приложение вызывает либо функцию GetClipboardData для получения данных, либо EmptyClipboard для его очистки и SetClipboardData для записи данных в буфер обмена.
  3. Приложение закрывает буфер обмена при помощи функции CloseClipboard.
Итак, после первого пункта больше никто, кроме приложения, не может получить доступ к буферу. Если вызвать OpenClipboard и не закрывать буфер (что и делает наше тестовое приложение), то можно отобрать доступ к буферу у всех остальных (не делайте так, пожалуйста). Также работа с буфером может вызвать исключение, если в момент вызова какое-то приложение находится на пункте 2 работы. Это довольно редкая ситуация, но её стоит учитывать — поэтому работа с буфером всегда должна обрабатывать ExternalException. Мы учтём это в следующей версии нашего монитора буфера обмена.

OpenClipboard Function (Windows)
Clipboard (Windows)

    Tuesday, June 1, 2010

    Уведомление о изменении буфера обмена (Clipboard) Windows в С#, часть 1

    Введение

    Пожалуй, буфер обмена — одно из найболее часто используемых функций в Windows, да и в других операционных системах. Сегодня мы взглянем на то, как при помощи.net Framework и нескольких API-функций Windows создать простое приложение, которое будет следить за буфером обмена и сообщать нам о изменении его содержимого.

    Класс Clipboard

    В .net-приложении буфер обмена Windows доступен в любой момент при помощи класса
    System. Windows. Forms. Clipboard. Для получения данных, которые находятся в буфере, предназначены четыре метода:
    public static string Clipboard.GetText();
    public static string Clipboard.GetText(TextDataFormat format);
    public static Object Clipboard.GetData(string format);
    public static IDataObject Clipboard.GetDataObject();
    
    Найболее простой из них — первый метод, который получает текстовые данные из буфера обмена. Если в буфере не текстовые данные (а, например, изображение), то результатом будет пустая строка.
    Метод GetText (TextDataFormat format) отличается от собственной перегрузки без параметров тем, что позволяет получить текст из буфера обмена в определенном формате (всего их пять — Text, UnicodeText, Rtf, Html и CommaSeparatedValue). Опять же, если в буфере обмена не текстовые данные, либо данные не запрошенного формата, результатом вызова метода будет пустая строка.
    Для проверки того, содержит ли буфер текст нужного формата, существуют следующие методы:
    public static bool ContainsText();
    public static bool ContainsText(TextDataFormat format);
    
    Синтаксис их вызова идентичен методам GetText, но результатом будет true, если буфер обмена содержит текст заданного формата и false в обратном случае.
    Последние два Get-метода предназначены для получения объекта определенного формата из буфера (о других форматах мы поговорим в следущей части статьи) и имеют соответствующие Contains-методы.

    Уведомления об изменении содержимого буфера обмена

    Что нужно сделать, чтобы узнать, когда содержимое буфера изменилось? Простейшее решение — воспользоваться таймером и периодически опрашивать буфер на предмет изменения. Это не лучшее решение — содержимое буфера может измениться несколько раз между итерациями таймера, а слишком частый опрос может быть губительным для производительности.
    В этой задаче нам на помощь приходят три WinAPI-функции:
    //Register a window handle as a clipboard viewer
    [DllImport("User32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr SetClipboardViewer(IntPtr hWnd);
    //Remove a window handle from the clipboard viewers chain
    [DllImport("User32.dll", CharSet = CharSet.Auto)]
    public static extern bool ChangeClipboardChain(
        IntPtr hWndRemove,  // handle to window to remove
        IntPtr hWndNewNext  // handle to next window
        );
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern int SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);
    
    Функция SetClipboardViewer регистрирует окно для получения сообщения WM_DRAWCLIPBOARD (о нём ниже), ChangeClipboardChain предназначена для удаления идентификатора окна из цепочки получения WM_DRAWCLIPBOARD, а SendMessage — функция WinAPI для отправки сообщений Windows другим окнам.
    Сообщение WM_DRAWCLIPBOARD рассылается системой при изменении содержимого буфера обмена. С помощью функции SetClipboardViewer мы подпишем наше приложение в цепочку получателей этого сообщения. Небольшой сложностью этого подхода будет то, что наше приложение должно будет передать сообщение дальше по цепочке — адрес окна, которому нужно будет передать сообщение, вернет функция SetClipboardViewer.
    Кроме обработки WM_DRAWCLIPBOARD, нашему приложению придётся обрабатывать также и сообщение WM_CHANGECBCHAIN. Система рассылает это сообщение, когда исключает одно из окон из цепочки рассылки.

    Время для кода

    Итак, откроем Visual Studio, создадим новый проект типа Windows Forms Application, и приступим к работе. Создадим простой интерфейс для приложения:
    Кнопки Start и Stop Monitoring будут соответственно регистрировать окно нашего приложения в цепочке уведомлений и удалять его оттуда, а в текстовое поле будет выводиться изменения в буфере и прочая диагностическая информация. Для этого будет предназначена вспомогательная функция Output:
    private void Output(string message)
    {
        tbOutput.Text += message + Environment.NewLine; // добавить наше сообщение
        tbOutput.SelectionStart = tbOutput.Text.Length; // установить курсор в конец текста
        tbOutput.ScrollToCaret(); // прокрутка до курсора для показа сообщения
    }
    
    Добавим переменную, в которой будем хранить дескриптор окна, которому будем передавать сообщения WM_DRAWCLIPBOARD и WM_CHANGECBCHAIN:
    private IntPtr nextClipboardViewer;
    
    Добавим обработчики событий для кнопок:
    private void btnStart_Click(object sender, EventArgs e)
    {
        nextClipboardViewer = SetClipboardViewer(this.Handle);
        Output("Registered clipboard viewer.");
    }
    
    private void btnStop_Click(object sender, EventArgs e)
    {
        ChangeClipboardChain(this.Handle, nextClipboardViewer);
        Output("Unregistered clipboard viewer.");
    }
    
    Переопределим функцию WndProc, чтобы обработать пришедшие нам сообщения:
    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case WM_DRAWCLIPBOARD:
                // содержимое буфера обмена изменилось
                {
                    // реагируем на изменение буфера
                    ClipboardChanged();
                    // пересылаем сообщение следующему окну в цепочке
                    SendMessage(nextClipboardViewer, WM_DRAWCLIPBOARD, IntPtr.Zero, IntPtr.Zero);
                    break;
                }
            case WM_CHANGECBCHAIN:
                // цепочка приложений, подписанных на WM_DRAWCLIPBOARD изменилась
                {
                    if (m.WParam == nextClipboardViewer)
                    {
                        // окно, которому мы передавали WM_DRAWCLIPBOARD, удалено из
                        // цепочки, и нам нужно обновить переменную
                        nextClipboardViewer = m.LParam;
                    }
                    else
                    {
                        // просто передаем новости дальше
                        SendMessage(nextClipboardViewer, WM_CHANGECBCHAIN, m.WParam, m.LParam);
                    }
                    m.Result = IntPtr.Zero; // уведомляем систему об обработке сообщения
                    break;
                }
            default:
                // поведение для остальных сообщений - по умолчанию
                {
                    base.WndProc(ref m); 
                    break;
                }
        }
    }
    
    И последнее, что осталось сделать — реализовать метод ClipboardChanged:
    private void ClipboardChanged()
    {
        Output("The clipboard content has been changed.");
        string s = "New clipboard content: ";
        if (Clipboard.ContainsText()) // будем выводить только текстовое содержимое
        {
            s += Clipboard.GetText();
        }
        else
        {
            s += "[non-text]";
        }
        Output(s);
    }
    
    Готово! Монитор буфера обмена готов к работе:
    Обратите внимание, что при регистрации монитора Windows тут же присылает нам сообщение о изменении буфера — что-то вроде «добро пожаловать» :)
    В следующей части мы рассмотрим работу с другими форматами данных буфера обмена, и пропагрейдим наш монитор до менеджера буфера обмена — с историей и прочими полезными функциями.
    Исходник проекта (Visual Studio 2008)
    SetClipboardViewer Function (Windows)
    Clipboard Class (System. Windows. Forms)