グラフィカルインターフェイスXI:ライブラリコードのリファクタリング(ビルド14.1)

Anatoli Kazharski | 12 9月, 2017


コンテンツ

はじめに

シリーズ第一弾のグラフィカルインタフェース I:ライブラリストラクチャの準備(チャプター 1)ではライブラリの目的が詳しく考察されました。リンクを含むチャプターのリストは各部の記事の終わりに設けられています。また、そこからはライブラリの最新のフルバージョンをダウンロードすることもできます。ファイルはアーカイブと同じディレクトリに配置される必要があります。

このライブラリの最新アップデートは、コードを最適化し、サイズを縮小し、実装をよりオブジェクト指向にすることを目的としています。コードの学習はこのすべてによってより容易になります。読者は、最新の変更の詳細な記述によって、短時間で、自らのタスクのためにライブラリを独自に修正することができるでしょう。 

このアップデートは大規模なため、2つの記事に分割されて説明されます。これが第一部です。

コントロールの一般的なプロパティ

まず変更されたのはライブラリコントロールの共通プロパティです。以前は、特定のプロパティ(背景、境界線、テキストなどの色)のクラスフィールドとメソッドは、個々のコントロールの派生クラスに格納されていました。オブジェクト指向プログラミングから見ると、これはオーバーロードに当たります。今では必要なグラフィカルインターフェイスコントロールがすべて実装されているので、頻繁に繰り返されるフィールドやメソッドを特定して一般的なプロパティを設定し、それらを基本クラスに移動するのは簡単です。 

ライブラリのすべてのコントロールに固有で、基本クラスに配置できるプロパティの完全なリストを定義しましょう。

  • ファイルから読み込むまたはプログラム的に描画する画像(アイコン)
  • X軸とY軸に沿った画像のインデント
  • 背景色
  • 境界線の色
  • テキストの色
  • 説明テキスト
  • 説明テキストのインデント

これらのプロパティを設定/取得するフィールドとメソッドを含むクラスを決定する必要があります。 

各コントロールのクラス階層には、CElementBaseCElementの2つのクラスがあります。座標、サイズ、識別子、インデックスプロパティのフィールドとメソッドは、リストからの各コントロールに固有のモードとともにCElementBase基本クラスに配置されます。コントロールの外観に関連するプロパティを管理するためのフィールドとメソッドはCElement派生クラスに配置されます。

さらに、コントロールのグラフィックオブジェクトの名前を作成するメソッドはCElementBase クラスに追加されます。以前は、これはコントロールの作成メソッドで生成されていました。各コントロールが別々のオブジェクトに描画されるようになったので、基本クラスに配置できる汎用メソッドを作成することができます。 

CElementBase::NamePart()メソッドは、コントロールの型を示す名前の一部を取得および設定するために設計されています。

//+------------------------------------------------------------------+
//| 基本コントロールクラス                                              |
//+------------------------------------------------------------------+
class CElementBase
  {
protected:
   //--- 名前の一部(コントロールの種類)
   string            m_name_part;
   //---
public:
   //--- コントロール名の一部を(1) 格納する (2)返す
   void              NamePart(const string name_part)                { m_name_part=name_part;                }
   string            NamePart(void)                            const { return(m_name_part);                  }
  };

CElementBase::ElementName()メソッドは、グラフィックオブジェクトの完全な名前を生成するために使用されます。このメソッドには、コントロールの型を示す名前の一部を渡さなければなりません。名前の一部が先に設定されている場合、渡された値は使用されません。ライブラリの最近の変更(下記参照)により、このアプローチは、あるコントロールが別のコントロールから派生し、その名前の一部を再定義する必要がある場合に使用されます。

class CElementBase
  {
protected:
   //--- コントロール名
   string            m_element_name;
   //---
public:
   //--- オブジェクト名の形成
   string            ElementName(const string name_part="");
  };
//+------------------------------------------------------------------+
//| 生成されたコントロール名を返す                                       |
//+------------------------------------------------------------------+
string CElementBase::ElementName(const string name_part="")
  {
   m_name_part=(m_name_part!="")?m_name_part : name_part;
//--- オブジェクト名の形成
   string name="";
   if(m_index==WRONG_VALUE)
      name=m_program_name+"_"+m_name_part+"_"+(string)CElementBase::Id();
   else
      name=m_program_name+"_"+m_name_part+"_"+(string)CElementBase::Index()+"__"+(string)CElementBase::Id();
//---
   return(name);
  }

特定のコントロールのマウスクリックを処理するときは、クリックされたグラフィックオブジェクトの名前を確認する必要があります。この確認は、しばしば多くのコントロールで繰り返し取られていたため、特別なCElementBase::CheckElementName()メソッドが基本クラスに追加されました。

class CElementBase
  {
public:
   //--- 行にコントロール名の重要な部分が含まれているかどうかを確認する
   bool              CheckElementName(const string object_name);
  };
//+------------------------------------------------------------------+
//| 生成されたコントロール名を返す                                       |
//+------------------------------------------------------------------+
bool CElementBase::CheckElementName(const string object_name)
  {
//--- 押されたのがこのコントロールであった場合
   if(::StringFind(object_name,m_program_name+"_"+m_name_part+"_")<0)
      return(false);
//---
   return(true);
  }

他のプロパティについては、画像を取り扱うメソッドの説明だけに留意することが理にかなっています。


画像データ操作メソッド

画像を扱うCImageクラスが実行され、以下の画像データはここで保存できます。

  • 画像画素の配列
  • 画像の大きさ(幅と高さ)
  • ファイルへのパス

これらのプロパティの値を取得するには、適切なメソッドが必要です。

//+------------------------------------------------------------------+
//| 画像データ格納クラス                                                |
//+------------------------------------------------------------------+
class CImage
  {
protected:
   uint              m_image_data[]; // 画像画素の配列
   uint              m_image_width;  // 画像幅
   uint              m_image_height; // 画像の高さ
   string            m_bmp_path;     // 画像ファイルへのパス
   //---
public:
                     CImage(void);
                    ~CImage(void);
   //--- (1) データ配列のサイズ (2) データ(画素色)を設定する/返す
   uint              DataTotal(void)                             { return(::ArraySize(m_image_data)); }
   uint              Data(const uint data_index)                 { return(m_image_data[data_index]);  }
   void              Data(const uint data_index,const uint data) { m_image_data[data_index]=data;     }
   //--- 画像幅を設定する/返す
   void              Width(const uint width)                     { m_image_width=width;               }
   uint              Width(void)                                 { return(m_image_width);             }
   ///--- 画像の高さを設定する/返す
   void              Height(const uint height)                   { m_image_height=height;             }
   uint              Height(void)                                { return(m_image_height);            }
   //--- 画像へのパスを設定する/返す
   void              BmpPath(const string bmp_file_path)         { m_bmp_path=bmp_file_path;          }
   string            BmpPath(void)                               { return(m_bmp_path);                }
  };

CImage::ReadImageData()メソッドは、画像を読み込んでそのデータを格納するために使用されます。このメソッドには、画像ファイルへのパスを渡す必要があります。

class CImage
  {
public:
   //--- 渡された画像のデータを読み込んで保存する
   bool              ReadImageData(const string bmp_file_path);
  };
//+------------------------------------------------------------------+
//| 渡された画像を配列に保存する                                        |
//+------------------------------------------------------------------+
bool CImage::ReadImageData(const string bmp_file_path)
  {
//--- 空の文字列の場合は終了する
   if(bmp_file_path=="")
      return(false);
//--- 画像へのパスを格納する
   m_bmp_path=bmp_file_path;
//--- 最後のエラーをリセットする
   ::ResetLastError();
//--- 画像データを読み込んで格納する
   if(!::ResourceReadImage("::"+m_bmp_path,m_image_data,m_image_width,m_image_height))
     {
      ::Print(__FUNCTION__," > Error when reading the image ("+m_bmp_path+"): ",::GetLastError());
      return(false);
     }
//---
   return(true);
  }

時には渡された画像のデータをコピーする必要があります。これはCImage::CopyImageData()メソッドで行われます。このオブジェクトの配列データをコピーするために、このメソッドにはCImage型のオブジェクトが参照によって渡されます。ここで、ソース配列のサイズが最初に取得され、同じサイズが受け取り側の配列に設定されます。次にCImage::Data() メソッドを使用して渡された配列のデータを取得し、それを受け取り側の配列に格納します

class CImage
  {
public:
   //--- 渡された画像のデータをコピーする
   void              CopyImageData(CImage &array_source);
  };
//+------------------------------------------------------------------+
//| 渡された画像のデータをコピーする                                     |
//+------------------------------------------------------------------+
void CImage::CopyImageData(CImage &array_source)
  {
//--- ソース配列のサイズを取得する
   uint source_data_total =array_source.DataTotal();
//--- 受信側の行のサイズを変更する
   ::ArrayResize(m_image_data,source_data_total);
//--- データをコピーする
   for(uint i=0; i<source_data_total; i++)
      m_image_data[i]=array_source.Data(i);
  }


画像データを削除するにはCImage::DeleteImageData()メソッドが使用されます。

class CImage
  {
public:
   //--- 画像データを読み込む
   void              DeleteImageData(void);
  };
//+------------------------------------------------------------------+
//| 画像データを削除する                                               |
//+------------------------------------------------------------------+
void CImage::DeleteImageData(void)
  {
   ::ArrayFree(m_image_data);
   m_image_width  =0;
   m_image_height =0;
   m_bmp_path     ="";
  }

CImageクラスはObjects.mqhファイルに含まれます。これですべてのコントロールがレンダリングされるため、グラフィックプリミティブを作成するためのクラスは不要になり、Objects.mqhファイルから削除されました。例外はサブチャートのみです。サブチャートでは銘柄のメインチャートと同様のチャートを作成できます。すべてのタイプのMQLアプリケーションはそのウィンドウに配置されているため、グラフィカルインタフェースが作成されます。

//+------------------------------------------------------------------+
//|                                                      Objects.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#include "Enums.mqh"
#include "Defines.mqh"
#include "Fonts.mqh"
#include "Canvas\Charts\LineChart.mqh"
#include <ChartObjects\ChartObjectSubChart.mqh>
//--- 迅速なナビゲーションのためのファイルでのクラスリスト (Alt+G)
class CImage;
class CRectCanvas;
class CLineChartObject;
class CSubChart;
...


画像操作メソッド

画像を操作するためのメソッドがいくつか実装され、CElementクラスに配置されています。特定のコントロールの画像グループ(配列)に必要な数を設定できるようになりました。これにより、コントロールの外観をより有益なものにすることができます。コントロールに表示されるアイコンの数は、MQLアプリケーションの開発者によって定義されます。 

この目的のためにEImagesGroup 構造体が作成され、インスタンスの動的配列CElement ファイルで宣言されていて、CImage型の動的配列に格納されている画像グループのプロパティ(インデントと表示用に選択された画像)と画像自体が含まれます 。 

//+------------------------------------------------------------------+
//| コントロールの派生クラス                                            |
//+------------------------------------------------------------------+
class CElement : public CElementBase
  {
protected:
   //--- 画像グループ
   struct EImagesGroup
     {
      //--- 画像の配列
      CImage            m_image[];
      //--- アイコンのマージン
      int               m_x_gap;
      int               m_y_gap;
      //---表示用に選択したグループの画像
      int               m_selected_image;
     };
   EImagesGroup      m_images_group[];
  };

コントロールに画像を追加するには、最初にグループを追加する必要があります。これは CElement::AddImagesGroup()メソッドの助けを借りて行うことができます。このグループのコントロールの左上からの画像のインデントを引数として渡す必要があります。デフォルトでは、グループ内の最初の画像が選択されて表示されます。

class CElement : public CElementBase
  {
public:
   //--- 画像グループの追加
   void              AddImagesGroup(const int x_gap,const int y_gap);
  };
//+------------------------------------------------------------------+
//| 画像グループの追加                                                 |
//+------------------------------------------------------------------+
void CElement::AddImagesGroup(const int x_gap,const int y_gap)
  {
//--- 画像グループ配列サイズを取得する
   uint images_group_total=::ArraySize(m_images_group);
//--- 1グループを追加する
   ::ArrayResize(m_images_group,images_group_total+1);
//--- 画像のインデントを設定する
   m_images_group[images_group_total].m_x_gap=x_gap;
   m_images_group[images_group_total].m_y_gap=y_gap;
//--- デフォルト画像
   m_images_group[images_group_total].m_selected_image=0;
  }

CElement::AddImage()メソッドはグループに画像を追加するために設定されていて、グループのインデックスと画像ファイルへのパスを引数として指定する必要があります。グループがない場合、画像は追加されません。また、ここには配列の範囲を超えないようにするための調整があり、範囲を超えた場合、画像は最後のグループに追加されます。

class CElement : public CElementBase
  {
public:
   //--- 画像の指定されたグループへの追加
   void              AddImage(const uint group_index,const string file_path);
  };
//+------------------------------------------------------------------+
//| 画像の指定されたグループへの追加                                     |
//+------------------------------------------------------------------+
void CElement::AddImage(const uint group_index,const string file_path)
  {
//--- 画像グループ配列サイズを取得する
   uint images_group_total=::ArraySize(m_images_group);
//--- グループがない場合は終了する
   if(images_group_total<1)
     {
      Print(__FUNCTION__,
      " > A group of images can be added using the CElement::AddImagesGroup() methods");
      return;
     }
//---範囲超過防止
   uint check_group_index=(group_index<images_group_total)?group_index : images_group_total-1;
//--- 画像配列サイズを取得する
   uint images_total=::ArraySize(m_images_group[check_group_index].m_image);
//--- 配列サイズを1要素で増やす
   ::ArrayResize(m_images_group[check_group_index].m_image,images_total+1);
//--- 画像を追加する
   m_images_group[check_group_index].m_image[images_total].ReadImageData(file_path);
  }


CElement::AddImagesGroup()メソッドの2番目のバージョンを使用して、すぐに画像の配列を含むグループを追加することは可能です。ここでは、インデントに加えて、ファイルパスを引数とする配列を渡す必要があります。画像要素配列のサイズを1要素ずつ増加させた後、プログラムはCElement::AddImage()メソッドを使用して、渡された画像の配列全体をループで追加します(上記を参照)。

class CElement : public CElementBase
  {
public:
   //--- 画像の配列を含む画像グループの追加
   void              AddImagesGroup(const int x_gap,const int y_gap,const string &file_pathways[]);
  };
//+------------------------------------------------------------------+
//| 画像の配列を含む画像グループの追加                                   |
//+------------------------------------------------------------------+
void CElement::AddImagesGroup(const int x_gap,const int y_gap,const string &file_pathways[])
  {
//--- 画像グループ配列サイズを取得する
   uint images_group_total=::ArraySize(m_images_group);
//--- 1グループを追加する
   ::ArrayResize(m_images_group,images_group_total+1);
//--- 画像のインデントを設定する
   m_images_group[images_group_total].m_x_gap =x_gap;
   m_images_group[images_group_total].m_y_gap =y_gap;
//--- デフォルト画像
   m_images_group[images_group_total].m_selected_image=0;
//--- 追加された画像の配列サイズを取得する
   uint images_total=::ArraySize(file_pathways);
//--- 空でない配列が渡された場合は画像を新しいグループに追加する
   for(uint i=0; i<images_total; i++)
      AddImage(images_group_total,file_pathways[i]);
  }

特定のグループの画像は、プログラム実行時に設定または置き換えられます。これにはCElement::SetImage()メソッドを使用して、引数として (1) グループのインデックス (2) 画像のインデックス (3) ファイルパスを渡します。 

class CElement : public CElementBase
  {
public:
   //--- 画像の設定/置き換え
   void              SetImage(const uint group_index,const uint image_index,const string file_path);
  };
//+------------------------------------------------------------------+
//| 画像の設定/置き換え                                                |
//+------------------------------------------------------------------+
void CElement::SetImage(const uint group_index,const uint image_index,const string file_path)
  {
//--- 配列の範囲が超えられているかどうかの確認
   if(!CheckOutOfRange(group_index,image_index))
      return;
//--- 画像を削除する
   m_images_group[group_index].m_image[image_index].DeleteImageData();
//--- 画像を追加する
   m_images_group[group_index].m_image[image_index].ReadImageData(file_path);
  }

しかし、必要な画像がすべてコントロールの作成時にすべて設定されている場合は、CElement :: ChangeImage()メソッドを使用して画像を切り替える方が簡単です。

class CElement : public CElementBase
  {
public:
   //--- 画像の切り替え
   void              ChangeImage(const uint group_index,const uint image_index);
  };
//+------------------------------------------------------------------+
//| 画像の切り替え                                                     |
//+------------------------------------------------------------------+
void CElement::ChangeImage(const uint group_index,const uint image_index)
  {
//--- 配列の範囲が超えられているかどうかの確認
   if(!CheckOutOfRange(group_index,image_index))
      return;
//--- 表示する画像のインデックスを格納するy
   m_images_group[group_index].m_selected_image=(int)image_index;
  }

特定のグループで現在選択されている画像を見つけるには CElement::SelectedImage()メソッドを使用します。グループが存在しない場合、または指定されたグループに画像がない場合、メソッドは負の値を返します

class CElement : public CElementBase
  {
public:
   //--- 指定されたグループに表示するために選択された画像を返す
   int               SelectedImage(const uint group_index=0);
  };
//+------------------------------------------------------------------+
//| 指定されたグループに表示するために選択された画像を返す                  |
//+------------------------------------------------------------------+
int CElement::SelectedImage(const uint group_index=0)
  {
//--- グループがない場合は終了する
   uint images_group_total=::ArraySize(m_images_group);
   if(images_group_total<1 || group_index>=images_group_total)
      return(WRONG_VALUE);
//--- 指定されたグループに画像がない場合は終了する
   uint images_total=::ArraySize(m_images_group[group_index].m_image);
   if(images_total<1)
      return(WRONG_VALUE);
//--- 表示するために選択された画像を返す
   return(m_images_group[group_index].m_selected_image);
  }


以前は、アイコンを表示する必要のあるコントロールのすべてのクラスには、画像を設定するためのメソッドが存在し、例えば、ボタンには、解除された状態と押された状態、およびロックされた状態のアイコンを割り当てることができました。この機能は、わかりやすく分かりやすいオプションであるため、そのまま残ります。前述のように、アイコンのインデントは CElement::IconXGap()メソッドとCElement::IconYGap()メソッドを使用して設定できます。

class CElement : public CElementBase
  {
protected:
   //--- アイコンのマージン
   int               m_icon_x_gap;
   int               m_icon_y_gap;
   //---
public:
   //--- アイコンのマージン
   void              IconXGap(const int x_gap)                       { m_icon_x_gap=x_gap;              }
   int               IconXGap(void)                            const { return(m_icon_x_gap);            }
   void              IconYGap(const int y_gap)                       { m_icon_y_gap=y_gap;              }
   int               IconYGap(void)                            const { return(m_icon_y_gap);            }
   //--- アクティブ状態とブロック状態のアイコンの設定
   void              IconFile(const string file_path);
   string            IconFile(void);
   void              IconFileLocked(const string file_path);
   string            IconFileLocked(void);
   //--- コントロールの押された(利用可能/ロックされた)状態のアイコンの設定
   void              IconFilePressed(const string file_path);
   string            IconFilePressed(void);
   void              IconFilePressedLocked(const string file_path);
   string            IconFilePressedLocked(void);
  };

CElement::IconFile()メソッドのコードが例として提示されます。コントロールにまだ画像グループがない場合は、ここで最初にグループが追加されます。メソッドを呼び出す前にインデントが指定されなかった場合は、ゼロ値が設定されます。グループを追加すると、引き数に渡された画像が追加され、コントロールがロックされた状態で画像のためのスペースが予約されます

//+------------------------------------------------------------------+
//| アクティブ状態のアイコンの設定                                       |
//+------------------------------------------------------------------+
void CElement::IconFile(const string file_path)
  {
//--- まだ画像グループがない場合
   if(ImagesGroupTotal()<1)
     {
      m_icon_x_gap =(m_icon_x_gap!=WRONG_VALUE)?m_icon_x_gap : 0;
      m_icon_y_gap =(m_icon_y_gap!=WRONG_VALUE)?m_icon_y_gap : 0;
      //--- グループと画像を追加する
      AddImagesGroup(m_icon_x_gap,m_icon_y_gap);
      AddImage(0,file_path);
      AddImage(1,"");
      //--- デフォルト画像
      m_images_group[0].m_selected_image=0;
      return;
     }
//--- 画像を最初の要素として最初のグループに設定する
   SetImage(0,0,file_path);
  }

画像グループ数または特定グループ内の画像数を調べるには、対応する方法を使用する必要があります(下記のコードを参照)。

class CElement : public CElementBase
  {
public:
   //--- 画像グループ数を返す
   uint              ImagesGroupTotal(void) const { return(::ArraySize(m_images_group)); }
   //--- 特定のグループの画像数を返す
   int               ImagesTotal(const uint group_index);
  };
//+------------------------------------------------------------------+
//| 特定のグループの画像数を返す                                         |
//+------------------------------------------------------------------+
int CElement::ImagesTotal(const uint group_index)
  {
//--- グループインデックスの確認
   uint images_group_total=::ArraySize(m_images_group);
   if(images_group_total<1 || group_index>=images_group_total)
      return(WRONG_VALUE);
//--- 画像数
   return(::ArraySize(m_images_group[group_index].m_image));
  }

ライブラリコード最適化の一部としてのコントロールの統合

今まで、多くのコントロールが事実上繰り返されてきましたが、一意のメソッドは数えるほどしかありませんでした。これによってコードが大幅に膨張したため、ライブラリが変更されてコードが簡素化され、理解が容易になりました。

1. 以前は、ボタンを作成するための2つのクラスが開発されました。

  • CSimpleButton — シンプルボタン
  • CIconButton — アイコンボタン

しかし、アイコンは基本的なツールを使ってどのコントロールでも作成できるようになったため、異なるプロパティを持つボタンを作成するのにもはや2つのクラスは必要ありません。変更されたクラスはCButtonのあと1つだけです 。テキストをボタンの中央に配置するには、対応するモードを CElement::IsCenterText()メソッドで有効にします。これはどのコントロールにも適用できます。

class CElement : public CElementBase
  {
protected:
   //--- テキストの配置モード
   bool              m_is_center_text;
   //--- 
public:

   //--- テキストを中央に揃える
   void              IsCenterText(const bool state)                  { m_is_center_text=state;          }
   bool              IsCenterText(void)                        const { return(m_is_center_text);        }
  };

2. ボタングループを作成する場合も同様です。以前のバージョンのライブラリでは、異なるプロパティを持つボタンのグループを作成するために3つのクラスが作成されました。

  • CButtonsGroup — シンプルボタンのグループ
  • CRadioButtons — ラジオボタンのグループ
  • CIconButtonsGroup — アイコンボタンのグループ

これらのクラスでは、標準のグラフィックプリミティブからボタンが作成されました。今残るのはCButtonsGroupクラスだけです。ボタンはCButton型の既成のコントロールとして作成されます。 

CButtonsGroup::RadioButtonsMode()メソッドを使用すると、ラジオボタンモード(グループ内の1つのボタンが常に選択されている場合)を有効にできます。ラジオボタンと同様の外観はCButtonsGroup::RadioButtonsStyle()メソッドを使用して有効にできます。

class CButtonsGroup : public CElement
  {
protected:
   //    ラジオボタンモード
   bool              m_radio_buttons_mode;
   //--- ラジオボタン表示のスタイル
   bool              m_radio_buttons_style;
   //--- 
public:
   //--- ラジオボタンの (1) モード設定 と (2) 表示スタイル
   void              RadioButtonsMode(const bool flag)              { m_radio_buttons_mode=flag;       }
   void              RadioButtonsStyle(const bool flag)             { m_radio_buttons_style=flag;      }
  };

3. エディットボックスを含むコントロールを作成するには、次の3つのクラスを検討します。

  • CSpinEdit — スピンエディットボックス
  • CCheckBoxEdit — チェックボックス付きのスピンエディットボックス
  • CTextEdit — テキストエディットボックス

上記のクラスのすべてのプロパティは CTextEditのように一つにコンパクトに配置できます。インクリメント/デクリメントスイッチ付きのスピンエディットボックスを作成する必要がある場合はCTextEdit::SpinEditMode()メソッドを使用して対応するモードを有効にします。コントロールにチェックボックスが必要な場合は、CTextEdit::CheckBoxMode()メソッドを使用してそのモードを有効にします。 

class CTextEdit : public CElement
  {
protected:
   //--- コントロールにチェックボックスをつけるモード
   bool              m_checkbox_mode;
   //--- ボタン付きのスピンエディットボックスのモード
   bool              m_spin_edit_mode;
   //--- 
public:
   //--- (1) チェックボックス と (2) スピンエディットボックスのモード
   void              CheckBoxMode(const bool state)          { m_checkbox_mode=state;              }
   void              SpinEditMode(const bool state)          { m_spin_edit_mode=state;             }
  };

4. コンボボックスを作成する要素も同じです。以前は2つのクラスがありました。

  • CComboBox — ドロップダウンリスト付きのボタン
  • CCheckComboBox — ドロップダウンリストとチェックボックス付きのボタン

ほぼ同じ2つのクラスを保持することは冗長なので、CComboBoxを1つだけ残します。チェックボックス付きのコントロールのモードは、 CComboBox::CheckBoxMode()メソッドを使用して有効にすることができます。

class CComboBox : public CElement
  {
protected:
   //--- コントロールにチェックボックスをつけるモード
   bool              m_checkbox_mode;
   //--- 
public:
   //--- コントロールにチェックボックスをつけるモードの設定
   void              CheckBoxMode(const bool state)         { m_checkbox_mode=state;                  }
  };

5. 以前は、2つのクラスがスライダの作成を意図していました。

  • CSlider — シンプルな数値スライダー
  • CDualSlider — 数値範囲を指定するデュアルスライダー

残るのはCSliderクラスだけです。デュアルスライダーモードは、CSlider::DualSliderMode()メソッドを使用して有効になります。

class CSlider : public CElement
  {
protected:
   //---デュアルスライダーモード
   bool              m_dual_slider_mode;
   //--- 
public:
   //---デュアルスライダーモード
   void              DualSliderMode(const bool state)           { m_dual_slider_mode=state;           }
   bool              DualSliderMode(void)                 const { return(m_dual_slider_mode);         }
  };

6. 以前、ライブラリにはスクロールバーでリストビューを作成するための2つのクラスがあり、そのうちの1つはチェックボックス付きのリストビューを作成することができました。

  • CListView — 1つの項目を選択できる単純なリストビュー
  • CCheckBoxList — チェックボックス付きリストビュー

このうちCListViewだけが残ります。チェックボックス付きリストを作成するには、 CListView::CheckBoxMode()メソッドを使用して対応するモードを有効にします。

class CListView : public CElement
  {
protected:
   //--- チェックボックス付きリストビューモード
   bool              m_checkbox_mode;
   //--- 
public:
   //--- コントロールにチェックボックスをつけるモードの設定
   void              CheckBoxMode(const bool state)         { m_checkbox_mode=state;                  }
  };

7. ライブラリの以前のバージョンには、3つのクラスのテーブルがありました。

  • CTable — エディットボックステーブル
  • CLabelsTable — テキストラベルテーブル
  • CCanvasTable — レンダーテーブル

ライブラリの改善過程で最も進歩したのはCCanvasTableクラスです。したがって、他のクラスは削除され、CCanvasTableクラスはCTableに改名されました。

8. タブの作成には2つのクラスがありました。

  • CTabs — シンプルなタブ
  • CIconTabs — アイコンタブ

ライブラリにこれら2つのクラスを保持する必要はありません。アイコンタブの配列は、以前にはグラフィカルプリミティブオブジェクトから作成されました。この目的のためにCButton型のボタンが使用されています。ここでアイコンは、上で説明した基本的な方法で定義できます。その結果 CTabsクラスだけを残します。

コントロールの階層

グラフィカルインターフェイスのコントロールの階層も変更されました。これまでは、すべてのコントロールがフォーム(CWindow)に取り付けられ、フォームの左上の点を基準にして配置されていました。グラフィカルインターフェイスを作成するときは、各コントロールの座標を指定する必要があります。また、要素の位置がファイナライズ中に変更された場合は、その領域内にあるすべてのコントロールに対して、その内部の特定の領域に関連するすべての座標を手動で再定義する必要がありました。たとえば、他のコントロールのグループはタブコントロールの領域内にあるとします。タブコントロールをフォームの他の部分に移動する必要があるとすると、個々のタブのすべてのコントロールが以前の位置にとどまります。これは非常に不便でしたが、この問題は解決しました。

以前は、カスタムMQLアプリケーションクラスで特定のコントロールを作成する前に、CElement::WindowPointer()メソッドを使用してフォームにオブジェクトを渡す必要がありました。今ではCElement::MainPointer()メソッドが使われ、引数として、作成されたコントロールが取り付けられるコントロールのオブジェクトが渡されます。 

class CElement : public CElementBase
  {
protected:
   //--- メインコントロールへのポインタ
   CElement         *m_main;
   //--- 
public:
   //--- メインコントロールへのポインタを格納する/返す
   CElement         *MainPointer(void)                               { return(::GetPointer(m_main));    }
   void              MainPointer(CElement &object)                   { m_main=::GetPointer(object);     }
  };

これまでのように、コントロールはメインコントロールに接続されていない限り作成することはできません。すべてのクラスでのコントロールを作成する主要メソッドは、メインコントロールへのポインタの存在を確認します。このような確認メソッドは改名されてCElement::CheckMainPointer()と呼ばれます。ここでは、フォームポインタマウスカーソルオブジェクトへのポインタが格納されています。また、、MQLアプリケーションとそのグラフィカルインタフェースが取り付けられるコントロールの識別子を決定して格納し、チャート識別子とサブウィンドウ番号を格納します。これまでは、このコードをクラスごとに繰り返しました。

class CElement : public CElementBase
  {
protected:
   //--- メインコントロールへのポインタの存在の確認
   bool              CheckMainPointer(void);
  };
//+------------------------------------------------------------------+
//| メインコントロールへのポインタの存在の確認                            |
//+------------------------------------------------------------------+
bool CElement::CheckMainPointer(void)
  {
//--- ポインタがない場合
   if(::CheckPointer(m_main)==POINTER_INVALID)
     {
      //--- メッセージを端末の操作ログに送る
      ::Print(__FUNCTION__,
              " > Before creating a control... \n...it is necessary to pass a pointer to the main control: "+
              ClassName()+"::MainPointer(CElementBase &object)");
      //--- アプリケーションのグラフィカルインタフェースの構築を終了する
      return(false);
     }
//--- フォームポインタを格納する
   m_wnd=m_main.WindowPointer();
//--- マウスカーソルポインタを格納する
   m_mouse=m_main.MousePointer();
//--- プロパティを格納する
   m_id       =m_wnd.LastId()+1;
   m_chart_id =m_wnd.ChartId();
   m_subwin   =m_wnd.SubwindowNumber();
//--- ポインタ存在フラグを送信する
   return(true);
  }

すべてのクラスが、コントロールをメインコントロールに接続するこの方法を使います。複雑なコンパウンドコントロールは、他のいくつかから組み立てられ、それらはすべて一定の順序で互いに結合されます。下の3つのクラスのみは例外です。

  • CTable — テーブル作成クラス
  • CListView — リストビュー作成クラス
  • CTextBox — マルチラインテキストボックス作成クラス

これらはOBJ_BITMAP_LABEL型の複数のグラフィックオブジェクトからアセンブルされますが、その内容もレンダリングされます。さまざまなアプローチを試してみると、ライブラリの開発のこの段階では、複数のオブジェクトを使用することが最適な方法だとわかります。

CButtonクラスは、事実上ライブラリの他のすべてのコントロールで使用されているユニバーサルな「レンガ」となっています。さらに、CButton(ボタン)型のコントロールは、次のような他のクラスのコントロールの基本になりました。 

  • CMenuItem — メニュー項目
  • CTreeItem — ツリービュー項目

その結果、「基本-派生」( 「親子」)形式のクラス関係の一般的なスキームは次のようになります。

図1 基本-派生(親子)としてのクラス関係のスキーム

図1 基本 - 派生(親子)としてのクラス関係のスキーム

現時点では、ライブラリには33の異なるコントロールが実装されています。以下は、ライブラリのすべてのコントロールをアルファベット順に示す一連のスキームです。ルートコントロールは緑色で番号が付けられています。次にくるのはネストされたコントロール全体です。各列は、特定のコントロールのネストされたコントロールの次のレイヤーです。‘[]’記号は、複数のインスタンスが存在するコントロールを印づけます。このような表現は、読者がライブラリのOOPスキームをより早く理解するのに役立つかと思います。

以下のコントロールのスキームを下の図に示します。

1. CButton — ボタン
2. CButtonsGroup — ボタングループ
3. CCalendar — カレンダー
4. CCheckBox — チェックボックス
5. CColorButton — カラーピッカーを呼び出すボタン
6. CColorPicker — カラーピッカー
7. CComboBox — ドロップダウンリストを呼び出すボタン
8. CContextMenu — コンテクストメニュー

図2 コントロールの概略図(パート1) 

図2 コントロールの概略図(パート1)

9. CDropCalendar — ドロップダウンカレンダーを呼び出すボタン
10. CFileNavigator — ファイルナビゲータ
11. CLineGraph — 線チャート
12. CListView — リストビュー

図3 コントロールの概略図(パート2)

図3 コントロールの概略図(パート2)

13. CMenuBar — メインメニュー
14. CMenuItem — メニュー項目
15. CPicture — 写真
16. CPicturesSlider — ピクチャスライダー
17. CProgressBar — プログレスバー
18. CScroll — スクロールバー
19. CSeparateLine — 区切り線
20. CSlider — 数値スライダー
21. CSplitButton — スプリットボタン
22. CStandartChart — 標準チャート
23. CStatusBar — ステータスバー
24. CTable — テーブル
 


図4 コントロールの概略図(パート3)

図4 コントロールの概略図(パート3)

25. CTabs — タブ
26. CTextBox — マルチラインモードを有効にするオプション付きのテキストエディットボックス
27. CTextEdit — エディットボックス
28. CTextLabel — テキストラベル
29. CTimeEdit — 開始時間のコントロール
30. CTooltip — ツールヒント
31. CTreeItem — ツリービュー項目
32. CTreeView — ツリービュー
33. CWindow — コントロールのフォーム(ウィンドウ)
 

図5 コントロールの概略図(パート4)

図5 コントロールの概略図(パート4)


ネストされたコントロールの配列

ライブラリの以前のバージョンでは、コントロールを作成するとき、それらの基本クラスはグラフィックプリミティブオブジェクトへのポインタを保存しましたが、今は、グラフィカルインターフェイスの特定のコントロールのコンポーネントであるコントロールへのポインタを格納します。 

コントロールへのポインタは、 protected CElement::AddToArray()メソッドを使用して、CElement型の動的配列に追加されます。このメソッドは内部使用のために設計されており、コントロールのクラスでのみ使用されます。 

class CElement : public CElementBase
  {
protected:
   //--- ネストされたコントロールへのポインタ
   CElement         *m_elements[];
   //---
protected:
   //---ネストされたコントロールにポインタを追加するメソッド
   void              AddToArray(CElement &object);
  };
//+------------------------------------------------------------------+
//| ネストされたコントロールへのポインタを追加するメソッド                  |
//+------------------------------------------------------------------+
void CElement::AddToArray(CElement &object)
  {
   int size=ElementsTotal();
   ::ArrayResize(m_elements,size+1);
   m_elements[size]=::GetPointer(object);
  }

指定されたインデックスの配列からコントロールポインタを取得することは可能です。これはpublic CElement::Element()メソッドを使用して行います。さらに、ネストされたコントロールの数を取得し、配列を解放する方法も利用できます。

class CElement : public CElementBase
  {
public:
   //--- (1) ネストされたコントロール数の取得 (2) ネストされたコントロール配列の解放
   int               ElementsTotal(void)                       const { return(::ArraySize(m_elements)); }
   void              FreeElementsArray(void)                         { ::ArrayFree(m_elements);         }
   //--- 指定されたインデックスにあるネストされたコントロールのポインタを返す
   CElement         *Element(const uint index);
  };
//+------------------------------------------------------------------+
//| ネストされたコントロールのポインタを返す                              |
//+------------------------------------------------------------------+
CElement *CElement::Element(const uint index)
  {
   uint array_size=::ArraySize(m_elements);
//--- オブジェクト配列のサイズを確認
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > This control ("+m_class_name+") has no nested controls!");
      return(NULL);
     }
//--- サイズが超過した場合の調整
   uint i=(index>=array_size)?array_size-1 : index;
//--- オブジェクトポインタを返す
   return(::GetPointer(m_elements[i]));
  }

仮想メソッドの基本コード

コントロールを管理するための仮想メソッドの基本コードが定義されています。これらのメソッドには以下が含まれます。

  • Moving() — 移動
  • Show() — 表示
  • Hide() — 非表示
  • Delete() — 削除
  • SetZorders() — 優先度の設定
  • ResetZorders() — 優先度の再設定

各コントロールは別々のグラフィックオブジェクトに描画されるので、前述の方法は普遍化し、コントロールのすべてのクラスにおける反復を排除することができます。これらのメソッドのいくつかは、特定のコントロールのクラスでオーバーライドされる場合があります。これが、これらが仮想メソッドがとして宣言された理由です。たとえば、このようなコントロールには、CListView,、CTableCTextBoxなどがあります。これがライブラリの現在のバージョンで複数のレンダリングされたグラフィックオブジェクトからアセンブルされた唯一のコントロールであることは以前に説明されています。 

例としてCElement::Moving()メソッドを考えてみましょう。今は引数がありません。以前は、座標と操作モードが渡されていましたが、今はその必要がなく、すべてがはるかに簡潔になっています。これはライブラリのコア( CWndEventsクラス)の大幅な変更に起因し、本稿の後のセクションで詳しく説明されます。 

コントロールは、接続されているメインコントロールの現在の位置に対して相対的に移動します。グラフィックオブジェクトが移動されると、そのネストされたコントロール(ある場合)がループで移動されます。ほとんどすべてのコントロールでCElement::Moving()メソッドの同じコピーが呼び出されます。ネストのすべてのレベルでも同様です。つまり、特定のコントロールを移動する必要がある場合は、そのメソッドを一度呼び出すだけで十分で、他のコントロールの同様のメソッドが自動的に(作成順に)順番に呼び出されます。 

以下はCElement::Moving()仮想メソッドのコードを示しています。

class CElement : public CElementBase
  {
public:
   //--- コントロールの移動
   virtual void      Moving(void);
  };
//+------------------------------------------------------------------+
//| コントロールの移動                                                 |
//+------------------------------------------------------------------+
void CElement::Moving(void)
  {
//--- コントロールが隠れている場合は終了する
   if(!CElementBase::IsVisible())
      return;
//--- 右に固定されている場合
   if(m_anchor_right_window_side)
     {
      //--- コントロールフィールドでの座標の格納
      CElementBase::X(m_main.X2()-XGap());
      //--- オブジェクトフィールドでの座標の格納
      m_canvas.X(m_main.X2()-m_canvas.XGap());
     }
   else
     {
      CElementBase::X(m_main.X()+XGap());
      m_canvas.X(m_main.X()+m_canvas.XGap());
     }
//--- 下に固定されている場合
   if(m_anchor_bottom_window_side)
     {
      CElementBase::Y(m_main.Y2()-YGap());
      m_canvas.Y(m_main.Y2()-m_canvas.YGap());
     }
   else
     {
      CElementBase::Y(m_main.Y()+YGap());
      m_canvas.Y(m_main.Y()+m_canvas.YGap());
     }
//--- グラフィックオブジェクトの座標の更新
   m_canvas.X_Distance(m_canvas.X());
   m_canvas.Y_Distance(m_canvas.Y());
//--- ネストされたコントロールの移動
   int elements_total=ElementsTotal();
   for(int i=0; i<elements_total; i++)
      m_elements[i].Moving();
  }

左マウスクリックの優先度の自動決定

以前のバージョンのライブラリでは、左マウスのクリックの優先度は各ライブラリコントロールクラスでハードコードされていましたが、今では各コントロールが単一のグラフィックオブジェクトなので、優先度の自動決定モードを実装することが可能になりました。他のオブジェクトの上にあるすべてのグラフィックオブジェクトは、より高い優先度を受け取ります。

図6 左マウスクリックの優先度の決定の視覚的表現 

図6 左マウスクリックの優先度の決定の視覚的表現

しかし、グラフィックオブジェクトをまったく持たないコントロールもあります。前述のように、複数のレンダリングされたオブジェクトからアセンブルされたコントロールもあります。どちらの場合も、優先度は各コントロールのクラスで調整されます。まず、そのような調整が必要なコントロールを特定します。

下記はグラフィックオブジェクトを持たないコントロールです。

  • CButtonsGroup — ボタングループ
  • CDropCalendar — ドロップダウンカレンダーを呼び出すボタン
  • CSplitButton — スプリットボタン
  • CStandardChart — 標準チャート

下記は複数のグラフィックオブジェクトを持つコントロールです。

  • CListView — リストビュー
  • CTable — テーブル
  • CTextBox — テキストエディットボックス

CElementクラスには、以下の優先度を設定して取得するためのメソッドが追加されています。

class CElement : public CElementBase
  {
protected:
   //--- マウスの左クリックの優先度
   long              m_zorder;
   //---
public:
   //--- マウスの左クリックの優先度
   long              Z_Order(void)                             const { return(m_zorder);                }
   void              Z_Order(const long z_order);
  };
//+------------------------------------------------------------------+
//| マウスの左クリックの優先度                                          |
//+------------------------------------------------------------------+
void CElement::Z_Order(const long z_order)
  {
   m_zorder=z_order;
   SetZorders();
  }

グラフィックオブジェクトを持たないコントロールは、他のコントロールがネストされている一種のコントロールモジュールです。このような制御には優先度を設定するものは何もありません。しかし、ネストされたコントロールの優先度は、メインコントロールの優先度に比例して計算されるので、正しく動作するためには、手動で値を設定する必要があります。

オブジェクトを持たないコントロールの優先度は、メインコントロールの優先度と同じに設定されています。

...
//--- コントロールにはクリックするための独自の領域がないため、メインコントロール同様の優先度を設定する
   CElement::Z_Order(m_main.Z_Order());
...

他のすべてのコントロールでは、優先度はキャンバスオブジェクトの作成後に設定されます。フォームではこの値はゼロに等しく、後で作成されるすべてのコントロールのメインコントロールに対して1ずつ増加します。

...
//--- フォーム以外のすべてのコントロールはメインコントロールより優先度が高くなる
   Z_Order((dynamic_cast<CWindow*>(&this)!=NULL)?0 : m_main.Z_Order()+1);
...

例として、もう一つのスキームを考えてみましょう。作成の順にリストされた以下のコントロールで構成されたグラフィカルインターフェースを想像してみてください。

  • コントロールのフォーム(CWindow)。フォームには、 (1)プログラムの終了、(2) フォームの最小化/最大化、(3) ツールヒントなどのためのボタン (CButton)があります。 将来的なライブラリのアップデートでは、このコントロールの機能を拡張する他のボタンの追加も可能です。
  • タブ (CTabs)。コントロールのグループがある作業領域に加えて、このコントロールにはタブを表すボタングループ(CButton)が含まれています。
  • タブの1つには、インクリメントボタンとデクリメントボタン(CButton)を持つスクロールバー (CScrollV)付きのリストビュー (CListView) が配置されます。
  • 別のタブには、水平スクロールバー (CScrollH) と垂直スクロールバー(CScrollV)を持つマルチラインテキストボックス (CTextBox) が含まれます。

グラフィカルインタフェースオブジェクトの優先度を設定するためのアクションは必要ありません。すべてが下記のスキームに従って自動的に設定されます。

図7 左マウスクリックの優先度を定義する例 

図7 左マウスクリックの優先度を定義する例

フォームは最も低い優先度である0の値を受け取ります。フォーム上のボタンの優先度は1です。 

コントロールのCTabs型(タブ)の各コンポーネントボタンは1の優先度を受け取り、タブの作業領域も1です。しかし、 CButtonsGroupコントロールはCButton型ボタンの制御モジュールにすぎないので、その値は0です。MQLアプリケーションのカスタムクラスでは、CElement::MainPointer()メソッドを使用してメインコントロールを指定します(以下のコードを参照)。ここでは、 CTabsコントロールが取り付けられたメインコントロールがフォーム (CWindow) になります。ポインタは、コントロールを作成するメソッドを呼び出す前に格納される必要があります。

...
//--- メインコントロールへのポインタを格納する
   m_tabs1.MainPointer(m_window);
...

ここでのリストビューのメインコントロールはCTabsなので、2の優先度が受け取られます。これは、コントロールを作成する前に指定する必要があります。

...
//--- メインコントロールへのポインタを格納する
   m_listview1.MainPointer(m_tabs1);
...

スクロールバーのメインコントロールはリストビュークラス(CListView)内に既に実装されているので、指定する必要はありません。他のコントロールのコンポーネントであるライブラリのすべてのコントロールにも同じことが適用されます。スクロールバーのメインコントロールが優先度が2のリストビュー(CListView)の場合、値は1増加します(3)。また、スクロールバーの主なコントロールであるスクロールバーのボタンの値は4になります。

リストビュー (CListView)で動作するものはすべてCTextBoxコントロールでも機能します。

コントロールを検証するためのアプリケーション

テスト目的のMQLアプリケーションを実装しました。そのグラフィカルインタフェースにはライブラリのすべてのコントロールが含まれており、そのすべてがどのように機能するかを見ることができます。これは次のようになります。 

図12 MQLテストアプリケーションのグラフィカルインタフェース

図12 MQLテストアプリケーションのグラフィカルインタフェース

このテストアプリケーションは本稿末尾に添付されているので、より詳細に研究することができます。


おわりに

このバージョンのライブラリは、グラフィカルインターフェイスX:マルチラインテキストボックスでのテキスト選択(ビルド13)稿のものとは大きく異なります。多くの作業が行われ、ライブラリのほとんどすべてのファイルに影響を及ぼしました。ライブラリのすべてのコントロールが別々のオブジェクトで描画されるようになりました。コードの可読性が向上し、コード量は30%程減少し、その機能は向上しました。ユーザによって報告された多数のエラーと欠陥が修正されました。

すでに以前のバージョンのライブラリを使用してMQLアプリケーションの作成を開始された方は、ライブラリを研究して徹底的にテストするためには、別途インストールされたMetaTrader 5端末のコピーに新しいバージョンをダウンロードすることをお勧めします。

グラフィカルインターフェイスを作成するためのライブラリは、開発の現段階では下の図のようになります。これは最終版ではありません。 ライブラリは将来さらに発展し、改善されていくでしょう。

図13 開発の現段階でのライブラリの構造

図13 開発の現段階でのライブラリの構造

これらのファイルに含まれている資料の使用についてご質問がある場合は、記事のいずれかでライブラリの開発の詳細をご参照になるか、本稿へのコメント欄でご質問ください。