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

Stanislav Korotky | 30 7月, 2020

パート1では、MQLプログラムのグラフィカルインターフェースのレイアウトをMQLで記述する際の基本的な原理を考えてみました。 これを実装するために、インターフェイス要素の初期化、共通の階層での結合、プロパティの調整を直接担当するクラスをいくつか作らなければなりませんでした。 ここでは、より複雑な例を見ていきます。実用的なことに気を取られないように、標準コンポーネントのライブラリに注目してみましょう。

標準コントロールライブラリのカスタマイズ

OLAPに関する以前の記事のウィンドウ・インターフェースを精緻化するにあたり、同じく標準ライブラリとCBoxコンテナをベースにしているため、標準ライブラリのコンポーネントを修正しなければなりませんでした。 提案されたレイアウトシステムを統合するために、コントロールズライブラリはさらに多くの修正が必要であることが判明しました - 関数の拡張に関する部分とエラーの修正に関する部分です。 そのため、すべてのクラスの完全コピー(バージョンブランチ)を作成し、ControlPlusフォルダに配置して、そのクラスだけで機能するようにしました。

主な更新内容をご紹介します。

実質的にすべてのクラスで、ライブラリの拡張性を確保するために、保護されたものに対してプライベートアクセスレベルを変更します。

GUI要素を含むプロジェクトのデバッグを容易にするために、CWindクラスに文字列フィールド_rttiを追加し、各派生クラスのコンストラクタでRTTIマクロを使って特定のクラス名を記入します。

  #define RTTI _rtti = StringFormat("%s %d", typename(this), &this);

これより、デバッガ・ウィンドウで、ベース・クラス・リンクによって参照されるオブジェクトの実際のクラスを見ることができます (この場合、デバッガはベース・クラスを表示します)。

クラスCWndの要素のフィールドと整列に関する情報に、2つの新しいオーバーロード・メソッドを使用してアクセスできるようになりました。 また、アライメントとフィールドを別々に変更することも可能になりました。

    ENUM_WND_ALIGN_FLAGS Alignment(void) const
    {
      return (ENUM_WND_ALIGN_FLAGS)m_align_flags;
    }
    CRect Margins(void) const
    {
      CRectCreator rect(m_align_left, m_align_top, m_align_right, m_align_bottom);
      return rect;
    }
    void Alignment(const int flags)
    {
      m_align_flags = flags;
    }
    void Margins(const int left, const int top, const int right, const int bottom)
    {
      m_align_left = left;
      m_align_top = top;
      m_align_right = right;
      m_align_bottom = bottom;
    }

メソッド CWnd::Align は、すべてのアライメントモードの期待される動作に従ってオーバーライドされました。 標準的な実施形態では、ストレッチが定義されている場合には、予め定義されたフィールドの境界へのシフトは保証されません(両方の寸法が陥りやすい)。

コンテナを削除する際に子要素を全て削除するためのメソッドDeleteAllをCWndContainerクラスに追加しました。 渡されたcontrolへのポインタにコンテナオブジェクトが含まれている場合、Delete(CWnd *control)から呼び出されます。

CWndClientクラスの別の場所に、リサイズによって変化する可能性のあるスクロールバーの表示を規制する文字列を追加しました。

クラス CAppDialog は、インターフェース要素に識別子を割り当てる際にウィンドウの instance_id を考慮するようになりました。 この修正を行わないと、同じ名前を持つ別のウィンドウでコントロールが競合する(互いに影響し合う)ことがありました。

コントロールのグループ、すなわちCRadioGroup、CCheckGroup、CListViewでは、「rubber」の子クラスがリサイズに正しく対応できるようにRedrawメソッドを仮想化しました。 また、子要素の幅の再計算を若干修正しました。

同じ目的で、CDatePicker、CCheckBox、CRadioButton クラスに仮想メソッド OnResize を追加しました。 クラスCDatePickerで、ポップアップカレンダー(マウスクリックが通過する)の安値優先エラーを修正しました。

メソッド CEdit::OnClick はマウスクリックを「食べる」ことはありません。

さらに、以前にもリサイズに対応した「コントロール」クラスを開発していましたが、今回のプロジェクトでは「rubber」クラスの数を増やしました。 ファイルは「レイアウト」フォルダにあります。

ボタンやインプット欄などの「コントロール」の中には、もともとストレッチに対応しているものがあることを忘れてはいけません。

標準要素ライブラリの一般的な構造は、"rubber "の性質とサードパーティのコンテナをサポートする適応されたバージョンを考慮して、クラス図で与えられています。

コントロールの階層

コントロールの階層


要素の生成とキャッシュ

これまでは、要素はオブジェクトウィンドウ内の自動インスタンスとして構築されていました。 実際には、これらは「ダミー」であり、Create などのメソッドによって初期化されます。 GUI要素のレイアウトシステムは、要素をウィンドウから取得するのではなく、独立して作成することができます。 それには、ストレージだけでいいのです。 LayoutCacheと名付けよう。

  template<typename C>
  class LayoutCache
  {
    protected:
      C *cache[];   // autocreated controls and boxes
      
    public:
      virtual void save(C *control)
      {
        const int n = ArraySize(cache);
        ArrayResize(cache, n + 1);
        cache[n] = control;
      }
      
      virtual C *get(const long m)
      {
        if(m < 0 || m >= ArraySize(cache)) return NULL;
        return cache[(int)m];
      }
      
      virtual C *get(const string name) = 0;
      virtual bool find(C *control);
      virtual int indexOf(C *control);
      virtual C *findParent(C *control) = 0;
      virtual bool revoke(C *control) = 0;
      virtual int cacheSize();
  };

実際には、基底クラスのポインタの配列(すべての要素に共通)であり、"save "メソッドを使って配置することができます。 インターフェースでは、番号、名前、リンク、または "親 "関係の事実(入れ子になった要素からコンテナへのフィードバック)によって要素を検索するメソッドを実装するか(この抽象レベルで可能であれば)宣言します(さらなる再定義のために)。

クラスLayoutBaseに静的メンバとしてキャッシュを追加してみましょう。

  template<typename P,typename C>
  class LayoutBase: public LayoutData
  {
    protected:
      ...
      static LayoutCache<C> *cacher;
      
    public:
      static void setCache(LayoutCache<C> *c)
      {
        cacher = c;
      }

各ウィンドウは自身でキャッシュインスタンスを作成し、CreateLayout のようなメソッドの最初に setCache を使用してキャッシュインスタンスをタスク用のものとして設定しなければなりません。 MQLプログラムはシングルスレッドなので、(複数必要な場合は)ウィンドウが並列に形成されたり、"cacher "ポインタ上で競合したりしないことが保証されています。 デストラクタLayoutBaseで自動的にポインタをクリーンアップする予定です。スタックが終了すると、レイアウト記述の直近の外部コンテナを残したことになり、他に何かを保存する必要はありません。

      ~LayoutBase()
      {
        ...
        if(stack.size() == 0)
        {
          cacher = NULL;
        }
      }

リンクをリセットしたからといって、キャッシュをクリアしているわけではありません。 これは、潜在的な次のレイアウトが誤って別のウィンドウの "コントロール "をそこに追加しないことを確認するためのちょうど良い方法です。

キャッシュを埋めるために、LayoutBaseに新しいタイプのメソッドinitを追加します - 今回は、パラメータにポインタやGUIの "サードパーティ "要素へのリンクがありません。

      // nonbound layout, control T is implicitly stored in internal cache
      template<typename T>
      T *init(const string name, const int m = 1, const int x1 = 0, const int y1 = 0, const int x2 = 0, const int y2 = 0)
      {
        T *temp = NULL;
        for(int i = 0; i < m; i++)
        {
          temp = new T();
          if(save(temp))
          {
            init(temp, name + (m > 1 ? (string)(i + 1) : ""), x1, y1, x2, y2);
          }
          else return NULL;
        }
        return temp;
      }
      
      virtual bool save(C *control)
      {
        if(cacher != NULL)
        {
          cacher.save(control);
          return true;
        }
        return false;
      }

テンプレートを使えば、レイアウト中に新しいTを書いたり、オブジェクトを生成したりすることができます(デフォルトでは1回に1個ですが、オプションで複数個のオブジェクトを生成することもできます)。

標準ライブラリの要素については、特定のキャッシュ実装である StdLayoutCache を書きました (ここでは省略して表示します。フルコードを添付します)。

  // CWnd implementation specific!
  class StdLayoutCache: public LayoutCache<CWnd>
  {
    public:
      ...
      virtual CWnd *get(const long m) override
      {
        if(m < 0)
        {
          for(int i = 0; i < ArraySize(cache); i++)
          {
            if(cache[i].Id() == -m) return cache[i];
            CWndContainer *container = dynamic_cast<CWndContainer *>(cache[i]);
            if(container != NULL)
            {
              for(int j = 0; j < container.ControlsTotal(); j++)
              {
                if(container.Control(j).Id() == -m) return container.Control(j);
              }
            }
          }
          return NULL;
        }
        else if(m >= ArraySize(cache)) return NULL;
        return cache[(int)m];
      }
      
      virtual CWnd *findParent(CWnd *control) override
      {
        for(int i = 0; i < ArraySize(cache); i++)
        {
          CWndContainer *container = dynamic_cast<CWndContainer *>(cache[i]);
          if(container != NULL)
          {
            for(int j = 0; j < container.ControlsTotal(); j++)
            {
              if(container.Control(j) == control)
              {
                return container;
              }
            }
          }
        }
        return NULL;
      }
      ...
  };

getメソッドは、そのインデックス番号(インプットが正の場合)か識別子(マイナスシンボルで符号化されている)のいずれかで "コントロール "を検索することに注意してください。 ここで、識別子は、標準コンポーネントライブラリがイベントをディスパッチするために割り当てた一意の番号を意味するものとします。 イベントでは、パラメータ lparam で渡されます。

ウィンドウのアプリケーションクラスでは、このStdLayoutCacheクラスを直接使用することもできますし、StdLayoutCacheから派生したものを書くこともできます。

キャッシングはどのようにウィンドウクラスの記述を減らすことができます、以下の例で示されます。 しかし、行く前に、キャッシュによって開かれた追加の機会を考えてみましょう。 また、例でも使用させていただきます。

Styler

キャッシュは要素を集中的に処理するオブジェクトなので、レイアウト以外の多くのタスクを解決するために利用すると便利です。 特に要素については、色やフォント、インデントなど、単一のスタイルルールを使って統一することができます。 同時に、このスタイルは、「コントロール」ごとに同じプロパティを別々に書かず、一箇所に設定すれば十分です。 また、キャッシュは、キャッシュされた要素に対するメッセージの処理を引き受けることができます。 可能性としては、絶対的にすべての要素を動的に構築し、キャッシュし、相互作用させることができます。 これなら、「明示的な」要素を宣言する必要は全くありません。 動的に作成された要素が自動化された要素に比べてどのような明らかな利点があるかは、もう少し後になってから見てみましょう。

クラス StdLayoutCache で集中スタイルをサポートするために、スタブメソッドが用意されています。

    virtual LayoutStyleable<C> *getStyler() const
    {
      return NULL;
    }

スタイルを使用したくない場合は、追加のコーディングは必要ありません。 しかし、スタイル管理を一元化するメリットを実感するならば、子孫クラスであるLayoutStyleableを実装しても良いでしょう。 インターフェイスはシンプルです。

  enum STYLER_PHASE
  {
    STYLE_PHASE_BEFORE_INIT,
    STYLE_PHASE_AFTER_INIT
  };
  
  template<typename C>
  class LayoutStyleable
  {
    public:
      virtual void apply(C *control, const STYLER_PHASE phase) {};
  };

メソッド apply は各 "control "に対して2回呼び出されます。初期化段階(STYLE_PHASE_BEFORE_INIT)とコンテナへの登録段階(STYLE_PHASE_AFTER_INIT)です。 このように、LayoutBase::initのメソッドでは、最初の段階で呼び出しが追加されます。

      if(cacher != NULL)
      {
        LayoutStyleable<C> *styler = cacher.getStyler();
        if(styler != NULL)
        {
          styler.apply(object, STYLE_PHASE_BEFORE_INIT);
        }
      }

デストラクタに入れる間、同様の文字列を追加しますが、2段目はSTYLE_PHASE_AFTER_INITを使っています。

スタイリングの目標が異なる場合があるので、2つのフェーズが必要です。 いくつかの要素では、スタイラーで設定されている共通のプロパティよりも優先度の高い個別のプロパティを設定する必要がある場合があります。 初期化の段階では、「コントロール」は空のまま、つまりレイアウトの設定は何も行われていません。 登録の段階では、すべてのプロパティがすでに設定されており、基づいてスタイルを変更することができます。 最もわかりやすい例は以下の通りです。 「読み取り専用」のフラグが付けられたすべてのインプットフィールドは、グレーで表示されるのが望ましいです。 ただし、「読み取り専用」プロパティは、初期化後、レイアウト中の「コントロール」にのみ割り当てられます。 そのため、ここでは1段目が似合わず、2段目が必要になります。 一方、すべてのフィールドがこのフラグを持つことは通常ありません; 以外のすべてのケースでは、レイアウト言語が選択的なカスタマイズを実行する場合、デフォルトの色を設定する必要があります。

ところで、MQLプログラムのインターフェイスを様々な言語に集中的にローカライズする際にも、同様の技術を用いることができます。

イベントの取り扱い

キャッシュに論理的に割り当てられる2つ目の関数は、イベント処理です。 このため、クラスLayoutCacheにスタブメソッド(Cはクラステンプレートパラメータ)が追加されています。

    virtual bool onEvent(const int event, C *control)
    {
      return false;
    }

繰り返しになりますが、派生クラスで実装することもできますが、その必要はありません。 イベントコードは、特定のライブラリで定義されています。

このメソッドが動作を開始するためには、標準ライブラリで利用可能なものと同様のイベント・インターセプション・マクロ定義が必要です。

  EVENT_MAP_BEGIN(Dialog)
    ON_EVENT(ON_CLICK, m_button1, OnClickButton1)
    ...
  EVENT_MAP_END(AppDialog)

新しいマクロは、イベントをキャッシュオブジェクトにリダイレクトします。 そのうちの一つ。

  #define ON_EVENT_LAYOUT_ARRAY(event, cache)  if(id == (event + CHARTEVENT_CUSTOM) && cache.onEvent(event, cache.get(-lparam))) { return true; }

ここでは、lparam(ただし符号を逆にして)に来る識別子によるキャッシュ内部の検索を見ることができ、その後、見つかった要素が上で考えたonEventプロセッサに送られます。 基本的には、イベントごとの処理時に要素の検索を省略して、要素のインデックスをキャッシュに記憶しておき、特定のプロセッサをインデックスにリンクさせればいいのです。

現在のキャッシュサイズは、新しい要素が保存されたばかりのインデックスです。 レイアウト中に必要な「コントロール」のインデックスを保存しておくことができます。

          _layout<CButton> button1("Button");
          button1index = cache.cacheSize() - 1;

ここで、button1indexはウィンドウクラスの整数変数です。 キャッシュインデックスで要素を処理するために定義された別のマクロで使用する必要があります。

  #define ON_EVENT_LAYOUT_INDEX(event, cache, controlIndex, handler)  if(id == (event + CHARTEVENT_CUSTOM) && lparam == cache.get(controlIndex).Id()) { handler(); return(true); }

さらに、イベントをキャッシュではなく、要素自体に直接送ることができます。 この目的には、要素は、必要な "control "クラスによってテンプレート化されたnotifiableインターフェースを自体で実装しなければならない。

  template<typename C>
  class Notifiable. public C
  {
    public:
      virtual bool onEvent(const int event, void *parent) = 0;
  };

親パラメータには、ダイアログボックスを含む任意のオブジェクトを渡すことができます。 Notifiableをベースにして、例えば、CButtonの子孫であるボタンを作ることができます。

  class NotifiableButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *anything) override
      {
        this.StateFlagsReset(7);
        return true;
      }
  };

"notifiable "要素を使って機能するためのマクロは2つあります。 パラメータの数が異なるだけです。ON_EVENT_LAYOUT_CTRL_ANY は直近のパラメータにランダムなオブジェクトを渡すことができますが、ON_EVENT_LAYOUT_CTRL_DLG は常にダイアログの "this" をオブジェクトとして送信するため、このパラメータはありません。

  #define ON_EVENT_LAYOUT_CTRL_ANY(event, cache, type, anything)  if(id == (event + CHARTEVENT_CUSTOM)) {type *ptr = dynamic_cast<type *>(cache.get(-lparam)); if(ptr != NULL && ptr.onEvent(event, anything)) { return true; }}
  #define ON_EVENT_LAYOUT_CTRL_DLG(event, cache, type)  if(id == (event + CHARTEVENT_CUSTOM)) {type *ptr = dynamic_cast<type *>(cache.get(-lparam)); if(ptr != NULL && ptr.onEvent(event, &this)) { return true; }}

第2の例の文脈でイベントを処理するための様々なオプションを検討していきます。

ケース2. コントロール付きダイアログ

デモプロジェクトは、標準ライブラリの "コントロール "の主な種類のクラスのCControlsDialogがあります。 最初のケースと同様に、作成するメソッドをすべて削除して、CreateLayoutという唯一のメソッドに置き換えます。 ちなみに、旧プロジェクトでは17個ものメソッドがあり、複合条件演算子を使って次々と呼ばれていました。

「コントロール」を生成する際にキャッシュに保存するために、シンプルなキャッシュクラスとスタイリングクラスを追加します。 まずはキャッシュです。

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      MyLayoutStyleable styler;
      CControlsDialog *parent;
      
    public:
      MyStdLayoutCache(CControlsDialog *owner): parent(owner) {}
      
      virtual StdLayoutStyleable *getStyler() const override
      {
        return (StdLayoutStyleable *)&styler;
      }
      
      virtual bool onEvent(const int event, CWnd *control) override
      {
        if(control != NULL)
        {
          parent.SetCallbackText(__FUNCTION__ + " " + control.Name());
          return true;
        }
        return false;
      }
  };

キャッシュクラスでは、イベントプロセッサであるonEventが宣言されており、イベントマップを介して接続します。 ここで、プロセッサはメッセージを親ウィンドウに送信し、その親ウィンドウは先行事例版と同様に情報フィールドに表示されます。

スタイラークラスでは、すべての要素に同一のフィールドを設定すること、すべてのボタンに標準外のフォントを使用すること、CEditを "read only "属性でグレーで表示することを提供しています(このようなものは1つしかありませんが、他のものが追加された場合は自動的に共通の設定の範囲内に収まります)。

  class MyLayoutStyleable: public StdLayoutStyleable
  {
    public:
      virtual void apply(CWnd *control, const STYLER_PHASE phase) override
      {
        CButton *button = dynamic_cast<CButton *>(control);
        if(button != NULL)
        {
          if(phase == STYLE_PHASE_BEFORE_INIT)
          {
            button.Font("Arial Black");
          }
        }
        else
        {
          CEdit *edit = dynamic_cast<CEdit *>(control);
          if(edit != NULL && edit.ReadOnly())
          {
            if(phase == STYLE_PHASE_AFTER_INIT)
            {
              edit.ColorBackground(clrLightGray);
            }
          }
        }
        
        if(phase == STYLE_PHASE_BEFORE_INIT)
        {
          control.Margins(DEFAULT_MARGIN);
        }
      }
  };

キャッシュへのリンクはウィンドウに保存され、コンストラクタとデストラクタでそれぞれ作成と削除が行われます。

  class CControlsDialog: public AppDialogResizable
  {
    private:
      ...
      MyStdLayoutCache *cache;
    public:
      CControlsDialog(void)
      {
        cache = new MyStdLayoutCache(&this);
      }

ここでは、CreateLayoutメソッドを段階的に考えてみましょう。 細かい記述を読んでいるため、メソッドが長く複雑に感じられるかもしれません。 しかし、そんなことはありません。 (実際のプロジェクトでは使われていない)有益なコメントを削除すると、メソッドは1つの画面内に収まり、複雑なロジックは含まれません。

最初の方では、setCacheを呼び出すことでキャッシュが有効になります。 その後、メインコンテナ、CControlsDialogは、最初のブロックで説明されています。 すでに作成されている「this」へのリンクを渡すので、キャッシュにはなりません。

  bool CControlsDialog::CreateLayout(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    StdLayoutBase::setCache(cache); // assign the cache object to store implicit objects
    
    {
      _layout<CControlsDialog> dialog(this, name, x1, y1, x2, y2);

そして、クラスCBoxのネストされたコンテナの暗黙のインスタンスが、ウィンドウのクライアント領域に作成されます。 縦向きなので、ネストになったコンテナが上から下へと空間を埋めていきます。 ウィンドウのサイズ変更時にそのメソッド Pack を呼び出す必要があるので、オブジェクトへのリンクを変数 m_main に保存します。 ダイアログが「rubber」ではない場合は、その必要はありません。 最後に、クライアント領域については、リサイズ時にもウィンドウ全体がパネルで埋め尽くされるように、ゼロフィールドと全方向への整列が設定されています。

      {
        // example of implicit object in the cache
        _layout<CBox> clientArea("main", ClientAreaWidth(), ClientAreaHeight(), LAYOUT_STYLE_VERTICAL);
        m_main = clientArea.get(); // we can get the pointer to the object from cache (if required)
        clientArea <= WND_ALIGN_CLIENT <= 0.0; // double type is important

次のレベルでは、コンテナは最初のものと同じように続き、ウィンドウの幅全体を埋めますが、インプットフィールドより少しだけ高くなります。 さらに、アライメント WND_ALIGN_TOP (WND_ALIGN_WIDTH と一緒に) を使用して、ウィンドウの上端に「接着」されます。

        {
          // another implicit container (we need no access it directly)
          _layout<CBox> editRow("editrow", ClientAreaWidth(), EDIT_HEIGHT * 1.5, (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_TOP|WND_ALIGN_WIDTH));

内部には、クラスCEditの「制御」が「読み取り専用」になっているだけです。 明示的な変数m_editは予約されているので、キャッシュには届きません。

          {
            // for editboxes default boolean property is ReadOnly
            _layout<CEdit> edit(m_edit, "Edit", ClientAreaWidth(), EDIT_HEIGHT, true);
          }
        }

この頃にはすでに3つの要素を初期化します。 閉じ括弧の後、"edit "レイアウトオブジェクトはブレイクダウンされ、そのデストラクタを実行する過程で、m_editがコンテナ "editrow "に追加されます。しかし、すぐに別の閉じ括弧が続きます。 レイアウトオブジェクトであるeditRowが "住んでいた "コンテキストをブレイクダウンします。そのため、このコンテナは、スタック上に残っているクライアント領域のコンテナに追加されます。 このようにして、m_mainの縦書きレイアウト用に1行目が形成されます。

そうすると、3つのボタンが並んでいる列になります。 まず、そのコンテナを作成します。

        {
          _layout<CBox> buttonRow("buttonrow", ClientAreaWidth(), BUTTON_HEIGHT * 1.5);
          buttonRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_WIDTH);

ここで、WND_ALIGN_CONTENTの非標準的な揃え方に注意してください。 以下のような意味です。

CBoxクラスに、ネストになった要素をコンテナサイズに合わせてスケールするアルゴリズムを追加しました。 AdjustFlexControlsメソッドで実行され、コンテナの整列のフラグでWND_ALIGN_CONTENTの空間値が指定されている場合にのみ有効になります。 標準の列挙であるENUM_WND_ALIGN_FLAGSの一部ではありません。 コンテナは、どのコントロールが固定サイズを持ち、どのコントロールが固定サイズを持たないかを分析します。 固定サイズの "コントロール "とは、コンテナの側面(特定の寸法)によって整列が指定されていないものを指します。 そのようなすべての「コントロール」について、コンテナはそれらのサイズの合計を計算し、コンテナの合計サイズからそれを差し引き、残りの「コントロール」の間で比例して分割します。例えば、コンテナの中に2つの「コントロール」があり、そのどれもがバインディングを持っていない場合、それらはコンテナの領域全体でお互いに半分になります。

これは非常に便利なモードですが、インターリーブされたコンテナのセットでは誤用しないでください - サイズを計算するシングルパスアルゴリズムのために、内部要素はコンテナの領域に整列し、次に内容に合わせて調整され、不確実性を生成します(このため、特別なイベント、ON_LAYOUT_REFRESHはレイアウトクラスで行われますを選択すると、ウィンドウはサイズの計算を繰り返すために自分自身に送信できます。

列に3つのボタンがある場合、ウィンドウの幅を変更すると、すべて比例して長さが変わります。 クラスCButtonの最初のボタンは暗黙的に作成され、キャッシュに格納されます。

          { // 1。
            _layout<CButton> button1("Button1");
            button1index = cache.cacheSize() - 1;
            button1["width"] <= BUTTON_WIDTH;
            button1["height"] <= BUTTON_HEIGHT;
          } // 1

2つ目のボタンはNotifiableButtonというクラスがあります(すでに上で説明しました)。 ボタンはメッセージを単独で処理します。

          { // 2。
            _layout<NotifiableButton> button2("Button2", BUTTON_WIDTH, BUTTON_HEIGHT);
          // 2

3 番目のボタンは、明示的に定義されたウィンドウ変数 m_button3 に基づいて作成され、"sticking" プロパティがあります。

          { // 3。
            _layout<CButton> button3(m_button3, "Button3", BUTTON_WIDTH, BUTTON_HEIGHT, "Locked");
            button3 <= true; // for buttons default boolean property is Locking
          } // 3
        }

すべてのボタンは中括弧で囲まれていますのでご注意ください。 このため、1,2,3と記された閉じ中括弧が現れた順に、自然なオーダーで行に追加されます。 これらの「個人的な」ブロックを各ボタンに作ることを省略して、コンテナの一般的なブロックで制限を受けることができます。 しかし、オブジェクトのデストラクタは常に作成時と逆の順番で呼び出されるため、ボタンは逆の順番で追加されることになります。 レイアウトのボタンの記述順を逆にすることで、「修正」することができました。

3列目には、コントロール、スピナー、カレンダーが入ったコンテナがあります。 コンテナは「匿名で」作成され、キャッシュに格納されます。

        {
          _layout<CBox> spinDateRow("spindaterow", ClientAreaWidth(), BUTTON_HEIGHT * 1.5);
          spinDateRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_WIDTH);
          
          {
            _layout<SpinEditResizable> spin(m_spin_edit, "SpinEdit", GROUP_WIDTH, EDIT_HEIGHT);
            spin["min"] <= 10;
            spin["max"] <= 1000;
            spin["value"] <= 100; // can set value only after limits (this is how SpinEdits work)
          }
          
          {
            _layout<CDatePicker> date(m_date, "Date", GROUP_WIDTH, EDIT_HEIGHT, TimeCurrent());
          }
        }

最後に、直近のコンテナは、ウィンドウの残りの領域をすべて埋め、要素を含む2つの列を含んでいます。 明るい色は、どのコンテナがウィンドウのどこにあるかを明確に示すために排他的に割り当てられています。

        {
          _layout<CBox> listRow("listsrow", ClientAreaWidth(), LIST_HEIGHT);
          listRow["top"] <= (int)(EDIT_HEIGHT * 1.5 * 3);
          listRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_CLIENT);
          (listRow <= clrMagenta)["border"] <= clrBlue;
          
          createSubList(&m_lists_column1, LIST_OF_OPTIONS);
          createSubList(&m_lists_column2, LIST_LISTVIEW);
          // or vice versa (changed order gives swapped left/right side location)
          // createSubList(&m_lists_column1, LIST_LISTVIEW);// createSubList(&m_lists_column2, LIST_OF_OPTIONS);
        }

ここで特に注意しなければならないのは、2つの列、m_lists_column1とm_lists_column2が、CreateLayoutメソッド自体ではなく、ヘルパーメソッドのcreateSubListを使っていることです。 レイアウト的には、中括弧の次のブロックに入るのと変わらない形で関数を呼び出します。 レイアウトが必ずしも長い静的リストで構成されているわけではなく、条件によって変更されたフラグメントを含んでいる可能性があることを意味します。 あるいは、同じフラグメントを別のダイアログにインクルードすることもできます。

この場合、関数の2番目のパラメータを変更することで、ウィンドウ内の列のオーダーを変更することができます。

      }
    }

すべての中括弧を閉じると、すべてのGUI要素が初期化され、互いに接続されます。 Packメソッドを呼び出します(直接、またはSelfAdjustmentを介して、"rubber "ダイアログをリクエストする際のレスポンスとしても呼び出されます)。

    // m_main.Pack();
    SelfAdjustment();
    return true;
  }

createSubListのメソッドの詳細は割愛します。 内部では、3つの「コントロール」(コンボボックス、オプション群、放射列群)やリスト(ListView)のセットを生成することができ、すべてが「rubber」で作られている可能性が実装されています。 興味深いのは、"コントロール "はItemGeneratorという別のクラスのジェネレーターを使って埋められていることです。

  template<typename T>
  class ItemGenerator
  {
    public:
      virtual bool addItemTo(T *object) = 0;
  };

このクラスの唯一のメソッドは、オブジェクト "control "のレイアウトから、メソッドがfalseを返すまで呼び出されます(データ終了のサイン)。

デフォルトでは、シンプルなジェネレータが標準ライブラリに提供されています( "controls "のメソッド、AddItemを使用します)。StdItemGenerator、StdGroupItemGenerator、SymbolsItemGenerator、ArrayItemGeneratorです。 特にSymbolsItemGeneratorでは、マーケットウォッチのシンボルを「コントロール」に塗りつぶしすることができます。

  template<typename T>
  class SymbolsItemGenerator: public ItemGenerator<T>
  {
    protected:
      long index;
      
    public:
      SymbolsItemGenerator() : index(0) {}。
      
      virtual bool addItemTo(T *object) override
      {
        object.AddItem(SymbolName((int)index, true), index);
        index++;
        return index < SymbolsTotal(true);
      }
  };

レイアウトでは、"コントロール "のジェネレータと同じ方法で指定されます。あるいは、レイアウトオブジェクトに、自動化されたものや静的なものではなく、動的に分散されたジェネレーターのオブジェクトへのポインタへのリンクを渡すこともできます(コードのどこか早い段階で記述されなければなりません)。

        _layout<ListViewResizable> list(m_list_view, "ListView", GROUP_WIDTH, LIST_HEIGHT);
        list <= WND_ALIGN_CLIENT < new SymbolsItemGenerator<ListViewResizable>();

このためには、演算子 < を使用します。 動的に分散されたジェネレーターは、タスク終了時に自動的に削除されます。

新しいイベントを接続するには、関連するマクロがマップに追加されます。

  EVENT_MAP_BEGIN(CControlsDialog)
    ...
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, NotifiableButton)
    ON_EVENT_LAYOUT_INDEX(ON_CLICK, cache, button1index, OnClickButton1)
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache)
  EVENT_MAP_END(AppDialogResizable)

マクロON_EVENT_LAYOUT_CTRL_DLGは、NotifiableButtonクラスの任意のボタン(ここでは1つのボタン)のマウスクリック時の通知を接続します。 マクロON_EVENT_LAYOUT_INDEXは、キャッシュ内の指定されたインデックスのボタンに同じイベントを送信します。 しかし、マクロ ON_EVENT_LAYOUT_ARRAY は、その識別子が lparam と一致していれば、キャッシュ内の任意の要素にマウスクリックの直近の文字列を送信するので、このマクロの記述を省略することができます。

基本的には、すべての要素をキャッシュに渡して、そのイベントを新しい方法で処理することができますが、古いものも機能し、組み合わせることができます。

以下の動画では、イベントへの反応を示します。

MQL マークアップ言語を使用して形成されたコントロールを含むダイアログ

MQL マークアップ言語を使用して形成されたコントロールを含むダイアログ

イベントの変換方法は、情報フィールドに表示されている関数のシグネチャによって間接的に識別できることに注意してください。 また、イベントが「コントロール」と「コンテナ」の両方で来ることがわかります。 コンテナの赤枠はデバッグ用に表示されるので、マクロ LAYOUT_BOX_DEBUG で無効にすることができます。

ケース3. DynamicFormの動的レイアウト

この直近の例では、すべての要素がキャッシュで動的に作成されるフォームを考えてみましょう。 これより、新たに重要な機会を得ることができます。

前回同様、キャッシュは要素のスタイリングに対応します。 唯一のスタイル設定は、コンテナのネストを見て、必要に応じて、マウスを使用してを選択することができます同一の特徴的なフィールドです。

CreateLayoutメソッド内には、以下のような簡単なインターフェース構造が記述されています。 いつものように、メインコンテナはウィンドウのクライアント領域全体を埋めます。 上部には2つのボタンがあるブロックがあります。インジェクションとエクスポートです。 その下のスペースは、左右の列に分けられたコンテナで埋め尽くされています。 グレーで表示されている左の列は元々空欄です。 右側の列には、コントロールの種類を選択できるラジオボタン群が配置されています。

      {
        // example of implicit object in the cache
        _layout<CBoxV> clientArea("main", ClientAreaWidth(), ClientAreaHeight());
        m_main = clientArea.get();
        clientArEA<= WND_ALIGN_CLIENT <= PackedRect(10, 10, 10, 10);
        clientArea["background"] <= clrYellow <= VERTICAL_ALIGN_TOP;
        
        {
          _layout<CBoxH> buttonRow("buttonrow", ClientAreaWidth(), BUTTON_HEIGHT * 5);
          buttonRow <= 5.0 <= (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_TOP|WND_ALIGN_WIDTH);
          buttonRow["background"] <= clrCyan;
          
          {
            // these 2 buttons will be rendered in reverse order (destruction order)
            // NB: automatic variable m_button3
            _layout<CButton> button3(m_button3, "Export", BUTTON_WIDTH, BUTTON_HEIGHT);
            _layout<NotifiableButton> button2("Inject", BUTTON_WIDTH, BUTTON_HEIGHT);
          }
        }
        
        {
          _layout<CBoxH> buttonRow("buttonrow2", ClientAreaWidth(), ClientAreaHeight(),
            (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_CONTENT|WND_ALIGN_CLIENT));
          buttonRow["top"] <= BUTTON_HEIGHT * 5;
          
          {
            {
              _layout<CBoxV> column("column1", GROUP_WIDTH, 100, WND_ALIGN_HEIGHT);
              column <= clrGray;
              {
                // dynamically created controls will be injected here
              }
            }
            
            {
              _layout<CBoxH> column("column2", GROUP_WIDTH, 100, WND_ALIGN_HEIGHT);
            
              _layout<RadioGroupResizable> selector("selector", GROUP_WIDTH, CHECK_HEIGHT);
              selector <= WND_ALIGN_HEIGHT;
              string types[3] = {"Button", "CheckBox", "Edit"};
              ArrayItemGenerator<RadioGroupResizable,string> ctrls(types);
              selector <= ctrls;
            }
          }
        }
      }

ラジオグループ内の要素タイプを選択した後、ユーザーがInjectボタンを押すと、ウィンドウの左側に関連する "コントロール "が作成されると考えられています。 もちろん、複数の「コントロール」を1つずつ作成することも可能です。 コンテナの設定に応じて自動的に中央に配置されます。 このロジックを実装するために、Inject ボタンはプロセッサ onEvent を持つクラス NotifiableButton があります。

  class NotifiableButton: public Notifiable<CButton>
  {
      static int count;
      
      StdLayoutBase *getPtr(const int value)
      {
        switch(value)
        {
          case 0.
            return new _layout<CButton>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
          case 1:
            return new _layout<CCheckBox>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
          case 2:
            return new _layout<CEdit>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
        }
        return NULL;
      }
      
    public:
      virtual bool onEvent(const int event, void *anything) override
      {
        DynamicForm *parent = dynamic_cast<DynamicForm *>(anything);
        MyStdLayoutCache *cache = parent.getCache();
        StdLayoutBase::setCache(cache);
        CBox *box = cache.get("column1");
        if(box != NULL)
        {
          // put target box to the stack by retrieving it from the cache
          _layout<CBox> injectionPanel(box, box.Name());
          
          {
            CRadioGroup *selector = cache.get("selector");
            if(selector != NULL)
            {
              const int value = (int)selector.Value();
              if(value != -1)
              {
                AutoPtr<StdLayoutBase> base(getPtr(value));
                (~base).get().Id(rand() + (rand() << 32));
              }
            }
          }
          box.Pack();
        }
        
        return true;
      }
  };

新しい要素が挿入されるコンテナは、最初に "column1 "という名前でキャッシュ内で検索されます。 このコンテナは、オブジェクトinjectionPanelを作成する際の最初のパラメータになります。 渡されるべき要素がすでにキャッシュにあるという事実は、レイアウトアルゴリズムで特に考慮されています。 これより、「古い」コンテナに要素を追加することができます。

ユーザーの選択に基づいて、ヘルパー メソッド getPtr の演算子 "new" を使用して、必要な型のオブジェクトが作成されます。 追加された「コントロール」が正しく動作するためには、一意の識別子がランダムに生成されます。 特別なクラス、AutoPtr はコードブロックからの終了時にポインタを確実に削除します。

あまりにも多くの要素が追加されると、コンテナの境界を超えてしまいます。 利用可能なコンテナクラスがオーバーフローに対応する方法をまだ学習していないために起こります。 この場合、例えばスクロールバーを表示し、境界線を越えた要素は非表示にすることができます。

これはそれほど重要ではありませんが。 この場合のポイントは、フォームを設定することで動的なコンテンツを生成し、必要な内容やコンテナのサイズを確保することです。

このダイアログでは、要素を追加するだけでなく、要素を削除することもできます。 フォーム内の任意の要素をマウスクリックで選択することができます。 同時に、要素のクラスと名前が記録され、要素自体が赤枠で強調表示されます。 既に選択されている要素をクリックすると、削除確認のリクエストがダイアログに表示され、確認された場合はその要素を削除します。 すべてキャッシュクラスに実装されています。

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      DynamicForm *parent;
      CWnd *selected;
      
      bool highlight(CWnd *control, const color clr)
      {
        CWndObj *obj = dynamic_cast<CWndObj *>(control);
        if(obj != NULL)
        {
          obj.ColorBorder(clr);
          return true;
        }
        else
        {
          CWndClient *client = dynamic_cast<CWndClient *>(control);
          if(client != NULL)
          {
            client.ColorBorder(clr);
            return true;
          }
        }
        return false;
      }
      
    public:
      MyStdLayoutCache(DynamicForm *owner): parent(owner) {}。
      
      virtual bool onEvent(const int event, CWnd *control) override
      {
        if(control != NULL)
        {
          highlight(selected, CONTROLS_BUTTON_COLOR_BORDER);
          
          CWnd *element = control;
          if(!find(element)) // this is an auxiliary object, not a compound control
          {
            element = findParent(control); // get actual GUI element
          }
          
          if(element == NULL)
          {
            Print("Can't find GUI element for ", control._rtti + " / " + control.Name());
            return true;
          }
          
          if(selected == control)
          {
            if(MessageBox("Delete " + element._rtti + " / " + element.Name() + "?", "Confirm", MB_OKCANCEL) == IDOK)
            {
              CWndContainer *container;
              container = dynamic_cast<CWndContainer *>(findParent(element));
              if(container)
              {
                revoke(element); // deep remove of all references (with subtree) from cache
                container.Delete(element); // delete all subtree of wnd-objects
                
                CBox *box = dynamic_cast<CBox *>(container);
                if(box) box.Pack();
              }
              selected = NULL;
              return true;
            }
          }
          selected = control;
          
          const bool b = highlight(selected, clrRed);
          Print(control.Name(), " -> ", element._rtti, " / ", element.Name(), " / ", b);
          
          return true;
        }
        return false;
      }
  };

キャッシュで利用可能なインターフェース要素はすべて削除することができます。 このようにして、例えば左半分全体を削除したり、右の "radiobox "を削除したりすることができます。最も興味深いのは、2つのボタンで上部のコンテナを削除しようとすると、何が起こるかということです。 これより、エクスポートボタンはダイアログに拘束されず、チャート内に留まります。

編集可能なフォーム:要素の追加と削除

編集可能なフォーム:要素の追加と削除

動的変数ではなく、意図的に自動変数として記述されている唯一の要素であるために起こります(フォームクラスでは、CButtonのインスタンスm_button3があります)。

標準ライブラリがインターフェースの要素を削除しようとするとき、を配列クラスCArrayObjに委譲し、配列クラスはポインタの型をチェックし、POINTER_DYNAMICを持つオブジェクトのみを削除します。 このように、要素が互いに入れ替わったり、完全に削除されたりするような適応的なインタフェースを構築するためには、動的配置を使用することが望ましいことが明らかになり、キャッシュはこれに対する解決策を提供しています。

最後に、ダイアログの2番目のボタン「エクスポート」を参照してみましょう。 その名前からもわかるように、ダイアログの現在の状態をMQL-layout構文でテキストファイルとして保存するように設計されています。 もちろん、この形態では、限られた範囲でしか外観の設定ができません。 しかし、外観を準備ができているMQLコードにエクスポートする可能性自体は、プログラムにコピーして同じインターフェイスを取得することができますが、潜在的には貴重な技術を表します。 もちろん、イベント処理コードや一般的な設定は独立して有効にする必要がありますが、インターフェースだけはエクスポートされます。

エクスポートはLayoutExporterクラスによって保証されています。

結論

今回は、MQLプログラムのグラフィカルインターフェースのレイアウトをMQL自体に記述するという概念の実装性を確認しました。 キャッシュに集中保管された要素の動的生成を使用することで、コンポーネントの階層の作成と制御を容易にすることができます。 キャッシュをベースに、特に統一されたスタイル変更、イベント処理、その場でのレイアウト編集、その後の使用に適した形式での保存など、インターフェイス設計に関連する大部分のタスクを実装することができます。

これらの関数を組み合わせれば、シンプルなビジュアルフォームエディタに実質的にすべての機能が利用可能であることがわかります。 多くの「コントロール」に共通する最も重要なプロパティだけをサポートすることができますが、それにもかかわらず、インターフェイステンプレートを形成することを可能にします。 しかし、この新しい概念を評価するための初期段階でさえ、多くのタスクを要していることがわかります。 したがって、新しいエディタの実用的な実装は、複雑な問題を表します。 しかし、それはまた別の話...