请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化平台 帖子:3364755 新帖:19

通过动态链接库(DLL)管理 MetaTrader 终端

有事您说话发表于:4 月 17 日 20:11回复(1)

任务定义

我们有一个包含四个以上传输地址的 MetaQuotes ID 列表. 我们知道, SendNotification函数只是使用"选项"窗口中"通知"页面设置的ID. 这样, 您使用MQL发推送通知到指定ID的时候, 每次不能发超过4个. 让我们尝试改掉这一点.

这个问题可以通过两种方法解决 – 我们可以从头开始开发一个通知传输函数, 或者修改终端的设置, 然后使用标准函数. 第一个选择是非常耗费时间的并且不具备通用性. 所以, 我选择的是第二个方法. 反过来, 终端设置的修改也有各种方法. 根据我的经验, 这可以通过用户界面或者通过替换处理内存中的数值来实现. 操作内存看起来更好一些, 因为这可以避免用户看到弹出的窗口. 但是, 如果出了一点轻微的错误, 它就可能打断整个终端的操作. 而通过用户界面做, 可能发生的最糟的事情就是某个窗口或者按钮消失不见.

在这篇文章中, 我们会尝试使用辅助的动态链接库(DLL), 通过用户界面来管理终端. 特别是我们将考虑修改设置. 和终端的互操作将使用通用的方法, 即使用窗口和相关组件. 对终端进程不会产生影响. 这种方法也可以用于解决其它问题.


1. 创建一个动态链接库(DLL)

在这里, 我们将主要集中在WinAPI的处理上. 所以, 首先让我们大致看看Delphi中是怎样开发动态链接库的.

library Set_Push;

uses
  Windows,
  messages,
  Commctrl,
  System.SysUtils;

var
   windows_name:string;
   class_name:string;
   Hnd:hwnd;


{$R *.res}
{$Warnings off}
{$hints on}

function FindFunc(h:hwnd; L:LPARAM): BOOL; stdcall;
begin
  ...
end;

function FindFuncOK(h:hwnd; L:LPARAM): BOOL; stdcall;
begin
  ...
end;

function Find_Tab(AccountNumber:integer):Hwnd;
begin
  ...
end;


function Set_Check(AccountNumber:integer):boolean; export; stdcall;
var
  HndButton, HndP:HWnd;
  i:integer;
  WChars:array[0..255] of WideChar;
begin
   ...
end;

function Set_MetaQuotesID(AccountNumber:integer; Str_Set:string):boolean; export; stdcall;
begin
  ...
end;

//--------------------------------------------------------------------------/
Exports Set_Check, Set_MetaQuotesID;

begin
end.

我们可以看到, Set_Check 和 Set_MetaQuotesID 函数将会被输出, 而其他的函数则是为了内部使用的. FindFunc 寻找所需的窗口(下方有描述), 而 Find_Tab 寻找所需的页面. 引入了 Windows, Messages, 和 Commctrl 库是为了使用 WinAPI.


1.1. 使用的工具

解决此项任务的基本原则是在 Delphi XE4 环境下使用 WinAPI. 也可以使用 С++, 因为 WinAPI 的语法几乎是一样的. 寻找组建的名称和类别, 可以通过使用Spy++工具, 它包含在Visual Studio发布中, 也可以使用下文描述的简单枚举.


1.2. 寻找 MetaTrader 窗口

可以通过标题寻找任何程序的窗口 (参见图1).

图 1. 窗口标题

图 1. 窗口标题

我们可以看到, MetaTrader 窗口的标题包含账号, 而根据选择的交易品种和时间框架, 标题本身也会改变. 因而, 搜索只会针对帐号进行. 我们也应该搜索之后出现的"选项(Options)"窗口. 它的标题是不会改变的.

对第一种情况, 我们将使用 EnumWindows 函数, 它使我们可以枚举所有存在的窗口. 处理枚举到的窗口的函数作为参数再传给EnumWindows函数. 在我们的例子中, 它是FindFunc 函数.

function FindFunc(h:hwnd; L:LPARAM): BOOL; stdcall;
var buff: ARRAY [0..255] OF WideChar;
begin
   result:=true;
   Hnd := 0;
   GetWindowText(h, buff, sizeof(buff));
   if((windows_name='') or (pos(windows_name, StrPas(buff))> 0)) then begin
      GetClassName(h, buff, sizeof(buff));
      if ((class_name='') or (pos(class_name, StrPas(buff))> 0)) then begin
         Hnd := h;
         result:=false;
      end;
   end;
end;

让我们仔细研究一下此函数. 函数头部除了函数和变量的名称之外没有什么改变. 每当发现一个新的窗口后, EnumWindows 函数会调用指定的函数, 并把窗口句柄传给它. 如果指定的函数返回 true, 枚举则继续进行. 否则, 它就结束.

使用收到的句柄, 我们可以检查窗口的标题(GetWindowText)和类的名称(GetClassName), 这些信息保存在缓冲区中. 下一步, 我们应该把窗口标题和类与所需的做比较. 如果匹配上的话, 我们记录下句柄(这是最重要的事情), 并通过返回false来退出枚举.

现在, 让我们注意一下 EnumWindows 函数调用.

windows_name:=IntToStr(AccountNumber);
class_name:='MetaTrader';
EnumWindows(@FindFunc, 0);

此处我们应该使用所需的类名和窗口标题进行赋值. 现在, 让我们调用函数以枚举所有可用窗口. 结果, 我们在Hnd全局变量中获得了主窗口的句柄.

往前看, 让我们研究另外一个窗口搜索函数. 因为我们需要修改终端设置, 我们当然必须面对"选项(Options)"窗口, 它在点击了对应菜单选项后会出现. 有另外一种方法来寻找窗口.

hnd:=FindWindow(nil, 'Options');

类名和窗口标题作为函数的参数, 返回值是所需的句柄, 如果没有找到则返回0. 和之前的例子不同, 此函数寻找的是名字的完全匹配而不是出现某个字符串.


1.3. 处理菜单

和其他组件类似, 获得菜单的父句柄(某个窗口)后, 就可以对之进行操作了. 然后, 我们应该找到对应菜单项和子菜单然后进行一个选择.

请注意: 根据图表窗口是否展开, 终端菜单项的数量会有所改变(参见图 2). 菜单项的枚举从0开始.

图 2. 菜单项数量的改变

图 2. 菜单项数量的改变

如果菜单项的数量改变了, "工具(Tools)"菜单项的索引编号也会发生改变. 所以, 我们应该使用GetMenuItemCount(Hnd:HMenu) 函数取得菜单项的总数, 传入菜单的句柄.

让我们看看下面的例子:

function Find_Tab(AccountNumber:integer; Language:integer):Hwnd;
var
  HndMen :HMenu;
  idMen:integer;
  ...
begin
   ...
   //_____处理菜单________
   HndMen:=GetMenu(Hnd);
   if (GetMenuItemCount(HndMen)=7) then
      HndMen:=GetSubMenu(HndMen,4)
   else
      HndMen:=GetSubMenu(HndMen,5);
   idMen:=GetMenuItemID(HndMen,6);
   if idMen<>0 then begin
      PostMessage(Hnd,WM_COMMAND,idMen,0);
      ...

在这个例子中, 我们根据父窗口找到主菜单的句柄. 然后, 我们根据菜单句柄找到相应的子菜单. 子菜单的索引值作为GetSubMenu函数的第二个参数. 然后, 我们就能够找到所需的子菜单了. 为了进行选择, 我们需要发送一条对应的消息. 在发送完消息之后, 我们必须等待"选项"窗口.

for i := 0 to 10000 do 
   hnd:=FindWindow(nil, 'Options');

不推荐使用无限的循环, 因为这可能导致终端在关闭了窗口后崩溃, 尽管此程序的执行比较快.


1.4. 寻找组件

我们已经获得了选项窗口, 现在我们需要寻找它的组件, 或者(使用WinAPI名词)子窗口.. 但是首先我们应该使用句柄找到它们. 使用"子窗口"的名词是有原因的, 因为我们寻找它们的方式和找窗口一样.

windows_name:='ОК';
class_name:='Button';
EnumChildWindows(HndParent, @FindFunc, 0);

或者

Hnd:=FindWindowEx(HndParent, 0, 'Button', 'OK');

就这样, 我们已经看到搜索组件实例的主要部分了. 在这个阶段, 我们还没有遇到特别复杂的部分, 除了函数名字要改, 另外要多传一个父句柄参数. 如果您需要知道组件的名称或者类名这些搜索时需要的特性, 那么经常会遇到困难. 如果出现那种情况, Spy++ 工具可能会有所帮助, 也可以把父窗口的所有子组件枚举出来并显示它们的名称. 为了做到这一点, 我们需要稍微修改一下传入参数的函数(FindFunc) - 在任何情况下都把返回值设为true, 并保存窗口名称和它们类的名称(例如, 把它们保存到文件中).

让我们查看一个组件的搜索特性: ОК 是一个系统按钮. 这说明按钮的文字在英文Windows操作系统的拉丁字母, 而在俄文版中是西里尔字母. 因而, 这种解决方案不是通用的.

搜索是基于名称长度是两个字母的事实(至少使用拉丁字母和西里尔字母的如此)来做的. 这样就使函数库更加多用途一些. 这种情况下的搜索函数看起来如下:

function FindFuncOK(h:hwnd; L:LPARAM): BOOL; stdcall;
var buff: ARRAY [0..255] OF WideChar;
begin
   result:=true;
   Hnd := 0;
   GetClassName(h, buff, sizeof(buff));
   if (pos('Button', StrPas(buff))> 0) then begin
      GetWindowText(h, buff, sizeof(buff));
      if(Length(StrPas(buff))=2) then  begin
         Hnd := h;
         result:=false;
      end;
   end;
end;

相应地, 寻找OK按钮按如下方式进行:

EnumChildWindows(HndParent, @FindFuncOK, 0);


1.5. 处理组件

我们应该在操作后得到以下窗口(图 3):

图 3. 选项窗口

图 3. 选项窗口

页面控件(TabControl)

窗口包含了多个页面, 我们不能确认选择的是哪一个. 负责页面管理的组件是TabControl, 或者按类名来说, 是 SysTabControl32. 让我们搜索它的句柄. 选项窗口作为它的父窗口:

Hnd:=FindWindowEx(Hnd, 0, 'SysTabControl32', nil);

然后, 我们对它发送一个页面改变的消息:

SendMessage(Hnd, TCM_SETCURFOCUS, 5, 0);

在上图中, 5是所需页面(通知)的索引编号. 现在, 我们可以寻找所需的页面:

Hnd:=GetParent(Hnd);
Hnd:=FindWindowEx(Hnd, 0, '#32770', 'Notifications');

选项窗口用作活动页面的父窗口. 因为我们有了TabControl的句柄, 我们可以得到它的父窗口. 之后, 我们就可以找到需要的页面了. 相应地, 页面的类是 "#32770".


复选框(CheckBox)

我们可以看到, 选项窗口有"允许推送通知(Enable Push Notifications)"的选项. 当然, 我们不能指望用户把每一点都设置正确. 负责允许/禁止的组件属于按钮(Button)类, 有专门为这类组件设计的消息.

首先, 让我们搜索组件. 通知(Notifications)页面作为它的父窗口. 如果找到了这个组件, 检查是否允许通知(选项是否被选中). 如果没有, 就把它选上. 所有对这些对象的操作都通过发送消息来实现.

Hnd:=FindWindowEx(Hnd, 0, 'Button', 'Enable Push Notifications');
if(Hnd<>0) then begin
   if (SendMessage(Hnd,BM_GETCHECK,0,0)<>BST_CHECKED) then
      SendMessage(Hnd,BM_SETCHECK,BST_CHECKED,0);
         ...


文本框(Edit)

这个组件是用于输入 MetaQuotes ID 地址的栏位. 它的父窗口也是通知(Notifications)页面, 它的类名是 Edit. 操作原则还是一样的 - 找到组件并发送消息.

Hnd:=FindWindowEx(Hnd, 0, 'Edit', nil);
if (Hnd<>0) then begin
   SendMessage(Hnd, WM_Settext,0,Integer(Str_Set));

其中 Str_Set 是一个 字符串 地址的列表.


按钮(Button)

现在, 让我们检查选项窗口底部的标准"确定(OK)"按钮. 这个组件不属于任何页面, 说明它的父窗口是选项窗口本身. 在完成所有必要的操作后, 我们需要给它发送一个按下按钮的消息.

EnumChildWindows(HndParent, @FindFuncOK, 0);
I:=GetDlgCtrlID(HndButton);
if I<>0 then begin
   SendMessage(GetParent(HndButton),WM_Command,MakeWParam(I,BN_CLICKED),HndButton);
   ...


2. 在 MQL4 中创建一个脚本程序

我们工作的成果是一个动态链接库(DLL), 它有两个外部输出函数: Set_Check 和 Set_MetaQuotesID, 分别表示允许发送推送通知以及从对应列表在栏位中填充 MetaQuotes ID地址. 如果在函数中所有的终端窗口和组件都找到了, 他们返回true. 现在, 让我们看如何在脚本程序中使用它们.

//+------------------------------------------------------------------+
//|                                                    Send_Push.mq4 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict
#property show_inputs

#import "Set_Push.dll"
bool Set_Check(int);
bool Set_MetaQuotesID(int,string);
#import

extern string text="test";
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   if(StringLen(text)<1)
     {
      Alert("错误: 没有可供发送的文字"); return;
     }
   if(!Set_Check(AccountNumber()))
     {
      Alert("错误: 未能允许发送推送通知. 检查终端的语言设置"); return;
     }
   string str_id="1C2F1442,2C2F1442,3C2F1442,4C2F1442";
   if(!Set_MetaQuotesID(AccountNumber(),str_id))
     {
      Alert("错误: dll 执行错误!也许进程有干扰"); return;
     }
   if(!SendNotification(text))
     {
      int Err=GetLastError();
      switch(Err)
        {
         case 4250: Alert("等待: 发送失败 ", str_id); break;
         case 4251: Alert("错误: 无效的消息文字 ", text); return; break;
         case 4252: Alert("等待: ID列表无效 ", str_id); break;
         case 4253: Alert("错误: 请求过于频繁!"); return; break;
        }
     }
  }
//+------------------------------------------------------------------+


结论

我们已经看到了通过动态链接库(DLL)来管理终端窗口的基本原则, 它可以使我们更加有效地使用所有终端的特性. 但是, 请注意这种方法只能作为通常方法无法解决情况下的最后选择, 因为它有一些缺点, 包括依赖选择的终端语言, 用户的干扰以及实现的复杂性. 如果被错误使用, 它可能引起致命的错误甚至程序崩溃.

全部回复

0/140

量化课程

    移动端课程