Thursday, June 10, 2010

Перехват сообщений Windows в WPF-приложении. Аналог WndProc.

Проблема

Для второй версии монитора буфера обмена (с которой мы будем разбираться в следующем посте) мы создадим WPF-приложение. Однако, у Windows Presentation Foundation есть одно существенное отличие от WinForms — в WPF-форме такой код из первой версии работать не будет:
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;
            }
    }
}
Аналогом класса Form в Windows Forms является класс Window, в котором нет метода WndProc, который мы могли бы переопределить. Возвращаемся в Windows Forms? Нет, решение всё-таки существует.

Решение

Для решения нашей задачи предназначен замечательный класс HwndSource, который находится в пространстве имен System.Windows.Interop. HwndSource — класс, который представляет Win32-окно, в котором находится контент WPF. Именно им нужно будет пользоваться, если нам нужно вызывать какие-либо функции WinAPI, которые требуют дескриптор окна в качестве одного из параметров. Также в экземпляре класса HwndSource есть два метода, которые позволяют получить доступ к сообщениям Windows:
public void AddHook(HwndSourceHook hook);
public void RemoveHook(HwndSourceHook hook);
Эти методы позволяют соответственно установить и удалить функцию-перехватчик. Сигнатура HwndSourceHook выглядит так:
public delegate IntPtr HwndSourceHook(
 IntPtr hwnd, // дескриптор окна
 int msg, // сообщение Windows
 IntPtr wParam, // параметры
 IntPtr lParam, // сообщения
 ref bool handled // обработано ли сообщение
)
Чтобы создать экземпляр класса HwndSource, мы можем воспользоваться его конструктором и создать новое окно, сообщения которому мы и будем перехватывать. Однако, можно сделать и по-другому. Поскольку HwndSource является наследником класса PresentationSource, достаточно будет воспользоваться статическим методом PresentationSource.FromVisual(Visual) с нашим главным окном в качестве параметра и привести результат к типу HwndSource.
В этом моменте я допустил ошибку, вызвав FromVisual в конструкторе формы. В момент вызова окно ещё не было полностью инициализировано, и результатом стало исключение. После этой попытки я переопределил вызов OnSourceInitialized и завершил инициализвцию HwndSource в нём.

Код

Полный код перехвата сообщений Windows выглядит так:
public partial class MainWindow : Window
{
    private const int WM_DRAWCLIPBOARD = 0x0308;
    private HwndSource hwndSource;

    // функция-перехватчик
    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (msg == WM_DRAWCLIPBOARD)
        {
            // обрабатываем сообщение
        }
        return IntPtr.Zero;
    }

    public MainWindow()
    {
        InitializeComponent();   
    }

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);
        // создаем экземпляр HwndSource
        hwndSource = PresentationSource.FromVisual(this) as HwndSource; 
        // и устанавливаем перехватчик
        hwndSource.AddHook(WndProc);
    }
}
HwndSource Class (System.Windows.Interop)
HwndSourceHook delegate (System.Windows.Interop)

1 comment:

  1. Добрый день, почему то не работает данный код...
    Сообщения приходят... но заветного M_DRAWCLIPBOARD
    при вставке в буфер обмена нового текста так и нет.. И в чем же проблема?

    ReplyDelete