English Русский 中文 Español Deutsch Português
MQLプログラムのグラフィカルインターフェイスのマークアップツールとしてのMQL 第1部

MQLプログラムのグラフィカルインターフェイスのマークアップツールとしてのMQL 第1部

MetaTrader 5 | 9 7月 2020, 11:12
1 028 0
Stanislav Korotky
Stanislav Korotky

そもそもMQLベースのプログラムにグラフィカルなウィンドウ・インターフェースが必要であること、 それついての合意が不足しています。 トレーダーの夢は、トレーディングロボットを最もシンプルな方法で利用し、魔法のように「資金を稼ぐ」ことです。 一方、夢のような話なので、現実とはかけ離れていますが、通常、システムが動作するまでには、長い時間をかけて、苦心して、さまざまな設定を選択しなければなりませんが、その後も、必要に応じて、手動で操作し、修正しなければなりません。 裁量トレード側が何もサポートしてくれない場合には、快適で直感的なトレードパネルを作る選択をするということは"戦い"です。 一般的には、一つの形であるウィンドウ・インターフェースは、すぐに必要になると言っていいでしょう。

GUIマークアップ技術の紹介

グラフィカル・インターフェースを構築するために、メタトレーダーは、チャート上に配置される独立したオブジェクトとして、また標準ライブラリの「コントロール」に包まれたものとして、リクエストの高いコントロール要素を提供します。 GUIを構築するための代替ソリューションもあります。 しかし、ライブラリはすべて、要素のレイアウトにはほとんど触れていません。

もちろん、誰かがメタトレーダーと同等のウィンドウをチャートに描画するというアイデアにぶつかることはまれですが、一見シンプルなトレードパネルであっても、何十もの「コントロール」で構成されていることがあり、MQLからどれを制御するかは真の単調さに変わります。

レイアウトは、インターフェイス要素の配置や属性を記述する統一された方法に基づいて自動的にウィンドウを作成し、制御コードにリンクすることを保証することができます。

MQLの標準インスタンスでインターフェースがどのように作られているかを覚えておきましょう。

  bool CPanelDialog::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    if(!CAppDialog::Create(chart, name, subwin, x1, y1, x2, y2)) return(false);
    // create dependent controls
    if(!CreateEdit()) return(false);
    if(!CreateButton1()) return(false);
    if(!CreateButton2()) return(false);
    if(!CreateButton3()) return(false);
    ...
    if(!CreateListView()) return(false);
    return(true);
  }
  
  bool CPanelDialog::CreateButton2(void)
  {
    // coordinates
    int x1 = ClientAreaWidth() - (INDENT_RIGHT + BUTTON_WIDTH);
    int y1 = INDENT_TOP + BUTTON_HEIGHT + CONTROLS_GAP_Y;
    int x2 = x1 + BUTTON_WIDTH;
    int y2 = y1 + BUTTON_HEIGHT;
  
    if(!m_button2.Create(m_chart_id, m_name + "Button2", m_subwin, x1, y1, x2, y2)) return(false);
    if(!m_button2.Text("Button2")) return(false);
    if(!Add(m_button2)) return(false);
    m_button2.Alignment(WND_ALIGN_RIGHT, 0, 0, INDENT_RIGHT, 0);
    return(true);
  }
  ...

すべてはオーダー型で行われ、同じ型の多くの呼び出しを使用します。 MQLコードは、要素ごとに繰り返すという点で、長くて非効率的であり、それぞれのケースで独自の定数(エラーの潜在的な原因と考えられるいわゆる「マジックナンバー」)が使用します。 このようなコードを書くことは感謝を欠いたタスクであり(特に、コピー&ペーストエラーは開発者の間で格言となっています)、新しい要素を挿入して古い要素をシフトする必要がある場合は、多くの "マジックナンバー "を手動で再計算して修正しなければならない可能性が高いでしょう。

以下はダイアログクラスでのインターフェース要素の記述がどのようになっているかを示します。

  CEdit        m_edit;          // the display field object
  CButton      m_button1;       // the button object
  CButton      m_button2;       // the button object
  CButton      m_button3;       // the fixed button object
  CSpinEdit    m_spin_edit;     // the up-down object
  CDatePicker  m_date;          // the datepicker object
  CListView    m_list_view;     // the list object
  CComboBox    m_combo_box;     // the dropdown list object
  CRadioGroup  m_radio_group;   // the radio buttons group object
  CCheckGroup  m_check_group;   // the check box group object

このフラットな「コントロール」のリストは長く、レイアウトが提供してくれる視覚的な「ヒント」がなければ、維持することは難しいかもしれません。

他のプログラミング言語では、インターフェイス設計は通常コーディングと切り離されています。 XMLやJSONなどの宣言型言語は、要素のレイアウトを記述するために使用します。

特に、Androidプロジェクトのインターフェイス要素を記述するための基本的な原則は、ドキュメントやtutorials にあります。 大まかな概要を知るには、XMLの一般的な考え方を持っていればいいのです。 このようなファイルでは、階層が明確に存在し、LinearLayoutやRelativeLayoutのようなコンテナ要素や、ImageViewやTextView、CheckBoxのような単一の「コントロール」が定義され、match_parentやwrap_contentのように、コンテンツに合わせて自動的にサイズを調整し、集中的なスタイル記述へのリンクが設定で定義され、イベントプロセッサがオプションで指定されていますが、すべての要素は確実に追加で調整することができ、実行コードから他のイベントプロセッサをそれらにアタッチすることができます。

.Netプラットフォームがわかる場合、また、XAMLを使用してインターフェイスの同様の宣言的な記述を使用します。 C#などのマネージドコードインフラ言語でコーディングしたことがない人でも(実はそのコンセプトはMetaTraderプラットフォームやその「マネージド」MQLとよく似ています)、「コントロール」「コンテナ」「プロパティ」「ユーザーのアクションに対するレスポンス」などのコア要素がオールインワンのようにここにも見えてきます。

なぜレイアウトがコードから分離され、特殊な言語で記述されているのでしょうか。 そのアプローチの基本的なメリットをご紹介します。

  • 要素やコンテナ間の階層的な関係を視覚的に表示します。
  • 論理的なグループ化。
  • レイアウトとアライメントの統一された定義。
  • プロパティとその値を書くことができます。
  • 宣言により、ライフサイクルを維持するコードの自動生成と、要素の作成、設定、データのやり取り、削除などの制御を実装することができます。
  • 一般化された抽象化レベル、すなわち、一般的なプロパティ、状態、初期化/処理フェーズで、コーディング上で独立してGUIを開発することができます。
  • レイアウトの繰り返し使用(複数)、すなわち、同じフラグメントを異なるダイアログに複数回インクルードすることができます。
  • タブ間の切り替えに似た方法で、動的なコンテンツの実装/生成をオンザフライで行い、それぞれに特定の要素のセットを使用します。
  • レイアウト内の「コントロール」を動的に作成し、標準MQLライブラリの場合はCWndのような基本クラスへのポインタの単一配列に保存します。
  • インタラクティブ・インターフェース・デザインに特定のグラフィック・エディタを使用します - この場合、レイアウトを記述する特別なフォーマットは、プログラムの外部表現とプログラミング言語の実行部分との間の接続リンクとして機能します。

MQL環境の場合、問題を解決するために、ほんの数ショットを行っただけです。 特に、ビジュアルダイアログデザイナーについては、オブジェクトクラスの設計と構築方法の記事で紹介しています。 これはMAterWindows ライブラリをベースに動作します。 しかし、レイアウトの配置方法やサポートされている要素タイプのリストはかなり限定されています。

ビジュアルデザイナーがいないそれにも関わらず、より高度なレイアウトシステムは GUIコントロールのためのレイアウトとコンテナの使用 CBox クラスCGrid クラス の記事で提案されています。 CWndObjやCWndContainerから継承した標準の制御要素やその他の要素をすべてサポートしていますが、コンポーネントの作成や配置を目的としたルーチンコーディングはユーザに任せています。

概念的には、コンテナを使ったこのアプローチは高度なものです(実質的にすべてのマークアップ言語でマークアップしていることを述べれば十分でしょう)。 つまり、その点に気をつける必要があります。 以前の記事(トレードにおけるOLAPの適用(その2)。インタラクティブな多次元データ分析結果の可視化)では、コンテナCBoxとCGridの修正と、"ラバー "プロパティをサポートするためのコントロール要素を提案しました。 以下では、その際の開発を利用して、標準ライブラリのオブジェクトに代表される要素を自動的に配置するという問題を解決するために、改良していきます。

インターフェイスのグラフィックエディタ。プロとコントラ

グラフィックインターフェースエディタの主な機能は、ユーザーのコマンドにより、ウィンドウ内の要素のプロパティをその場で作成・設定することです。 インプットフィールドを使用してプロパティを選択することを提案します。 したがって、すべての "コントロール "は、相互に関連した2つのバージョンを持たなければなりません。いわゆるランタイムのもの(標準的な操作のもの)と、デザインタイムのもの(データのやり取り的にインターフェイスをデザインするためのもの)です。 "Controls "は、デフォルトでは最初のものがある - Windowsで動作するクラスです。 2つ目のバージョンは、「コントロール」のラップであり、その利用可能なプロパティを変更することを目的とします。 要素の種類ごとにこのようなラップを書くのは大変だと思います。 したがって、この処理を自動化することが望ましい。 理論的には、MQL による MQLパーシング という記事で説明されている MQL パーサを使用することができます。 多くのプログラミング言語では、言語構文にプロパティの概念を入れて、オブジェクトの特定の内部フィールドの「セッター」と「ゲッター」を組み合わせています。 MQLには今のところありませんが、標準ライブラリのウィンドウクラスでも同様の原理が使われています。同じフィールドを設定したり読み込んだりするために、同じ名前の「ミラー」メソッドのペアが使われます。 例えば、CEditインプットフィールドの "ReadOnly "プロパティはこのように定義されています。

    bool ReadOnly(void) const;
    bool ReadOnly(const bool flag);

そして、このようにしてCSpinEditの上限でのタスクを可能にします。

    int  MaxValue(void) const;
    void MaxValue(const int value);

MQL パーサーを使用して、各クラスでメソッドのペアを見つけ、継承階層を考慮して一般的なリストにインクルードすることができ、その後、見つけたプロパティをデータのやり取り的に設定して読み込むラッパクラスを生成することができます。 「controls」の各クラスに対して一度だけ行う必要があります(クラスがパブリックプロパティを変更しないことを条件とします)。

大規模なものであっても、実施可能なプロジェクトです。 取り組む前に、すべての長所と短所を考慮する必要があります。

2つのコアな設計目標を強調してみましょう。要素の階層的な依存関係とその特性を特定することです。 これを実現するための代替手段が見つかった場合は、ビジュアルエディタを省略することができます。

意識的に考えてみると、すべての要素の基本的な特性は、タイプ、サイズ、配置、テキスト、スタイル(色)などの標準的なものであることが明らかになります。 また、MQL コードで特定のプロパティを設定することもできます。 ありがたいことに、これらは通常ビジネスロジックに関連する単一の操作です。 タイプ、サイズ、整列に関しては、オブジェクト階層自体によって暗黙的に設定されています。

このように、ほとんどの場合、本格的なエディタの代わりに、インターフェイス要素の階層化を行う便利な方法があれば十分であるという結論に達します。

ダイアログクラス内のすべての制御要素とコンテナは、連続したリストではなく、ネスト/依存性のツリー構造を模したインデントで記述されていると想像してください。

    CBox m_main;                       // main client window
    
        CBox m_edit_row;                   // top level container/group
            CEdit m_edit;                      // control
      
        CBox m_button_row;                 // top level container/group
            CButton m_button1;                 // control
            CButton m_button2;                 // control
            CButton m_button3;                 // control
      
        CBox m_spin_date_row;              // top level container/group
            SpinEdit m_spin_edit;              // control
            CDatePicker m_date;                // control
      
        CBox m_lists_row;                  // top level container/group
      
            CBox m_lists_column1;              // nested container/group
                ComboBox m_combo_box;              // control
                CRadioGroup m_radio_group;         // control
                CCheckGroup m_check_group;         // control
        
            CBox m_lists_column2;              // nested container/group
                CListView m_list_view;             // control

このようにして、構造を見やすくします。 しかし、変更されたフォーマットは、もちろん、オブジェクトを特別な方法で解釈するプログラムの能力には何ら影響を与えません。

理想的には、定義された階層に従って、制御要素が自ら作成され、画面上の適切な場所を見つけ、適切なサイズを計算するようなインターフェースを記述する方法があればと考えています。

マークアップ言語の設計

したがって、ウィンドウ・インターフェースの一般的な構造とその個々の要素の特性を記述するマークアップ言語を開発しなければなりません。 ここでは、広く使われているXMLフォーマットに頼り、関連するタグのセットを予約することができます。 上記のような別のフレームワークから借りることもできます。 しかし、そうすると、XMLをパースしてMQLに解釈し、オブジェクトを作成したり調整したりする操作に変換しなければならなくなります。 また、ビジュアルエディタが不要になったため、エディタと実行環境との通信手段として「外部」マークアップ言語も不要になりました。

そんな状況の中で、あるアイデアが浮かび上がってきます。MQL自体がマークアップ言語として使えるのかどうかという問いです。 それは確かに可能です。

階層構造は、最初はMQLに組み込まれています。 継承したクラスはすぐに思い浮かびます。 しかし、クラスはコードを実行する前に形成された静的な階層を記述します。 しかし、MQLコードが実行されていると解釈できるような階層が必要です。 他のプログラミング言語では、このために、つまりプログラム自体からクラスの階層や内部構造を解析するために、いわゆるランタイム型情報、RTTI、別名reflectionsと呼ばれるツールが組み込まれているものがあります。 しかし、MQLにはそのようなツールはありません。

しかし、MQLには、ほとんどのプログラミング言語と同様に、もう一つの階層があります。つまり、コードフラグメントを実行するコンテキストの階層です。 関数/メソッド(つまり、クラスや構造体を記述するために使用するものを除く)の各中括弧のペアは、コンテキスト、つまりローカル変数の生活領域を形成します。 単位のネストは限定されていないので、ランダムな階層を記述するのに使うことができます。

同様のアプローチはすでに MQL で使われており、特にコードの実行速度を測定する自作プロファイラを実装しています (MQL の OOP ノートを参照してください。静的オブジェクトと自動オブジェクトの自作プロファイラを参照してください)。 その動作原理はシンプルです。 応用問題を解く演算と一緒に、コード単位でローカル変数を宣言している場合。

  {
    ProfilerObject obj;
    
    ... // code lines of your actual algorithm
  }

このの場合は、ユニットに入るとすぐに作成され、ユニットから出る前に削除されます。 この動作を考慮できるものを含む、任意のクラスのオブジェクトに当てはまります。 特に、コンストラクタやデストラクタでオーダーの時間をメモしておくことで、適用されたアルゴリズムの持続時間を計算することができます。 当然のことながら、測定値を蓄積するためには、別のより優れた物体、すなわちプロファイラそのものが必要となります。 しかし、ここでは両者間の交換機はあまり重要ではありません(詳細はブログで)。 問題は、レイアウトの記述にも同じ原理を適用することです。 つまり、以下のようになります。

  container<Dialog> dialog(&this);
  {
    container<classA> main; // create classA internal object 1
    
    {
      container<classB> top_level(name, property, ...); // create classB internal object 2
      
      {
        container<classC> next_level_1(name, property, ...); // create classC internal object 3
        
        {
          control<classX> ctrl1(object4, name, property, ...); // create classX object 4
          control<classX> ctrl2(object5, name, property, ...); // create classX object 5
        } // register objects 4&5 in object 3 (via ctrl1, ctrl2 in next_level_1) 
      } // register object 3 in object 2 (via next_level_1 in top_level)
      
      {
        container<classC> next_level2(name, property, ...); // create classC internal object 6
        
        {
          control<classY> ctrl3(object7, name, property, ...); // create classY object 7
          control<classY> ctrl4(object8, name, property, ...); // create classY object 8
        } // register objects 7&8 in object 6 (via ctrl3, ctrl4 in next_level_2)
      } // register object 6 in object 2 (via next_level_2 in top_level)
    } // register object 2 in object 1 (via top_level in main)
  } // register object 1 (main) in the dialog (this)

このコードが実行されると、ダイアログ内で生成される特定のGUI要素のクラスを定義するテンプレート・パラメータを使って、あるクラス(概念的には "コンテナ "という名前)のオブジェクトが生成されます。 すべてのコンテナオブジェクトは、スタックモードの特別な配列に配置されます。各次のネスティングレベルはコンテナを配列に追加し、現在のコンテキストユニットはスタックの一番上にあり、ウィンドウは常に一番下にあります。 各ユニットを閉じると、そのユニットで作成されたすべての子要素は自動的に即時の親(スタックの一番上にある)にバインドされます。

この「マジック」はすべて、「コンテナ」と「コントロール」クラスの内部によって確保されなければなりません。 実際には、同じクラスである「レイアウト」になりますが、見やすくするために、上のチャートではコンテナとコントロールの差を強調します。 実際のところ、差はテンプレートのパラメータでどのクラスが指定されているかに依存しているだけです。 したがって、上の例のクラスDialog、classA、classB、classCはウィンドウコンテナ、つまり「コントロール」を格納することをサポートしていなければなりません。

ここでは、レイアウトの一時的な補助オブジェクト(上では main, top_level, next_level_1, ctrl1, ctrl2, next_level2, ctrl3, ctrl4 と名付けられています)と、よって制御されるインターフェースクラスのオブジェクト(object 1 ... object 8)を区別しなければなりません。 このコードはすべてダイアログメソッド(Createメソッドと同様)として実行されます。 そのため、ダイアログオブジェクトは "this "として利用可能です。

レイアウト・オブジェクトに対しては、GUIオブジェクトをクラス変数(オブジェクト4,5,7,8)として送りますが、レイアウト・オブジェクトに対しては送りません(名前とプロパティが指定されています)。 いずれの場合も、GUIオブジェクトは存在しなければなりませんが、必ずしも明示的に必要とは限りません。 「コントロール」がその後のアルゴリズムとのデータのやり取りに使用する場合は、そのアルゴリズムへのリンクがあると便利です。 コンテナは通常、プログラムのロジックとは関係がなく、「コントロール」を配置する関数を果たすだけなので、レイアウトシステムの内部では非明示的に作成されます。

プロパティを記録する具体的な構文を開発して、少し後にリストアップします。

インターフェースレイアウトのクラスです。抽象レベル

インターフェース要素の階層形成を実装できるクラスを書いてみましょう。 潜在的に、このアプローチは "コントロール "の任意のライブラリに適用することができます。したがって,クラスの集合を2つの部分に分けることにします.抽象的なもの(一般的な関数を持つ)と,標準制御要素の特定のライブラリの特定の側面に関連する応用的なもの(CWndの子孫クラス)です. この概念の実行可能性を標準ダイアログ上で検証し、希望者は抽象層に導かれて他のライブラリに適用することができます。

クラス LayoutDataが中心です。

  class LayoutData
  {
    protected:
      static RubbArray<LayoutData *> stack;
      static string rootId;
      int _x1, _y1, _x2, _y2;
      string _id;
    
    public:
      LayoutData()
      {
        _x1 = _y1 = _x2 = _y2 = 0;
        _id = NULL;
      }
  };

任意のレイアウト要素に固有の、最低限の情報が格納されています。(固有の名前 _id と座標) ちなみに、このフィールド_idは抽象レベルで定義されており、GUIは各ライブラリで独自の "control "プロパティに "表示 "することができます。 特に標準ライブラリでは,このフィールドはm_nameという名前で,パブリックメソッドCWnd::Nameで利用できます. 2つのオブジェクトに対して名前を一致させることはできません。 CWndでは,"long "型のm_idフィールドも定義されています. 適用される実装に来たとき、_idと混同されてはいけません。

また、LayoutDataクラスは、スタックとウィンドウのインスタンス識別子(rootId)としてインスタンスを静的に保存します。 各MQLプログラムは1つのスレッド内で実行されるため、直近の2つのメンバのスタティックは問題ではありません。 複数のウィンドウが入るとしても、一度に1つしか作成できません。 ウィンドウが描画されるとすぐに、スタックはすでに空になり、別のウィンドウで作業する準備ができます。 Windwoの識別子であるrootIdは、クラスCAppDialogのフィールドm_instance_idとして標準ライブラリで知られています。 他のライブラリの場合は、似たようなもの(文字列である必要はありませんが、独自のもの、文字列に変換可能なもの)が必要です。 この問題については、後ほど改めて取り上げたいと思います。

型付けされたLayoutBaseは、クラスLayoutDataの子孫になります。 中括弧の単位をオーダーとしてMQLコードでインターフェイス要素を生成する、まさにレイアウトクラスのプロトタイプです。

  template<typename P,typename C>
  class LayoutBase: public LayoutData
  {
    ...

その2つのテンプレート・パラメータであるPとCは、コンテナや "コントロール "として動作する要素のクラスに関連します。

コンテナは、設計上、「コントロール」および/または他のコンテナを含み、「コントロール」は全体として知覚され、何も含むことができません。 ここでは、「制御」は論理的にモノリシックなインターフェースの単位を意味し、実際には多くの補助的なオブジェクトから構成されることができることに特に注意する必要があります。 特に、標準ライブラリのCListViewやCComboBoxのクラスは「コントロール」ですが、内部では複数のオブジェクトを使って実装されています。 実装の技術的な問題ですが、似たようなタイプの制御要素は、ボタンやテキストが描かれた単一のアウトラインとして他のライブラリに実装することができます。 抽象的なレイアウトクラスの文脈では、カプセル化の原則を破って掘り下げるべきではありませんが、特定のライブラリに設計された応用実装は、もちろん、このニュアンスを考慮しなければなりません(そして、実際のコンテナを複合的な「コントロール」と区別しなければなりません)。

標準ライブラリの場合,テンプレートのパラメータであるPとCの最適な候補はCWndContainerとCWndです. 少し前に飛びますが、多くの "コントロール "はCWndContainerを継承しているので、CWndObjは "コントロール "のクラスとしては使えないかもしれないことに注意しなければなりません。 例えば、CComboBox、CListView、CSpinEdit、CDatePickerなどです。 しかし、パラメータCとして、すべての「コントロール」の中で最も近い共通クラスを選択する必要があり、標準ライブラリではCWndがあたります。 このように、CWndContainerのようなコンテナのクラスは、実際にはシンプルな要素を満たすことができます。したがって、特定のインスタンスがコンテナであるかどうかのより正確なチェックをさらに確保する必要があります。 同様に、すべてのコンテナの中で最も近い共通クラスをパラメータPとして選択する必要があります。 しかし、CBox ブランチのクラスを使用してダイアログ内の要素をグループ化し、CWndClient の子孫である CWndClient から派生します。 したがって、最も近い共通の祖先はCWndContainerです。

LayoutBaseクラスのフィールドは、レイアウトオブジェクトによって生成されたインターフェース要素へのポインタを格納します。

    protected:
      P *container; // not null if container (can be used as flag)
      C *object;
      C *array[];
    public:
      LayoutBase(): container(NULL), object(NULL) {}

ここでは、コンテナとオブジェクトは同じものを指していますが、要素が本当にコンテナであれば、コンテナはNULLではありません。

この配列は、1つのレイアウトオブジェクトを使用して、ボタンのような同じタイプの要素のグループを作成することができます。 この場合、ポインタコンテナとオブジェクトはNULLと等しくなります。 すべてのメンバに対して、些細な「ゲッター」の方法がありますので、すべてを提示することはありません。 例えば、オブジェクトへのリンクを取得するには get() メソッドを使うのが簡単です。

次の3つのメソッドは、レイアウトオブジェクトを実行できなければならないバインド要素に対する抽象的な操作を宣言します。

    protected:
      virtual bool setContainer(C *control) = 0;
      virtual string create(C *object, const string id = NULL) = 0;
      virtual void add(C *object) = 0;

メソッドsetContainerは、渡されたパラメータでコンテナを通常の "コントロール "と区別することができます。 このメソッドで、コンテナフィールドに入力します。 NULLでない場合はtrueを返します。

メソッドの作成は要素を開始します(同様のメソッド、Createは標準ライブラリのすべてのクラスにありますが、私の意見では、EasyAndFastGUIなどの他のライブラリにも同様のメソッドが含まれていますが、EasyAndFastGUIの場合は何らかの理由で異なるクラスで異なる名前が付けられています。: CBox と CGrid に似たクラスを作成する方がはるかに重要です。 要素の識別子をメソッドに渡すことができますが、必ずしもエグゼクティブアルゴリズムがこの全部または一部を考慮するとは限りません(特に、instance_idを追加することができます)。 そのため、返される文字列から本当の識別子を知ることができます。

メソッド "add" は、親コンテナの要素に要素を追加します (標準ライブラリでは、この操作は Add メソッドで実行されますが、EasyAndFastGUI では MainPointer で実行されるようです)。

では、この3つのメソッドが抽象レベルでどのように関わっているかを見てみましょう。 インターフェースの各要素はレイアウトオブジェクトにバインドされており、2つのフェーズを経ています。生成(コードユニット内のローカル変数を開始する時)と削除(コードユニットから抜け出してローカル変数のデストラクタを呼び出す時)です。 第一段階では、子孫クラスのコンストラクタから呼び出されるメソッドinitを書きます。

      template<typename T>
      void init(T *ref, const string id = NULL, const int x1 = 0, const int y1 = 0, const int x2 = 0, const int y2 = 0)
      {
        object = ref;
        setContainer(ref);
        
        _x1 = x1;
        _y1 = y1;
        _x2 = x2;
        _y2 = y2;
        if(stack.size() > 0)
        {
          if(_x1 == 0 && _y1 == 0 && _x2 == 0 && _y2 == 0)
          {
            _x1 = stack.top()._x1;
            _y1 = stack.top()._y1;
            _x2 = stack.top()._x2;
            _y2 = stack.top()._y2;
          }
          
          _id = rootId + (id == NULL ? typename(T) + StringFormat("%d", object) : id);
        }
        else
        {
          _id = (id == NULL ? typename(T) + StringFormat("%d", object) : id);
        }
        
        string newId = create(object, _id);
        
        if(stack.size() == 0)
        {
          rootId = newId;
        }
        if(container)
        {
          stack << &this;
        }
      }

最初のパラメータは、該当するクラスの要素へのポインタです。 ここではここまでに限定して、外部から要素を渡す場合を考えてみます。 しかし、上のレイアウト構文の草案では、暗黙の要素がありました(名前だけが指定されていました)。 この操作方法については、もう少し後になってから振り返ることにします。

このメソッドは要素へのポインタを object に格納し、 setContainer を使ってそれがコンテナであるかどうかをチェックし(もしそうであればコンテナフィールドも記入されることを示唆しています)、指定された座標を入力から、あるいはオプションで親コンテナから取得します。 create "を呼び出すと、インターフェース要素が開始されます。 スタックがまだ空の場合は、そのインデントを rootId (標準ライブラリの場合は instance_id) に保存します。スタック上の最初の要素は常に最上位のコンテナ、つまりすべての降順要素を担当するウィンドウ (標準ライブラリの場合は CDialog クラスまたは派生のもの) になるからです。 最後に、現在の要素がコンテナであれば、スタックに入れます(stack << &this)。

initメソッドはテンプレート的なものです。 これより、型によって "コントロール "の名前を自動的に生成することができます; さらに、すぐに他の同様のメソッドを追加する予定です。 そのうちの1つは、外部から準備した要素を取るのではなく、内部で要素を生成し、この場合は特定の型が必要です。 initのもう一つのバージョンは,レイアウトに同じ型の要素を一度に登録するように設計されています(array[]メンバを覚えておいてください).配列はリンクによって渡され,リンクは型の変換をサポートしていません(コード構造によっては,"パラメータの変換は許可されていません","オーバーロードのどれも関数呼び出しに適用できません "など). したがって、すべてのメソッドinitは、同じ「テンプレート」取引、すなわち使用のルールを持つことになります。

一番面白いのは、LayoutBaseのデストラクタの中で起こることです。

      ~LayoutBase()
      {
        if(container)
        {
          stack.pop();
        }
        
        if(object)
        {
          LayoutBase *up = stack.size() > 0 ? stack.top() : NULL;
          if(up != NULL)
          {
            up.add(object);
          }
        }
      }
  };

現在のバインドされている要素がコンテナの場合は、該当する中括弧の単位から出ている(コンテナが終わっている)ので、スタックから削除します。 問題は、各ユニットの内部では、スタックの最上位に位置するコンテナが含まれており、そこにユニット内部で発生する要素が追加されている(実際にはすでに追加されている)ということであり、その要素は「コントロール」であることも、より小さなコンテナであることもあり得るということです。 その後、現在の要素は、順番に、スタックのトップに持っているコンテナに "追加 "のメソッドを使用して追加されます。

インターフェースレイアウトのクラスです。標準ライブラリの要素の応用レベル

より具体的なものに行きましょう - 標準ライブラリのインターフェイス要素のレイアウトのクラスを実装します。 CWndContainerとCWndをテンプレートパラメータとして,中間クラスであるStdLayoutBaseを定義してみます.

  class StdLayoutBase: public LayoutBase<CWndContainer,CWnd>
  {
    public:
      virtual bool setContainer(CWnd *control) override
      {
        CDialog *dialog = dynamic_cast<CDialog *>(control);
        CBox *box = dynamic_cast<CBox *>(control);
        if(dialog != NULL)
        {
          container = dialog;
        }
        else if(box != NULL)
        {
          container = box;
        }
        return true;
      }

メソッド setContainer は、動的キャストを使用して、要素 CWnd が CDialog または CBox から降順しているかどうかをチェックし、はいの場合、コンテナであるかどうかを確認します。

      virtual string create(CWnd *child, const string id = NULL) override
      {
        child.Create(ChartID(), id != NULL ? id : _id, 0, _x1, _y1, _x2, _y2);
        return child.Name();
      }

メソッド "create "は、要素を開始し、その名前を返します。 現在のチャート(ChartID()とメインウィンドウでのみ動作することに注意してください(サブウィンドウはこのプロジェクトでは考慮されていませんが、必要に応じてコードを適応させることができます)。

      virtual void add(CWnd *child) override
      {
        CDialog *dlg = dynamic_cast<CDialog *>(container);
        if(dlg != NULL)
        {
          dlg.Add(child);
        }
        else
        {
          CWndContainer *ptr = dynamic_cast<CWndContainer *>(container);
          if(ptr != NULL)
          {
            ptr.Add(child);
          }
          else
          {
            Print("Can't add ", child.Name(), " to ", container.Name());
          }
        }
      }
  };

メソッド "add "は親要素に子要素を追加し、標準ライブラリのAddメソッドは仮想的ではないので、予備的に可能な限りの "アップキャスト "を行います(技術的には、標準ライブラリで関連する変更を行うことができますが、修正することについては後述します)。

StdLayoutBaseクラスをベースに、MQLでレイアウトを記述したコードに存在するタスククラス_layoutを作成します。 名前は、このクラスのオブジェクトの非標準的な目的に注意を促すためにアンダースコアで始まります。 クラスを簡略化したものを考えてみましょう。 後ほど関数を追加していく予定です。 実際には、すべてのアクティビティはコンストラクタによって開始され、その中でメソッドinitや別のメソッドがLayoutBaseから呼び出されます。

  template<typename T>
  class _layout: public StdLayoutBase
  {
    public:
      
      _layout(T &ref, const string id, const int dx, const int dy)
      {
        init(&ref, id, 0, 0, dx, dy);
      }
      
      _layout(T *ptr, const string id, const int dx, const int dy)
      {
        init(ptr, id, 0, 0, dx, dy);
      }
      
      _layout(T &ref, const string id, const int x1, const int y1, const int x2, const int y2)
      {
        init(&ref, id, x1, y1, x2, y2);
      }
      
      _layout(T *ptr, const string id, const int x1, const int y1, const int x2, const int y2)
      {
        init(ptr, id, x1, y1, x2, y2);
      }
      
      _layout(T &refs[], const string id, const int x1. const int y1, const int x2, const int y2)
      {
        init(refs, id, x1, y1, x2, y2);
      }
  };

以下のクラス図を使って全体像をざっと見渡すことができます。 知らなければならないことが少しありますが、ほとんどのクラスは馴染みのあるものです。

GUIレイアウトクラスの図

GUIレイアウトクラスの図

ここで、_layout<CButton> button(m_button, 100, 20)のようなオブジェクトの記述が、以下のような外部ユニットで記述されていれば、オブジェクトm_buttonがどのように起動してダイアログに登録されるのかを、実際に確認することができます。_layout<CAppDialog> dialog(this, name, x1, y1, x2, y2) のように外部ユニットで記述します。 しかし、要素はサイズ以外にも多くのプロパティがあります。 側面による整列のようなプロパティは、座標よりもレイアウトにとって重要性が低いわけではありません。 実際、もし要素が標準ライブラリの「整列」の観点から水平方向に整列している場合、親コンテナ領域の全幅から、左と右のあらかじめ定義されたフィールドを差し引いて引き伸ばされることになります。 したがって、アライメントは座標よりも優先されます。 さらに、CBoxクラスコンテナでは、子要素を配置する方向(方向)が重要で、水平(デフォルトでは水平)か垂直のどちらかになります。 また、フォントサイズや色などの外部表現に影響を与える他のプロパティや、読み取り専用、"スティッキー "ボタンなどの操作モードをサポートするのが正しいでしょう。

GUIオブジェクトがウィンドウクラスで記述され、レイアウトに渡される場合、edit.Text("text")のようにプロパティを設定する "ネイティブ "メソッドを使用することができます。 レイアウトシステムは、この古い手法に対応していますが、単一でも最適でもありません。 多くの場合、オブジェクトの作成は、レイアウトシステムに割り当てると便利でしょう、その後、直接ウィンドウから利用可能ではありません。 このように、要素の調整に関しては、クラス_layoutの関数をなんとか拡張する必要があります。

プロパティが多いので、同じクラスにタスクを鞍替えするのではなく、特別なヘルパークラスと分担して機能することをお勧めします。 同時に、_layoutは要素を登録するための出発点であることに変わりはありませんが、セットアップの詳細はすべて新しいクラスに委譲されます。 これはレイアウトテクニックをコントロールの特定のライブラリにできるだけ依存しないようにするために、より重要なことです。

要素のプロパティを設定するクラス

抽象レベルでは、プロパティのセットは、その値の型によって分割されます。 MQLの基本的な組み込み型はもちろん、後述する型にも対応していきます。 構文的には、ビルダーという既知のパターンのコールチェーンでプロパティを割り当てるのが便利でしょう。

  _layout<CBox> column(...);
  column.style(LAYOUT_STYLE_VERTICAL).color(clrGray).margin(5);

しかし、この構文では、1つのクラス内に長いメソッドのセットが存在することになり、後者はレイアウト・クラスでなければなりません。 クラス_layoutでは、以下のように、プロパティのヘルパーオブジェクトのインスタンスを返すメソッドを予約することができます。

  _layout<CBox> column(...);
  column.properties().style(LAYOUT_STYLE_VERTICAL).color(clrGray).margin(5);

しかし、コンパイルの段階で割り当てられたプロパティの正しさを検証するために、多くのプロキシクラスを定義することは問題ではないでしょう。 プロジェクトが複雑になってしまいますが、最初のテスト実装はできるだけ行いたいと考えています。 さて、このアプローチには現在、さらなる拡張が残されています。

また、"builder"テンプレートのメソッドの名前は、LAYOUT_STYLE_VERTICALやclrGrayなどの値が自明であり、他の型は詳細な説明を必要としないことが多いため、CButtonの"読み取り専用"フラグを意味します。 その結果、オーバーロードされた演算子を使用して値を代入することになります。 しかし、不思議なことに、代入演算子はコールチェーンをスレッド化できないので、合わないです。

  _layout<CBox> column(...);
  column = LAYOUT_STYLE_VERTICAL = clrGray = 5; // 'clrGray' - l-value required ...

単一行の代入演算子は、オーバーロードされた代入が導入されたオブジェクトから開始するのではなく、右から左に実行されます。 次のように動作します。

  ((column = LAYOUT_STYLE_VERTICAL) = clrGray) = 5; 

でも、これは少し大変そうです。

Version:

  column = LAYOUT_STYLE_VERTICAL; // orientation
  column = clrGray;               // color
  column = 5;                     // margin

これも長すぎます。 そこで、演算子<=をオーバーロードして以下のように使うことにしました。

  column <= LAYOUT_STYLE_VERTICAL <= clrGray <= 5.0;

LayoutBaseクラスにスタブがあります。

    template<typename V>
    LayoutBase<P,C> *operator<=(const V value) // template function cannot be virtual
    {
      Print("Please, override " , __FUNCSIG__, " in your concrete Layout class");
      return &this;
    }

この二重の目的は、演算子オーバーロードを使用する意図を宣言し、派生クラスのメソッドをオーバーライドすることを思い出させることです。 理論的には、メディエータクラスオブジェクトは、以下のインターフェイス(完全ではありませんが示されています)を使用して、そこで使用する必要があります。

  template<typename T>
  class ControlProperties
  {
    protected:
      T *object;
      string context;
      
    public:
      ControlProperties(): object(NULL), context(NULL) {}
      ControlProperties(T *ptr): object(ptr), context(NULL) {}
      void assign(T *ptr) { object = ptr; }
      T *get(void) { return object; }
      virtual ControlProperties<T> *operator[](const string property) { context = property; StringToLower(context); return &this; };
      virtual T *operator<=(const bool b) = 0;
      virtual T *operator<=(const ENUM_ALIGN_MODE align) = 0;
      virtual T *operator<=(const color c) = 0;
      virtual T *operator<=(const string s) = 0;
      virtual T *operator<=(const int i) = 0;
      virtual T *operator<=(const long l) = 0;
      virtual T *operator<=(const double d) = 0;
      virtual T *operator<=(const float f) = 0;
      virtual T *operator<=(const datetime d) = 0;
  };

ご覧のように、設定する要素(オブジェクト)へのリンクがメディエータークラスに格納されています。 バインディングはコンストラクタで行うか、assign メソッドを使用して行います。 クラスMyControlPropertiesの特定のメディエーターを書いたと仮定します。

  template<typename T>
  class MyControlProperties: public ControlProperties<T>
  {
    ...
  };

とすると、_layoutクラスでは、そのオブジェクトを以下のようなスキームで利用することができます(文字列やメソッドの追加はコメントされています)。

  template<typename T>
  class _layout: public StdLayoutBase
  {
    protected:
      C *object;
      C *array[];
      
      MyControlProperties helper;                                          // +
      
    public:
      ...
      _layout(T *ptr, const string id, const int dx, const int dy)
      {
        init(ptr, id, 0, 0, dx, dy); // this will save ptr in the 'object'
        helper.assign(ptr); // +。
      }
      ...
      
      // non-virtual function override                                     // +
      template<typename V>                                                 // +
      _layout<T> *operator<=(const V value)                                // +
      {
        if(object != NULL)
        {
          helper <= value;
        }
        else
        {
          for(int i = 0; i < ArraySize(array); i++)
          {
            helper.assign(array[i]);
            helper <= value;
          }
        }
        return &this;
      }

_layoutの演算子 <= がテンプレートの 1 であるため、ControlProperties のインターフェイスから正しいパラメーター型の呼び出しが自動的に生成されます (もちろん、インターフェイスの抽象メソッドではなく、派生クラス MyControlProperties に実装する方法について、特定のウィンドウ ライブラリに対してすぐに 1 つを記述します)。

場合によっては、同じデータ型を使用して複数の異なるプロパティを定義することもあります。 例えば、要素の可視性やアクティブ状態のフラグを設定する際には、上述の "read only"(CEditの場合)や "sticking"(CButtonの場合)のモードと共に、CWndでも同じboolを使用します。 プロパティ名を明示的に指定できるようにするために、インターフェイスControlPropertiesでは文字列型パラメータを持つ演算子[]が用意されています。 "コンテキスト "フィールドを設定し、派生クラスが必要な特性を修正することができるようにします。

インプットタイプと要素クラスのそれぞれの組み合わせについて、プロパティのうちの1つ(最も頻繁に使用するもの)がデフォルト値によるプロパティとみなされます(CEditとCButtonに対する例を上に示します)。 その他のプロパティは、コンテキストを指定する必要があります。

例えば、CButtonの場合は以下のようになります。

  button1 <= true;
  button2["visible"] <= false;

最初の文字列では、コンテキストが指定されていないので、"ロック "プロパティ(2ポジションボタン)が暗黙の了解となります。 2つ目の場合、ボタンは初期状態では不可視の状態で作成されており、通常はレアケースです。

標準要素のライブラリに対するメディエーターStdControlPropertiesの実装の基本的な内容を考えてみましょう。 完全なコードは添付ファイルにあります。 冒頭で、operator <=が型 "bool "に対してオーバーライドされていることがわかります。

  template<typename T>
  class StdControlProperties: public ControlProperties<T>
  {
    public:
      StdControlProperties()を使用します。ControlProperties() {}。
      StdControlProperties(T *ptr). ControlProperties(ptr) {} {}。
      
      // we need dynamic_cast throughout below, because control classes
      // in the standard library does not provide a set of common virtual methods
      // to assign specific properties for all of them (for example, readonly
      // is available for edit field only)
      virtual T *operator<=(const bool b) override
      {
        if(StringFind(context, "enable") > -1)
        {
          if(b) object.Enable();
          else  object.Disable();
        }
        else
        if(StringFind(context, "visible") > -1)
        {
          object.Visible(b);
        }
        else
        {
          CEdit *edit = dynamic_cast<CEdit *>(object);
          if(edit != NULL) edit.ReadOnly(b);
          
          CButton *button = dynamic_cast<CButton *>(object);
          if(button != NULL) button.Locking(b);
        }
        
        return object;
      }

文字列に対しては、以下のルールが適用されます。font" コンテキストが指定されていない場合は、すべてのテキストが "control" ヘッダに入ります。

      virtual T *operator<=(const string s) override
      {
        CWndObj *ctrl = dynamic_cast<CWndObj *>(object);
        if(ctrl != NULL)
        {
          if(StringFind(context, "font") > -1)
          {
            ctrl.Font(s);
          }
          else // default
          {
            ctrl.Text(s);
          }
        }
        return object;
      }

StdControlPropertiesクラスでは、標準ライブラリにのみ固有の型の<==オーバーライドを追加導入しました。 特に、アライメントバージョンを記述する列挙ENUM_WND_ALIGN_FLAGSを取ることができます。 この列挙では、4辺(左、右、上、下)とともに、すべての組み合わせではなく、幅を揃えたり(WND_ALIGN_WIDTH = WND_ALIGN_LEFT|WND_ALIGN_RIGHT)、クライアント領域全体を揃えたり(WND_ALIGN_CLIENT = WND_ALIGN_WIDTH|WND_ALIGN_HEIGHT)など、よく使われる組み合わせの記述があることに注意してください。 しかし、要素を幅と上辺で整列させる必要がある場合は、このフラグの組み合わせはもはや列挙の一部にはなりません。 そのため、対する型変換を明示的に指定する必要があります((ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_WIDTH|WND_ALIGN_TOP))。 そうでなければ、ビット単位のOR演算ではint型が生成され、整数プロパティの設定の間違ったオーバーロードが呼び出されてしまいます。 もう一つの解決策は、"align "コンテキストを指定することです。

int型のオーバーライドが最も手間のかかるものであることは当然です。 特に、幅、高さ、余白、フォントサイズなどのプロパティを設定することができます。 このような状況を容易にするために、レイアウトオブジェクトのコンストラクタで直接サイズを指定することが可能になりましたが、余白はダブルタイプの数値を使用したり、PackedRectという名前の特別なパッキングを使用して指定することもできます。 もちろん、演算子オーバーロードも追加されています。

  button <= PackedRect(5, 100, 5, 100); // left, top, right, bottom

ダブルタイプの値が1つだけの等辺フィールドを分離する方が簡単です。

  button <= 5.0;

しかし、ユーザーは代替案、すなわち「margin」コンテキストを選択することができます; その後、ダブルを必要とせず、同等のレコードは次のようになります。

  button["margin"] <= 5;

余白やインデントに関しては、一つだけ注意点があります。 標準ライブラリには、"コントロール "の周りに自動的に追加される余白を含むアライメント用語があります。同時に、CBoxクラスでは、自身のパディング機構が実装されており、コンテナの外部境界とチルデ "コントロール"(コンテンツ)との間のコンテナ内部のギャップを表します。 このように、「コントロール」という意味のフィールドと、「コンテナ」という意味のインデントは、本質的には同じ意味です。 残念ながら、2つのポジション決めアルゴリズムはお互いを考慮していないので、余白とインデントの両方を同時に使用すると問題が発生する可能性があります(中で最も明白なのは要素の移動であり、期待に応えられません)。 一般的に推奨されるのは、インデントをゼロにして余白で操作することです。 しかし、必要に応じて、特に一般的な設定ではなく、特定のコンテナに関するものであれば、インデントも含めてみてはいかがでしょうか。

本論文は概念実証(POC)の研究であり、既成の解決策を提供するものではありません。 この目的は、提案された技術を、執筆時点で利用可能な標準ライブラリのクラスとコンテナ上で、すべてのコンポーネントの最小限の変更で試してみることです。 理想的には、コンテナ(CBoxのものである必要はありません)はGUI要素ライブラリの不可欠な部分として書かれ、モードのすべての可能な組み合わせを考慮して動作しなければなりません。

対応しているプロパティと要素の一覧表です。 クラスCWndはすべての要素へのプロパティの適用性を意味し、クラスCWndObjはシンプルな "コントロール "のものです(そのうちの2つ、CEditとCButtonも表の中で与えられています)。 CWndClientクラスは,コントロール(CCheckGroup, CRadioGroup, CListView)を汎用化したクラスであり,コンテナCBox/CGridの親クラスです.

データ型と要素のクラスでサポートされているプロパティの表

type/control CWnd CWndObj CWndClient CEdit CButton CSpinEdit CDatePicker CBox/CGrid
bool visible
enable
visible
enable
visible
enable
(readonly)
visible
enable
(locking)
visible
enable
visible
enable
visible
enable
visible
enable
color (text)
background
border
(background)
border
(text)
background
border
(text)
background
border
(background)
border
string (text)
font
(text)
font
(text)
font
int width
height
margin
left
top
right
bottom
align
width
height
margin
left
top
right
bottom
align
fontsize
width
height
margin
left
top
right
bottom
align
width
height
margin
left
top
right
bottom
align
fontsize
width
height
margin
left
top
right
bottom
align
fontsize
(value)
width
height
margin
left
top
right
bottom
align
min
max
width
height
margin
left
top
right
bottom
align
width
height
margin
left
top
right
bottom
align
long (id) (id)
zorder
(id) (id)
zorder
(id)
zorder
(id) (id) (id)
double (margin) (margin) (margin) (margin) (margin) (margin) (margin) (margin)
float (padding)
left *

right *
bottom *
datetime (value)
PackedRect (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4])
ENUM_ALIGN_MODE (text align)
ENUM_WND_ALIGN_FLAGS (alignment) (alignment) (alignment) (alignment) (alignment) (alignment) (alignment) (alignment)
LAYOUT_STYLE (style)
VERTICAL_ALIGN (vertical align)
HORIZONTAL_ALIGN (horizonal align)


StdControlPropertiesクラスの完全なソースコードが添付されており、レイアウト要素のプロパティを確実に変換し、標準コンポーネントライブラリのメソッドを呼び出すことができます。

レイアウトクラスを試してみましょう。 シンプルなものから複雑なものへと移行して、ようやくインスタンスの勉強を始めることができます。 コンテナを使ってGUIをレイアウトするという2つのオリジナル記事を発表して以来の伝統に従って、新しい技術にスライドパズル(SlidingPuzzle4)と "コントロール "を扱うための標準的なデモ(ControlsDialog4)を適応させてみましょう。 インデックスは、プロジェクトの更新段階に対応します。 articleでは、同じプログラムがインデックス3で表示され、ソースコードを比較することができます。 例は、MQL5/Experts/Examples/Layouts/フォルダにあります。

実施例1. SlidingPuzzle

CSlidingPuzzleDialogのメインフォームのパブリックインターフェースの大幅な変更は、CreateLayoutという新しいメソッドだけです。 従来のCreateではなく、ハンドラOnInitから呼び出す必要があります。 どちらのメソッドもパラメータのリストは同じです。 ダイアログ自体がレイアウトオブジェクト(最外層)であり、そのCreateメソッドは新しいフレームワークによって自動的に呼び出されるので、この置換が必要でした(上で検討したStdLayoutBase::createメソッドが行います)。 フォームとその内容に関するフレームワークのすべての情報は、MQLベースのマークアップ言語を使用してCreateLayoutメソッドで具体的に定義されています。 ここにメソッドそのものがあります。

  bool CSlidingPuzzleDialog::CreateLayout(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    {
      _layout<CSlidingPuzzleDialog> dialog(this, name, x1, y1, x2, y2);
      {
        _layout<CGridTkEx> clientArea(m_main, NULL, 0, 0, ClientAreaWidth(), ClientAreaHeight());
        {
          SimpleSequenceGenerator<long> IDs;
          SimpleSequenceGenerator<string> Captions("0", 15);
          
          _layout<CButton> block(m_buttons, "block");
          block["background"] <= clrCyan <= IDs <= Captions;
          
          _layout<CButton> start(m_button_new, "New");
          start["background;font"] <= clrYellow <= "Arial Black";
          
          _layout<CEdit> label(m_label);
          label <= "click new" <= true <= ALIGN_CENTER;
        }
        m_main.Init(5, 4, 2, 2);
        m_main.SetGridConstraints(m_button_new, 4, 0, 1, 2);
        m_main.SetGridConstraints(m_label, 4, 2, 1, 2);
        m_main.Pack();
      }
    }
    m_empty_cell = &m_buttons[15];
    
    SelfAdjustment();
    return true;
  }

ここでは、2つのネストになったコンテナが連続して形成され、それぞれが独自のレイアウトオブジェクトによって制御されます。

  • CSlidingPuzzleDialogのインスタンス(変数this)のダイアログ
  • clientArEAfor element CGridTkEx m_main;

そして、クライアント領域では、ボタンのセットであるCButton m_buttons[16]が初期化され、単一のレイアウトオブジェクトであるブロックにバインドされ、ゲーム開始ボタン("start "オブジェクト内のCButton m_button_new)と通知ラベル(CEdit m_label、オブジェクト "label")と同様に、ゲーム開始ボタン("start "オブジェクト内のCButton m_button_new)と通知ラベル(CEdit m_label、オブジェクト "label")にバインドされています。 すべてのローカル変数、すなわち、ダイアログ、clientArea、ブロック、開始、およびラベルは、コードが実行されると自動的にCreateを呼び出し、定義された追加のパラメータ(パラメータについては後述する)で割り当て、削除するとき、すなわち、次の中括弧のブロックの可視性を超えたときに、バインドされたインタフェース要素をより上位のコンテナに登録することを確実にします。 このように、m_main クライアント領域は "this" ウィンドウに含まれ、すべての "コントロール" はクライアント領域に含まれます。 この場合、ブロックは最もネストになっているものから順に閉じられるので、逆のオーダーで実行されます。 しかし、それらすべてが重要という訳ではありません。 従来の方法でダイアログを作成した場合も、実質的には同じことが起こります。大きなインターフェイスグループが小さなインターフェイスグループを作成し、後者のインターフェイスグループは、個々の「コントロール」のレベルまで、さらに小さなインターフェイスグループを作成し、初期化された要素を逆のオーダー(昇順)で追加し始めます。最初に「コントロール」が中のブロックに追加され、次に中のブロックがより大きなブロックに追加されます。

ダイアログとクライアント領域の場合、すべてのパラメータはコンストラクタのパラメータを介して渡されます(標準のCreateメソッドのようなものです)。 クラス GridTkEx は自動的に正しく割り当て、他のパラメーターは演算子 <=を使用して渡されるため、"コントロール"にサイズを渡す必要はありません。

16個のボタンのブロックは、目に見えるループなしで初期化されます(現在はレイアウトオブジェクトに隠されています)。 すべてのボタンの背景色は、文字列 block["background"] <= clrCyan. そして、まだ知らないヘルパーオブジェクトを同じレイアウトオブジェクト(SimpleSequenceGenerator)に渡します。

ユーザインタフェースを形成する際には、同じタイプの複数の要素を生成し、バッチモードで既知のデータで埋めていくことが必要な場合が多いです。 いわゆるジェネレーターを使うと便利です。

Generatorは、あるリストの中から次の要素を取得するためにループ内で呼び出すことができるメソッドを持つクラスです。

  template<typename T>
  class Generator
  {
    public:
      virtual T operator++() = 0;
  };

通常、ジェネレータは必要な要素の数を知っている必要があり、カーソル(現在の要素のインデックス)を格納します。 特に、整数や文字列などの特定の埋め込み型の値のシーケンスを作成する必要がある場合は、以下のSimpleSequenceGeneratorの簡単な実装が適します。

  template<typename T>
  class SimpleSequenceGenerator: public Generator<T>
  {
    protected:
      T current;
      int max;
      int count;
      
    public:
      SimpleSequenceGenerator(const T start = NULL, const int _max = 0): current(start), max(_max), count(0) {}
      
      virtual T operator++() override
      {
        ulong ul = (ulong)current;
        ul++;
        count++;
        if(count > max) return NULL;
        current = (T)ul;
        return current;
      }
  };

ジェネレータはバッチ処理の利便性に追加されています(Generators.mqhファイル)が、レイアウトクラスのジェネレータに演算子<=のオーバーライドがあります。 これより、識別子とキャプションを有するボタン16を1行で記入することができます。

CreateLayoutメソッドの以下の文字列では、m_button_newボタンを作成します。

        _layout<CButton> start(m_button_new, "New");
        start["background;font"] <= clrYellow <= "Arial Black";

文字列 "New" は識別子であり、キャプションでもあります。 別のキャプションを割り当てる必要がある場合は、次のようにすることができます: start <= "Caption"。 一般的には、識別子も(必要なければ)定義する必要はありません。 システムが勝手に生成してくれます。

2 番目の文字列では、コンテキストが定義されており、2つのツールチップ(背景とフォント)が同時に含まれています。 前者は色clrYellowを正しく解釈するために必要なものです。 ボタンはCWndObjの子孫なので、"unnamed "カラーはそのボタンのテキストカラーを意味します。 2つ目のツールチップは、文字列 "Arial Black "で使用するフォントを確実に変更します(コンテキストがなければ、文字列はキャプションを変更します)。 ご希望の方は、詳しく書いていただいても構いません。

        start["background"] <= clrYellow;
        start["font"] <= "Arial Black";

もちろん、ボタンにはまだメソッドがあります、つまり、以前のように書くことができます。

        m_button_new.ColorBackground(clrYellow);
        m_button_new.Font("Arial Black");

しかし、ボタンオブジェクトが必要ですが、必ずしもそうとは限りません - 後で、レイアウトシステムが要素の構築や保存を含むすべての責任を負うというスキームに行き着きます。

ラベルを設定するには、以下の文字列を使用します。

        _layout<CEdit> label(m_label);
        label <= "click new" <= true <= ALIGN_CENTER;

ここで自動識別子を持つオブジェクトが作成されます(チャート上のオブジェクトをリストアップしているウィンドウを開くと、インスタンスのユニークな番号が表示されます)。 2番目の文字列では、ラベルテキスト、"read only "属性、テキストの中央揃えを定義します。

そして、CGridTKExクラスのm_mainオブジェクトを調整する文字列に沿って、CGridTKExクラスのm_mainオブジェクトを調整します。

      m_main.Init(5, 4, 2, 2);
      m_main.SetGridConstraints(m_button_new, 4, 0, 1, 2);
      m_main.SetGridConstraints(m_label, 4, 2, 1, 2);
      m_main.Pack();

CGridTKExは、少し改良されたCGridTk(前の記事)です。 CGridTkExでは、SetGridConstraintsという新しいメソッドを使って、子「コントロール」の制限を定義する方法を実装しました。 GridTkでは、Gridメソッド内の要素を同時に追加することでのみ可能です。 これは本質的に良くないことです。オブジェクト間の関係を確立することと、プロパティを調整することです。 さらに、グリッドに要素を追加するにはAddを使うべきではないことが判明しましたが、このメソッドを使うしかありません(制限を定義する唯一の方法であり、ないとGridTkが動作しないため)。 Addが常にこの目的に使用するというライブラリの一般的なアプローチに違反します。 そして、自動マークアップシステムの運用は、順番に結びついています。 CGridTkExクラスでは、2つの操作を分離し、それぞれが独自のメソッドを持つようにしました。

CBox/CGridTkクラスのメインコンテナ(ウィンドウ全体を含む)では、Packメソッドを呼び出すことが重要であることに注意してください。

SlidingPuzzle3.mqhとSlidingPuzzle4.mqhのソースコードを比較してみると、かなりコンパクトになっていることがよくわかります。 メソッド Create、CreateMain、CreateButton、CreateButtonNew、CreateLabel は"左寄り"クラスです。 CreateLayoutだけが動作するようになりました。

プログラムを開始すると、要素が作成され、期待通りに動作していることがわかります。

まあ、クラス内の「コントロール」と「コンテナ」を全て宣言しているリストが残っていますからね。 プログラムが複雑になり、構成要素の数が増えてくると、その記述をウィンドウクラスとレイアウトで重複させるのは不便になってきます。 すべてはレイアウトを使ってできるのでしょうか。その 可能性があることは容易に推測できます。 ただし、これについては後編で述べたいと思います。

結論

この論文では、グラフィカル・インターフェース・マークアップ言語の理論的な基礎と目標を紹介しました。 MQLでマークアップ言語を実装するという考え方を展開し、体現するコアクラスを考えてみました。 しかし、もっと複雑で建設的な例も出てくるでしょう。

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/7734

添付されたファイル |
MQL5GUI1.zip (86.86 KB)
時系列の予測(第2部):最小二乗サポートベクターマシン(LS-SVM) 時系列の予測(第2部):最小二乗サポートベクターマシン(LS-SVM)
この記事では、サポートベクター法に基づいて時系列を予測するアルゴリズムの理論と実際の使用法について説明します。また、このメソッドのMQL実装を提案し、テスト指標とエキスパートアドバイザーを提示します。このテクノロジーはまだMQLに実装されていません。まず、そのための数学を理解する必要があります。
取引シグナルの多通貨監視(その3):検索アルゴリズムの紹介 取引シグナルの多通貨監視(その3):検索アルゴリズムの紹介
前回の記事では、アプリケーションの視覚的な部分と、GUI要素の基本的なインタラクションを開発しました。 今回は、内部ロジックと取引シグナルのデータ準備のアルゴリズムを追加するだけでなく、検索し、モニターで可視化するために、シグナルを設定する機能を追加します。
トレードシグナルの多通貨監視(その4)。機能強化とシグナル検索システムの改善 トレードシグナルの多通貨監視(その4)。機能強化とシグナル検索システムの改善
このパートでは、トレードシグナルの検索・編集システムを拡張し、カスタムインジケータの使用可能性やプログラムのローカリゼーションを追加することを紹介します。 以前、シグナルを検索するための基本的なシステムを作ったことがありますが、小さなインジケータとシンプルな検索ルールのセットをベースにしていました。
トレーディングにおけるOLAPの適用(その4)。テスターレポートの定量的・視覚的分析 トレーディングにおけるOLAPの適用(その4)。テスターレポートの定量的・視覚的分析
この記事では、シングルパスや最適化結果に関連するテスターレポートのOLAP分析のための基本的なツールを提供しています。 このツールは標準フォーマットのファイル(tstとopt)を扱うことができ、グラフィカルなインターフェイスも提供します。 最後にMQLのソースコードを添付します。