グラフィカルインタフェースX:レンダーテーブルの新機能(ビルド9)

Anatoli Kazharski | 21 4月, 2017


コンテンツ


はじめに

シリーズ第一弾のグラフィカルインタフェース I: ライブラリストラクチャの準備(チャプター 1)ではライブラリの目的が詳しく考察されました。各章の末尾では、記事へのリンクの完全なリストを参照し開発の現段階でのライブラリの完全版をダウンロードすることができます。ファイルはアーカイブと同じディレクトリに配置される必要があります。

今日までは、ライブラリで最も高度なテーブルはCTableでした。このテーブルはOBJ_EDIT型のエディットボックスから組み立てられており、さらなる開発は難しいです。たとえば、テーブルの個々のグラフィックオブジェクトの表示領域の管理が不可能なので、ヘッダーの境界線をドラッグして列の手動リサイズを実装することは困難です。限界に達しました。

したがって、ライブラリ開発の現段階では、 CCanvasTable型のレンダーテーブルの開発に移る方が合理的です。レンダリングされたテーブルの以前のバージョンと更新に関する情報は、ここにあります:

スクリーンショットは、最新バージョンのレンダーテーブルの外観を示しています。ご覧のように、現時点ではまったく動きません。単にデータが入っているテーブルセルのグリッドです。セルの配置法は指定できます。スクロールバーとフォームサイズの自動調整を除いて、このテーブルには他の対話機能はありません。

 図1 1つ前のバージョンのレンダーテーブル

図1 1つ前のバージョンのレンダーテーブル

この状況を修正するために、レンダーテーブルに新しい機能を追加してみましょう。このアップデートには以下の機能が含まれます。

  • テーブルの縞模様つけ
  • テーブル行の選択と再度クリックしたときの選択解除
  • マウスでホバーしてクリックしたときに色を変更する機能を備えた列ヘッダーの追加
  • セルに十分なスペースがない場合のテキストの列幅への自動調整
  • 境界線をドラッグして各列の見出し幅を変更する機能

テーブルの縞模様つけ

CTableテーブルの縞模様付けは最近の記事で追加されました。これによって、テーブルに多数のセルが含まれている場合にテーブルをよりよく操作できます。このモードをレンダーテーブルにも実装しましょう。

モードの有効化にはCCanvasTable::IsZebraFormatRows()メソッドを使います。スタイルの2番目の色が渡され、一般的なセルの色は最初の色として使用されます。

//+------------------------------------------------------------------+
//| レンダーテーブル作成クラス                                          |
//+------------------------------------------------------------------+
class CCanvasTable : public CElement
  {
private:
   //--- 縞模様をつけるモード
   color             m_is_zebra_format_rows;
   //---
public:
   テーブルの縞模様付け
   void              IsZebraFormatRows(const color clr)   { m_is_zebra_format_rows=clr;      }
      };

このスタイルを視覚化する方法はテーブルの種類によって異なります。 CCanvasTableの場合は、通常モードではテーブルの背景(描画のためのキャンバス)は通常のセルの色で塗りつぶされます。縞模様モードが有効になると、ループが開始します。それぞれの反復はすべての行の座標を計算し、領域は2つの色で交互に色付けされます。これはFillRectangle()メソッドで行われます。これは塗りつぶされた長方形を作成するものです。 

class CCanvasTable : public CElement
  {
public:
   //--- テーブル行の背景を描画する
   void              DrawRows(void);
  };
//+------------------------------------------------------------------+
//| テーブル行の背景を描画する                                          |
//+------------------------------------------------------------------+
void CCanvasTable::DrawRows(void)
  {
//--- 縞模様をつけるモードが無効な場合
   if(m_is_zebra_format_rows==clrNONE)
     {
      //--- キャンバスを1色で塗りつぶす
      m_table.Erase(::ColorToARGB(m_cell_color));
      return;
     }
//--- ヘッダーの座標
   int x1=0,x2=m_table_x_size;
   int y1=0,y2=0;
テーブルの縞模様付け
   for(int r=0; r<m_rows_total; r++)
     {
      //--- 座標の計算
      y1=(r*m_cell_y_size)-r;
      y2=y1+m_cell_y_size;
      //--- 行の色
      uint clr=::ColorToARGB((r%2!=0)?m_is_zebra_format_rows : m_cell_color);
      //--- 行の背景を描画する
      m_table.FillRectangle(x1,y1,x2,y2,clr);
     }
      }

行の色は好みに合わせて設定できます。その結果、縞模様モードでのレンダーテーブルは次のようになります。

 図2 縞模様モードでのレンダーテーブル

図2 縞模様モードでのレンダーテーブル 

 


テーブル行の選択と選択解除

行を選択するには、保存と設定のための追加のフィールドとメソッドが必要です。

  • 選択された行の背景とテキストの色
  • インデックスとテキスト

class CCanvasTable : public CElement
  {
private:
   //---(1)背景と(2)選択された行のテキストの色
   color             m_selected_row_color;
   color             m_selected_row_text_color;
   //--- 選択された行の(1) インデックス と(2) テキストの色
   int               m_selected_item;
   string            m_selected_item_text;
   //---
public:
   //--- 選択された行の(1) インデックス と(2) テキストの色を返す
   int               SelectedItem(void)             const { return(m_selected_item);         }
   string            SelectedItemText(void)         const { return(m_selected_item_text);    }
   //---
private:
   //--- テーブル行の背景を描画する
   void              DrawRows(void);
      };

行選択モードはCCanvasTable::SelectableRow()メソッドで有効化/無効化できます。

class CCanvasTable : public CElement
  {
private:
   //--- 行選択モード
   bool              m_selectable_row;
   //---
public:
   //--- 行の選択
   void              SelectableRow(const bool flag)       { m_selectable_row=flag;           }
      };

行を選択するには、ユーザー定義領域を描画するための別メソッドが必要です。下にはCCanvasTable::DrawSelectedRow() メソッドのコ―ドが示されています。これは、キャンバス上の選択領域の座標を計算して塗りつぶされた長方形を描画します。 

class CCanvasTable : public CElement
  {
private:
   //--- 選択された行を描画する
   void              DrawSelectedRow(void);
  };
//+------------------------------------------------------------------+
//| 選択された行を描画する                                              |
//+------------------------------------------------------------------+
void CCanvasTable::DrawSelectedRow(void)
  {
//--- 条件をチェックするための初期座標を設定する
   int y_offset=(m_selected_item*m_cell_y_size)-m_selected_item;
//--- 座標
   int x1=0,x2=0,y1=0,y2=0;
//---
   x1=0;
   y1=y_offset;
   x2=m_table_x_size;
   y2=y_offset+m_cell_y_size-1;
//--- 塗りつぶされた長方形を描画する
   m_table.FillRectangle(x1,y1,x2,y2,::ColorToARGB(m_selected_row_color));
      }

テキストの再描画には補助的なCCanvasTable::TextColor()メソッドが使われます。これは、セルでのテキストの色を決定します。 

class CCanvasTable : public CElement
  {
private:
   //--- セルテキストの色を返す
   uint              TextColor(const int row_index);
  };
//+------------------------------------------------------------------+
//| セルテキストの色を返す                                              |
//+------------------------------------------------------------------+
uint CCanvasTable::TextColor(const int row_index)
  {
   uint clr=::ColorToARGB((row_index==m_selected_item)?m_selected_row_text_color : m_cell_text_color);
//--- ヘッダーの色を返す
   return(clr);
      }

テーブル行を選択するには、それをダブルクリックする必要があります。これにはCCanvasTable::OnClickTable()メソッドが必要です。これはコントロールのイベントハンドラでCHARTEVENT_OBJECT_CLICK識別子で呼ばれます。

メソッドの初めでは複数のチェックが必要です。下記の場合にはプログラムはメソッドを終了します。

  • 行選択モードが無効
  • スクロールバーがアクティブ
  • クリックされたのがテーブルでなかった 

条件が満たされた場合は、クリック座標を計算するためにキャンバスのエッジからの現在のオフセットとマウスカーソルのY座標を取得する必要があります 。その後クリックされた行をループで決定します。行が見つかったら、それが現在選択されているかどうかを確認し、選択されている場合は選択を解除します。行が選択されている場合は、そのインデックスとテキストを最初の列から格納する必要があります。テーブルは、ループ内の行の検索が完了した後に再描画されます。下記を含むメッセージが送信されます。

  • ON_CLICK_LIST_ITEMイベントの識別子
  • コントロールの識別子
  • 選択された行のインデックス
  • 選択された行のテキスト 
class CCanvasTable : public CElement
  {
private:
   //--- 要素の押下を処理する
   bool              OnClickTable(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| コントロールのクリックの処理                                         |
//+------------------------------------------------------------------+
bool CCanvasTable::OnClickTable(const string clicked_object)
  {
//--- 行選択モードが無効の場合は終了する
   if(!m_selectable_row)
      return(false);
//--- スクロールバーがアクティブな場合は終了する
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      return(false);
//--- オブジェクト名が異なる場合には終了する
   if(m_table.Name()!=clicked_object)
      return(false);
//--- XおよびY軸に沿ってオフセットを取得する
   int xoffset=(int)m_table.GetInteger(OBJPROP_XOFFSET);
   int yoffset=(int)m_table.GetInteger(OBJPROP_YOFFSET);
//--- マウスカーソルの下にあるテキストエディットボックスの座標を決定する
   int y=m_mouse.Y()-m_table.Y()+yoffset;
//--- クリックされた行を決定する
   for(int r=0; r<m_rows_total; r++)
     {
      //--- 条件をチェックするための初期座標を設定する
      int y_offset=(r*m_cell_y_size)-r;
      //--- Y軸に沿った条件確認
      bool y_pos_check=(y>=y_offset && y<y_offset+m_cell_y_size);
      //--- クリックされたのがこの行ではなかった場合は次に行く
      if(!y_pos_check)
         continue;
      //--- 選択された行がクリックされた場合には選択解除する
      if(r==m_selected_item)
        {
         m_selected_item      =WRONG_VALUE;
         m_selected_item_text ="";
         break;
        }
      //--- 行インデックスを格納する 
      m_selected_item      =r;
      m_selected_item_text =m_vcolumns[0].m_vrows[r];
      break;
     }
//--- テーブルを描く
   DrawTable();
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,m_selected_item_text);
   return(true);
      }

選択された行を含むレンダーテーブルは、次のように見えます。

図3 レンダーテーブルの行の選択と選択解除のデモ

図3 レンダーテーブルの行の選択と選択解除のデモ 

 

列のヘッダー

ヘッダーのないテーブルは空白のように見えます。ヘッダーはこのタイプのテーブルでも描画されますが、別のキャンバスにです。このためにはCRectCanvasクラスのあと1つのインスタンスをCCanvasTableクラスに含み 別のキャンバス作成クラスを作成します。このメソッドのコードはテーブル作成のものとほぼ同じなので、ここでは提供されません。唯一の違いは、サイズの事前定義とオブジェクトの位置です。

class CCanvasTable : public CElement
  {
private:
   //--- テーブル作成のためのオブジェクト
   CRectCanvas       m_headers;
   //---
private:
   bool              CreateHeaders(void);
      };

次に、列のヘッダーに関連するプロパティを検討します。これは、テーブルを作成する前に設定することができます。

  • テーブルヘッダーの表示モード
  • ヘッダーのサイズ(高さ)
  • 異なる状態でのヘッダーの背景色
  • ヘッダーテキストの色

これらのプロパティに関連するフィールドとメソッド: 

class CCanvasTable : public CElement
  {
private:
   //--- テーブルヘッダーの表示モード
   bool              m_show_headers;
   //--- ヘッダーのサイズ(高さ)
   int               m_header_y_size;
   //--- 異なる状態でのヘッダーの色(背景)
   color             m_headers_color;
   color             m_headers_color_hover;
   color             m_headers_color_pressed;
   //--- ヘッダーテキストの色
   color             m_headers_text_color;
   //---
public:
   //--- (1) ヘッダー表示モード (2) ヘッダーの高さ
   void              ShowHeaders(const bool flag)         { m_show_headers=flag;             }
   void              HeaderYSize(const int y_size)        { m_header_y_size=y_size;          }
   //--- ヘッダーの(1) 背景と (2) テキストの色
   void              HeadersColor(const color clr)        { m_headers_color=clr;             }
   void              HeadersColorHover(const color clr)   { m_headers_color_hover=clr;       }
   void              HeadersColorPressed(const color clr) { m_headers_color_pressed=clr;     }
   void              HeadersTextColor(const color clr)    { m_headers_text_color=clr;        }
      };

ヘッダーの名前を設定するメソッドが必要です。さらに、それらの値を格納する配列も必要です。配列のサイズは列数に等しく、テーブルサイズを設定するときに同じCCanvasTable :: TableSize ()メソッドで設定されます。 

class CCanvasTable : public CElement
  {
private:
   //--- ヘッダーテキスト
   string            m_header_text[];
   //---
public:
   //--- 指定されたヘッダーのテキストの設定
   void              SetHeaderText(const int column_index,const string value);
  };
//+------------------------------------------------------------------+
//| 指定されたインデックスでのヘッダー配列に書き込む                       |
//+------------------------------------------------------------------+
void CCanvasTable::SetHeaderText(const uint column_index,const string value)
  {
//--- 列範囲の超過を確認
   uint csize=::ArraySize(m_vcolumns);
   if(csize<1 || column_index>=csize)
      return;
//--- 配列に値を格納する
   m_header_text[column_index]=value;
      }

セルとヘッダーテキストの配置は、共通のCCanvasTable :: TextAlign() メソッドを使用して行います。X軸に沿ったテーブルセルとヘッダーの配列のメソッドとYに沿った配列は渡された値で定義されます。このバージョンでは、Y軸に沿ったヘッダーテキストはTA_VCENTERで中央に配置され、セルはTA_TOPを持ってセルの上端からのオフセットが調整されます。 

class CCanvasTable : public CElement
  {
private:
   //--- 指定された列のテキスト配列モードを返す
   uint              TextAlign(const int column_index,const uint anchor);
  };
//+------------------------------------------------------------------+
//| 指定された列のテキスト配列モードを返す                                |
//+------------------------------------------------------------------+
uint CCanvasTable::TextAlign(const int column_index,const uint anchor)
  {
   uint text_align=0;
//--- 現在の列のテキスト配列
   switch(m_vcolumns[column_index].m_text_align)
     {
      case ALIGN_CENTER :
         text_align=TA_CENTER|anchor;
         break;
      case ALIGN_RIGHT :
         text_align=TA_RIGHT|anchor;
         break;
      case ALIGN_LEFT :
         text_align=TA_LEFT|anchor;
         break;
     }
//--- 配列の種類を返す
   return(text_align);
      }

多くのテーブルおよびOS環境では、カーソルが2つのヘッダー間の境界線を横切るとポインタが変化します。以下のスクリーンショットはMetaTrader 5取引端末のツールボックスウィンドウに表示されているテーブルの例を示しています。この新しく出現したポインタがクリックされると、列の幅を変更するモードが切り替わります。この列の背景色も変わります。

図4 ヘッダージョイントの境界線をホバリングするときのマウスポインタ

図4 ヘッダージョイントの境界線をホバリングするときのマウスポインタ

 

開発中のライブラリのために同じ画像を準備しましょう。本稿末尾に添付されたファイルには、ライブラリコントロール用のすべての画像を含むフォルダが含まれています。X軸とYに沿ったサイズ変更のためにはENUM_MOUSE_POINTERポインタ列挙に新しい識別子をEnums.mqhファイルに追加します。 

//+------------------------------------------------------------------+
//| ポインタタイプの列挙                                               |
//+------------------------------------------------------------------+
enum ENUM_MOUSE_POINTER
  {
   MP_CUSTOM            =0,
   MP_X_RESIZE          =1,
   MP_Y_RESIZE          =2,
   MP_XY1_RESIZE        =3,
   MP_XY2_RESIZE        =4,
   MP_X_RESIZE_RELATIVE =5,
   MP_Y_RESIZE_RELATIVE =6,
   MP_X_SCROLL          =7,
   MP_Y_SCROLL          =8,
   MP_TEXT_SELECT       =9
      };

このポインタ型をコントロールのクラスで使用できるようにするには、 CPointerクラスでの対応する追加が必要です。 

//+------------------------------------------------------------------+
//|                                                      Pointer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- リソース
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp"
//+------------------------------------------------------------------+
//| マウスカーソルを作成するクラス                                       |
//+------------------------------------------------------------------+
class CPointer : public CElement
  {
private:
   //--- マウスカーソルの画像を設定する
   void              SetPointerBmp(void);
  };
//+------------------------------------------------------------------+
//| カーソルタイプに基づいてカーソルアイコンを設定する                     |
//+------------------------------------------------------------------+
void CPointer::SetPointerBmp(void)
  {
   switch(m_type)
     {
      ...
      case MP_X_RESIZE_RELATIVE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp";
         break;
      case MP_Y_RESIZE_RELATIVE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp";
         break;
      ...
     }
//--- カスタムタイプ(MP_CUSTOM)が示された場合
   if(m_file_on=="" || m_file_off=="")
      ::Print(__FUNCTION__," > Both images must be set for the cursor!");
      }

ここではフィールドの追加が必要となります。

  • ヘッダーの境界線をドラッグする瞬間を判断する。
  • マウスカーソルがあるヘッダーの領域から別のヘッダーの領域に移動する瞬間を判断する。これは、リソースを節約してヘッダーが隣接する領域の境界線が交差する場合にのみ再描画されるために必要です。

class CCanvasTable : public CElement
  {
private:
   //--- ヘッダーから別のヘッダーへのマウスカーソルの移動の瞬間を判断する
   int               m_prev_header_index_focus;
   //--- ヘッダーの境界線をドラッグして列幅を変更する状態
   int               m_column_resize_control;
      };

CCanvasTable::HeaderColorCurrent()メソッドを使うと現在のモード、マウスカーソルの位置、およびマウスの左ボタンの状態に応じたヘッダーの現在の色が取得できます。ヘッダーの上のフォーカスは、ヘッダーの背景を描画するように設計された CCanvasTable :: DrawHeaders () メソッドで決定され、チェックの結果としてここに渡されます 。

class CCanvasTable : public CElement
  {
private:
   //--- 現在のヘッダー背景色を返す
   uint              HeaderColorCurrent(const bool is_header_focus);
  };
//+------------------------------------------------------------------+
//| 現在のヘッダー背景色を返す                                          |
//+------------------------------------------------------------------+
uint CCanvasTable::HeaderColorCurrent(const bool is_header_focus)
  {
   uint clr=clrNONE;
//--- フォーカスのない場合
   if(!is_header_focus || !m_headers.MouseFocus())
      clr=m_headers_color;
   else
     {
      //--- マウスの左ボタンが押されて列の幅が変更されている途中でない場合
      bool condition=(m_mouse.LeftButtonState() && m_column_resize_control==WRONG_VALUE);
      clr=(condition)?m_headers_color_pressed : m_headers_color_hover;
     }
//--- ヘッダーの色を返す
   return(::ColorToARGB(clr));
      }

下にはCCanvasTable::DrawHeaders()メソッドのコ―ドが示されています。ここで、マウスカーソルがヘッダー領域にない場合はキャンバス全体が指定された色で塗りつぶされます。フォーカスがヘッダーにある場合は、そのうちどこにフォーカスがあるかを決定する必要があります。これを行うには、マウスカーソルの相対座標を決定し、ループでヘッダー座標を計算する際に各ヘッダーにフォーカスがあるかどうかを確認する必要があります。さらに、列幅を変更するモードを検討します。このモードでは計算に追加のオフセットが使われますフォーカスが見つかった場合は、列インデックスを格納する必要があります。 

class CCanvasTable : public CElement
  {
private:
   //--- 列幅を変更するモードでマウスポインタを表示する区切り線の境界からのオフセット
   int               m_sep_x_offset;
   //---
private:
   //--- ヘッダーを描画する
   void              DrawHeaders(void);
  };
//+------------------------------------------------------------------+
//| ヘッダーの背景を描画する                                            |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeaders(void)
  {
//--- フォーカスがない場合は、ヘッダーの色をリセットする
   if(!m_headers.MouseFocus())
     {
      m_headers.Erase(::ColorToARGB(m_headers_color));
      return;
     }
//--- ヘッダーへのフォーカスのチェック
   bool is_header_focus=false;
//--- マウスカーソルの座標
   int x=0;
//--- 座標
   int x1=0,x2=0,y1=0,y2=m_header_y_size;
//--- マウスカーソルの相対座標を取得する
   if(::CheckPointer(m_mouse)!=POINTER_INVALID)
     {
      //--- X軸に沿ったオフセットを取得する
      int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
      //--- マウスカーソルの座標を決定する
      x=m_mouse.X()-m_headers.X()+xoffset;
     }
//--- ヘッダー背景をクリアする
   m_headers.Erase(::ColorToARGB(clrNONE,0));
//--- 列幅を変更するモードを考慮したオフセット
   int sep_x_offset=(m_column_resize_mode)?m_sep_x_offset : 0;
//--- ヘッダーの背景を描画する
   for(int i=0; i<m_columns_total; i++)
     {
      //--- 座標を計算する
      x2+=m_vcolumns[i].m_width;
      //--- フォーカスをチェックする
      if(is_header_focus=x>x1+((i!=0)?sep_x_offset : 0) && x<=x2+sep_x_offset)
         m_prev_header_index_focus=i;
      //--- ヘッダーの背景を描画する
      m_headers.FillRectangle(x1,y1,x2,y2,HeaderColorCurrent(is_header_focus));
      //--- 次のヘッダーのオフセットを計算する
      x1+=m_vcolumns[i].m_width;
     }
      }

ヘッダーの背景が描画されたら、グリッド(ヘッダーフレーム)を描画する必要があります。この目的にはCCanvasTable::DrawHeadersGrid()メソッドが使われます。初めに共通フレームが描画され次にループで区切り線が適応されます。

class CCanvasTable : public CElement
  {
private:
   //--- テーブルヘッダーのグリッドを描画する
   void              DrawHeadersGrid(void);
  };
//+------------------------------------------------------------------+
//| テーブルヘッダーのグリッドを描画する                                  |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeadersGrid(void)
  {
//--- グリッドの色
   uint clr=::ColorToARGB(m_grid_color);
//--- 座標
   int x1=0,x2=0,y1=0,y2=0;
   x2=m_table_x_size-1;
   y2=m_header_y_size-1;
//--- フレームを描画する
   m_headers.Rectangle(x1,y1,x2,y2,clr);
//--- 区切り線
   x2=x1=m_vcolumns[0].m_width;
   for(int i=1; i<m_columns_total; i++)
     {
      m_headers.Line(x1,y1,x2,y2,clr);
      x2=x1+=m_vcolumns[i].m_width;
     }
      }

最後に、ヘッダーテキストを描画します。このタスクはCCanvasTable::DrawHeadersText()メソッドで実行されます。ここでは、ループ内のすべてのヘッダーを調べ、各反復でテキスト座標整列モードを決定する必要があります。ヘッダー名は、ループの最後の操作として適用されます。列の幅に対するテキストの調整もここで使用されます。この目的にはCCanvasTable::CorrectingText()メソッドが使われます。これについては、次のセクションで詳しく説明しています。 

class CCanvasTable : public CElement
  {
private:
   //--- テーブルヘッダーのテキストを描画する
   void              DrawHeadersText(void);
  };
//+------------------------------------------------------------------+
//| テーブルヘッダーのテキストを描画する                                  |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeadersText(void)
  {
//--- 座標とオフセットの計算
   int x=0,y=m_header_y_size/2;
   int column_offset =0;
   uint text_align   =0;
//--- テキストの色
   uint clr=::ColorToARGB(m_headers_text_color);
//--- フォントプロパティ
   m_headers.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//--- テキストを描画する
   for(int c=0; c<m_columns_total; c++)
     {
      //--- テキストのX座標を取得する
      x=TextX(c,column_offset);
      //--- テキスト配列モードを取得する
      text_align=TextAlign(c,TA_VCENTER);
      //--- 列の名前を描画する
      m_headers.TextOut(x,y,CorrectingText(c,0,true),clr,text_align);
     }
      }

ヘッダーを描画するためにリストされたすべてのメソッドは、共通のCCanvasTable::DrawTableHeaders()メソッドで呼び出されます。ヘッダー表示モードが無効の場合、このメソッドへの入力はブロックされます。 

class CCanvasTable : public CElement
  {
private:
   //--- テーブルヘッダーを描画する
   void              DrawTableHeaders(void);
  };
//+------------------------------------------------------------------+
//| テーブルヘッダーを描画する                                          |
//+------------------------------------------------------------------+
void CCanvasTable::DrawTableHeaders(void)
  {
//--- ヘッダーが無効の場合は終了する
   if(!m_show_headers)
      return;
//--- ヘッダーを描画する
   DrawHeaders();
//--- グリッドを描画する
   DrawHeadersGrid();
//--- ヘッダーテキストを描画する
   DrawHeadersText();
      }

ヘッダーへのフォーカスはCCanvasTable::CheckHeaderFocus()メソッドでチェックされます。プログラムは、次の2つの場合にメソッドを終了します。

  • ヘッダー表示モードが無効な場合
  • または列幅を変更するプロセスが開始された場合。

その後、キャンバス上のカーソルの相対座標が取得されます。ループはいずれかのヘッダーへのフォーカスを探しそれがこのメソッドが最後に呼び出されたときから変わったかをチェックします。新しいフォーカスが登録されている場合(ヘッダーの境界を越える瞬間)以前に保存されたヘッダーインデックスをリセットしてループを停止する必要があります。

class CCanvasTable : public CElement
  {
private:
   //--- ヘッダーのフォーカスを確認する
   void              CheckHeaderFocus(void);
  };
//+------------------------------------------------------------------+
//| ヘッダーのフォーカスの確認                                          |
//+------------------------------------------------------------------+
void CCanvasTable::CheckHeaderFocus(void)
  {
//--- (1) ヘッダーが無効の場合や (2) 列幅を変更するプロセスが開始された場合は終了する
   if(!m_show_headers || m_column_resize_control!=WRONG_VALUE)
      return;
//--- ヘッダーの座標
   int x1=0,x2=0;
//--- X軸に沿ったオフセットを取得する
   int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- マウスカーソルの相対座標を取得する
   int x=m_mouse.X()-m_headers.X()+xoffset;
//--- 列幅を変更するモードを考慮したオフセット
   int sep_x_offset=(m_column_resize_mode)?m_sep_x_offset : 0;
//--- フォーカスを探す
   for(int i=0; i<m_columns_total; i++)
     {
      //--- 右の座標を計算する
      x2+=m_vcolumns[i].m_width;
      //--- ヘッダーフォーカスが変わった
      if((x>x1+sep_x_offset && x<=x2+sep_x_offset) && m_prev_header_index_focus!=i)
        {
         m_prev_header_index_focus=WRONG_VALUE;
         break;
        }
      //--- 左の座標を計算する
      x1+=m_vcolumns[i].m_width;
     }
      }

境界線が交差するときだけ、ヘッダーが再描画されます。これにより、CPUリソースが節約されます。CCanvasTable::ChangeHeadersColor()はこのタスクのために設計されています。ここでは、ヘッダー表示モードが無効になっている場合や幅変更の途中の場合は、プログラムを終了します。メソッドの先頭にある条件が満たされると、ヘッダーのフォーカスがチェックされて再描画されます。 

class CCanvasTable : public CElement
  {
private:
   //--- ヘッダーの色を変える
   void              ChangeHeadersColor(void);
  };
//+------------------------------------------------------------------+
//| ヘッダーの色を変える                                                |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeHeadersColor(void)
  {
//--- ヘッダーが無効の場合は終了する
   if(!m_show_headers)
      return;
//--- カーソルがアクティブな場合
   if(m_column_resize.IsVisible() && m_mouse.LeftButtonState())
     {
      //--- ドラッグされた列のインデックスを格納する
      if(m_column_resize_control==WRONG_VALUE)
         m_column_resize_control=m_prev_header_index_focus;
      //---
      return;
     }
//--- フォーカスされていない場合
   if(!m_headers.MouseFocus())
     {
      //--- フォーカスされていないことがまだ示されていない場合
      if(m_prev_header_index_focus!=WRONG_VALUE)
        {
         //--- フォーカスをリセットする
         m_prev_header_index_focus=WRONG_VALUE;
         //--- 色を変更する
         DrawTableHeaders();
         m_headers.Update();
        }
     }
//--- フォーカスされている場合
   else
     {
      //--- ヘッダーのフォーカスをチェックする
      CheckHeaderFocus();
      //--- フォーカスのない場合
      if(m_prev_header_index_focus==WRONG_VALUE)
        {
         //--- 色を変更する
         DrawTableHeaders();
         m_headers.Update();
        }
     }
      }

下記がCCanvasTable::CheckColumnResizeFocus()メソッドのコ―ドです。これはヘッダー間の境界線へのフォーカスを決定するために必要で、列幅を変更するためのカーソルの表示/非表示を担当します。メソッドの冒頭には2つのチェックがあります。列幅変更モードが無効の場合、プログラムはこのメソッドを終了します。モードが有効で、列幅の変更が進行中の場合は、マウスカーソルの座標を更新してメソッドを終了する必要があります。

列幅を変更するプロセスがまだ開始されておらずカーソルがヘッダーの領域にある場合はそのうち一つの境界へのフォーカスをループで決定しようとします i。フォーカスが見つかった場合はマウスカーソルのざひょうを更新して表示し、メソッドを終了します。フォーカスが見つからなかった場合はポインタは非表示にされるべきです。 

class CCanvasTable : public CElement
  {
private:
   //--- ヘッダーの境界線に焦点を合わせて幅を変更する
   void              CheckColumnResizeFocus(void);
  };
//+------------------------------------------------------------------+
//| ヘッダーの境界線に焦点を合わせて幅を変更する                           |
//+------------------------------------------------------------------+
void CCanvasTable::CheckColumnResizeFocus(void)
  {
//---列幅を変更するモードが無効な場合は終了する
   if(!m_column_resize_mode)
      return;
//--- 列幅が変更され始めている場合は終了する
   if(m_column_resize_control!=WRONG_VALUE)
     {
      //--- カーソル座標を更新して表示する
      m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
      return;
     }
//--- ヘッダーの境界へのフォーカスのチェック
   bool is_focus=false;
//--- マウスカーソルがヘッダーの領域にある場合
   if(m_headers.MouseFocus())
     {
      //--- ヘッダーの座標
      int x1=0,x2=0;
      //--- X軸に沿ったオフセットを取得する
      int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
      //--- マウスカーソルの相対座標を取得する
      int x=m_mouse.X()-m_headers.X()+xoffset;
      //--- フォーカスを探す
      for(int i=0; i<m_columns_total; i++)
        {
         //--- 座標の計算
         x1=x2+=m_vcolumns[i].m_width;
         //--- フォーカスの検証
         if(is_focus=x>x1-m_sep_x_offset && x<=x2+m_sep_x_offset)
            break;
        }
      //--- フォーカスがある場合
      if(is_focus)
        {
         //--- カーソル座標を更新して表示する
         m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
         //--- カーソルを表示する
         m_column_resize.Show();
         return;
        }
     }
//--- フォーカスされていない場合はポインタを非表示にする
   if(!m_headers.MouseFocus() || !is_focus)
      m_column_resize.Hide();
      }

最終的な結果は次の通りです。

 図5 列のヘッダー

図5 列のヘッダー

 

 


列の幅に対する文字列の長さの調整

以前は、テキストが隣接するセルと重ならないようにするには、列の幅を手動で選択し、ファイルを再コンパイルして結果を確認する必要がありました。当然、これは不便です。

文字列の長さがテーブルのセルに合わない場合は自動的に調整するようにしましょう。以前に調整された文字列は、テーブルを再描画するときには再調整されません。これらの文字列を格納するためにテーブルプロパティの構造体に別の配列を追加します。

class CCanvasTable : public CElement
  {
private:
   //--- テーブルの値とプロパティの配列
   struct CTOptions
     {
      string            m_vrows[];
      string            m_text[];
      int               m_width;
      ENUM_ALIGN_MODE   m_text_align;
     };
   CTOptions         m_vcolumns[];
      };

結果としてm_vrows[] はテキスト全体を格納し、m_text[]は調整されたテキストを格納します。

CCanvasTable::CorrectingText() メソッドはヘッダーとテーブルセルの両方で文字列の長さを調整する責任を負います。操作するテキストが識別されるとその幅が取得されます。次に、セルの端からのすべてのオフセットを考慮して、文字列の全文がセルに収まるかどうかを確認します。収まる場合にはm_text[] 配列に保存してメソッドを終了します。現在のバージョンでは、調整されたテキストはセルに対してのみ保存されますが、ヘッダーには保存されません。

テキストが収まらない場合は余分な文字を削除して省略記号( '...')を追加する必要があります。省略記号は、表示されたテキストが短縮されたことを示します。この手順は簡単に実装できます。

1)文字列の長さを取得します。

2)ループで最後の文字から反復を始めて、最後の文字を削除し、トリムされたテキストを一時変数に保存します。

文字が残っていない場合は、空の文字列を返します。

4)文字が残っている限り、省略記号を含む結果の文字列の幅を取得します。

5)セルの端からの指定されたオフセットを考慮して、文字列がこの形式で表のセルに収まるかどうかを確認します。

6)収まる場合は、文字列をメソッドのローカル変数に格納してサイクルを停止します。

7)その後、調整済の文字列をm_text[]配列に格納してメソッドを終了します。 

class CCanvasTable : public CElement
  {
private:
   //--- 列幅に合わせたテキストを返す
   string            CorrectingText(const int column_index,const int row_index,const bool headers=false);
  };
//+------------------------------------------------------------------+
//| 列幅に合わせたテキストを返す                                        |
//+------------------------------------------------------------------+
string CCanvasTable::CorrectingText(const int column_index,const int row_index,const bool headers=false)
  {
//--- 現在のテキストを取得する
   string corrected_text=(headers)?m_header_text[column_index]: m_vcolumns[column_index].m_vrows[row_index];
//--- セルの端からのX軸に沿ったオフセット
   int x_offset=m_text_x_offset*2;
//--- キャンバスオブジェクトへのポインタを取得する
   CRectCanvas *obj=(headers)?::GetPointer(m_headers) : ::GetPointer(m_table);
//--- テキストの幅を取得する
   int full_text_width=obj.TextWidth(corrected_text);
//--- セルに収まる場合には別の 配列に保存して返す
   if(full_text_width<=m_vcolumns[column_index].m_width-x_offset)
     {
      //--- それらがヘッダーでない場合、調整されたテキストを保存する
      if(!headers)
         m_vcolumns[column_index].m_text[row_index]=corrected_text;
      //---
      return(corrected_text);
     }
//--- テキストがセルに収まらない場合は、テキストを調整する必要がある(余分な文字をトリミングし、省略記号を追加する)
   else
     {
      //--- 文字列の操作
      string temp_text="";
      //--- 文字列の長さを取得する
      int total=::StringLen(corrected_text);
      //--- 目的のテキスト幅に達するまで、文字列から文字を1つずつ削除する
      for(int i=total-1; i>=0; i--)
        {
         //--- 一文字を削除する
         temp_text=::StringSubstr(corrected_text,0,i);
         //--- 文字が残っていない場合は、空の文字列を返す
         if(temp_text=="")
           {
            corrected_text="";
            break;
           }
         //--- チェックする前に省略記号を追加する
         int text_width=obj.TextWidth(temp_text+"...");
         //--- セルに収まる場合
         if(text_width<m_vcolumns[column_index].m_width-x_offset)
           {
            //--- テキストを格納しループを停止する
            corrected_text=temp_text+"...";
            break;
           }
        }
     }
//--- それらがヘッダーでない場合、調整されたテキストを保存する
   if(!headers)
      m_vcolumns[column_index].m_text[row_index]=corrected_text;
//--- 調整したテキストを返す
   return(corrected_text);
      }

テーブルを再描画する際に調整された文字列を使用することは、列幅を変更するプロセスで特に重要です。すべてのテーブルセルのテキストを何度も何度も調整するのではなく、幅が変更されている列のセルでのみこれを行うだけで十分です。これにより、CPUリソースが節約されます。

CCanvasTable::Text()メソッドは、指定された列のテキストを調整する必要があるかどうか、または以前に調整されたバージョンを送信するので十分であるかどうかを判断します。コードは下記です。 
class CCanvasTable : public CElement
  {
private:
   //--- テキストを返す
   string            Text(const int column_index,const int row_index);
  };
//+------------------------------------------------------------------+
//| テキストを返す                                                     |
//+------------------------------------------------------------------+
string CCanvasTable::Text(const int column_index,const int row_index)
  {
   string text="";
//--- 列幅を変更するモード出ない場合はテキストを調整する
   if(m_column_resize_control==WRONG_VALUE)
      text=CorrectingText(column_index,row_index);
//--- 列幅を変更するモードである場合は...
   else
     {
      //--- ...幅が変更されている列のテキストのみを調整する
      if(column_index==m_column_resize_control)
         text=CorrectingText(column_index,row_index);
      //--- それ以外の場合はすべて以前調整したテキストを使用する
      else
         text=m_vcolumns[column_index].m_text[row_index];
     }
//--- テキストを返す
   return(text);
      }

下にあるのは列幅の変更のために設計されたCCanvasTable::ChangeColumnWidth()メソッドのコ―ドです。

最小の列幅は30画素に設定されています。ヘッダー表示が無効の場合は、プログラムはメソッドを終了します。条件が満たされると、ヘッダーの境界でのフォーカスが確認されます。このチェックでプロセスが開始/終了していないと判断された場合は、補助変数はゼロになり、プログラムはメソッドを終了します。プロセスが実行中の場合は、カーソルのX相対座標を取得します。プロセスが始まったばかりの場合は、カーソルの現在のX座標( x_fixed変数)とドラッグされた列の幅(prev_width変数)を格納する必要があります。この目的のためのローカル変数は静的変数です。したがって、このメソッドが実行されるたびに、その値はプロセスの完了時にゼロになるまで格納されます。 

そして、新しい列幅を計算します。最小列幅に達したことが判明した場合、プログラムはメソッドを終了します。他の場合は新しい幅は、指定された列のテーブルプロパティの構造体に格納されます。その後、テーブルの寸法が再計算されて再適用され、メソッドの最後にはテーブルが再描画されます。 

class CCanvasTable : public CElement
  {
private:
   //--- 列の最小幅
   int               m_min_column_width;
   //---
private:
   //--- ドラッグされた列の幅の変更
   void              ChangeColumnWidth(void);
  };
//+------------------------------------------------------------------+
//| コンストラクタ                                                     |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) : m_min_column_width(30)
  {
   ...
  }
//+------------------------------------------------------------------+
//|ドラッグされた列の幅の変更                                           |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeColumnWidth(void)
  {
//--- ヘッダーが無効の場合は終了する
   if(!m_show_headers)
      return;
//--- ヘッダー境界のフォーカスを確認する
   CheckColumnResizeFocus();
//--- 補助変数
   static int x_fixed    =0;
   static int prev_width =0;
//--- 完成したら値をリセットする
   if(m_column_resize_control==WRONG_VALUE)
     {
      x_fixed    =0;
      prev_width =0;
      return;
     }
//--- X軸に沿ったオフセットを取得する
   int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- マウスカーソルの相対座標を取得する
   int x=m_mouse.X()-m_headers.X()+xoffset;
//--- 列幅を変更するプロセスが開始されたばかりの場合
   if(x_fixed<1)
     {
      //--- 列の現在のX座標と幅を格納する
      x_fixed    =x;
      prev_width =m_vcolumns[m_column_resize_control].m_width;
     }
//--- 新しい列幅を計算する
   int new_width=prev_width+(x-x_fixed);
//--- 指定されたリミット以下の場合はそのままにする
   if(new_width<m_min_column_width)
      return;
//--- 新しい列幅を保存する
   m_vcolumns[m_column_resize_control].m_width=new_width;
//--- テーブルサイズを計算する
   CalculateTableSize();
//--- テーブルのサイズを変える
   ChangeTableSize();
//--- テーブルを描く
   DrawTable();
      }

結果は次の通りです。

 図5 列の可変幅に対する文字列の長さの調整

図5 列の可変幅に対する文字列の長さの調整 

 


イベント処理

テーブルオブジェクトの色の管理列幅の変更はコントロールのマウス移動 (CHARTEVENT_MOUSE_MOVE)イベントハンドラで行われます。 

//+------------------------------------------------------------------+
//| イベントハンドラ                                                   |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- カーソル移動イベントの処理
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- コントロールが隠れている場合は終了する
      if(!CElementBase::IsVisible())
         return;
      //--- サブウィンドウの番号が一致しない場合は終了する
      if(!CElementBase::CheckSubwindowNumber())
         return;
      //--- 要素上のフォーカスの確認
      CElementBase::CheckMouseFocus();
      m_headers.MouseFocus(m_mouse.X()>m_headers.X() && m_mouse.X()<m_headers.X2() &&
                           m_mouse.Y()>m_headers.Y() && m_mouse.Y()<m_headers.Y2());
      //--- スクロールバーがアクティブな場合
      if(m_scrollv.ScrollBarControl() || m_scrollh.ScrollBarControl())
        {
         ShiftTable();
         return;
        }
      //--- オブジェクトの色の変更
      ChangeObjectsColor();
      //--- ドラッグされた列の幅の変更
      ChangeColumnWidth();
      return;
     }
   ...
      }

左マウスボタンの状態が変更される瞬間を識別するためには、あと1つの新しいイベント識別子が必要です。イベントハンドラコードでの複数のブロックでの繰り返したチェックと同時処理を取り除く必要があります。ON_CHANGE_MOUSE_LEFT_BUTTON識別子をDefine.mqhファイルに追加します。 

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
  #define ON_CHANGE_MOUSE_LEFT_BUTTON (33) // 左マウスボタンの状態の変更

さらに、現在のマウスパラメータ(CMouse)を取得するためのCMouse::CheckChangeLeftButtonState() メソッドがクラスに追加されました。 これによってマウスの左ボタンの状態が変化する瞬間を識別することができます。このメソッドはクラスのハンドラで呼び出されます。マウスの左ボタンの状態が変化した場合、メソッドは ON_CHANGE_MOUSE_LEFT_BUTTON識別子をもつメッセージを送信します。このメッセージは後に任意のコントロールで受信して処理できます。 

//+------------------------------------------------------------------+
//| マウスパラメータを取得するクラス                                     |
//+------------------------------------------------------------------+
class CMouse
  {
private:
   //--- 左マウスボタンの状態の変更の確認
   bool              CheckChangeLeftButtonState(const string mouse_state);
  };
//+------------------------------------------------------------------+
//| マウスカーソル移動イベントを処理する                                  |
//+------------------------------------------------------------------+
void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- カーソル移動イベントの処理
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- マウスの左クリックの座標と状態
      m_x                 =(int)lparam;
      m_y                 =(int)dparam;
      m_left_button_state =CheckChangeLeftButtonState(sparam);
      ...
     }
  }
//+------------------------------------------------------------------+
//| 左マウスボタンの状態の変更の確認                                     |
//+------------------------------------------------------------------+
bool CMouse::CheckChangeLeftButtonState(const string mouse_state)
  {
   bool left_button_state=(bool)int(mouse_state);
//--- 左マウスボタンの状態の変更についてのメッセージを送信する
   if(m_left_button_state!=left_button_state)
      ::EventChartCustom(m_chart.ChartId(),ON_CHANGE_MOUSE_LEFT_BUTTON,0,0.0,"");
//---
   return(left_button_state);
      }

CCanvasTableクラスにはON_CHANGE_MOUSE_LEFT_BUTTON識別子をもつイベント処理が必要です。

  •  クラスのフィールドをゼロにする
  •  スクロールバーの調整
  •  テーブルの再描画 
//+------------------------------------------------------------------+
//| イベントハンドラ                                                   |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 左マウスボタンの状態の変更
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_MOUSE_LEFT_BUTTON)
     {
      //--- ヘッダーが無効の場合は終了する
      if(!m_show_headers)
         return;
      //--- マウスの左ボタンが押されていない場合
      if(!m_mouse.LeftButtonState())
        {
         //--- 幅変更モードをリセットする
         m_column_resize_control=WRONG_VALUE;
         //--- カーソルを非表示にする
         m_column_resize.Hide();
         //--- 最近の変更を考慮してスクロールバーを調整する
         HorizontalScrolling(m_scrollh.CurrentPos());
        }
      //--- 最後のヘッダーフォーカスのインデックスをリセットする
      m_prev_header_index_focus=WRONG_VALUE;
      //--- オブジェクトの色の変更
      ChangeObjectsColor();
     }
      }

本稿のアニメーションスクリーンショットは、MQLアプリケーションの操作結果を示しています。MQLアプリケーションは下のリンクでダウンロードされさらに学習されることができます。

 

おわりに

今回のライブラリの更新では CCanvasTable型のレンダーテーブルが改善されました。これはテーブルの最終版ではありません。テーブルはさらに開発され、新しい機能が追加されます。

下にあるのは、このグラフィカルインタフェース作成ライブラリの現在の状態を示す図です。

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

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

 

テスト用のライブラリとファイルの最新バージョンは以下でダウンロードできます。

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