Все статьи раздела «Программирование»

Основы разработки плагинов для WinAmp

Краткое описание основных принципов разработки плагинов для популярного проигрывателя WinAmp на примере плагина SimpleKeys — простой программы для управления WinAmp с помощью произвольных комбинаций клавиш.

Постановка задачи

Изначально я поставил перед собой простую задачу: управлять WinAmp с помощью стандартных multimedia-клавиш, которые сейчас встречаются на клавиатурах весьма часто. Эти клавиши имеют встроенную поддержку в Windows (им присвоены свои скан-коды, правда, начиная с Win2k), их умеет поддерживать стандартный Media Player, а вот для того, чтобы управлять ими WinAmp'ом нужен дополнительный софт.

В поиска софта я отправился в Сеть. Я сразу наметил для себя основные критерии: во-первых, исключил из рассмотрения самостоятельные программы. Они занимают место в трее, висят в памяти даже когда проигрыватель не работает, грузятся при запуске системы, растягивая этот и так небыстрый процесс. Значит, остаются плагины к самому WinAmp. Искал двумя способами: на сайте WinAmp через его собственный поиск плагинов и в Google. Потратив около часа и скачав несколько плагинов (описания там не грешат многословностью, так что понять, подойдет софт или нет, его нужно скачать), ничего подходящего я не обнаружил. Наиболее близким был плагин KeyControl v1.0 beta, но он отказался работать с мультимедиа-клавишами, а по ссылке на сайт производителя я не нашел никакого упоминания не только о новых версиях, но и о программах вообще. Один песок. Остальные и того хуже – множество навороченных плагинов с непонятными возможностями управления мышью, джойстиком и чуть ли не телепатически, но в основном реализованные через внешние программы, а вот простого плагина для управления от клавиатуры нет...

Решив больше не тратить время на поиски, а переформулировал начальную задачу: попробовать написать плагин, который будет "управлять WinAmp с помощью стандартных multimedia-клавиш" (формулировка из предыдущей версии задачи :)). В конце концов, после написания своего плагина, помимо решения основной задачи, будут приобретены определенные новый знания и навыки, чего нельзя достичь, скачивая чужие программы.

SDK и интерфейс

Чтобы писать свои плагины, сказано на сайте WinAmp, нужно иметь три вещи (в моем вольном переводе): знание языка С++ (чтобы было на чем писать), компилятор с этого языка (чтобы было чем компилировать) и Windows (чтобы было на чем запускать). Вроде бы все есть, поэтому вперед.

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

Нас интересует так называемый "generic plugin" – т.к. мы не собираемся заниматься ни вводом (т.е. чтением музыки из файлов разных форматов), ни выводом (проигрыванием на различных устройствах), ни обработкой звука, ни визуализацией. Сразу отмечу одну интересную особенность, которая очень простая, но может заставить потерять много времени, если не знать, в чем тут дело: WinAmp определяет тип плагина по его имени. В частности, для плагина общего назначения имя DLL-ки должно начинаться с gen_. Если плагин называется не так, то загружаться в качестве "generic plugin" он не будет.

Интерфейс плагинов этого типа (может и других тоже, я не смотрел) достаточно простой: это динамически загружаемая библиотека (DLL), у которой есть экспортируемая функция winampGetGeneralPurposePlugin. Эта функция не принимает никаких аргументов, а возвращает указатель на частично заполненую структуру типа winampGeneralPurposePlugin, который объявлен следующим образом

typedef struct {
  int version;        // Версия структуры (значение 0x10)
  char *description;  // Описание плагина
  int (*init)();      // Указатель на функцию инициализации
  void (*config)();   // Указатель на функцию настройки
  void (*quit)();     // Указатель на функцию финализации
  HWND hwndParent;    // Хэндл главного окна программы WinAmp
  HINSTANCE hDllInstance;   // Хэндл загруженной DLL плагина
} winampGeneralPurposePlugin;

Взаимодействие плагина с программой WinAmp (далее, для краткости, "программа") происходит следующим образом: программа загружает плагин и вызывает у него функцию winampGetGeneralPurposePlugin. Плагин должен создать структуру типа winampGeneralPurposePlugin, запомнить указатель на нее для будущего использования, заполнить поля version (фиксированное значение 0x10), description, init, config, quit и вернуть указатель.

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

При завершении работы программы будет вызвана функция quit, где, логично предположить, нужно освободить все, что было занято при init'е и выполнить все прочие действия по корректной финализации.

Третья функция, config, вызывается, когда пользователь выбрал плагин в диалоге свойств программы и нажал кнопку "Configure...".

Интерфейс пользователя

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

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

Настройки плагина хранятся в реестре, в ветке HKEY_CURRENT_USER\Software\Winamp\Plugins\DenVo\Simple_Keys. Они представляют собой DWORD-значения с кодами клавиш и модификаторов, их имена соответствуют выполняемым функциям. Сейчас поддерживаются следующие имена/функции: "Stop", "Play", "Pause", "PlayPause", "PrevTrack", "NextTrack". Каждому имени соответствует функция (из названия понятно какая). Код клавиши, записанный в реестре, представляет собой двойное слово (4 байта) состоит из двух частей: в старших двух байтах хранится модификатор нажатия (флаги MOD_ALT, MOD_CTRL, MOD_SHIFT, MOD_WIN), а сам виртуальный код клавиши записан в двух младших байтах. Плагин читает список кодов клавиш при загрузке, а также после нажатия кнопки "Да" в окне настроек. Само это окно представляет собой простой MessageBox с краткой информацией о плагине.

Скачать готовый плагин

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

Метод реализации

Первая версия программы использовала установку глобального фильтра (hook) на систему. Она неплохо работала, но клавиатурный фильтр почему-то не предотвращал передачу обработанной клавиши дальше приложению, что для алфавитных клавиш было неприятно :) Потом я нашел, что для таких вещей предлагают еще использовать фильтр на системные сообщения, но это решение мне показалось еще более загрузным для системы. В той версии ничего особенно интересного не было: при загрузке DLL создаем общую область памяти для параметров (т.к. DLL фильтра выполняется в адресных пространствах различных процессов), устновка фильтра во время выполнения init, функция фильтра проверяет приходящие клавиши и вызывает выполнение команд программы, при выполнении quit снимается фильтр, при выгрузке библиотеки освобождается общая память. Поскольку специфичные для плагина вещи остались и в новой версии плагина, код старой я приводить не буду. Если будет интерес к фильтрам, напишу про них отдельную статью, пишите комментарии.

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

Алгоритмы и реализация

Блок-схема получившегося плагина приведена на рисунке.

Блок-схема плагина

Краткое описание простых функций:

  • Инициализация – вызывает установку фильтра
  • Финализация – снимает снятие фильтра
  • Настройка – выводит диалог "О программе", где по нажатию одной из кнопок можно снять и заново установить фильтр (при этом перечитывается конфигурация, это единственный способ поменять ее, не перезапуская программу)

Теперь подробно о содержательных функциях.

Функция регистрации плагина, возвращающая указатель на структуру типа winampGeneralPurposePlugin, имеет одну особенность: она должна быть экспортируемой из DLL, а также объявлена в "С"-стиле, чтобы компилятор не добавлял в имя информации об аргументах. Конкретные "заклинания", которые нужно написать в объявлении такой функции, зависят от вашего компилятора. Для Visual C++ это extern "C" __declspec( dllexport ), для Borland C++ Builder extern "C" winampGeneralPurposePlugin * __stdcall __declspec(dllexport) (обратите внимание на тип возвращаемого значения, который вставлен между двумя частями "заклинания").

Установка фильтра состоит из трех этапов: чтение конфигурации из реестра, подмена функции окна программы, регистрация "горячих клавиш".

Чтение конфигурации ничего хитрого не содержит: открываем нужный ключ реестра и последовательно пытаемся читать из него значения. Если открыть не удалось, загружаем значения по умолчанию. Если удалось – последовательно читаем значения кодов клавиш для каждого действия. Закрываем ключ, чтобы не занимать память.

  HKEY registryKey;
  if(ERROR_SUCCESS != RegOpenKeyEx(HKEY_CURRENT_USER, SETTINGS_REG_KEY, 
      0, KEY_READ, &registryKey))
  {
    for(unsigned n = 0; n < ActionCount; ++ n)
      ActionList[n].KeyCode = ActionList[n].DefKeyCode;
    MessageBox(KeyControlPlugin.hwndParent, TEXT_NO_REGISTRY_KEYS,
      TEXT_PLUGIN_TITLE, MB_ICONEXCLAMATION);
    return true;
  }
  for(unsigned n = 0; n < ActionCount; ++ n)
  {
    DWORD value;
    DWORD valueSize = sizeof value;
    if(ERROR_SUCCESS == RegQueryValueEx(registryKey, ActionList[n].RegName,
        NULL, NULL, (char *)&value, &valueSize) )
      ActionList[n].KeyCode = value;
    else
      ActionList[n].KeyCode = 0;
  }
  RegCloseKey(registryKey);

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

  OriginalWndProc = (WNDPROC)SetWindowLong(KeyControlPlugin.hwndParent,
    GWL_WNDPROC, (LONG)HookWndProc);

Регистрация "горячих клавиш" производится вызовом функции RegisterHotKey: ей передается хэндл окна, которое будет получать сообщение WM_HOTKEY, уникальный в рамках приложения идентификатор "горячей клавиши" (он будет указываться в сообщении, здесь в качестве идентификатора "горячей клавиши" используем идентификатор команды), модификатор клавиши (флаги, определяющие, нажимается клавиша одна или с Ctrl, Alt, Shift, Win) и собственно виртуальный код клавиши. Сама функция подробно описана в MSDN, так что примера из моего кода будет достаточно. Чтобы не заводить отдельные ключи в реестре под настройки модификаторов клавиш, я храню их в старших двух байтах кода клавиши, а сам виртуальный код клавиши – в двух младших.

  bool IsOk = true;
  for(unsigned n = 0; n < ActionCount; ++ n)
  {
    if(ActionList[n].KeyCode)
    {
      IsOk &= (RegisterHotKey(KeyControlPlugin.hwndParent, ActionList[n].Id,
        ActionList[n].KeyCode >> 16, ActionList[n].KeyCode & 0xFFFF) != 0);
    }
  }

Снятие фильтра состоит из двух этапов: восстановление функции окна и отмена регистрации "горячих клавиш".

Восстановление функции окна осуществляется так же, как и установка, только функции SetWindowLong передается сохраненный ранее указатель на оригинальную функцию окна.

  SetWindowLong(KeyControlPlugin.hwndParent, GWL_WNDPROC, (LONG)OriginalWndProc);

Отмена регистрации "горячих клавиш" выполняется вызовом UnregisterHotKey для всех возможных идентификаторов, чтобы не думать, какие были установлены, а какие нет. Параметры опять же интуитивно понятны: хэндл окна, для которого ранее регистрировали клавиши, и идентификатор "горячей клавиши". Описание функции в MSDN.

  for(unsigned n = 0; n < ActionCount; ++ n)
    UnregisterHotKey(KeyControlPlugin.hwndParent, ActionList[n].Id);

Рассмотрим функцию окна. Ее задача: обрабатывать сообщения WM_HOTKEY с "нашими" идентификаторами "горячих клавиш" и вызывать оригинальную функцию окна для всех остальных сообщений. Решается достаточно просто: мы посылаем функции окна программы сообщения WM_COMMAND о нажатии той или иной кнопки. Константы WINAMP_BUTTON1 .. WINAMP_BUTTON5 – идентификаторы кнопок ровно в том порядке, как они находятся на панели WinAmp, от "предыдущий трек" до "следующий трек".

LRESULT CALLBACK HookWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  if(message == WM_HOTKEY)
  {
    HWND winAmpWnd = KeyControlPlugin.hwndParent;
    switch(wParam)
    {
    case WA_ACTION_STOP:
      PostMessage(winAmpWnd, WM_COMMAND, WINAMP_BUTTON4, 0);
      break;
    case WA_ACTION_PLAY:
      PostMessage(winAmpWnd, WM_COMMAND, WINAMP_BUTTON2, 0);
      break;
    case WA_ACTION_PAUSE:
      PostMessage(winAmpWnd, WM_COMMAND, WINAMP_BUTTON3, 0);
      break;
    case WA_ACTION_PLAY_PAUSE:
      PostMessage(winAmpWnd, WM_COMMAND, 
        (SendMessage(winAmpWnd, WM_WA_IPC, 0, IPC_ISPLAYING) == 1) ? 
        WINAMP_BUTTON3 : WINAMP_BUTTON2, 0);
      break;
    case WA_ACTION_PREV_TRACK:
      PostMessage(winAmpWnd, WM_COMMAND, WINAMP_BUTTON1, 0);
      break;
    case WA_ACTION_NEXT_TRACK:
      PostMessage(winAmpWnd, WM_COMMAND, WINAMP_BUTTON5, 0);
      break;
    default:
      return CallWindowProc(OriginalWndProc, hwnd, message, wParam, lParam);
    }
    return 1;
  }
  return CallWindowProc(OriginalWndProc, hwnd, message, wParam, lParam);
}

Обратите внимание на обработку "горячей клавиши" с идентификатором WA_ACTION_PLAY_PAUSE. Сначала вызывается SendMessage(winAmpWnd, WM_WA_IPC, 0, IPC_ISPLAYING) – это запрос состояния проигрывателя. Функция вернет 1, если сейчас играет музыка, 3 – если стоит на "паузе", 0 – если воспроизведение совсем остановлено. Если вернули 1, ставим на паузу, если что-то другое – запускаем воспроизведение.

За рамками этого краткого изложения осталась обработка ошибок, вывод различных сервисных сообщений и остальные тонкости, которые отличают реализацию алгоритма от программы, но вы сможете реализовать их самостоятельно, в своем собственном проекте. Я же надеюсь, что после прочтения этой статьи вы пополнили свой программистский багаж знаний полезными приемами разработки плагинов для WinAmp, по крайней мере теперь знаете, с чего начать.