Die Verwaltung des Handelsterminals MetaTrader via DLL

Sofiia Butenko | 29 April, 2016

Definition der Aufgabenstellung

Wir verfügen über eine MetaQuotes-ID-Liste, die mehr als vier Zustellungsadressen enthält. Wie wir bereits wissen, benutzt die Funktion SendNotification lediglich diejenigen IDs, die im Benachrichtigungs-Reiter des Optionsfensters eingestellt worden. Daher können Sie Push-Nachrichten lediglich an die voreingestellten IDs verschicken, und mithilfe von MQL auch nur eine Maximalzahl von vier eingestellten Adressen bedienen. Wir werden versuchen, dieses Problem zu beheben.

Das Problem kann auf zwei verschiedene Arten gelöst werden – wir können entweder die Funktion zur Versendung von Push-Nachrichten von Grund auf neu erstellen, oder wir können die Einstellungen im Terminal verändern und zum Versand die Standardfunktion verwenden. Die erste Option ist ziemlich zeitaufwendig und auch nicht universell einsetzbar. Daher habe ich mich für die zweite Option entschieden. Die Einstellungen des Terminals können ebenfalls auf mehrere verschiedene Arten verändert werden. Meine Erfahrung lehrt mich, dass man dies entweder über die Benutzerschnittstelle tun kann, oder indem man einzelne Werte im Prozess-Speicher verändert. Die Arbeit mit den Werten im Prozess-Speicher erscheint mir als viel lohnender, weil es nämlich den Benutzern gestattet, Popup-Fenster zu vermeiden. Allerdings kann ein Fehler dabei auch dazu führen, dass das gesamte Terminal kompromittiert wird und es seine Arbeit einstellt. Das Schlimmste, was bei der Arbeit mit der Benutzeroberfläche passieren kann, ist, dass ein Fenster oder eine Schaltfläche plötzlich verschwindet.

In diesem Artikel werden wir die Möglichkeiten erörtern, wie man das Terminal mithilfe der Benutzeroberfläche verwalten kann, indem man eine spezielle DLL-Bibliothek verwendet. Insbesondere werden wir uns dabei überlegen, wie man die Voreinstellungen ändern kann. Die Interaktion mit dem Terminal wird in der üblichen Art und Weise erfolgen, was bedeutet, dass wir die ganz normalen Fenster und Komponenten verwenden. Es wird keinerlei Manipulation des Terminalprozesses stattfinden. Diese Methode kann ebenfalls dazu angewendet werden, um andere Problemstellungen zu lösen.


1. Die Erstellung einer DLL

Wir werden uns hierbei hauptsächlich auf die Arbeit mit WinAPI konzentrieren. Lassen Sie uns also einmal ganz kurz ausloten, wie eine dynamische Bibliothek mithilfe von Delphi entwickelt werden kann.

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.

Wie wir sehen können, sollen die Funktionen Set_Check und Set_MetaQuotesID exportiert werden, während die anderen für den internen Gebrauch dienen sollen. FindFunc sucht nach einem notwendigen Fenster (siehe unten), während Find_Tab nach einem notwendigen Reiter sucht. Fenster, Nachrichten und die Bibliothek Commctrl werden aktiviert, um WinAPI verwenden zu können.


1.1. Angewandte Werkzeuge

Das grundlegende Prinzip für die Lösung dieser Aufgabe besteht in der Verwendung von WinAPI in der Umgebung von Delphi XE4 . С++ kann ebenfalls verwendet werden, weil die Syntax von WinAPI dazu fast identisch ist. Die Suche nach Komponentennamen und Klassen kann entweder mithilfe von Spy++ erfolgen – einem Werkzeug, welches sich im Paket von Visual Studio befindet –, oder ganz einfach durch eine Auflistung, wie sie unten beschrieben wird.


1.2. Die Suche nach MetaTrader-Fenstern

Jedes Programmfenster kann gemäß seinem Titel identifiziert werden (siehe Fig. 1).

Fig. 1. Fenstertitel

Fig. 1. Window title

Wie wir sehen können, enthält das Fenster von MetaTrader die Handelskontonummer, während der Fenstertitel selbst verändert wird, abhängig vom ausgewählten Symbol und Zeitrahmen. Daher wird die Suche lediglich nach der Kontonummer forschen. Wir sollten ebenfalls das Optionsfenster finden, welches nachher erscheint. Dieses verfügt über einen stets gleichbleibenden Titel.

Im ersten Fall werden wir die Funktion EnumWindows benutzen, was es uns erlaubt, sämtliche verfügbaren Fenster aufzulisten. Die Funktion für die Verarbeitung der aufgelisteten Fenster wird als Parameter von EnumWindows weitergereicht werden. In unserem Falle handelt es sich hierbei um die Funktion 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;

Lassen Sie uns diese Funktion detaillierter analysieren. Die Titelleiste der Funktion bleibt unverändert, mit Ausnahme der Namen der Funktion und der Variablen. Sobald ein neues Fenster ausgemacht wurde, ruft die Funktion EnumWindows die spezifizierte Funktion auf und übergibt die Kontrolle über das Fenster an diese neue Funktion. Falls die spezifizierte Funktion den Wert „wahr“ ausgibt, wird die Kontrolle wieder zurückgegeben. Andernfalls ist der Prozess abgeschlossen.

Mithilfe des empfangenen Identifikators können wir den Titel des Fensters identifizieren (GetWindowText), und ebenfalls den Namen der Klasse (GetClassName), indem wir ihn in den Zwischenspeicher kopieren. Als nächstes sollten wir den Fenstertitel und die Klasse mit den Benötigten vergleichen. Falls es eine Übereinstimmung geben sollte, werden wir uns an den Identifikator erinnern (das ist das Allerwichtigste) – und wir werden aus der Auflistung aussteigen, indem wir den Wert „falsch“ zurückmelden.

Jetzt wollen wir den Aufruf der Funktion EnumWindows beschreiben.

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

Hier sollten wir die nötige Klasse angeben, sowie Teilwerte des Fenstertitels. Jetzt möchten wir die Funktion zur Auflistung sämtlicher verfügbaren Fenster aufrufen. Als Resultat erhalten wir den Identifikator des Hauptfensters in der Hnd globalen Variable.

Wir machen weiter und untersuchen noch eine andere Fenster-Suchfunktion. Weil wir die Terminal-Einstellungen verändern müssen, werden wir mit Sicherheit mit dem neuen Options-Fenster arbeiten müssen, welches erscheint, nachdem die adäquate Option im Menü ausgewählt wurde. Es gibt auch noch einen anderen Weg, um dieses Fenster zu finden.

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

Der Name der Klasse und der Titel des Fensters werden als Funktionsparameter benutzt, während der ausgegebene Wert ein benötigter Identifikator ist – oder er ist 0, falls nichts gefunden wurde. Im Gegensatz zum vorher genannten Fall sucht diese Funktion nach exakt übereinstimmenden Namen, statt nach dem Auftreten des Stichworts in einem String.


1.3. Die Arbeit mit einem Menü

Wie bei allen anderen Komponenten funktioniert die Arbeit mit einem Menü dadurch, indem man einen übergeordneten Identifikator (ein bestimmtes Fenster) findet. Danach sollten wir den korrespondierenden Menüeintrag finden und den passenden Untereintrag lokalisieren, und dann unsere Auswahl vornehmen.

Zur Beachtung: Die Anzahl der verfügbaren Menüeinträge des Terminals verändert sich abhängig davon, ob ein Chart-Fenster ausgeklappt wird oder nicht (siehe Fig. 2). Die Auflistung der Einträge erfolgt ausgehend von 0.

Fig. 2. Änderung der Anzahl der Menüeinträge

Fig. 2. Änderung der Anzahl der Menüeinträge

Falls die Anzahl der Menüeinträge verändert wird, so wird die Indexnummer der Werkzeuge ebenfalls verändert. Daher sollten wir die Gesamtzahl der Punkte in unsere Überlegungen miteinbeziehen, indem wir die Funktion GetMenuItemCount(Hnd:HMenu) verwenden, an welche der Menü-Identifikator weitergeleitet wird.

Lassen Sie uns das folgende Beispiel betrachten:

function Find_Tab(AccountNumber:integer; Language:integer):Hwnd;
var
  HndMen :HMenu;
  idMen:integer;
  ...
begin
   ...
   //_____working in the menu________
   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);
      ...

In diesem Beispiel lokalisieren wir den Identifikator des Hauptmenüs mithilfe des übergeordneten Identifikators. Danach finden wir das passende Untermenü mithilfe des Menü-Identifikators. Die Indexnummer des Untermenüs wird als der zweite Parameter der Funktion GetSubMenu verwendet. Danach lokalisieren wir den passenden Untermenü-Eintrag. Um unsere Auswahl vorzunehmen, müssen wir eine geeignete Nachricht senden. Nachdem wir die Nachricht versandt haben, müssen wir auf das Optionsfenster warten.

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

Das Einstellen einer unendlichen Schleife wird nicht empfohlen, weil es nämlich zu einem Crash des Terminals führen kann, nachdem das Fenster geschlossen wurde – trotz der schnellen Ausführungsgeschwindigkeit des Programms.


1.4. Die Suche nach Komponenten

Wir haben jetzt das Optionsfenster gefunden, und wir müssen seine einzelnen Komponenten adressieren, oder (mithilfe der Mittel von WinAPI) seine Unterfenster. Aber zunächst müssen wir diese finden, indem wir den Identifikator verwenden. Wir benutzen den Terminus Unterfenster aus einem guten Grund, weil wir nämlich nach diesen Unterfenstern auf dieselbe Art und Weise suchen wie nach richtigen Fenstern.

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

or

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

Jetzt haben wir also die Hauptbeispiele für die Suche nach Komponenten gesehen. Zu diesem Zeitpunkt sind wir noch auf keine größeren Schwierigkeiten gestoßen, abgesehen von veränderten Funktionsnamen und der zusätzlichen Angabe des übergeordneten Identifikators. Man stößt allerdings meist dann auf Schwierigkeiten, wenn man versucht, die Besonderheiten der einzelnen Komponenten zu berücksichtigen, sobald man die Namen der Komponenten oder Klassen kennt, mit deren Hilfe die Suche durchgeführt wird. In diesem Fall kann das kleine Werkzeug Spy++ eine große Hilfe sein, und ebenso die Auflistung sämtlicher übergeordneten Fensterkomponenten, gefolgt von einer Anzeige all ihrer Werte. Um dieses Ziel zu erreichen, müssen wir die weitergeleitete Funktion (FindFunc) ein wenig verändern – wir setzen den ausgegebenen Wert in allen Fällen auf „wahr“ und speichern die Namen der Fenster und ihrer Klassen ab (beispielsweise sichern wir diese in einer Datei).

Lassen Sie uns jetzt eine der Suchfunktionen der Komponente analysieren: ОК ist eine System-Schaltfläche. Dies bedeutet, dass der Text dieser Schaltfläche in lateinischen Buchstaben dargestellt wird, zumindest in der englischen Version des Betriebssystems Windows, während er in der russischen Version von Windows in kyrillischen Buchstaben dargestellt wird. Daher ist diese Lösung nicht universal einsetzbar.

Die Suche basiert auf der Tatsache, dass die Länge des Namens (dies gilt wenigstens für Sprachen mit lateinischen oder kyrillischen Buchstaben) aus zwei Buchstaben besteht. Dies sorgt bereits dafür, dass die Bibliothek universeller einsetzbar ist. Die Suchfunktion schaut in diesem Fall wie folgt aus :

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;

Dementsprechend funktioniert die Suche nach der Schaltfläche OK wie folgt :

EnumChildWindows(HndParent, @FindFuncOK, 0);


1.5. Die Arbeit mit Komponenten

Infolge unserer Aktionen sollten wir jetzt das folgende Fenster zu sehen bekommen (Fig. 3):

Fig. 3. Optionsfenster

Fig. 3. Optionsfenster

TabControl

Dieses Fenster enthält mehrere verschiedene Reiter – und wir können uns dabei nicht sicher sein, dass der erforderliche Reiter auch tatsächlich ausgewählt wurde. Die Komponente, welche für die Auswahl der Reiter erforderlich ist, nennt sich TabControl – oder in diesem speziellen Fall SysTabControl32, wie es in ihrer Klasse auch angegeben ist. Lassen Sie uns also nach ihrem Identifikator suchen. Das Optionsfenster wird als das übergeordnete Fenster verwendet:

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

Danach senden wir eine Nachricht zur Veränderung des Reiters an diese Komponente:

SendMessage(Hnd, TCM_SETCURFOCUS, 5, 0);

Im obigen Beispiel stellt die 5 eine Indexzahl für den benötigten Reiter dar (Benachrichtigungen). Jetzt können wir nach dem benötigten Reiter suchen:

Hnd:=GetParent(Hnd);
Hnd:=FindWindowEx(Hnd, 0, '#32770', 'Notifications');
Das Optionsfenster wird als ein übergeordnetes Fenster für einen aktiven Reiter verwendet. Weil wir über den Identifikator von TabControl verfügen, nehmen wir den Identifikator des übergeordneten Fensters. Danach wird die Suche nach dem erforderlichen Reiter ausgeführt. Demzufolge ist die Klasse des Reiters "#32770".


CheckBox

Wie wir sehen können, verfügt das Optionsfenster über die Option "Enable Push Notifications". Natürlich sollten wir nicht erwarten, dass ein normaler Benutzer jetzt alles korrekt eingestellt hat. Die Komponente, welche für die Aktivierung bzw. Deaktivierung verantwortlich ist, verfügt über die Klasse „Button“, und es gibt Nachrichten, welche speziell für diesen Komponententyp entworfen worden.

Lassen Sie uns zunächst nach der Komponente suchen. Der Benachrichtigungsreiter fungiert als das übergeordnete Fenster. Wenn Sie die Komponente gefunden haben, dann überprüfen Sie bitte, ob Benachrichtigungen zugelassen sind (d.h., ob die Option markiert wurde oder nicht). Falls sie nicht markiert wurde, dann markieren Sie sie bitte. Sämtliche Aktionen mit diesem Objekt werden durch das Senden von Nachrichten durchgeführt.

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);
         ...


Editieren

Diese Komponente ist ein Feld zur Eingabe von MetaQuotes-ID-Adressen. Das übergeordnete Fenster dieser Funktion ist der Benachrichtigungs-Reiter, während ihre Klasse die Klasse Edit ist. Das Arbeitsprinzip ist dabei dasselbe – finden Sie die Komponente und senden Sie eine Nachricht.

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

wobei Str_Set eine Liste von String -Adressen darstellt.


Schaltfläche

Jetzt möchten wir einmal einen Blick auf den standardmäßigen OK-Knopf am unteren Ende des Optionsfensters werfen. Diese Komponente gehört nicht zu irgendeinem Reiter, was bedeutet, dass das übergeordnete Fenster für diese Komponente das Fenster selbst ist. Nachdem wir alle erforderlichen Aktionen durchgeführt haben, sollten wir die Nachricht, dass die Schaltfläche gedrückt wurde, an diese Komponente senden.

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


2. Die Erstellung eines Skriptes in MQL4

Das Ergebnis unserer Arbeit ist eine DLL, die über zwei externe Funktionen verfügt, Set_Check und Set_MetaQuotesID, welche es uns ermöglichen, Push-Nachrichten zu versenden und das Feld mit dem MetaQuotes-ID-Adressen aus der Liste ganz nach unserem Bedarf auszufüllen. Falls sich sämtliche Terminal-Fenster und Komponenten in den Funktionen finden, dann melden sie „wahr“ . Jetzt möchten wir uns einmal ansehen, wie man das in einem Skript verwenden kann.

//+------------------------------------------------------------------+
//|                                                    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("Error: No text to send"); return;
     }
   if(!Set_Check(AccountNumber()))
     {
      Alert("Error: Failed to enable sending push. Check the terminal language"); return;
     }
   string str_id="1C2F1442,2C2F1442,3C2F1442,4C2F1442";
   if(!Set_MetaQuotesID(AccountNumber(),str_id))
     {
      Alert("Error: dll execution error! Possible interference with the process"); return;
     }
   if(!SendNotification(text))
     {
      int Err=GetLastError();
      switch(Err)
        {
         case 4250: Alert("Waiting: Failed to send ", str_id); break;
         case 4251: Alert("Err: Invalid message text ", text); return; break;
         case 4252: Alert("Waiting: Invalid ID list ", str_id); break;
         case 4253: Alert("Err: Too frequent requests! "); return; break;
        }
     }
  }
//+------------------------------------------------------------------+

Schlussfolgerung

Wir haben die grundlegenden Prinzipien der Verwaltung der Terminalfenster mithilfe von DLLs kennengelernt – und dies versetzt uns in die Lage, sämtliche Terminal-Funktionen viel effizienter zu nutzen. Allerdings sollten Sie hierbei bitte beachten, dass diese Methode lediglich als letzter Notnagel zum Einsatz kommen sollte – d.h. immer dann, falls ein Problem nicht mit konventionellen Methoden gelöst werden kann. Diese Methode verfügt nämlich über einige inhärente Nachteile, wie zum Beispiel die Abhängigkeit von der gewählten Terminal-Sprache, die möglichen Benutzer-Interaktionen und die Komplexität der Implementierung. Falls es bei der Anwendung der gezeigten Methode zu Fehlern kommen sollte, dann kann dies zu fatalen Programmfehlern führen, bis hin zum Absturz des Programms.