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)

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)

    Tuesday, May 11, 2010

    Использование неявно типизированных переменных в C#

    Ещё в C# 3.0 появилась довольно интересная фича — неявная типизация переменных. При объявлении переменной мы можем не задавать явно её тип — его определит компилятор. В основном это будет полезно при использовании анонимных типов и LINQ-запросов, но абсолютно ничего не мешает нам использовать её и в обычном коде:
    int i = 42; // явное задание типа
    var j = 42; // неявное задание типа
    
    В примере выше переменная j будет типа int точно так же, как и i. При желании можно легко в этом убедиться, заглянув в MSIL:
    IL_0000:  nop
    IL_0001:  ldc.i4.s   42
    IL_0003:  stloc.0
    IL_0004:  ldc.i4.s   42
    
    IntelliSense в Visual Studio также без проблем разбирается с неявно типизированными переменными:
    Оператор var
    можно использовать как в циклах for, так и в объявлениях using. Но по-настоящему он пригодится, когда мы возпользуемся анонимными типами, например, в LINQ-запросе. В таком случае, наша переменная может быть объявлена только через идентификатор var, потому что внутреннее название сгенерированного анонимного типа будет известно только компилятору:
    struct SomeStruct
    {
        public int a;
        public int b;
        public int c;
    }
    static void Main(string[] args)
    {
        int[] numbers = new[] { 0, 1, 2, 3, 4, 5 };
        // здесь использование var необязательно - мы можем использовать
        // вместо него явно заданный IEnumerable
        var numberQuery = from number in numbers
                          where number > 1 && number <= 4
                          select number;
        // так как numberQuery - объект неанонимного типа, в foreach можно
        // использовать явно типизированный итератор
        foreach (int i in numberQuery)
        {
            Console.WriteLine(i.ToString());
        }
    
        SomeStruct[] structures = new[] {
            new SomeStruct { a=1,b=2,c=3},
            new SomeStruct { a=3,b=4,c=5},
            new SomeStruct { a=7,b=8,c=9},
        };
    
        // результат этого запроса - коллекция экземпляров анонимного типа.
        // в данном случае использование var обязательно.
        var structQuery = from structure in structures
                          where structure.a <= 3
                          select new { NewA = structure.a, NewB = structure.b };
        // опять же, из-за анонимности типа в LINQ-запросе, мы не можем явно
        // указать тип итератора, поэтому обязательно использовать var.
        foreach (var j in structQuery)
        {
            Console.WriteLine(String.Format("NewA={0},NewB={1}", j.NewA, j.NewB));
        }
    }
    

    Итак, неявно типизированные переменные и идентификатор var  — отличное средство улучшения читабельности и лёгкости написания кода (сравните var someQuery и IEnumerable<somelongandnastytype> someQuery !) и незаменимое решение при использовании анонимных типов.

    Implicitly Typed Local Variables (C# Programming Guide)
    Anonymous Types (C# Programming Guide)

    Saturday, May 1, 2010

    Презентация и код со сбора .NET User Group

    Windows Phone на Imagine Cup 2010

    На студенческом технологическом конкурсе Imagine Cup 2010 объявлена номинация по Windows Phone 7! Всё, что от вас требуется — зарегистрироваться командой до четырех человек и в до 24 мая представить общественности ваше Windows Phone 7-приложение. Каким оно будет — решать вам;)
    Призы, надо сказать, очень и очень интересны:
    Award Finalists receive:
    • First Prize: $8,000 USD, a trip to the Worldwide Finals in Warsaw, Poland from July 3–8, 2010, and a Windows Phone for each team member.
    • Second Prize: $4,000 USD and a Windows Phone for each team member
    • Third Prize: $3,000 USD and a Windows Phone for each team member
    Так что, если вы ещё студент, а также соответствуете остальным условиям участия, удачи!
    Imagine Cup — Windows Phone 7 «Rockstar» Award

    Обновление Windows Phone Developer Tools

    Вчера в центре разработки Windows Phone была выложена новая версия инструментов разработки Windows Phone Developer Tools. Давайте же посмотрим, что нового:
    • Совместимость с финальной версией Visual Studio 2010;
    • Обновлённый образ Windows Phone 7 OS для эмулятора;
    • Изменения в API и документации (подробнее о них в другой раз);
    • Поддержка в эмуляторе (пока ограниченная) диалогов запуска и выбора (Launchers and Choosers);
    • Поддержка событий Pause/Resume;
    • Возможность использования эмулятора не-административными пользователями компьютера;
    • Исправления багов.
    Советы по установке новой версии Windows Phone Developer Tools:
    • Удалите перед установкой предыдущую версию инструментов разработки (Установка/удаление приложений → Microsoft Windows Phone Developer Tools CTP-ENU);
    • Если у вас установлен релиз-кандидат Visual Studio 2010, удалите его перед установкой Developer Tools и установите финальную версию VS2010;
      • С другой стороны, вам не требуется установленная Visual Studio для того, чтобы установить Windows Phone Developer Tools.
    В следующем посте мы посмотрим на изменения в API и попробуем в деле новую версию эмулятора.
    Источник: Windows Phone Developer Tools Refresh!
    Загрузить апрельское обновление Windows Phone Developer Tools

    Thursday, April 29, 2010

    Офисная функциональность Windows Phone 7

    На Ютубе неделю назад появились видео функциональности Windows Phone 7, а точнее, офисной её части. Будем надеяться, что и в «живых» устройствах всё будет работать так же отлично.

    Ещё одно видео под катом.