Сообщений 15    Оценка 290 [+0/-1]         Оценить  
Система Orphus

Rsdn.Editor – работа с клавиатурой

Автор: Чистяков Влад aka VladD2
The RSDN Group

Источник: RSDN Magazine #3-2005
Опубликовано: 07.10.2005
Исправлено: 09.01.2006
Версия текста: 1.0
Введение
Реализация
Чтение описания
Поиск файла описания
Преобразование намерений в действия
Соединяем все воедино
Использование
Развитие идеи
Заключение

Введение

Казалось бы, поддержка работы с клавиатурой в .NET, да и вообще в Windows – это совсем простая тема. Перехватил сообщение нажатия кнопки (OnKeyDown в WinForms или WM_KEYDOWN в Windows) и if-ами или switch-ами обрабатывай себе нужные клавиши. Однако такой подход обладает рядом существенных недостатков. Во-первых, если речь идет об обработке клавиатуры в более менее большом элементе управления, то довольно быстро получается гора малопонятного кода, с которой очень не просто управляться. Причем очень часто код обработки отдельных клавиатурных сокращений не выносится в отдельные функции, а размещается, что называется, по месту. Во-вторых, для добавления нового обработчика клавиатуры нужно править исходные коды программы. Это приводит к тому, что конкретные клавиатурные сокращения определяет программист, не давая переопределить их конечному пользователю или прикладному программисту.

Первым что нужно сделать в плане улучшения читаемости и рефакторинга кода – это вынести обработчики клавиатурных сокращений в отдельные методы, не имеющие параметров выполняющие одно конкретное действие. Это частично решает проблему читаемости, но не решает проблему настройки клавиатурных сокращений конечными пользователями.

Идеальным решением было бы описать настройку клавиатурных сокращений декларативно и каким-то образом читать эту настройку при загрузке класса control-а.

При разработке Rsdn.Editor передо мной встала эта проблема, и естественно, захотелось создать более менее универсальное решение.

В качестве формата описания клавиатурных сокращений я выбрал XML-файл следующего вида:

<?xml version="1.0" encoding="utf-8" ?>
<Shortcuts>
  <!-- Навигация по тексту -->
  <Shortcut Key="Up"                      Action="CaretViewLineUp"/>
  <Shortcut Key="Down"                    Action="CaretViewLineDown"/>
  <Shortcut Key="Shift | Up"              Action="CaretViewLineUpExtend"/>
  <Shortcut Key="Shift | Down"            Action="CaretViewLineDownExtend"/>
  <Shortcut Key="Home"                    Action="CaretViewLineHome"/>
  <Shortcut Key="End"                     Action="CaretViewLineEnd"/>
  <Shortcut Key="Shift | Home"            Action="CaretViewLineHomeExtend"/>
  <Shortcut Key="Shift | End"             Action="CaretViewLineEndExtend"/>
  <Shortcut Key="Left"                    Action="CaretLeft"/>
  <Shortcut Key="Control | Left"          Action="CaretWordLeft"/>
  <Shortcut Key="Control | Right"         Action="CaretWordRight"/>
  <Shortcut Key="Shift | Control | Left"  Action="CaretWordLeftExtend"/>
  <Shortcut Key="Shift | Control | Right" Action="CaretWordRightExtend"/>
  <Shortcut Key="Shift | Left"            Action="CaretLeftExtend"/>
  <Shortcut Key="Shift | Right"           Action="CaretRightExtend"/>
  <Shortcut Key="Right"                   Action="CaretRight"/>
  <Shortcut Key="PageUp"                  Action="PageUp"/>
  <Shortcut Key="PageDown"                Action="PageDowd"/>
  <Shortcut Key="Control | Home"          Action="CaretDocumentHome"/>
  <Shortcut Key="Control | End"           Action="CaretDocumentEnd"/>
  <Shortcut Key="Shift | Control | Home"  Action="CaretDocumentHomeExtend"/>
  <Shortcut Key="Shift | Control | End"   Action="CaretDocumentEndExtend"/>
  <Shortcut Key="Control | A"             Action="SelectAll"/>
  <!-- Клипборд -->
  <Shortcut Key="Control | C"             Action="Copy"/>
  <Shortcut Key="Control | Insert"        Action="Copy"/>
  <Shortcut Key="Control | V"             Action="Paste"/>
  <Shortcut Key="Shift | Insert"          Action="Paste"/>
  <Shortcut Key="Control | X"             Action="Cut"/>
  <Shortcut Key="Shift | Delete"          Action="Cut"/>
  <!-- Редактирование -->
  <Shortcut Key="Delete"                  Action="Delete"/>
  <Shortcut Key="Back"                    Action="DeleteBack"/>
  <Shortcut Key="Control | Z"             Action="Undo"/>
  <Shortcut Key="Alt | Back"              Action="Undo"/>
  <Shortcut Key="Control | Y"             Action="Redo"/>
  <Shortcut Key="Alt | Shift | Back"      Action="Redo"/>
  <!-- Отладка -->
  <Shortcut Key="F5"                      Action="Test1"/>
  <Shortcut Key="F6"                      Action="PaintTest"/>
</Shortcuts>

Каждый тег Shortcut определяет одно клавиатурное сокращение и реакцию на него. Атрибут Key содержит описание клавиатурного сокращения. В нем нужно перечислить значения из перечисления Keys (их может быть одно или несколько). Если значений несколько, то они должны быть отделены знаком «|». Это позволяет задавать комбинации клавиш вроде «Control + C». Атрибут Action определяет метод, который должен быть вызван для обработки данного клавиатурного сокращения.

Реализация

Итак, идея уже есть, остается рассказать как она была воплощена на практике.

Чтение описания

Прочитать XML-файл в .NET труда не составляет. Это можно сделать десятком разных способов. От очень высокоуровневых вроде DataSet-а, до самого низкоуровневого XmlReader. С точки зрения красоты и компактности кода лучшим выбором является DataSet. Возможно, многие читатели не привыкли смотреть на этот класс как на средство чтения XML, но, тем не менее, когда нужно читать данные аналогичные формату, выбранному мной (то есть список одинаковых тегов с одинаковым набором атрибутов), он подходит как нельзя лучше. Вот как бы мог выглядеть код чтения файла с настройками клавиатуры при использовании DataSet:

DataSet shortcutsDataSet = new DataSet();
shortcutsDataSet.ReadXml(OpenReader(controller.GetType(), mapFileName));
DataTable shortcutsTable = shortcutsDataSet.Tables["Shortcuts"];

foreach (DataRow row in shortcutsTable.Rows)
{
  string key = (string)row["Key"];
  string action = (string)row["Action"];
  ...
}

Однако я выбрал самый сложный способ, использование XmlReader, и вот почему. Код элемента управления по своей природе является библиотечным кодом. А библиотечный код может загружаться в приложения, для которых время инициализации может быть критичным. Конечно, один не очень эффективный участок кода вряд ли может сильно затормозить загрузку приложения, но ведь таких участков может быть много. К тому же чтение данных с помощью XmlReader ненамного сложнее, чем при использовании других методов.

Собственно вот этот код:

using (XmlReader reader = OpenReader(controller.GetType(), mapFileName))
{
  // Считываем первый тег «<?xml version="1.0" encoding="utf-8" ?>»
  if (!reader.Read() || reader.Name != "xml")
    throw new ApplicationException(
      "Неверный формат файла '" + mapFileName + "'");

  // Пропускаем возможно имеющиеся пустые строки.
  while (reader.Read() && string.IsNullOrEmpty(reader.Name))
    ;

  // Текущий тег должен быть "Shortcuts".
  if (reader.Name != "Shortcuts")
    throw new ApplicationException(
      "Неверный формат файла '" + mapFileName + "'");

  object[] args = new object[0];
  StringBuilder errors = new StringBuilder(); // сообщения об ошибках

  // Читаем список тегов, вложенных в "Shortcuts".
  while (reader.Read())
  {
    // Обрабатываем все теги "Shortcut", пропуская любые другие.
    if (reader.Name == "Shortcut")
    {
      string key = null;
      string action = null;

      try
      {
        key = reader.GetAttribute("Key"); // читаем ключ
        action = reader.GetAttribute("Action"); // читаем имя делегата

Как видите, код пополнился рядом низкоуровневых чтений и проверок, но в целом остался довольно понятным и не сильно распух. Зато можно точно быть уверенным, что быстрее уже просто некуда. XmlReader – это самый быстрый класс .NET, позволяющий читать XML.

Этот код демонстрирует чтение файла описания, но не демонстрирует, как этот файл ищется и открывается. А это довольно важный момент.

Поиск файла описания

Где же должен лежать файл с описанием клавиатурных сокращений?

Иногда удобно иметь один файл, лежащий рядом с исполняемыми файлами приложения. Это удобно, когда файл изменяется очень редко, и все пользователи пользуются единой настройкой клавиатуры. Недостатками такого подхода является то, что пользователи не смогут настроить разные клавиатурные сокращения, к тому же каталоги, в которых лежат исполняемые модули приложений, обычно бывают доступны рядовому пользователю только на чтение.

Другим подходом является хранение файла настроек в скрытом каталоге, принадлежащем пользователю. Обычно таким каталогом является каталог «Application Data». Путь к нему можно получить из системной переменной окружения «%APPDATA%». В .NET-приложении ее можно прочесть следующим образом:

Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

Естественно, что недостатком такого подхода будет потеря файла настроек при смене машины или учетной записи (например, при переходе в другой домен) и невозможность единой настройки клавиатурных сокращений для всех пользователей одновременно.

Оба этих способа обладают еще одним общим недостатком – при порче или утере файла настроек клавиатурных сокращений элемент управления не сможет вообще обрабатывать клавиатурные нажатия.

Чтобы устранить проблемы, присущие этим подходам хранения файла настроек, я решил искать файл настроек последовательно в нескольких местах. Сначала файл ищется в каталоге «Application Data». Если он там не находится, то производится поиск в каталоге, где располагается исполняемый модуль элемента управления, и если он не находится и там, то берется версия, которая встраивается в ресурсы элемента управления при компиляции. Вот как выглядит код открытия XmlReader для файла с описанием клавиатурных сокращений:

/// <summary>
/// Открывает XML-файл, содержащий описание клавиатурных сокращений на чтение
/// </summary>
private static XmlReader OpenReader(controllerType, mapFileName)
{
  XmlReader reader;

  string path = Environment.GetFolderPath(
    Environment.SpecialFolder.ApplicationData);

  path = Path.Combine(Path.Combine(path, 
    GetProductName(controllerType)), mapFileName);

  // Если файл с клавиатурными настройками существует в папке 
  // "Application Data" (%APPDATA%), или в папке где расположена DLL-а, то
  // читаем настройки из него. Иначе читаем настройки из файла,
  // сохраненного при компиляции в ресурсах.
  if (File.Exists(path))
    reader = XmlReader.Create(path);
  else if (File.Exists(path = Path.Combine(
    GetModulePath(controllerType), mapFileName)))
    reader = XmlReader.Create(path);
  else
    reader = XmlReader.Create(new StringReader(
      Properties.Resources.KeyboardShortcutsMap));
  return reader;
}

Метод GetModulePath несколько запутан, но поверьте – это самый лучший способ из известных мне чтобы получить путь к реальному исполняемому модулю, в котором размещен тип.

Преобразование намерений в действия

Итак, описание прочитано. Осталось преобразовать его в некоторый вид, пригодный для программного использования. Напомню, что класс, который должен получиться в результате, должен при нажатии клавиатурного сокращения вызывать соответствующий метод класса-контроллера (методы которого должны вызываться при срабатывании клавиатурного сокращения).

Преобразование описания клавиатурного сокращения в значение перечисления Keys

Первый вопрос – как преобразовать описание клавиатурного сокращения в значение, пригодное для обработки внутри обработчика OnKeyDown. OnKeyDown предоставляет параметр типа KeyEventArgs. Из его свойств нам интересны следующие:

Реальные данные о клавиатурном сокращении возвращаются свойством KeyData. Все остальные свойства всего лишь производят дополнительную обработку этих данных. К тому же KeyData предоставляет клавиатурное сокращение в виде единого значения. Не нужно производить смелых научных экспериментов, чтобы понять, что его легко использовать в хэш-табице.

Изумительно! Но как же преобразовать текстовое описание в значение типа Keys (чтобы его можно было сравнивать со значением свойства KeyData)? В общем-то, это не вопрос. Значение придется распарсить. Если бы код писался на C++ или другом языке, не имеющем столь мощных средств, как рефлексия и мощнейшая библиотека (FCL), то парсинг мог бы превратиться в серьезную проблему. Но .NET предоставляет прекрасные средства для решения этой проблемы. Класс System.Enum содержит метод Parse:

public static object Parse(Type enumType, string value);

который позволяет легко распознать строковое значение для перечисления, тип которого задается параметром Parse. Так что необходимо только преобразовать строку, содержащую перечисление отдельных значений (разделенных символом ‘|’) в массив отдельных строк, каждая из которых соответствует отдельному значению перечисления и затем скормить получившиеся значения функции Enum.Parse. Естественно, этот код лучше выделить в отдельную функцию:

/// <summary>
/// Преобразует строку, содержащую имена клавиш в Keys.
/// Имена клавиш могут объединяться по или (знаком "|").
/// </summary>
/// <example>"Shift | Control | Right"</example>
/// <param name="key">Клавиатурное сокращение в виде строки.</param>
private static Keys ParseKeys(string key)
{
  Keys keys = 0;
  // Разбиваем ключ на отдельные значения
  string[] keyStrs = key.Split('|');

  foreach (string value in keyStrs)
    keys |= (Keys)Enum.Parse(typeof(Keys), value);

  return keys;
}

Функция Enum.Parse не отличается невероятной производительностью. Как минимум она использует рефлексию и занимается boxing-ом значений. Так что ее не стоит использовать в критических местах. Однако пара сотен ее вызовов незаметны для невооруженного взгляда, а клавиатурных сокращений обычно бывает значительно меньше. Так что можно смело вызвать эту функцию при инициализации элемента класса элемента управления.

Итак магия .NET помогла легко решить проблему преобразования текстового описания в значение перечисления Keys, но как преобразовать имя метода в нечто, что можно вызвать во время выполнения?

Преобразование имени метода в ссылку на него

Рефлексия – это та магия .NET, которая может помочь решить множество проблем автоматизации деятельности программиста. Но она не всегда приемлема, так как привносит значительные непроизводительные затраты времени. Говоря проще, рефлексия зачастую приводит к «тормозам». Если она используется для получения информации о типах, все еще ничего, но вот вызовы методов или свойств произведенные с помощью рефлексии оказываются на несколько порядков медленнее, чем самый медленный вариант обычного вызова метода (например, виртуального или вызова через делегаты).

Откровенно говоря, на современных процессорах для обработки клавиатуры скорости вызова через рефлексию более чем достаточно. Но ведь «тормоза» складываются. Так что может оказаться, что замедление от рефлексии акцентирует не слишком быструю реакцию некоторого действия. Да и возможность вызова метода по его имени может оказаться полезной и в других, более критичных к скорости выполнения случаях. Как же быть?

Можно конечно отказаться от рефлексии и создать, например, словарь, ключом которого являлись бы имена методов, а значениями – делегаты, ссылающиеся на соответствующие методы контроллера. Это решило бы проблему, но это плохое решение. Ведь при изменении имени метода или при добавлении/удалении метода придется постоянно изменять этот словарь. А это и утомительно, и чревато ошибками.

И тут снова может помочь магия .NET. Дело в том, что делегаты можно создавать динамически и через рефлексию подключать к неизвестным заранее методом неизвестного заранее типа. Более того, для этого даже не требуется напрямую использовать рефлексию. Как и в случае с классом Enum, класс Delegate содержит статический метод CreateDelegate, позволяющий создать делегат по имени метода, описанию класса и ссылке на объект. Вот описание этого метода:

public static Delegate CreateDelegate(Type type, object target, 
  string method, bool ignoreCase, bool throwOnBindFailure);

Таким образом, остается описать тип делегата:

public delegate void KeyHandler();

и динамически создать экземпляр делегата, привязав его к методу класса-контроллера по имени:

KeyHandler hendler = (KeyHandler)Delegate.CreateDelegate(
  typeof(KeyHandler), controller, action, false, true);

Здесь controller – это ссылка на объект-контроллер, к методу которого нужно подключиться. А action – строка, содержащая имя метода, к которому нужно подключиться.

Соединяем все воедино

Теперь, имея ссылку на метод (делегат), значение, описывающее клавиатурное сокращения, и умея читать описания клавиатурных сокращений из XML-файла, нужно всего лишь прочесть описания, преобразовать их в значения перечисления/делегаты и заполнить этими значениями словарь (хэш-таблицу). Естественно, при этом нужно не забыть об обработке ошибок. Ниже приведен полный код класса KeyboardShortcutsMap инкапсулирующий всю логику:

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.IO;
using System.Diagnostics;
using System.Windows.Forms;
using System.Reflection;

namespace Rsdn.Editor
{
  /// <summary>
  /// Хэлпер-класс, считывающий информацию о клавиатурных сокращениях из 
  /// файла настроек или ресурсов, и преобразующий его во внутренний словарь.
  /// После инициализации можно пользоваться индексером объекта, чтобы
  /// получать делегаты-обработчики клавиатурных событий.
  /// Файл с описанием клавиатурных сокращений может находиться
  /// в каталоге %APPDATA%\Rsdn.Editor иди каталоге, где располагается сборка,
  /// в которой находится элемент управления.
  /// Если файл не найден в одном из этих каталогов, то описание 
  /// клавиатурных сокращений берется из ресурсов.
  /// </summary>
  public class KeyboardShortcutsMap
  {
    /// <summary>
    /// Тип метода, вызываемого при клавиатурном сокращении.
    /// </summary>
    public delegate void KeyHandler();

    public KeyboardShortcutsMap(object controller, string mapFileName)
    {
      Type controllerType = controller.GetType();

      using (XmlReader reader = OpenReader(controllerType, mapFileName))
      {
        // Считываем первый тег «<?xml version="1.0" encoding="utf-8" ?>»
        if (!reader.Read() || reader.Name != "xml")
          throw new ApplicationException(
            "Неверный формат файла '" + mapFileName + "'");

        // Пропускаем возможно имеющиеся пустые строки.
        while (reader.Read() && string.IsNullOrEmpty(reader.Name))
          ;

        // Текущий тег должен быть "Shortcuts".
        if (reader.Name != "Shortcuts")
          throw new ApplicationException(
            "Неверный формат файла '" + mapFileName + "'");

        object[] args = new object[0];
        StringBuilder errors = new StringBuilder(); // сообщения об ошибках

        // Читаем список тегов, вложенных в "Shortcuts".
        while (reader.Read())
        {
          // Обрабатываем все теги "Shortcut", пропуская любые другие.
          if (reader.Name == "Shortcut")
          {
            string key = null;
            string action = null;

            try
            {
              key = reader.GetAttribute("Key"); // читаем ключ
              action = reader.GetAttribute("Action"); // читаем имя делегата

              // Получам клавиатурное сокращение плюс делегат и инициализируем
              // этими значениями внутренний словарь.
              SetKeyboardHandler(ParseKeys(key),
                (KeyHandler)Delegate.CreateDelegate(
                typeof(KeyHandler), controller, action, false, true));
            }
            catch (Exception ex)
            {
              errors.AppendLine(
                "Невозможно добавить клавиатурное сокращение '"
                + key + "' с обработчиком '"
                + action + "'. " + ex.Message);
            }
          }
        }

        if (errors.Length > 0)
          MessageBox.Show(Form.ActiveForm,
            "При считывании файла с клавиатурными сокращениями были "
            + "обнаружены следующие ошибки: "
            + Environment.NewLine + errors, GetProductName(controllerType),
            MessageBoxButtons.OK, MessageBoxIcon.Warning);
      }
    }

    /// <summary>
    /// Открывает XML-файл, содержащий описание клавиатурных сокращений
    /// на чтение.
    /// </summary>
    private static XmlReader OpenReader(
      Type controllerType, 
      string mapFileName)
    {
      XmlReader reader;

      string path = Environment.GetFolderPath(
        Environment.SpecialFolder.ApplicationData);

      path = Path.Combine(Path.Combine(path, 
        GetProductName(controllerType)), mapFileName);

      // Если файл с клавиатурными настройками существует в папке 
      // "Application Data" (%APPDATA%), или в папке где расположена DLL-а, то
      // читаем настройки из него. Иначе читаем настройки из файла,
      // сохраненного при компиляции в ресурсах.
      if (File.Exists(path))
        reader = XmlReader.Create(path);
      else if (File.Exists(path = Path.Combine(
        GetModulePath(controllerType), mapFileName)))
        reader = XmlReader.Create(path);
      else
        reader = XmlReader.Create(new StringReader(
          Properties.Resources.KeyboardShortcutsMap));
      return reader;
    }

    /// <summary>
    /// Преобразует строку, содержащую имена клавиш, в Keys.
    /// Имена клавиш могут объединяться по или (знаком "|").
    /// </summary>
    /// <example>"Shift | Control | Right"</example>
    /// <param name="key">Клавиатурное сокращение в виде строки.</param>
    private static Keys ParseKeys(string key)
    {
      Keys keys = 0;
      // Разбиваем ключ на отдельные значения
      string[] keyStrs = key.Split('|');

      foreach (string value in keyStrs)
        keys |= (Keys)Enum.Parse(typeof(Keys), value);
      return keys;
    }

    /// <summary>
    /// Возвращает делегат-обработчик события, соотвествующий клавиатурному
    /// сокращению, или null.
    /// </summary>
    /// <param name="shortcut">Клавиатурное сокращение.</param>
    /// <returns>Делегат-обработчик.</returns>
    public KeyHandler this[Keys shortcut]
    {
      get
      {
        KeyHandler keyHandler;

        if (_kbdMap.TryGetValue(shortcut, out keyHandler))
          return keyHandler;

        return null;
      }
    }

    /// <summary>
    /// Вспомогательный метод, позволяющий упростить инициализацию _kbdMap.
    /// Ассоциирует клавиатурное сокращение и его обработчик.
    /// </summary>
    /// <param name="keyData">Клавиатурное сокращение.</param>
    /// <param name="keyHandler">Обработчик.</param>
    private void SetKeyboardHandler(Keys keyData, KeyHandler keyHandler)
    {
      if (_kbdMap.ContainsKey(keyData))
        Trace.WriteLine("Найдено два вхождения для клавиатурного сокращения '"
          + keyData + "'.");

      _kbdMap[keyData] = keyHandler;
    }

    /// <summary>
    /// Ассоциативный массив, ключем которого является клавиатурное 
    /// сокращение, а значением – обработчик, выполняемый при нажатии 
    /// этого клавиатурного сокращения.
    /// </summary>
    private Dictionary<Keys, KeyHandler> _kbdMap =
      new Dictionary<Keys, KeyHandler>();

    /// <summary>
    /// Возвращает путь к исполняемому модулю, в котором находится код
    /// класса <paramref name="type"/>.
    /// </summary>
    /// <param name="type">
    /// Тип, для которого нужно определить, в каком модуле он находится.
    /// </param>
    public static string GetModulePath(Type type)
    {
      return Path.GetDirectoryName(new Uri(type.Assembly.CodeBase).LocalPath);
    }

    public static string GetProductName(Type type)
    {
      object[] attributes = type.Assembly.GetCustomAttributes(
        typeof(AssemblyProductAttribute), false);

      if (attributes.Length != 1)
        throw new ApplicationException("Сборка должна содержать один атрибут"
          + " AssemblyProduct.");

      AssemblyProductAttribute product = 
        (AssemblyProductAttribute)attributes[0];

      return product.Product;
    }
  }
} 

Как видите, парсинг значений клавиатурных сокращений и создание делегатов обрамлены конструкцией try/catch, отлавливающей все управляемые исключения. Часто можно услышать, что:

catch (Exception ex)

это плохой стиль, так как он перекрывает все исключения без исключения (извините за каламбур :) ), что может «замазать» ошибку и не позволит обработать исключение вызывающему коду. В принципе правило верное, но исключения из него все же случаются.

В данном случае ошибка совершенно точно связана с обработкой отдельной записи в конфигурационном файле. Даже если это ошибка программиста, ничего страшного не случится, если одна запись будет проигнорирована. Информация об ошибке в любом случае будет показана пользователю.

Но ошибок может быть много. Если каждая из них будет показываться пользователю по отдельности, то до конца инициализации он может тихо (а может и громко) возненавидеть создателя этого приложения. Так что лучше накапливать сообщения об ошибках в промежуточном буфере и выводить информацию об ошибках за один прием. Это несколько напоминает подход, используемый в компиляторах.

В случае дублирования клавиатурных сокращений тоже желательно сообщать об этом пользователю (хотя это и не столь серьезная ошибка). Несерьезность данной ошибки натолкнула меня на мысль, что вместо того чтобы сообщать об этом в виде диалога, лучше вывести это сообщение в отладочную консоль с помощью метода Trace.WriteLine(). Если это поведение не соответствует вашим убеждениям, то просто измените эту строку и выводите эти сообщения также в накапливающий StringBuilder.

Вообще сообщать об ошибках лучше всегда. Лично я больше всего не люблю ситуаций, когда что-то не работает, но причина этого не ясна. Лучше уж знать суровую правду, чем быть в неведении. Так что если уж перекрыли исключения или обнаружили ошибочную ситуацию другим образом, то позаботьтесь о том, чтобы довести информацию об ошибках до пользователя хоть в каком-то виде.

Использование

Использовать получившийся класс проще простого. Нужно создать экземпляр класса KeyboardShortcutsMap передав ему ссылку на класс-контроллер и имя XML-файла с настройками клавиатуры:

/// <summary>
/// Ассоциативный массив, ключем которого является клавиатурное сокращения,
/// а значением обработчик выполняемый при нажатии этого клавиатурного 
/// сокращения.
/// </summary>
private KeyboardShortcutsMap _keyboardMap;

/// <summary>
/// Инициализация обработчиков клавиатурных нажатий.
/// </summary>
private void InitKeybordHandlers()
{
  _keyboardMap = new KeyboardShortcutsMap(this, "KeyboardShortcutsMap.xml");
}

Далее в обработчике OnKeyDown нужно обращаться к этому экземпляру, получая через его индексатор значения делегатов, соответствующих полученному клавиатурному сокращению:

protected override void OnKeyDown(KeyEventArgs e)
{
  if (e.KeyValue >= 16 && e.KeyValue <= 18) // Alt, Control, Shift
    return;

  // Получаем обработчик, ассоциированный с текущим клавиатурным сокращением.
  KeyboardShortcutsMap.KeyHandler keyDownHandler = _keyboardMap[e.KeyData];

  if (keyDownHandler != null)
  {
    // Вызваем реальный обработчик клавиатурного сокращения.
    keyDownHandler();
    // Подавляем дальнейшую обработку клавиатурного сокращения.
    e.SuppressKeyPress = true;
  }
  else
    base.OnKeyDown(e);
}

Индексатор вернет делегат, или null, если для заданного клавиатурного сокращения обработчик не назначен.

В реальном проекте – Rsdn.Editor пришлось также добавить обработчик события OnKeyPress:

protected override void OnKeyPress(KeyPressEventArgs e)
{
  Document.Replace(e.KeyChar.ToString(), 
    _selectionStartDocument, _selectionEndDocument);
}

(это потребовалось, поскольку некоторые нажатия, вроде конца строки, не приходят в OnKeyDown) и IsInputKey:

protected override bool IsInputKey(Keys keyData)
{
  return true; // Иначе контрол не отдаст стрелки и т.п.
}

который говорит форме о том, что не нужно специальным образом обрабатывать такие клавиатурные сокращения как стрелки.

Развитие идеи

Может оказаться так, что вам не подойдет прямая ассоциация между именами методов и названиями действий (значений атрибута «Action»). В этом случае можно ввести атрибут (Custom Attribute, т.е. .NET-атрибут) которым помечать методы являющиеся обработчиками клавиатурных сокращений. В параметрах этого атрибута можно задать понятное для пользователя название действия (например, с пробелами и на русском языке). При этом придется немного усложнить код инициализации класса KeyboardShortcutsMap, получая список нужных методов помеченных атрибутом через reflection. Для создания делегата придется использовать методы класса System.Delegate, принимающие в качестве параметра описание метода в виде ссылки на экземпляр класса MethodInfo. Я не буду демонстрировать это решение. Думаю, вы без проблем справитесь с ним сами. Если что, форум http://rsdn.ru/forum/?group=dotnet всегда доступен, а в нем не составит труда найти помощь по подобному вопросу.

Заключение

В этой статье я продемонстрировал решение из совершенно конкретного проекта – Rsdn.Editor. Но, тем не менее, оно является совершенно универсальным и может быть использовано для обработки клавиатуры в любом GUI-приложении или библиотеке.

Более того, подходы, примененные в описанном классе, универсальны и могут быть применены для автоматизации тех или иных аспектов создания приложения или ускорения уже имеющегося кода, основанного на медлительной рефлексии. В сочетании с атрибутами, XML и другими возможностями .NET, они могут существенно сократить объемы кодирования и сделать его более интересным занятием. Хорошим примером сочетания атрибутов и динамического создания делегатов может служить «Крекер» Windows-сообщений в AOP-стиле.

Главное – научиться смотреть на решение задач по-новому. Помните, что декларативное решение в большинстве случаев предпочтительнее императивного. А .NET обладает изумительными средствами для создания гибких декларативных решений.


Эта статья опубликована в журнале RSDN Magazine #3-2005. Информацию о журнале можно найти здесь
    Сообщений 15    Оценка 290 [+0/-1]         Оценить