Особенности подключения DLL-библиотек

В импортируемые из DLL функции нельзя передавать в качестве параметров:

  • Классы (объекты и указатели на них);
  • Структуры, содержащие динамические массивы, строки, классы, другие сложные структуры;
  • Массивы строк или вышеперечисленных сложных объектов.

Все параметры простых типов передаются по значению, если явно не указано, что они  передаются по ссылке. При передаче строки передается адрес буфера скопированной строки; если строка передается по ссылке, то в функцию, импортируемую из DLL, передается адрес буфера именно этой строки без копирования.

При передаче в DLL массива всегда (независимо от флага AS_SERIES) передается адрес начала буфера данных. Функция внутри DLL ничего не знает о флаге AS_SERIES, переданный массив является массивом неизвестной длины, для указания его размера нужен дополнительный параметр.

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

При импорте DLL-библиотек требуется дать разрешение на их использование в свойствах конкретной MQL-программы или в общих настройках терминала. В связи с этим в разделе Разрешения мы представляли скрипт EnvPermissions.mq5, в котором, в частности, имеется функция чтения содержимого системного буфера обмена Windows, использующая системные DLL-библиотеки. Там эта функция была приведена в факультативном порядке: её вызов был закомментирован, потому что мы еще не знали, как работать с библиотеками, но сейчас перенесем её в отдельный скрипт LibClipboard.mq5.

Запуск скрипта может инициировать запрос подтверждения у пользователя (так как по умолчанию DLL-библиотеки запрещены из соображений безопасности). При необходимости включите опцию в диалоге, на закладке с зависимостями.

Вместе с терминалом поставляются заголовочные файлы в каталоге MQL5/Include/WinApi, где уже прописаны директивы #import для востребованных системных функций, таких как работа с буфером обмена (OpenClipboard, GetClipboardData, CloseClipboard), управление памятью (GlobalLock, GlobalUnlock), окнами Windows и многие другие. Мы подключим лишь два файла — winuser.mqh и winbase.mqh. В них есть требуемые директивы импорта и, опосредовано — через подключение windef.mqh, макросы терминов Windows (HANDLE и PVOID):

#define HANDLE  long
#define PVOID   long
   
#import "user32.dll"
...
int             OpenClipboard(HANDLE wnd_new_owner);
HANDLE          GetClipboardData(uint format);
int             CloseClipboard(void);
...
#import
   
#import "kernel32.dll"
...
PVOID           GlobalLock(HANDLE mem);
int             GlobalUnlock(HANDLE mem);
...
#import

Кроме того, мы самостоятельно импортируем функцию lstrcatW из библиотеки kernel32.dll, потому что нас не устраивает её описание в winbase.mqh, предоставленное по умолчанию: тем самым у функции появляется второй прототип, подходящий для передачи в первом параметре значения PVOID.

#include <WinApi/winuser.mqh>
#include <WinApi/winbase.mqh>
   
#define CF_UNICODETEXT 13 // один из стандартных форматов обмена - текст Unicode
#import "kernel32.dll"
string lstrcatW(PVOID string1const string string2);
#import

Суть работы с буфером обмена заключается в "захвате" доступа к нему с помощью OpenClipboard, после чего следует получить дескриптор данных (GetClipboardData), преобразовать его в адрес в памяти (GlobalLock) и, наконец, скопировать данные из системной памяти в свою переменную (lstrcatW). Далее выполняется освобождение занятых ресурсов в обратном порядке (GlobalUnlock, CloseClipboard).

void ReadClipboard()
{
   if(OpenClipboard(NULL))
   {
      HANDLE h = GetClipboardData(CF_UNICODETEXT);
      PVOID p = GlobalLock(h);
      if(p != 0)
      {
         const string text = lstrcatW(p"");
         Print("Clipboard: "text);
         GlobalUnlock(h);
      }
      CloseClipboard();
   }
}

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

Функции, импортируемые из DLL, подчиняются соглашению о связывании (linking) двоичных исполняемых файлов, принятому для функций Windows API. Для обеспечения такого соглашения в исходном тексте программ используются специфические для конкретного компилятора ключевые слова, такие как, например, __stdcall в C или C++. Данные правила связывания подразумевают следующее:

  • Вызывающая функция (в нашем случае, MQL-программа) должна "видеть" прототип вызываемой (импортируемой из DLL) функции, для того чтобы правильно сложить параметры на стек;
  • Вызывающая функция (в нашем случае, MQL-программа) складывает параметры на стек в обратном порядке, справа налево — именно в таком порядке импортируемая функция считывает переданные ей параметры;
  • Параметры передаются по значению, за исключением тех, которые явно передаются по ссылке (в нашем случае, строк);
  • Импортируемая функция, считывая переданные ей параметры, сама очищает стек.

Приведем еще один пример скрипта, использующего DLL, — LibWindowTree.mq5. Его задача — проход по дереву всех окон терминала и получение их имен классов (согласно регистрации в системе средствами WinApi) и заголовков. Под окнами здесь имеются в виду стандартные элементы интерфейса Windows, включающие также и элементы управления. Данная процедура может пригодиться для автоматизации работы с терминалом: эмуляции нажатия кнопок в окнах, переключения режимов, которые недоступны через MQL5 и так далее.

Для импорта требуемых системных функций подключим заголовочный файл WinUser.mqh, использующий user32.dll.

#include <WinAPI/WinUser.mqh>

Получить название класса окна и его заголовок можно с помощью функций GetClassNameW и GetWindowTextW: они вызываются в функции GetWindowData.

void GetWindowData(HANDLE wstring &clazzstring &title)
{
   static ushort receiver[MAX_PATH];
   if(GetWindowTextW(wreceiverMAX_PATH))
   {
      title = ShortArrayToString(receiver);
   }
   if(GetClassNameW(wreceiverMAX_PATH))
   {
      clazz = ShortArrayToString(receiver);
   }
}

Суффикс 'W' в названиях функций означает, что они предназначены для строк формата Unicode (2 байта на символ) — наиболее распространенного сегодня (суффикс 'A' для ANSI-строк имеет смысл использовать только для обратной совместимости со старыми библиотеками).

При наличии некоего начального дескриптора окна Windows проход вверх по иерархии его родительских окон обеспечивает функция TraverseUp: её работа основывается на системной функции GetParent. Для каждого найденного окна TraverseUp вызывает GetWindowData и выводит полученные имя класса и заголовок в журнал.

HANDLE TraverseUp(HANDLE w)
{
   HANDLE p = 0;
   while(w != 0)
   {
      p = w;
      string clazztitle;
      GetWindowData(wclazztitle);
      Print("'"clazz"' '"title"'");
      w = GetParent(w);
   }
   return p;
}

Обход вглубь иерархии производится функцией TraverseDown: для перечисления дочерних окон применяется системная функция FindWindowExW.

HANDLE TraverseDown(const HANDLE wconst int level = 0)
{
   // запрашиваем первое дочернее окно (если есть)
   HANDLE child = FindWindowExW(wNULLNULLNULL);
   while(child)          // цикл, пока есть дочерние окна
   {
      string clazztitle;
      GetWindowData(childclazztitle);
      Print(StringFormat("%*s"level * 2""), "'"clazz"' '"title"'");
      TraverseDown(childlevel + 1);
      // запрашиваем следующее дочернее окно
      child = FindWindowExW(wchildNULLNULL);
   }
   return child;
}

В функции OnStart найдем главное окно терминала за счет обхода окон вверх от дескриптора текущего графика, на котором запущен скрипт. А затем построим все дерево окон терминала.

void OnStart()
{
   HANDLE h = TraverseUp(ChartGetInteger(0CHART_WINDOW_HANDLE));
   Print("Main window handle: "h);
   TraverseDown(h1);
}

По идее мы можем искать требуемые окна по имени класса и/или заголовку, а потому главное окно можно было бы сразу получить, вызвав FindWindowW, поскольку его атрибуты известны.

   h = FindWindowW("MetaQuotes::MetaTrader::5.00"NULL); 

Вот пример журнала (фрагмент):

 'AfxFrameOrView140su' ''

 'Afx:000000013F110000:b:0000000000010003:0000000000000006:00000000000306BA' 'EURUSD,H1'

 'MDIClient' ''

 'MetaQuotes::MetaTrader::5.00' '12345678 - MetaQuotes-Demo: Demo Account - Hedge - ...'

Main window handle: 263576

  'msctls_statusbar32' 'For Help, press F1'

  'AfxControlBar140su' 'Standard'

    'ToolbarWindow32' 'Timeframes'

    'ToolbarWindow32' 'Line Studies'

    'ToolbarWindow32' 'Standard'

  'AfxControlBar140su' 'Toolbox'

    'Afx:000000013F110000:b:0000000000010003:0000000000000000:0000000000000000' 'Toolbox'

      'AfxWnd140su' ''

        'ToolbarWindow32' ''

...

  'MDIClient' ''

    'Afx:000000013F110000:b:0000000000010003:0000000000000006:00000000000306BA' 'EURUSD,H1'

      'AfxFrameOrView140su' ''

        'Edit' '0.00'

    'Afx:000000013F110000:b:0000000000010003:0000000000000006:00000000000306BA' 'XAUUSD,Daily'

      'AfxFrameOrView140su' ''

        'Edit' '0.00'

    'Afx:000000013F110000:b:0000000000010003:0000000000000006:00000000000306BA' 'EURUSD,M15'

      'AfxFrameOrView140su' ''

        'Edit' '0.00'