Showing posts with label winapi. Show all posts
Showing posts with label winapi. Show all posts

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)