DLL 连接细节

下列实体不能作为参数传递给从 DLL导入的函数:

  • 类(对象和指向它们的指针)
  • 包含动态数组、字符串、类和其他复杂结构的结构
  • 字符串数组或上述复杂对象

所有简单类型参数都是通过值传递的,除非明确声明它们是通过引用传递的。传递一个字符串时,传递被复制字符串的缓冲区地址;如果字符串是通过引用传递的,那么这个特定字符串的缓冲区地址将被传递给从 DLL 导入的函数,而不进行复制。

将数组传递给 DLL 时,始终传递数据缓冲区开头的地址(不考虑 AS_SERIES 标志)。DLL 内部的函数对 AS_SERIES 标志一无所知,传递的数组是一个长度未知的数组,因此需要一个附加参数来指定它的大小。

在描述导入函数的原型时,可以使用带有默认值的参数。

导入 DLL 时,应在特定 MQL 程序的特性中或在终端的常规设置中授予使用它们的权限。对此,在 权限 一节中,我们演示了 EnvPermissions.mq5,它具有一个使用系统 DLL 读取 Windows 系统剪贴板内容的函数。这个函数是可选的:它的调用被注释掉了,因为当时我们不知道如何使用库。现在,我们将它转移到一个单独的脚本 LibClipboard.mq5

运行该脚本可能会提示用户进行确认(出于安全原因,默认情况下 DLL 是禁用的)。如有必要,可在对话框中的“依赖项”选项卡上启用该选项。

头文件 MQL5/Include/WinApi 目录中提供,该目录还包括 #import 指令,用于非常需要的系统功能,如剪贴板管理(openclipboardGetClipboardDataCloseClipboard)、内存管理(GlobalLockGlobalUnlock、Windows 窗口以及许多其他函数。我们将只包括两个文件:winuser.mqhwinbase.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

此外,我们还从 kernel32.dll 库导入 lstrcatW 函数,因为我们不满意 winbase.mqh 默认提供的说明:这给了函数第二个原型,适用于在第一个参数中传递 PVOID 值。

#include <WinApi/winuser.mqh>
#include <WinApi/winbase.mqh>
   
#define CF_UNICODETEXT 13 // one of the standard exchange formats - Unicode text
#import "kernel32.dll"
string lstrcatW(PVOID string1const string string2);
#import

使用剪贴板的本质是使用 OpenClipboard 捕获对剪贴板的访问权限,之后你应获得一个数据句柄 (GetClipboardData),将其转换为内存地址 (GlobalLock),最后将数据从系统内存复制到你的变量 (lstrcatW)。然后,以相反的顺序释放被占用的资源(GlobalUnlockCloseClipboard)。

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 导入的函数须遵循 Windows API 函数的二进制可执行链接约定。为了确保这种约定,编译器特定的关键字被用在程序的源文本中,例如 C 或 C++ 中的 __stdcall。这些链接规则意味着:

  • 调用函数(在我们的例子中,是 MQL 程序)必须看到被调用(从 DLL 导入)函数的原型,以便正确地将参数堆叠到栈上。
  • 调用函数(在我们的例子中,是 MQL 程序)以相反的顺序从右到左堆叠参数 - 导入函数就是按这个顺序读取传递给它的参数。
  • 参数通过值传递,除了那些通过引用显式传递的参数(在我们的例子中是字符串)。
  • 导入的函数读取传递给它的参数并清除堆栈。

下面是另一个使用 DLL 的脚本示例 –LibWindowTree.mq5。它的任务是遍历所有终端窗口的树,并获得它们的类名(根据使用 WinApi 在系统中的注册)和标题。这里的窗口指的是 Windows 界面的标准元素,也包括控件。这个过程对于自动化使用终端非常有用:模拟窗口中的按钮按压,通过 MQL5 切换不可用的模式,等等。

为了导入所需的系统函数,我们包含使用 user32.dll 的头文件 WinUser.mqh

#include <WinAPI/WinUser.mqh>

你可以使用 GetClassNameWGetWindowTextW 函数获得窗口类的名称,在 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 个字节),这是当今最常用的格式(ANSI 字符串中的 A 后缀仅在向后兼容旧库时才有意义)。

给定一个 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)
{
   // request first child window (if any)
   HANDLE child = FindWindowExW(wNULLNULLNULL);
   while(child)          // oop while there are child windows
   {
      string clazztitle;
      GetWindowData(childclazztitle);
      Print(StringFormat("%*s"level * 2""), "'"clazz"' '"title"'");
      TraverseDown(childlevel + 1);
      // requesting next child window
      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'