MQLプログラムをグラフィカルに表示するためのマークアップツールとしてのMQL(その3)。 フォームデザイナー

7 9月 2020, 16:17
Stanislav Korotky
0
775

最初の2つの記事(その1その2)では、MQLでインターフェイスマークアップシステムを構築する一般的な概念と、インターフェイス要素の階層的な初期化、キャッシュ、スタイリング、プロパティの設定、イベントの処理を表す基本クラスの実装について考察しました。 リクエスト要素を動的に作成することで、シンプルなダイアログレイアウトをその場で変更することができました。一方で、既に作成された要素の単一ストレージの可用性は、提案されたMQL構文で作成することを日常的に可能にし、その後、GUIが必要なMQLプログラムに「そのまま」挿入することができます。 このように、フォームのグラフィカルなエディタを作成することに近づいてきました。 今回の記事では、このタスクに密着していきます。

問題

エディタは、ウィンドウ内の要素を配置し、その基本的なプロパティを調整することを確認する必要があります。 以下にサポートされているプロパティの一般的なリストを示しますが、すべてのプロパティがすべてのタイプの要素で利用できるわけではありません。

  • タイプ
  • 名前
  • 高さ
  • 内部コンテンツの整列スタイル
  • テキストまたはヘッダ
  • 背景色
  • 親コンテナ内のアライメント
  • コンテナ境界のオフセット/フィールド

他の多くのプロパティは、フォント名やサイズ、様々なタイプの「コントロール」の固有のプロパティなど、ここには含まれていません(特に、「くっつく」ボタンのプロパティ)。 基本的に概念実証(POC)を目的としたプロジェクトをシンプル化するために意図的に行われています。 必要に応じて、追加のプロパティのサポートは後でエディタに追加することができます。

絶対座標でのポジシ ョ ンは、オフセットを通じて間接的に利用できますが、 推奨されるオプションではありません。 CBoxコンテナを使用することは、ポジション合わせの設定に従ってコンテナ自身が自動的にポジション合わせを行うことを示唆します。

エディタは、標準ライブラリのインターフェース要素のクラスに設計されています。 他のライブラリ用の同様のツールを作成するには、提案されているマークアップシステムから、すべての抽象的なエンティティの具体的な実装を記述する必要があります。 同時に、標準ライブラリのマークアップクラスの実装に導かれるようにしてください。

"標準コンポーネントのライブラリ "の定義が事実上正しくないことに注意する必要があります。 ここで、改良していきます。

エディタがサポートする要素の種類をリストアップしてみましょう。

  • 水平方向(CBoxH)と垂直方向(CBoxV)のコンテナCBox。
  • CButton,
  • CEditのインプットボックス。
  • CLabel,
  • SpinEditResizable,
  • CDatePicker calendar,
  • ドロップダウンリスト ComboBoxResizable.
  • List ListViewResizable,
  • CheckGroupResizable,
  • RadioGroupResizable.

すべてのクラスは適応的なリサイズを保証します (標準型は最初にできましたが、他の型についてはかなりの変更をしなければなりませんでした)。

プログラムは2つのウィンドウで構成されています。ユーザーが作成するコントロールの必要なプロパティを選択するダイアログ「インスペクタ」と、要素が作成され、設計されるグラフィカル・インターフェースの外観を形成するフォーム「デザイナー」です。

GUI MQLデザイナー プログラムインターフェイススケッチ

GUI MQLデザイナー プログラムインターフェイススケッチ

MQLに関しては、プログラムはInspectorDialogとDesignerFormという2つの基本的なクラスを持ち、それぞれの名前のヘッダファイルに記述されています。

  #include "InspectorDialog.mqh"
  #include "DesignerForm.mqh"
  
  InspectorDialog inspector;
  DesignerForm designer;
  
  int OnInit()
  {
      if(!inspector.CreateLayout(0, "Inspector", 0, 20, 20, 200, 400)) return (INIT_FAILED);
      if(!inspector.Run()) return (INIT_FAILED);
      if(!designer.CreateLayout(0, "Designer", 0, 300, 50, 500, 300)) return (INIT_FAILED);
      if(!designer.Run()) return (INIT_FAILED);
      return (INIT_SUCCEEDED);
  }

いずれのウィンドウも、MQLマークアップによって形成されたAppDialogResizable(以下、CAppDialog)の子孫です。 そのため、CreateではなくCreateLayoutを呼び出していることがわかります。

各ウィンドウは、インターフェース要素の独自のキャッシュがあります。 しかし、Inspectorでは最初から「コントロール」で埋め尽くされており、かなり複雑なレイアウトで記述されていますが、(一般論)Designerでは空っぽです。 この説明は簡単です。プログラムの実質的にすべてのビジネスロジックはインスペクタに格納されていますが、デザイナーはダミーであり、インスペクタはユーザーのコマンドによって、新しい要素を徐々に実装していきます。

PropertySet

上記の各プロパティは、特定の型の値で表されます。 例えば、要素名は文字列、幅と高さは整数です。 値の完全なセットは、デザイナーで表示する必要があるオブジェクトを完全に記述します。 セットを一箇所に格納するのは合理的であり、PropertySetという特別なクラスが導入されました。 しかし、どのようなメンバ変数が入っていなければならないのでしょうか?

一見すると、シンプルな埋め込み型の変数を使用することが明らかな解決策のように見えます。 しかし、今後必要とされるであろう重要な機能を欠いています。 MQLはシンプルな変数へのリンクをサポートしていません。 同時に、ユーザーインターフェースを処理するアルゴリズムにおいて、リンクは重要なものです。 これは価値観の変化に対する複雑な反応を意味することが多いです。 例えば、フィールドの1つにインプットされた範囲外の値は、依存する "コントロール "をブロックしなければなりません。 "コントロール "が、チェックされる値を格納する単一の場所に導かれて、自身の状態を制御できると便利です。 一番簡単なのは、同じ変数へのリンクの「ギブアウェイ」です。 そのため、シンプルな埋め込み型の代わりに、以下のようなテンプレートのラッパクラスを使用することになります。

  template<typename V>
  class Value
  {
    protected:
      V value;
      
    public:
      V operator~(void) const // getter
      {
        return value;
      }
      
      void operator=(V v)     // setter
      {
        value = v;
      }
  };

理由があって「だいたい」という言葉を入れています。 実際には、このクラスにはさらに機能が追加されますが、これについては後述します。

オブジェクトラッパを利用することで、シンプルな型を使用する場合には不可能な、オーバーロードされた演算子 '=' での新しい値の代入を傍受することができます。 そして、そのうち必要とするでしょう。

このクラスを考えると、新しいインターフェースオブジェクトのプロパティの集合は、おおよそ次のように記述することができます。

  class PropertySet
  {
    public:
      Value<string> name;
      Value<int> type;
      Value<int> width;
      Value<int> height;
      Value<int> style; // VERTICAL_ALIGN / HORIZONTAL_ALIGN / ENUM_ALIGN_MODE
      Value<string> text;
      Value<color> clr;
      Value<int> align; // ENUM_WND_ALIGN_FLAGS + WND_ALIGN_CONTENT
      Value<ushort> margins[4];
  };

インスペクタダイアログでは、インスペクタのコントロールからインプットした現在の設定を一元的に保存するものとして、このクラスの変数を導入します。

各プロパティを定義するために、インスペクタで適切なコントロールが使用するのは明らかです。 例えば、作成する「コントロール」の種類を選択するには、CComboBoxというドロップダウンリストを使用し、名前はCEditというインプットボックスを使用します。 プロパティは、リスト内の行、数、インデックスなどの型の単一の値を表します。 4辺それぞれに別々に定義されたオフセットのような複合プロパティであっても、4つのインプットフィールドはをインプットするために予約され、したがって、各値は割り当てられたコントロールに接続されるので、独立して考慮されるべきです(左、上など)。

このように、インスペクタダイアログでは、それぞれのコントロールが関連したプロパティを定義し、常に特定の型の値を持つようになっています。 これより、以下のような構築的な解決策が導き出されます。

「コントロール」の特性

以前の記事では、特定のコントロールのイベント処理を定義することができる特別なインターフェイス、Notifiableを紹介しました。

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

ここで、CはCEditやCSpinEditなどの「コントロール」クラスの一つです。 プロセッサのonEventは、関連する要素とイベントタイプに対してレイアウトキャッシュによって自動的に呼び出されます。 当然のことながら、正しい文字列がイベントマップに追加されている場合にのみ発生します。 例えば、前の部分では、この原理でInjectボタンのクリック処理を調整していました(Notifiable<CButton>の子孫と記載されていました)。

コントロールがあらかじめ定義された型のプロパティを調整するために使用する場合、より専門的なインターフェイスであるPlainTypeNotifiableを作成したくなります。

  template<typename C, typename V>
  class PlainTypeNotifiable: public Notifiable<C>
  {
    public:
      virtual V value() = 0;
  };

メソッド値は、C要素からCの最も特徴的なV型の値を返すことを目的とします。例えば、CEditクラスの場合、文字列型の値を返すことは自然に見えます(ある仮説クラスExtendedEditの場合)。

  class ExtendedEdit: public PlainTypeNotifiable<CEdit, string>
  {
    public:
      virtual string value() override
      {
        return Text();
      }
  };

各タイプの「コントロール」には、単一の特性データ型、またはその限られた範囲(例えば、整数の場合は、short、int、longの精度を選択することができます)があります。 すべての "コントロール "は、オーバーロード可能な "値 "メソッドで値を提供する準備ができている1つまたは別の "ゲッター "メソッドがあります。

このように、アーキテクチャ的な解決策のポイントに来ています - ValueとPlainTypeNotifiableクラスの調和です。 子孫クラスであるPlainTypeNotifiableを使用して実装されており、InspectorからリンクされたValueプロパティに "control "の値を移動させます。

  template<typename C, typename V>
  class NotifiableProperty: public PlainTypeNotifiable<C,V>
  {
    protected:
      Value<V> *property;
      
    public:
      void bind(Value<V> *prop)
      {
        property = prop;     // pointer assignment
        property = value();  // overloaded operator assignment for value of type V
      }
      
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CHANGE || event == ON_END_EDIT)
        {
          property = value();
          return true;
        }
        return false;
      };
  };

テンプレート・クラスPlainTypeNotifiableを継承しているため、新しいクラスNotifiablePropertyは、Cの "コントロール "クラスとV型値のプロバイダの両方を表します。

メソッドのバインドでは、"control "内にValueへのリンクを保持し、"control "でのユーザーの操作に応じて自動的に(参照によって)プロパティの値を変更することができます。

例えば、文字列型のインプットフィールドについては、ExtendedEditインスタンスに似たEditPropertyが導入されましたが、NotifiablePropertyを継承します。

  class EditProperty: public NotifiableProperty<CEdit,string>
  {
    public:
      virtual string value() override
      {
        return Text(); // Text() is a standard method of CEdit
      }
  };

ドロップダウン・リストの場合、同様のクラスは整数値を持つプロパティを記述します。

  class ComboBoxProperty: public NotifiableProperty<ComboBoxResizable,int>
  {
    public:
      virtual int value() override
      {
        return (int)Value(); // Value() is a standard method of CComboBox
      }
  };

プロパティ「コントロール」のクラスは、すべての基本的なタイプの要素についてプログラムに記述されています。

「通知可能なプロパティ」クラスの図

「通知可能なプロパティ」クラスの図

あとは、「だいたい」というエピテートから逃れて、フルクラスを知ることができます。

StdValue: 値、監視、および依存関係

標準的な状況として、ある「コントロール」の変化を監視して、他の「コントロール」の状態の妥当性や変化を確認することが必要であることは既に述べたとおりです。つまり、ある「制御」の変化を監視し、その変化を他の「制御」に知らせることができるオブザーバが必要です。

このため、StateMonitor(オブザーバー)というインターフェースが導入されました。

  class StateMonitor
  {
    public:
      virtual void notify(void *sender) = 0;
  };

メソッド通知は、必要に応じて、このオブザーバが応答できるようにするために、変更のソースによって呼び出されることを意図します。 変更のソースは、"sender "パラメータによって識別することができます。 もちろん、変化は、特定の観察者が通知を受けることに興味があることを、何となく事前に知っていなければなりません。 このためには、ソースはPublisherインターフェースを実装する必要があります。

  class Publisher
  {
    public:
      virtual void subscribe(StateMonitor *ptr) = 0;
      virtual void unsubscribe(StateMonitor *ptr) = 0;
  };

subscribe "メソッドを使用して、オブザーバーは自分自身へのリンクをパブリッシャーに渡すことができます。 推測するのは簡単ですが、変更のソースはプロパティになりますので、仮想クラス Value は、実際には Publisher から継承され、以下のように表示されます。

  template<typename V>
  class ValuePublisher: public Publisher
  {
    protected:
      V value;
      StateMonitor *dependencies[];
      
    public:
      V operator~(void) const
      {
        return value;
      }
      
      void operator=(V v)
      {
        value = v;
        for(int i = 0; i < ArraySize(dependencies); i++)
        {
          dependencies[i].notify(&this);
        }
      }
      
      virtual void subscribe(StateMonitor *ptr) override
      {
        const int n = ArraySize(dependencies);
        ArrayResize(dependencies, n + 1);
        dependencies[n] = ptr;
      }
      ...
  };

登録されたオブザーバーは、"dependencies "にアクセスし、値が変更された場合は、その "notify "メソッドを呼び出すことで通知されます。

プロパティは、導入された "コントロール "に一意に関連付けられているので、標準ライブラリのプロパティの直近のクラス、すなわちStdValue(すべてのCWindの "コントロール "の基本型を使用しています)に "コントロール "へのリンクを保存するために提供する予定です。

  template<typename V>
  class StdValue: public ValuePublisher<V>
  {
    protected:
      CWnd *provider;
      
    public:
      void bind(CWnd *ptr)
      {
        provider = ptr;
      }
      
      CWnd *backlink() const.
      {
        return provider;
      }
  };

このリンクは後々役に立つと思います。

PropertySetを満たすStdValueインスタンスです。

標準値通信図

標準値通信図

上記のNotifiablePropertyクラスでは、実際にはStdValueも使われており、メソッド「bind」ではプロパティの値を「コントロール」()にバインドします。

  template<typename C, typename V>
  class NotifiableProperty: public PlainTypeNotifiable<C,V>
  {
    protected:
      StdValue<V> *property;
    public:
      void bind(StdValue<V> *prop)
      {
        property = prop;
        property.bind(&this); // +。
        property = value();
      }
      ...
  };

「コントロール」状態の自動管理 - EnableStateMonitor

設定の変更に対応するための最も関連性の高い方法は、他の依存する "コントロール "をブロック/ブロックすることです。そのような適応的な "コントロール "のそれぞれの状態は、設定に依存しているかもしれません(1つだけでは必要ありません)。 それらを監視するために、EnableStateMonitorBaseという特別な抽象クラスが開発されました。

  template<typename C>
  class EnableStateMonitorBase: public StateMonitor
  {
    protected:
      Publisher *sources[];
      C *control;
      
    public:
      EnableStateMonitorBase(): control(NULL) {}。
      
      virtual void attach(C *c)
      {
        control = c;
        for(int i = 0; i < ArraySize(sources); i++)
        {
          if(control)
          {
            sources[i].subscribe(&this);
          }
          else
          {
            sources[i].unsubscribe(&this);
          }
        }
      }
      
      virtual bool isEnabled(void) = 0;
  };

"Control "には、与えられたオブザーバによって監視されている状態を表す "control "が配置されます。 配列「sources」には、状態に影響を与える変更のソースがあります。 配列は子孫クラスで埋めなければなりません。 "attach "を呼び出してオブザーバーを特定の "コントロール "に接続すると、オブザーバーは変更のすべてのソースをサブスクライブします。 そして、その "notify "メソッドを呼び出すことで、ソースの変更についての通知が開始されます。

「コントロール」がブロックされるべきかデブロックされるべきかは、isEnabledメソッドで決まりますが、ここでは抽象的に宣言されており、子孫クラスで実装されることになります。

Standard Libraryクラスについては、EnableとDisableの両方を使って「コントロール」を有効化/無効化する仕組みが知られています。 これらを使って、EnableStateMonitorという特定のクラスを実装してみましょう。

  class EnableStateMonitor: public EnableStateMonitorBase<CWnd>
  {
    public:
      EnableStateMonitor() {}
      
      void notify(void *sender) override
      {
        if(control)
        {
          if(isEnabled())
          {
            control.Enable();
          }
          else
          {
            control.Disable();
          }
        }
      }
  };

実際には、このクラスはプログラムの中で頻繁に使われることになりますが、ここでは一例だけ考えてみます。 デザイナーで新規オブジェクトを作成したり、変更したプロパティを使用したりするには、インスペクタのダイアログボックスに「適用」ボタンがあります(Notifiable<CButton>から派生したクラスApplyButtonが定義されています)。

  class ApplyButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CLICK)
        {
          ...
        }
      };
  };

オブジェクト名が定義されていないか、そのタイプが選択されていない場合は、ボタンをブロックする必要があります。 そこで、2つの変更ソース(「パブリッシャー」)を持つApplyButtonStateMonitorを実装します。名前とタイプです。

  class ApplyButtonStateMonitor: public EnableStateMonitor
  {
    // what's required to detect Apply button state
    const int NAME;
    const int TYPE;
    
    public:
      ApplyButtonStateMonitor(StdValue<<string> *n, StdValue<<int *t). NAME(0), TYPE(1)
      {
        ArrayResize(sources, 2);
        sources[NAME] = n;
        sources[TYPE] = t;
      }
      
      virtual bool isEnabled(void) override
      {
        StdValue<string> *name = sources[NAME];
        StdValue<int> *type = sources[TYPE];
        return StringLen(~name) > 0 && ~type != -1 && ~name != "Client";
      }
  };

クラスのコンストラクタは、関連するプロパティを指す 2 つのパラメータを取ります。 これらは "sourcees "配列に保存されます。 メソッドisEnabledは、名前が記入されているかどうか、タイプが選択されているかどうか(-1でないかどうか)を確認するために使用します。 条件を満たしていれば、ボタンを押すことができます。 さらに、その名前は、標準ライブラリのダイアログでクライアント領域に予約されているClientという特別な文字列にチェックされており、したがって、ユーザ要素の名前には現れません。

インスペクタダイアログクラスには ApplyButtonStateMonitor 型の変数があり、名前と型を格納する StdValue オブジェクトへのリンクによってコンストラクタで初期化されます。

  class InspectorDialog: public AppDialogResizable
  {
    private:
      PropertySet props;
      ApplyButtonStateMonitor *applyMonitor;
    public:
      InspectorDialog::InspectorDialog(void)
      {
        ...
        applyMonitor = new ApplyButtonStateMonitor(&props.name, &props.type);
      }

ダイアログ・レイアウトでは、名前とタイプのプロパティは関連する「コントロール」に結合され、オブザーバーは「適用」ボタンに結合されます。

          ...
          _layout<EditProperty> edit("NameEdit", BUTTON_WIDTH, BUTTON_HEIGHT, "");
          edit.attach(&props.name);
          ...
          _layout<ComboBoxProperty> combo("TypeCombo", BUTTON_WIDTH, BUTTON_HEIGHT);
          combo.attach(&props.type);
          ...
          _layout<ApplyButton> button1("Apply", BUTTON_WIDTH, BUTTON_HEIGHT);
          button1["enable"] <= false;
          applyMonitor.attach(button1.get());

applyMonitorオブジェクトの "attach "メソッドはすでに知っていますが、_layoutオブジェクトの "attach "は新しいものです。 クラス_layoutについては第2回の記事で詳しく取り上げましたが、そのバージョンと比べて変わったのは "attach "メソッドだけです。 この中間メソッドは、インスペクタダイアログ内の_layoutオブジェクトによって生成されたコントロールを "bind "するだけです。

  template<typename T>
  class _layout: public StdLayoutBase
  {
      ...
      template<typename V>
      void attach(StdValue<V> *v)
      {
        ((T *)object).bind(v);
      }
      ...
  };

この例のように、EditPropertyやComboBoxPropertyを含むすべてのプロパティ "コントロール "は、クラスNotifiablePropertyの子孫であることを覚えておく必要があり、その中には "コントロール "を関連するプロパティを格納するStdValue変数にバインドする "bind "メソッドがあります。 このように、インスペクタウィンドウ内の「コントロール」は、関連するプロパティにバインドされていることがわかり、後者のプロパティは、オブザーバのApplyButtonStateMonitorによって監視されています。 ユーザーが2つのフィールドのいずれかの値を変更するとすぐにPropertySetに表示され(NotifiablePropertyのイベントON_CHANGEとON_END_EDITのonEventプロセッサを覚えておいてください)、ApplyButtonStateMonitorを含む登録されたオブザーバーに通知されます。 その結果、現在のボタンの状態を自動的に変更することができます。

インスペクタダイアログには、同様の方法で "コントロール "の状態を監視するモニターが必要です。 具体的なブロッキングのルールをユーザーマニュアルの一節に記載します。

StateMonitor クラス

StateMonitor クラス

さて、作成するオブジェクトのプロパティとインスペクタダイアログの「コントロール」のすべてのプロパティの最終的な関連性を表してみましょう。

  • name — EditProperty, string;
  • type - ComboBoxProperty、整数、サポートされる要素のリストからの型番号です。
  • width — SpinEditPropertySize, integer, pixels;
  • height — SpinEditPropertySize, integer, pixels;
  • style - ComboBoxProperty,列挙のいずれかの値に等しい整数(要素の種類に依存します).VERTICAL_ALIGN(CBoxV),HORIZONTAL_ALIGN(CBoxH),ENUM_ALIGN_MODE(CEdit).
  • text — EditProperty, string;
  • background color - ComboBoxColorProperty, リストの色の値。
  • 境界アライメント - AlignCheckGroupProperty,ビットマスク,独立フラグのグループ(ENUM_WND_ALIGN_FLAGS + WND_ALIGN_CONTENT);および
  • indents — four SpinEditPropertyShort, integers;

"Property "要素のクラス名は、"シンプルな "SpinEditProperty、ComboBoxProperty、CheckGroupPropertyなどが提供する基本的な実装と比較して、特殊化、つまり機能の拡張を指します。 何に使うのかは、ユーザーマニュアルを見れば一目瞭然です。

「コントロール」を正確かつ明確に表示するために、ダイアログマークアップには確かに追加のコンテナとデータラベルがあります。 コード全体は添付ファイルにあります。

イベントの処理

すべての「コントロール」に対するイベントの処理は、イベントマップで定義されています。

  EVENT_MAP_BEGIN(InspectorDialog)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_END_EDIT, cache, EditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, SpinEditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, SpinEditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, SpinEditPropertyShort)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, ComboBoxProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, ComboBoxColorProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, AlignCheckGroupProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, ApplyButton)
    ...
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache) // default (stub)
  EVENT_MAP_END(AppDialogResizable)

キャッシュ内のイベントの処理効率を高めるために、特別なステップが行われています。 第2回で紹介したマクロON_EVENT_LAYOUT_CTRL_ANYとON_EVENT_LAYOUT_CTRL_DLGは、システムからパラメータlparamで受け取った一意の番号でキャッシュ配列内の「コントロール」を検索することに基づいて動作します。 同時に、基本的なキャッシュの実装は、配列を介して線形探索を実行します。

プロセスを高速化するために、クラスMyStdLayoutCache(StdLayoutCacheの子孫)にメソッドbuildIndexが追加され、そのインスタンスがInspectorに格納されて使用するようになりました。 標準ライブラリの特徴である、すべての要素に一意の番号を割り当てることで、便利なインデックス付け機能が実装されています。 CAppDialog::Runメソッドでは、ウィンドウによって作成されたすべてのチャートオブジェクトに番号が付けられているところから始まる乱数、すなわち既に知られているm_instance_idを指定します。 このようにして、得られた値の範囲を知ることができます。 m_instance_idを差し引くと、イベントに付随するlparamの各値がオブジェクトの直数になります。 しかし、プログラムはキャッシュに保存されたものよりもはるかに多くのオブジェクトをチャート内に作成します。なぜなら、多くの「コントロール」(および、フレーム、ヘッダ、最小化ボタンなどの集合体としてのウィンドウ自体)が複数の低レベルのオブジェクトから構成されているからです。 したがって、キャッシュ内のインデックスは、オブジェクト識別子から m_instance_id を引いた値と一致することはありません。 そのため、特別なインデックス配列(そのサイズはウィンドウ内のオブジェクトの数と同じ)を割り当てて、キャッシュにある "実際の "コントロールの連番をどうにかして書かなければなりませんでした。 その結果、間接アドレスの原則に基づいて、実質的に瞬時にアクセスが提供されます。

この配列は、基本的なCAppDialog::Runの実装で一意の番号が割り当てられた後、プロセッサのOnInitが動作を決済する前にのみ満たされるべきです。 そのためには、Runメソッドを仮想化して(標準ライブラリにはありません)、InspectorDialogの中でオーバーライドして、例えば以下のようにするのが最良の解決策です。

  bool InspectorDialog::Run(void)
  {
    bool result = AppDialogResizable::Run();
    if(result)
    {
      cache.buildIndex();
    }
    return result;
  }

メソッドbuildIndex自体はシンプルです。

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      InspectorDialog *parent;
      // fast access
      int index[];
      int start;
      
    public:
      MyStdLayoutCache(InspectorDialog *owner): parent(owner) {}。
      
      void buildIndex()
      {
        start = parent.GetInstanceId();
        int stop = 0;
        for(int i = 0; i < cacheSize(); i++)
        {
          int id = (int)get(i).Id();
          if(id > stop) stop = id;
        }
        
        ArrayResize(index, stop - start + 1);
        ArrayInitialize(index, -1);
        for(int i = 0; i < cacheSize(); i++)
        {
          CWnd *wnd = get(i);
          index[(int)(wnd.Id() - start)] = i;
        }
      ...
  };

あとは、「コントロール」を数字で検索するメソッドの簡単な実装を書いてみましょう。

      virtual CWnd *get(const long m) override
      {
        if(m < 0 && ArraySize(index) > 0)
        {
          int offset = (int)(-m - start);
          if(offset >= 0 && offset < ArraySize(index))
          {
            return StdLayoutCache::get(index[offset]);
          }
        }
        
        return StdLayoutCache::get(m);
      }

しかし、インスペクターの内部構造については十分です。

実行中のプログラムでは、このようにウィンドウが表示されます。

ダイアログインスペクタとフォームデザイナー

ダイアログインスペクタとフォームデザイナー

プロパティと並んで、ここでは未知の要素を見ることができます。 すべて後に記述しています。 では、「適用」ボタンを見てみましょう。 ユーザーがプロパティの値を設定した後、このボタンを押すことで、リクエストされたオブジェクトをデザイナーフォームに生成することができます。 Notifiableから派生したクラスを持つことで、ボタンは独自のメソッドonEventで押下を処理することができます。

  class ApplyButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CLICK)
        {
          Properties p = inspector.getProperties().flatten();
          designer.inject(p);
          ChartRedraw();
          return true;
        }
        return false;
      };
  };

変数のインスペクタとデザイナーは、それぞれインスペクタ・ダイアログとデザイナー・フォームのグローバル・オブジェクトであることに注意してください。 そのプログラム・インターフェースでは、Inspectorには、上述したプロパティの現在のセットであるPropertySetを提供するgetPropertiesメソッドがあります。

    PropertySet *getProperties(void) const
    {
      return (PropertySet *)&props;
    }

PropertySetは、レンジな(通常の)構造体、Properties、デザイナーメソッドに渡すために自分自身をパックすることができます注入します。 ここでは、デザイナーウィンドウに移行します。

Designer

追加のチェックはさておき、メソッド "inject" の本質は第二回の記事の最後に見たものと似ています。Form はターゲットコンテナをレイアウトスタックに配置し (2回目の記事では静的に設定されていました、つまり常に同じでした)、渡されたプロパティを含む要素を生成します。 新しいフォームでは、マウスクリックですべての要素を選択することができ、よって挿入コンテキストを変更することができます。 さらに、このようなクリックは、選択された要素のプロパティをインスペクタに転送します。 このように、既に作成されたオブジェクトのプロパティを編集し、同じ適用ボタンを使用して更新する機能があるようです。 デザイナーは、ユーザーが新しい要素を導入したいか、既存の要素を編集したいかを、要素の名前とタイプを比較することで検出します。 そのような組み合わせがすでにデザイナーキャッシュに存在する場合、編集を意味します。

一般的には、新しい要素を追加するとどのように見えるかということです。

    void inject(Properties &props)
    {
      CWnd *ptr = cache.get(props.name);
      if(ptr != NULL)
      {
        ...
      }
      else
      {
        CBox *box = dynamic_cast<CBox *>(cache.getSelected());
        
        if(box == NULL) box = cache.findParent(cache.getSelected());
        
        if(box)
        {
          CWnd *added;
          StdLayoutBase::setCache(cache);
          {
            _layout<CBox> injectionPanel(box, box.Name());
            
            {
              AutoPtr<StdLayoutBase> base(getPtr(props));
              added = (~base).get();
              added.Id(rand() + ((long)rand() << 32));
            }
          }
          box.Pack();
          cache.select(added);
        }
      }

変数 "cache "はDesignerFormで記述されており、StdLayoutCacheから派生したDefaultStdLayoutCacheクラスのオブジェクトを含んでいます(以前の記事で紹介しています)。 StdLayoutCache では、メソッド "get" を使用して名前でオブジェクトを見つけることができます。存在しない場合は、新しいオブジェクトがあることを意味し、デザイナーはユーザーが選択した現在のコンテナを検出します。 このために、getSelectedメソッドは新しいクラスDefaultStdLayoutCacheに実装されています。 具体的にどのように選択が行われるのかは、後ほど少し見てみましょう。 ここで注意しなければならないのは、新しい要素を実装する場所はコンテナ(ここではCBoxコンテナを使用します)のみであるということです。 その瞬間にコンテナが選択されていない場合、このアルゴリズムはfindParentを呼び出して親コンテナを検出し、ターゲットとして使用します。 挿入場所が定義されると、ネストになったブロックを用いた従来のマークアップ方式が動作を開始します。 外部ブロックでは、ターゲットコンテナを持つオブジェクト_layoutを作成し、その中に文字列でオブジェクトを生成します。

  AutoPtr<StdLayoutBase> base(getPtr(props));

すべてのプロパティはヘルパーメソッド getPtr に渡されます。 サポートされているすべてのタイプのオブジェクトを作成することができますが、するために、タイプのオブジェクトに対してどのように見えるかを示すだけにします。

    StdLayoutBase *getPtr(const Properties &props)
    {
      switch(props.type)
      {
        case _BoxH.
          {
            _layout<CBoxH> *temp = applyProperties(new _layout<CBoxH>(props.name, props.width, props.height), props);
            temp <= (HORIZONTAL_ALIGN)props.style;
            return temp;
          }
        case _Button:
          return applyProperties(new _layout<CButton>(props.name, props.width, props.height), props);
        case _Edit.
          {
            _layout<CEdit> *temp = applyProperties(new _layout<CEdit>(props.name, props.width, props.height), props);
            temp <= (ENUM_ALIGN_MODE)LayoutConverters::style2textAlign(props.style);
            return temp;
          }
        case _SpinEdit:
          {
            _layout<SpinEditResizable> *temp = applyProperties(new _layout<SpinEditResizable>(props.name, props.width, props.height), props);
            temp["min"] <= 0;
            temp["max"] <= DUMMY_ITEM_NUMBER;
            temp["value"] <= 1 <= 0;
            return temp;
          }
        ...
      }
    }

GUI要素の定義済みタイプによってテンプレート化されたオブジェクト_layoutは、MQLマークアップの静的記述によって既知のコンストラクタを使用して作成されます。 オブジェクト_layoutは、オーバーロードされた演算子<=を使用してプロパティを定義することを可能にします。特に、CBoxHでスタイルHORIZONTAL_ALIGNがどのように埋められるか、テキストフィールドでENUM_ALIGN_MODEがどのように埋められるか、またはスピナーの範囲がどのように埋められるかを示します。 インデント、テキスト、色などの他の一般的なプロパティの設定は、ヘルパーメソッド applyProperties に委譲されます (詳細はソースコードを参照してください)。

    template<typename T>
    T *applyProperties(T *ptr, const Properties &props)
    {
      static const string sides[4] = {"left", "top", "right", "bottom"};
      for(int i = 0; i < 4; i++)
      {
        ptr[sides[i]] <= (int)props.margins[i];
      }
      
      if(StringLen(props.text))
      {
        ptr <= props.text;
      }
      else
      {
        ptr <= props.name;
      }
      ...
      return ptr;
    }

キャッシュ内に名前でオブジェクトが見つかった場合は、以下のようになります(簡略化した形で)。

    void inject(Properties &props)
    {
      CWnd *ptr = cache.get(props.name);
      if(ptr != NULL)
      {
        CWnd *sel = cache.getSelected();
        if(ptr == sel)
        {
          update(ptr, props);
          Rebound(Rect());
        }
      }
      ...
    }

ヘルパーメソッド "update "は、構造体 "props "から見つかったptrオブジェクトにプロパティを転送します。

    void update(CWnd *ptr, const Properties &props)
    {
      ptr.Width(props.width);
      ptr.Height(props.height);
      ptr.Alignment(convert(props.align));
      ptr.Margins(props.margins[0], props.margins[1], props.margins[2], props.margins[3]);
      CWndObj *obj = dynamic_cast<CWndObj *>(ptr);
      if(obj)
      {
        obj.Text(props.text);
      }
      
      CBoxH *boxh = dynamic_cast<CBoxH *>(ptr);
      if(boxh)
      {
        boxh.HorizontalAlign((HORIZONTAL_ALIGN)props.style);
        boxh.Pack();
        return;
      }
      CBoxV *boxv = dynamic_cast<CBoxV *>(ptr);
      if(boxv)
      {
        boxv.VerticalAlign((VERTICAL_ALIGN)props.style);
        boxv.Pack();
        return;
      }
      CEdit *edit = dynamic_cast<CEdit *>(ptr);
      if(edit)
      {
        edit.TextAlign(LayoutConverters::style2textAlign(props.style));
        return;
      }
    }

ここで、フォーム内のGUI要素を選択する問題に戻りましょう。 ユーザーによって開始されたイベントを処理するために、キャッシュオブジェクトによって解決されます。 マクロ ON_EVENT_LAYOUT_ARRAY を使用してマップ上のチャートイベントに接続するために、プロセッサ onEvent はクラス StdLayoutCache に予約されています。

  EVENT_MAP_BEGIN(DesignerForm)
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache)
    ...
  EVENT_MAP_END(AppDialogResizable)

派生クラス DefaultStdLayoutCache で定義したプロセッサ onEvent に、すべてのキャッシュ要素のマウスクリックを送信します。 クラス内にユニバーサルウィンドウ型のCWndの "selected "ポインタを作成し、プロセッサonEventで埋めなければなりません。

  class DefaultStdLayoutCache: public StdLayoutCache
  {
    protected:
      CWnd *selected;
      
    public:
      CWnd *getSelected(void) const
      {
        return selected;
      }
      
      ...
      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(element); // get actual GUI element
          }
          ...
          
          selected = element;
          const bool b = highlight(selected, clrRed);
          Print(control.Name(), " -> ", element._rtti, " / ", element.Name(), " / ", element.Id());
          EventChartCustom(CONTROLS_SELF_MESSAGE, ON_LAYOUT_SELECTION, 0, 0.0, NULL);
          return true;
        }
        return false;
      }
  };

要素は、フォーム内で赤枠を使って視覚的に選択されます(ColorBorderを呼び出す)。 プロセッサは、先に選択された要素の選択を解除し(フレームカラーの設定、CONTROLS_BUTTON_COLOR_BORDER)、クリックされたチャートオブジェクトに対応するキャッシュ要素を見つけ、そのポインタを "selected "変数に保存します。 最後に、新しい選択されたオブジェクトは赤枠でマークされ、イベントON_LAYOUT_SELECTIONがチャートに送信されます。 フォームで新しい要素が選択されたことをインスペクタに通知し、インスペクタダイアログにそのプロパティを表示します。

Inspector では、このイベントはプロセッサ OnRemoteSelection で傍受され、Designer から select オブジェクトへのリンクをリクエストし、ライブラリの標準 API を介してそのオブジェクトからすべての属性を読み込みます。

  EVENT_MAP_BEGIN(InspectorDialog)
    ...
    ON_NO_ID_EVENT(ON_LAYOUT_SELECTION, OnRemoteSelection)
  EVENT_MAP_END(AppDialogResizable)

以下はOnRemoteSelectionメソッドの冒頭です。

  bool InspectorDialog::OnRemoteSelection()
  {
    DefaultStdLayoutCache *remote = designer.getCache();
    CWnd *ptr = remote.getSelected();
    
    if(ptr)
    {
      string purename = StringSubstr(ptr.Name(), 5); // cut instance id prefix
      CWndObj *x = dynamic_cast<CWndObj *>(props.name.backlink());
      if(x) x.Text(purename);
      props.name = purename;
      
      int t = -1;
      ComboBoxResizable *types = dynamic_cast<ComboBoxResizable *>(props.type.backlink());
      if(types)
      {
        t = GetTypeByRTTI(ptr._rtti);
        types.Select(t);
        props.type = t;
      }
      
      // width and height
      SpinEditResizable *w = dynamic_cast<SpinEditResizable *>(props.width.backlink());
      w.Value(ptr.Width());
      props.width = ptr.Width();
      
      SpinEditResizable *h = dynamic_cast<SpinEditResizable *>(props.height.backlink());
      h.Value(ptr.Height());
      props.height = ptr.Height();
      ...
    }
  }

デザイナーキャッシュから選択されたオブジェクトへの ptr リンクを受け取った後、アルゴリズムはその名前を検出し、ウィンドウ識別子からそれをクリアします(CAppDialog クラスのこのフィールド m_instance_id は、異なるウィンドウからのオブジェクト間の競合を防ぐために、すべての名前の接頭辞であり、2 つのウィンドウがあります)。 ここで注意しなければならないのは、プロパティStdValue<string>名から「コントロール」(backlink())へのバックリンクを使用しているところです。 また、内部からフィールドを変更するので、その変更に関連するイベントは生成されません(変更がユーザによって開始される場合もあります)ので、PropertySetの関連するプロパティ(props.name)に新しい値を書き込む必要があります。

技術的には、OOPの観点からは、プロパティ「control」の型ごとにその仮想的な変更方法をオーバーライドして、リンクされているStdValueインスタンスを自動的に更新する方が正しいでしょう。 ここでは、例えば、CEditでどのようにできるかを説明します。

  class EditProperty: public NotifiableProperty<CEdit,string>
  {
    public:
      ...
      virtual bool OnSetText(void) override
      {
        if(CEdit::OnSetText())
        {
          if(CheckPointer(property) != POINTER_INVALID) property = m_text;
          return true;
        }
        return false;
      }    
  };

その後、Text() メソッドを使用してフィールドの内容を変更すると、その後に OnSetText を呼び出して自動的にプロパティを更新することになります。 しかし、CCheckGroupのような複合コントロールの場合はあまり便利ではないので、より実用的な実装を目指しました。

同様に、「コントロール」へのバックリンクを利用して、デザイナーで選択したオブジェクトの高さや幅、種類などのプロパティのフィールドの内容を更新します。

サポートされている型を特定するために、前回の記事で最下層で追加した特殊変数_rttiを元に、その要素を検出できる列挙をクラスCWndに用意し、派生クラスの全てのクラスで特定のクラス名で埋めています。

クイックスタートガイド

インスペクタダイアログには、現在のオブジェクト(デザイナーで選択されている)または作成するオブジェクトのプロパティを持つ様々なタイプのインプットフィールドがあります。

必須フィールドは、名前(文字列)とタイプ(ドロップダウンリストで選択)です。

幅と高さのフィールドは、ピクセル単位でオブジェクトのサイズを定義することができます。 ただし、以下に特定のストレッチモードが指定されている場合は、設定は考慮されません。例えば、左右の境界線に束ねるというのは、コンテナにフィットした幅を意味します。 Shiftキーを押しながら高さまたは幅のフィールドをマウスでクリックすると、プロパティをデフォルト値(幅100、高さ20)にリセットすることができます。

SpinEdit タイプのすべての「コントロール」(サイズプロパティに限らず)は、マウスキーを押しながら「コントロール」内のマウスを左右に動かす(ドラッグはするがドロップはしない)と、「スピナー」の値がピクセル単位でカバーする距離に比例して素早く変化するように改良されています。 小さなポンピングボタンを押してもあまり便利ではない編集を容易にするために行われました。 ControlsPlusフォルダから「コントロール」を使用するプログラムに変更を加えることができます。

コンテンツアライメントスタイル(スタイル)を持つドロップダウンリストは、CBoxV、CBoxH、CEditの要素にのみ利用可能です(他のすべてのタイプではブロックされています)。 CBoxコンテナでは、すべてのアライメントモード("center", "justify", "left/top", "right/bottom", "stack")が有効になります。 CEditでは、ENUM_ALIGN_MODE("center", "left", "right")に対応するタスクのみを行います。

フィールド "Text "は、CButton、CLabel、またはCEditの内容のヘッダを定義することができます。 その他のタイプでは、このフィールドは無効になっています。

ドロップダウンリスト「カラー」は、Webカラーの一覧から背景色を選択するためのものです。 CBoxH、CBoxV、CButton、CEditでのみ利用可能です。 他のタイプの「コントロール」は、複合的なものであるため、すべてのコンポーネントの色を更新するには、より洗練された技術を必要とするため、を色付けすることはまだサポートしないことにしました。 色を選択するために、CListViewクラスを修正しました。 リスト項目の値をカラーコードとして解釈し、各項目の背景を関連する色で描画する特別な「カラー」モードが追加されました。 このモードは SetColorMode メソッドで有効にして、新しいクラス ComboBoxWebColors (Layouts フォルダの ComboBoxResizable の特殊化) で使用します。

ライブラリGUIの標準色はデフォルト色の定義に問題があるため、現時点では選択できません。 ユーザーが特定の色を選択していない場合、リストで選択されたように表示されないようにするために、「コントロール」の各タイプのデフォルトの色を知っておくことが重要です。 最もシンプルなアプローチは、特定の型の空の "コントロール "を作成し、その中にColorBackgroundのプロパティを読み込むことですが、限られた数の "コントロール "でしか動作しません。問題は、色は、原則として、クラスのコンストラクタでは割り当てられていませんが、チャート内の実際のオブジェクトの作成を含む多くの不必要な初期化を産み出すメソッドCreateであるということです。 もちろん、不要なものは必要ありません。 さらに、多くの複合オブジェクトの背景色は、基本的な "コントロール "ではなく、基板の背景に由来します。ニュアンスを考慮に入れることの複雑さに、標準ライブラリ "コントロール "の任意のクラスのすべてのデフォルト色を非選択とみなすことにしました。そうでなければ、ユーザがそのような色を選択することはできても、インスペクタで選択したことを確認することができないため、リストにインクルードすることができないことを意味します。 ウェブカラーと標準GUIカラーのリストは、ファイルLayoutColors.mqhに記載されています。

色をデフォルト値("コントロール "タイプごとに異なる)にリセットするには、clrNONEに関連するリストの最初の "空 "項目を選択する必要があります。

独立したスイッチャのグループである Alignment のフラグは、列挙 ENUM_WND_ALIGN_FLAGS からの両側のアライメントモードに対応し、さらに特別なモードである WND_ALIGN_CONTENT が追加されています。 スイッチャーを押しているときにShiftキーを押し続けると、プログラムはENUM_WND_ALIGN_FLAGSの4つのフラグすべてを同期して切り替えます。 このオプションが有効になっている場合は、他のものも有効になり、その逆もまた然りで、オプションが無効になっている場合は、他のものもリセットされます。 これにより、WND_ALIGN_CONTENT以外のグループ全体をワンクリックで切り替えることができます。

"スピナー" 余白は、この要素が配置されているコンテナの矩形の側面に関連して、要素のインデントを定義します。 フィールドの順番。左、上、右、下の順。 すべてのフィールドは、Shiftキーを押しながら任意のフィールドをクリックすることで、すばやくゼロにリセットすることができます。 すべてのフィールドは、Ctrlキーを押しながら必要な値を持つフィールドをクリックすることで、等しく設定することができます - この結果、値は他の3つのフィールドにコピーされます。

適用ボタンはすでに知っています - 変更を適用し、その結果、デザイナーに新しい「コントロール」を作成するか、既存のものを変更するかのどちらかになります。

新しいオブジェクトは、選択されたコンテナオブジェクトまたは選択された「コントロール」を含むコンテナに挿入されます(「コントロール」が選択されている場合)。

デザイナーで要素を選択するには、マウスで要素をクリックする必要があります。 選択された要素は赤枠で強調表示されます。 唯一の例外はCLabelです - この機能はCLabelではサポートされていません。

新しい要素が挿入されるとすぐに自動的に選択されます。

空のダイアログにはコンテナCBoxVまたはCBoxHのみを挿入することができ、クライアント領域を事前に選択する必要はありません。 この最初で最大のコンテナは、デフォルトではウィンドウ全体に張られています。

既に選択されている要素を繰り返しクリックすると、削除リクエストを呼び出します。 削除はユーザーが確認した場合のみ行われます。

2 ポジション ボタン TestMode は、Designer の 2 つの動作モードを切り替えます。 デフォルトでは押していない状態で、テストモードは無効になっており、デザイナーインターフェースの編集が動作します - ユーザーはマウスでクリックして要素を選択したり、削除したりすることができます。 押すとテストモードになります。 同時に、ダイアログは実際のプログラムとほぼ同じように動作しますが、レイアウトの編集や要素の選択は無効になっています。

ボタンのエクスポートでは、デザイナーインターフェイスの現在の設定をMQLレイアウトとして保存することができます。 ファイル名は、プレフィックスレイアウトで始まり、現在のタイムマスクと拡張子txtがあります。 Exportを押すときにShiftキーを押したままにすると、フォームの設定はテキストではなくバイナリ形式で、拡張子mqlという独自の形式のファイルに保存されます。 レイアウト設計のタスクを中断して、しばらくしたら元に戻せるので便利です。 バイナリ・レイアウトのmql-fileをアップロードするには、要素のフォームとキャッシュが空であることを条件に、同じExportボタンを使用します。 現在のバージョンでは、常にlayout.mqlというファイルをインポートします。 ご希望であれば、ファイル選択はインプットでもMQLでも実装できます。

インスペクタダイアログの上部には、デザイナーで作成されたすべての要素がドロップダウンで表示されます。 リスト内の要素を選択すると、デザイナーで自動的に選択され、ハイライト表示されます。 逆に、フォーム内の要素を選択するとリスト内で現在の状態になります。

さて、編集では、2つのカテゴリのエラーが発生することがあります。MQLのレイアウトを解析すれば直るものと、もっと深刻なものです。 前者には、「コントロール」やコンテナがウィンドウや親コンテナの境界を越えてしまうような設定の組み合わせがあります。 この場合、通常はマウスでの選択をストップし、インスペクタのセレクタを使用してのみ有効にすることができます。 どのプロパティが偽であるか、テキストのMQLマークアップを分析することによって見つけることができます - その現在の状態を取得するためにエクスポートを押すだけで十分です。 マークアップを解析したら、インスペクタのプロパティを修正して、フォームの正しい表示を復元してください。

このバージョンのプログラムはコンセプトを検証するためのもので、ソースコードでは、適応コンテナのサイズを再計算する際に発生する可能性のあるパラメータのすべての組み合わせについてのチェックは行われていません。

エラーの第2のカテゴリには、特に要素が誤って間違ったコンテナに挿入されている状況があります。 この場合は、要素を削除して再度別の場所に追加するしかありません。

定期的にバイナリ形式でフォームを保存することをお勧めします(エクスポートボタンを押しながら、Shiftキーを押しながら)ので、解決できない問題が発生した場合には、直近の良い設定からタスクを続けることができます。

プログラムを使ったタスク例を考えてみましょう。

まずは、Designerでインスペクタの構造を再現してみます。 下の動画では、上の4つの文字列とフィールドを追加して名前、種類、幅を設定するところから始まるプロセスを見ることができます。 異なるタイプの「コントロール」、整列、配色が使用されています。 フィールド名を含むラベルは、CLabelの機能が限られている(特に、テキストの整列と背景色はサポートされていない)ため、CEditのインプットフィールドを使用して形成されます。 ただし、Inspectorでは「読み取り専用」属性の設定はできません。 したがって、ラベルを非編集可能であると表現する唯一の方法は、グレーの背景を割り当てることです(純粋に視覚的な効果です)。 MQLコードでは、このようなCEditオブジェクトは、必ず応じて追加で調整されなければなりません。 まさにインスペクターそのものです。

フォームの編集

フォームの編集

フォームの編集は、マークアップ技術の適応性を明確に示しており、外部表現として、MQL-markupに一意に縛られています。 エクスポートボタンをいつでも押すことができ、結果のMQLコードを見ることができます。

最終バージョンでは、(一部の詳細を除いて)インスペクタウィンドウに完全に対応したダイアログを事前に取得することができます。

インスペクタダイアログのマークアップをデザイナーで復元

インスペクタダイアログのマークアップをデザイナーで復元

しかし、Inspectorの内部では、「コントロール」の多くのクラスは、特定のx-Propertyを継承し、アルゴリズムの追加的なハーネスを表しているため、非標準であることに注意する必要があります。 しかし、この例では、「コントロール」の標準クラス(ControlsPlus)のみが使用されています。 言い換えれば、結果として得られるレイアウトは、常にプログラムの外部表現と、「コントロール」のみの標準的な動作を含んでいます。 要素の状態を追跡し、クラスのカスタマイズの可能性を含めて、その変更に対する応答をコーディングすることは、プログラマーの特権です。 作成したシステムでは、通常のMQLと同様にMQLのマークアップに含まれるアーティファクトを変更することができます。 つまり、例えば ComboBox を ComboBoxWebColors に置き換えることができます。 しかし、いずれの場合も、レイアウトで言及されているすべてのクラスは、#includeのディレクティブを使用してプロジェクトに含まれている必要があります。

上記のダイアログ(インスペクタの複製)は、Exportコマンドを使用してテキストファイルとバイナリファイルに保存されました - 両方とも、それぞれlayout-inspector.txtとlayout-inspector.mqlという名前で添付されています。

テキストファイルを分析した後は、アルゴリズムやデータにバインドすることなく、インスペクタのマークアップの意味を理解することができます。

基本的には、マークアップをファイルにエクスポートした後、その内容を任意のプロジェクトに挿入することができ、レイアウトシステムのヘッダファイルや使用するすべてのGUIクラスを含む。 その結果、タスク用のインターフェイスが得られます。 特に、空のDummyFormダイアログを持つプロジェクトが添付されています。 希望すれば、その中にCreateLayoutを見つけて、Designerであらかじめ用意しておくMQLマークアップを挿入することもできます。

これはlayout-inspector.txtでもできます。 このファイルの内容全体をクリップボードにコピーし、CreateLayoutメソッド内のDummyForm.mqhファイルに挿入します。

ダイアログサイズは、作成されたレイアウトのテキスト表現(この場合、200*350)に記載されていることに注意してください。 そのため、ソースコードのCreateLayoutには、フォーム_layout<DummyForm>ダイアログ(this...)でオブジェクトを作成した文字列の後、コピーしたレイアウトの前に以下の文字列を挿入する必要があります。

  Width(200);
  Height(350);
  CSize sz = {200, 350};
  SetSizeLimit(sz);

これにより、すべての「コントロール」の十分なスペースが確保され、ダイアログを小さくすることができなくなります。

エクスポート時に自動的に関連するフラグメントを生成することはありません。 なぜなら、レイアウトはダイアログの一部だけを表現したり、最終的にはメソッドが存在しない他のクラスのウィンドウやコンテナに使用されたりするからです。

例をコンパイルして実行してみると、Inspectorのコピーが得られます。 しかし、やはり差はあります。

復元されたインスペクタインタフェース

復元されたインスペクタインタフェース

まず、ドロップダウンリストはすべて空なので、機能しません。 「スピナー」も調整されていないので、どちらも機能しません。 アラインメントフラグのグループは、レイアウト内の任意のチェックボックスを生成していないので、視覚的に空ですが、関連する「コントロール」が存在し、さらに、「コントロール」の初期サイズに基づいて、標準コンポーネントのライブラリによって生成される5つの隠されたチェックボックスがあります(チャートオブジェクトのリスト、コマンドオブジェクトリストですべてのオブジェクトを見ることができます)。

2つ目は、インデント値を持つ「スピナー」のグループが本当に存在しないことです。インスペクタでは配列として1つのレイアウトオブジェクトによって作成されているので、フォームには転送しませんでした。 今回のエディタはそんなことはできません。 4つの独立した要素を作ることもできますが、そうするとコードの中で似たような調整をしなければならなくなります。

「コントロール」が押されると、フォームはその名前、クラス、識別子をログに表示します。

また、バイナリファイルlayout-inspector.mql(事前にlayout.mqlにリネームしておいた)をInspectorにアップロードして、編集を続けることもできます。 この目的には、メインプロジェクトを鳴らして、フォームがまだ空のままなので早めにExportを押せば十分です。

Designerでは、説明に、リストやグループを持つすべての「コントロール」に対して、ある程度のデータを生成し、スピナーの範囲を設定することに注意してください。 そのため、TestModeに切り替えたときに要素で遊べるようにします。 この疑似データのサイズは、マクロDUMMY_ITEM_NUMBERによってデザイナーフォームで定義され、デフォルトでは11となっています。

では、デザイナーでのトレーディングパネルの表示方法を見てみましょう。

トレードパネルのレイアウト。カラーキューブ・トレードパネル

トレードパネルのレイアウト。カラーキューブ・トレードパネル

超機能性を装うものではなく、特定のトレーダーの好みに合わせて赤面的に変化させることができることが問題なのです。 この形は、先ほどの形と同様に、色付きのコンテナを使って配置を見やすくします。

ここでは外観だけを意味しているということを、また気にしておかなければならない。 デザイナーの出力では、ウィンドウの生成と "コントロール "の初期状態のみを担当するMQLコードが表示されます。いつものように、すべての計算アルゴリズム、ユーザーのアクションへの応答、誤ってインプットされたデータからの保護、およびトレードオーダーの送信は、手動でプログラムする必要があります。

このレイアウトでは、タイプの "コントロール "をより適切なものに置き換える必要があります。 このように、保留オーダーのテイクプロフィットは、時間のインプットをサポートしていないカレンダーで表示されます。 すべてのドロップダウン リストには、関連するオプションをインプットする必要があります。 例えば、ストップレベルは、価格、距離をpips単位でインプットしたり、リスク/ロスを入金率として絶対値でインプットしたり、ボリュームを固定、金額単位、余剰証拠金率として設定することができ、トレーリングはアルゴリズムの1つです。

このマークアップは、2つのレイアウト・カラーキューブ・トレードパネルファイルとして添付されています。テキストとバイナリです。 前者はDummyFormのような空のフォームに挿入して、データとイベントの処理で完成させることができます。 後者はDesignerにロードして編集することができます。 しかし、グラフィカルエディタは必須ではないことを覚えておきましょう。 マークアップは、そのテキスト表現を修正することもできます。 エディターの唯一の利点は、設定を弄ってその場で変更点を確認できることです。 ただし、最も基本的な機能にしか対応していません。

結論

本論文では、MQLマークアップ技術に基づいたプログラムのグラフィカルインタフェースをインタラクティブに開発するための簡易エディタを検討しました。 提示された実装には基本的な機能のみが含まれていますが、コンセプトのタスク性を示すのにはまだ十分であり、他のタイプの「コントロール」へのさらなる拡張、様々なプロパティ、GUIコンポーネントの他のライブラリ、および編集メカニズムのより完全なサポートがあります。 特にエディタには、操作のキャンセル、コンテナ内の任意のポジションへの要素の挿入(つまり、既に存在する「コントロール」のリストのトレーリングストップに追加するだけではなく)、グループ操作、クリップボードからのコピー・貼り付けなどの機能がまだまだ不足します。 しかし、オープンソースコードを使用することで、技術を補完したり、ニーズに合わせて適応させることができます。

MetaQuotes Software Corp.によりロシア語から翻訳された
元の記事: https://www.mql5.com/ru/articles/7795

添付されたファイル |
MQL5GUI3.zip (112.66 KB)
DoEasyライブラリの時系列(第43部): 指標バッファオブジェクトクラス DoEasyライブラリの時系列(第43部): 指標バッファオブジェクトクラス

この記事では、DoEasyライブラリに基づくカスタム指標プログラムを作成しながら、抽象バッファオブジェクトの子孫としての指標バッファオブジェクトクラスの開発を考察し、宣言を簡略化して指標バッファを操作します。

DoEasyライブラリの時系列(第42部): 抽象指標バッファオブジェクトクラス DoEasyライブラリの時系列(第42部): 抽象指標バッファオブジェクトクラス

この記事では、DoEasyライブラリの指標バッファクラスの開発を開始します。さまざまな種類の指標バッファの開発の基礎として使用される抽象バッファの基本クラスを作成します。

DLLなしのMT4およびMT5用ネイティブTwitterクライアント DLLなしのMT4およびMT5用ネイティブTwitterクライアント

ツイートにアクセスしたり、Twitterに取引シグナルを投稿したりしたかったことがおありですか?検索をおやめください。この連載では、DLLを使用せずにそれを行う方法を示します。MQLを使用してTweeter APIを実装する旅をお楽しみください。この第1部では、Twitter APIにアクセスする際の認証と承認の栄光の道をたどります。

ネイティブTwitterクライアント: 第2部 ネイティブTwitterクライアント: 第2部

MQLクラスとして実装した、写真付きのツイートを送信できるようにするTwitterクライアントです。1つの自己完結型インクルードファイルを含めるだけで、すぐにすべての素晴らしいチャートとシグナルをツイートできるようになります。