グラフィカルインターフェイスX:レンダーテーブルの更新とコード最適化(ビルド10)
Anatoli Kazharski | 27 4月, 2017
コンテンツ
- はじめに
- 指定されたキャンバス上のマウスカーソルの相対座標
- テーブルの構造体の変化
- 可視部分の行の範囲の決定
- テーブルセルのアイコン
- ホバーされた場合の行の強調表示
- テーブルのセルを高速に再描画するメソッド
- コントロールを検証するためのアプリケーション
- おわりに
はじめに
シリーズ第一弾のグラフィカルインタフェース I: ライブラリストラクチャの準備(チャプター 1)ではライブラリの目的が詳しく考察されました。各章の末尾では、記事へのリンクの完全なリストを参照し開発の現段階でのライブラリの完全版をダウンロードすることができます。ファイルはアーカイブと同じディレクトリに配置される必要があります。
レンダーテーブル(CCanvasTable)に新しい機能を補完していきます。今回は以下の機能が含まれます。
- ホバーされた場合に行を強調表示
- 各セルにアイコン配列を追加する機能とそれらを切り替えるメソッド
- 実行時にセル内のテキストを設定および変更する機能
さらに、テーブルを高速に再描画するために、コードと特定のアルゴリズムが最適化されています。
指定されたキャンバス上のカーソルの相対座標
キャンバス上の相対座標を計算するための多くのクラスおよびメソッドで重複したコードを削除するためにCMouse::RelativeX() および CMouse::RelativeY()メソッドが 座標を取得するためのCMouseクラスに追加されました。canvasの可視部分の現在のオフセットを考慮した相対座標を計算するには、これらのメソッドにCRectCanvas型のオブジェクトへの参照を渡す必要があります。
//+------------------------------------------------------------------+ //| マウスパラメータを取得するクラス | //+------------------------------------------------------------------+ class CMouse { public: //--- マウスカーソルの渡されたキャンバスオブジェクトとの相対座標を返す int RelativeX(CRectCanvas &object); int RelativeY(CRectCanvas &object); }; //+------------------------------------------------------------------+ //| マウスカーソルの渡されたキャンバスオブジェクトとの | //| 相対X座標を取得する | //+------------------------------------------------------------------+ int CMouse::RelativeX(CRectCanvas &object) { return(m_x-object.X()+(int)object.GetInteger(OBJPROP_XOFFSET)); } //+------------------------------------------------------------------+ //| マウスカーソルの渡されたキャンバスオブジェクトとの | //| 相対Y座標を取得する | //+------------------------------------------------------------------+ int CMouse::RelativeY(CRectCanvas &object) { return(m_y-object.Y()+(int)object.GetInteger(OBJPROP_YOFFSET)); }
ライブラリは、さらにこれらのメソッドを使用して描画されたすべてのコントロールの相対座標を取得するように開発されます。
テーブルの構造体の変化
レンダーテーブルのコ―ド実行を可能な限り最適化するためにはCTOptions型のテーブル構造体を少し変更して補完し、多次元配列を構築できる新しい構造体を追加する必要がありました。ここでの作業は、以前に計算された値に基づいてテーブルの特定のフラグメントを再描画することです。たとえば、これらはテーブルの列と行の境界線の座標です。
たとえば CCanvasTable::DrawGrid()メソッドでは、列の境界のX 座標の計算と格納は CCanvasTable::DrawGrid() だけで合理的です。これは、テーブル全体を描画するときだけグリッドを描画します。また、ユーザがテーブル行を選択すると、事前定義したの値を使用することができます。これは、ホバー時のテーブル行の強調表示にも当てはまります(これについては、本稿で詳しく説明します)。
別の構造体(CTRowOptions)を作成してそのインスタンスの配列を宣言してテーブル行の Y座標を格納します。将来的にはおそらく行の他のプロパティも含まれます。行のY座標はCCanvasTable::DrawRows()メソッドで計算され、行の背景を描画するために設計されています。このメソッドはグリッドを描画する前に呼び出されるため、CCanvasTable::DrawGrid()メソッドは CTRowOptions構造体からの事前に計算された値を使用します。
テーブルセルのプロパティを格納するにはCTCell型の別の構造体を作成します。 CTRowOptions構造体インスタンスの配列はこのタイプでテーブル行の配列として宣言されています。この構造体には次のものが格納されます。
- アイコンの配列
- アイコンサイズの配列
- セル内の選択された(表示された)アイコンのインデックス
- 完全なテキスト
- 短くされたテキスト
- テキストの色
各アイコンはピクセルの配列なので、格納するための動的配列を持つ別の構造体(CTImage)が必要です。これらの構造体のコードは以下にあります。
class CCanvasTable : public CElement { private: //--- アイコンピクセルの配列 struct CTImage { uint m_image_data[]; }; //--- テーブルセルのプロパティ struct CTCell { CTImage m_images[]; // アイコンの配列 uint m_image_width[]; // アイコン幅の配列< uint m_image_height[]; // アイコンの高さの配列< int m_selected_image; // 選択(表示)されたアイコンのインデックス string m_full_text; // 完全なテキスト string m_short_text; // 短縮されたテキスト color m_text_color; // テキストの色 }; //--- テーブル列の行とプロパティの配列 struct CTOptions { int m_x; // 列の左端のX座標 int m_x2; // 列の右端のX座標 int m_width; // 列の幅 ENUM_ALIGN_MODE m_text_align; // 列セル内のテキスト整列モード int m_text_x_offset; // テキストオフセット string m_header_text; // 列ヘッダテキスト CTCell m_rows[]; // テーブル行の配列 }; CTOptions m_columns[]; //--- テーブル行プロパティの配列 struct CTRowOptions { int m_y; // 行の上端のY座標 int m_y2; // 行の下端のY座標 }; CTRowOptions m_rows[]; };
これらのデータ型が使用されるすべてのメソッドに適切な変更が加えられました。
可視部分の行の範囲の決定
テーブルには多数の行がある可能性があるため、行のフォーカスの検索に続いたテーブルの再描画の処理は大幅に遅くなる可能性があります。列幅の手動での変更中に、行を選択してテキストの長さを調整する場合も同様です。遅れを避けるためには、テーブルの可視部分の最初と最後のインデックスを決定し、ループがその範囲内でのみ反復するようにする必要があります。CCanvasTable::VisibleTableIndexes()メソッドはこのたみに実装されました。まず、可視部分の境界を決定します。上の境界は可視部分のY軸に沿ったオフセットであり、下の境界は、上の境界に可視部分のY軸に沿ったオフセットを足したものとして定義されます。
可視部分の上と下の行のインデックスを決定するためには 得られた境界値をテーブル設定で定義された行の高さで除算するだけで十分です。最後のテーブル行の範囲が超えられた場合は、メソッドの最後で調整を行います。
class CCanvasTable : public CElement { private: //--- テーブルの可視部分のインデックスの決定 int m_visible_table_from_index; int m_visible_table_to_index; //--- private: //--- テーブルの可視部分のインデックスの決定 void VisibleTableIndexes(void); }; //+------------------------------------------------------------------+ //| コンストラクタ | //+------------------------------------------------------------------+ CCanvasTable::CCanvasTable(void) : m_visible_table_from_index(WRONG_VALUE), m_visible_table_to_index(WRONG_VALUE) { ... } //+------------------------------------------------------------------+ //| テーブルの可視部分のインデックスの決定 | //+------------------------------------------------------------------+ void CCanvasTable::VisibleTableIndexes(void) { //--- テーブルの可視部分のオフセットを考慮して境界を決定する int yoffset1 =(int)m_table.GetInteger(OBJPROP_YOFFSET); int yoffset2 =yoffset1+m_table_visible_y_size; //--- テーブルの可視部分の最初と最後のインデックスを決定する m_visible_table_from_index =int(double(yoffset1/m_cell_y_size)); m_visible_table_to_index =int(double(yoffset2/m_cell_y_size)); //--- 範囲外でない場合は下のインデックスを1つ増やす m_visible_table_to_index=(m_visible_table_to_index+1>m_rows_total)?m_rows_total : m_visible_table_to_index+1; }
インデックスはCCanvasTable::DrawTable()メソッドで決定されます。このメソッドに引数を渡してテーブルの可視部分のみを再描画する必要があることを指定することができます。引数のデフォルト値はfalseで、テーブル全体の再描画が意味されます。下記はメソッドの短縮版です。
//+------------------------------------------------------------------+ //| テーブルを描画する | //+------------------------------------------------------------------+ void CCanvasTable::DrawTable(const bool only_visible=false) { //--- 表示されていない場合は、テーブルの可視部分のみを再描画する if(!only_visible) { f//--- テーブル全体の行インデックスを最初から最後まで設定する m_visible_table_from_index =0; m_visible_table_to_index =m_rows_total; } //--- テーブルの可視部分の行インデックスを取得する else VisibleTableIndexes(); //--- テーブルの行の背景を描画する //--- 選択された行を描画する //--- グリッドを描画する //--- アイコンを描画する //--- テキストを描画する //--- 直近に描画された変化を表示する //--- 有効な場合はヘッダーラインを更新する //--- スクロールバーに相対したテーブルの調整 }
CCanvasTable::VisibleTableIndexes()の呼び出しはテーブル行のフォーカスを決定するメソッドでも必要です。
//+------------------------------------------------------------------+ //| テーブル行のフォーカスの確認 | //+------------------------------------------------------------------+ int CCanvasTable::CheckRowFocus(void) { int item_index_focus=WRONG_VALUE; //--- マウスカーソルの下の相対Y座標を取得する int y=m_mouse.RelativeY(m_table); ///--- テーブルのローカルエリアのインデックスを取得する VisibleTableIndexes(); //--- フォーカスを探す for(int i=m_visible_table_from_index; i<m_visible_table_to_index; i++) { //--- 行のフォーカスが変わった場合 if(y>m_rows[i].m_y && y<=m_rows[i].m_y2) { item_index_focus=i; break; } } //--- フォーカスのある行のインデックスを返す return(item_index_focus); }
テーブルセルのアイコン
各セルには複数のアイコンを割り当てることができ、プログラム実行中に切り替えることができます。アイコンのセルの上端と左端からのオフセットを設定するフィールドとメソッドを追加します。
class CCanvasTable : public CElement { private: //--- セルの端からのアイコンのオフセット int m_image_x_offset; int m_image_y_offset; //--- public: //--- セルの端からのアイコンのオフセット void ImageXOffset(const int x_offset) { m_image_x_offset=x_offset; } void ImageYOffset(const int y_offset) { m_image_y_offset=y_offset; } };
指定されたセルにアイコンを割り当てるには、端末のローカルディレクトリでのパスを含む配列を渡す必要があります。その前に、それらはリソースとしてMQLアプリケーションに含める必要があります(#resource)。CCanvasTable::SetImages()メソッドはこの目的をもって設定されています。ここで、空の配列が渡された場合、または配列のオーバーランが検出された場合、プログラムはメソッドを終了します。
条件が満たされた場合、セル配列のサイズが変更されます。その後 ::ResourceReadImage() メソッドを使用してアイコンコンテンツを1次元配列に読み込み、各ピクセルの色を配列に格納します。アイコンサイズは対応する配列に格納されます。キャンバスにアイコンを描くためのループを整えるために必要になります。デフォルトでは配列の最初のアイコンがセル内で選択されます。
class CCanvasTable : public CElement { public: //--- 指定したセルにアイコンを設定する void SetImages(const uint column_index,const uint row_index,const string &bmp_file_path[]); }; //+------------------------------------------------------------------+ //| 指定したセルにアイコンを設定する | //+------------------------------------------------------------------+ void CCanvasTable::SetImages(const uint column_index,const uint row_index,const string &bmp_file_path[]) { int total=0; //---サイズが0の配列が渡された場合は終了する if((total=CheckArraySize(bmp_file_path))==WRONG_VALUE) return; //--- 配列の範囲が超えられているかどうかの確認 if(!CheckOutOfRange(column_index,row_index)) return; //--- 配列のサイズを変更する ::ArrayResize(m_columns[column_index].m_rows[row_index].m_images,total); ::ArrayResize(m_columns[column_index].m_rows[row_index].m_image_width,total); ::ArrayResize(m_columns[column_index].m_rows[row_index].m_image_height,total); //--- for(int i=0; i<total; i++) { //--- 配列の最初のアイコンがデフォルトで選択される m_columns[column_index].m_rows[row_index].m_selected_image=0; //--- 渡されたアイコンを配列に書き込みてサイズを格納する if(!ResourceReadImage(bmp_file_path[i],m_columns[column_index].m_rows[row_index].m_images[i].m_image_data, m_columns[column_index].m_rows[row_index].m_image_width[i], m_columns[column_index].m_rows[row_index].m_image_height[i])) { Print(__FUNCTION__," > error: ",GetLastError()); return; } } }
特定のセルにいくつのアイコンがあるか調べるにはCCanvasTable::ImagesTotal()メソッドを使用します。
class CCanvasTable : public CElement { public: //--- 指定したセルのアイコンの総数を返す int ImagesTotal(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| 指定したセルのアイコンの総数を返す | //+------------------------------------------------------------------+ int CCanvasTable::ImagesTotal(const uint column_index,const uint row_index) { //--- 配列の範囲が超えられているかどうかの確認 if(!CheckOutOfRange(column_index,row_index)) return(WRONG_VALUE); //--- アイコン配列のサイズを返す return(::ArraySize(m_columns[column_index].m_rows[row_index].m_images)); }
次に、アイコンを描画するメソッドについて考えてみます。まず、新しいCColors::BlendColors() メソッドが CColors </ b2>クラスに追加され、オーバーレイアイコンの透明度を考慮して、上下の色を正しく混ぜることができます。渡された色の透明度値を取得する、補助的なCColors::GetA() メソッドも追加されました。
CColors::BlendColors()メソッドでは、渡された色は初めにRGB コンポーネントに分けられ、アルファチャンネルは上の色から抽出されます。アルファチャンネルは0と1の間の値に変換されます。渡された色に透明度が含まれていない場合、色は混ぜられません。透明度がある場合、2つの渡された色の各要素は上の色の透明度を考慮して混ぜられます.。その後、取得したコンポーネントの値が範囲外の場合は( 255 )調整されます。
//+------------------------------------------------------------------+ //| 色操作のためのクラス | //+------------------------------------------------------------------+ class CColors { public: double GetA(const color aColor); color BlendColors(const uint lower_color,const uint upper_color); }; //+------------------------------------------------------------------+ //| Aコンポーネント値の取得 | //+------------------------------------------------------------------+ double CColors::GetA(const color aColor) { return(double(uchar((aColor)>>24))); } //+------------------------------------------------------------------+ //| 上の色の透明度を考慮して2つの色を混ぜる //+------------------------------------------------------------------+ color CColors::BlendColors(const uint lower_color,const uint upper_color) { double r1=0,g1=0,b1=0; double r2=0,g2=0,b2=0,alpha=0; double r3=0,g3=0,b3=0; //--- 色をARGB形式に変換する uint pixel_color=::ColorToARGB(upper_color); //--- 下の色と上の色のコンポーネントを取得する ColorToRGB(lower_color,r1,g1,b1); ColorToRGB(pixel_color,r2,g2,b2); //--- 0.00から1.00の透明度のパーセンテージを取得する alpha=GetA(upper_color)/255.0; //--- 透明度がある場合は if(alpha<1.0) { //--- アルファチャンネルを考慮してコンポーネントを混ぜる r3=(r1*(1-alpha))+(r2*alpha); g3=(g1*(1-alpha))+(g2*alpha); b3=(b1*(1-alpha))+(b2*alpha); //---得られた値の調整 r3=(r3>255)?255 : r3; g3=(g3>255)?255 : g3; b3=(b3>255)?255 : b3; } else { r3=r2; g3=g2; b3=b2; } //--- 得られた成分を組み合わせて色を返す return(RGBToColor(r3,g3,b3)); }
これでアイコンを描画するためのメソッドを書くのは簡単です。CCanvasTable::DrawImage() メソッドのコ―ドは下に示されています。アイコンが描かれるテーブルセルのインデックスが渡されなければなりません。このメソッドの開始時には、選択されたセルのインデックスおよびそのサイズだけでなくオフセットを考慮したアイコンの座標が取得されます。次に、二重ループがアイコンをピクセルごとに出力します。指定されたピクセルが空の場合(色がない場合)、ループは次のピクセルに進みます。色がある場合は、セルの背景色と現在のピクセルの色が決定されこれらの2つの色はオーバーレイ色の透明度を考慮して混ぜられてキャンバスに描画されます 。
class CCanvasTable : public CElement { private: //--- 指定したセルでアイコンを描画する void DrawImage(const int column_index,const int row_index); }; //+------------------------------------------------------------------+ //| 指定したセルでアイコンを描画する | //+------------------------------------------------------------------+ void CCanvasTable::DrawImage(const int column_index,const int row_index) { //--- 座標の計算 int x =m_columns[column_index].m_x+m_image_x_offset; int y =m_rows[row_index].m_y+m_image_y_offset; //--- セルで選択されたアイコンとそのサイズ int selected_image =m_columns[column_index].m_rows[row_index].m_selected_image; uint image_height =m_columns[column_index].m_rows[row_index].m_image_height[selected_image]; uint image_width =m_columns[column_index].m_rows[row_index].m_image_width[selected_image]; //--- 描画 for(uint ly=0,i=0; ly<image_height; ly++) { for(uint lx=0; lx<image_width; lx++,i++) { //--- 色がない場合は次のピクセルに移る if(m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i]<1) continue; //--- アイコンの指定されたピクセルの下レイヤの色(セルの背景)と色を取得する uint background =(row_index==m_selected_item)?m_selected_row_color : m_table.PixelGet(x+lx,y+ly); uint pixel_color =m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i]; //--- 色を混ぜる uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color)); //--- オーバーレイアイコンのピクセルを描画する m_table.PixelSet(x+lx,y+ly,foreground); } } }
CCanvasTable::DrawImages()メソッドは、テーブルの目に見える部分だけを描く必要があるときを考慮してテーブルの全部のアイコンを一度に描画するために設計されています。現在のバージョンでは、アイコンはテーブル列内のテキストが左揃えの場合にのみ描画できます。さらに、アイコンがセルに割り当てられているかどうか、またそのピクセルの配列が空であるかどうかは各反復で確認されます。条件がすべて満たされた場合には、アイコン描画のためにCCanvasTable::DrawImage() メソッドが呼ばれます。
class CCanvasTable : public CElement { private: //--- テーブルのすべてのアイコンを描く void DrawImages(void); }; //+------------------------------------------------------------------+ //| テーブルのすべてのアイコンを描く | //+------------------------------------------------------------------+ void CCanvasTable::DrawImages(void) { //--- 座標の計算 int x=0,y=0; //--- 列 for(int c=0; c<m_columns_total; c++) { //--- テキストが左揃えでない場合は次の列に移動する if(m_columns[c].m_text_align!=ALIGN_LEFT) continue; //--- 行 for(int r=m_visible_table_from_index; r<m_visible_table_to_index; r++) { //--- このセルにアイコンが含まれていない場合は次のセルに移動する if(ImagesTotal(c,r)<1) continue; //--- セル内の選択されたアイコン(デフォルトで最初の[0]) int selected_image=m_columns[c].m_rows[r].m_selected_image; //--- ピクセル配列が空の場合は次に行く if(::ArraySize(m_columns[c].m_rows[r].m_images[selected_image].m_image_data)<1) continue; //--- アイコンを描画する DrawImage(c,r); } } }
以下のスクリーンショットは、セルにアイコンを含むテーブルの例を示しています。
図1 セルにアイコンを含むテーブル
ホバーされた場合の行の強調表示
ホバーされたレンダーテーブルの行が強調表示されるためには、追加のフィールドとメソッドが必要です。強調表示モードを有効化するにはCCanvasTable::LightsHover()メソッドを使います。行の色はCCanvasTable::CellColorHover()メソッドの助けを借りて設定することができます。
class CCanvasTable : public CElement { private: //--- 異なる状態にあるセルの色 color m_cell_color; color m_cell_color_hover; //--- カーソルがホバーした時の強調表示された行のモード bool m_lights_hover; //--- public: //--- 異なる状態にあるセルの色 void CellColor(const color clr) { m_cell_color=clr; } void CellColorHover(const color clr) { m_cell_color_hover=clr; } //--- カーソルがホバーした時の強調表示された行のモード void LightsHover(const bool flag) { m_lights_hover=flag; } };
行を強調表示しても、カーソルの移動に合わせてテーブル全体を何度も再描画する必要はありません。さらに、テーブル全体の繰り返した再描画は、アプリケーションを非常に遅くし、CPUリソースを使いすぎるため、避けることが強く推奨されます。テーブル領域にマウスカーソルが最初に入ったときにフォーカスを一度探すだけで十分です(行の配列全体で反復します)。これにはCCanvasTable::CheckRowFocus()メソッドが使われます。フォーカスが見つかって行インデックスが保存されたら、カーソルの移動時に、保存されたインデックスを持つ行のフォーカスが変更されたかどうかを確認します。記述されたアルゴリズムは、以下のCCanvasTable::ChangeRowsColor() メソッドで実装されています。CCanvasTable::RedrawRow()メソッドは行の色を変更するために使用され、そのコードは後で紹介されます。テーブルオブジェクトの色を変えるCCanvasTable::ChangeRowsColor()メソッドはCCanvasTable::ChangeObjectsColor() メソッドで呼ばれます。
class CCanvasTable : public CElement { private: //--- 行のフォーカスの決定 int m_item_index_focus; //---ある行から別の行へのマウスカーソルの移動の瞬間の決定 int m_prev_item_index_focus; //--- private: //--- ホバーされたときの行の色の変更 void ChangeRowsColor(void); }; //+------------------------------------------------------------------+ //| ホバーされたときの行の色の変更 | //+------------------------------------------------------------------+ void CCanvasTable::ChangeRowsColor(void) { //---ホバリングされたときの行のハイライトが無効になっている場合は終了する if(!m_lights_hover) return; //--- フォーカスされていない場合 if(!m_table.MouseFocus()) { //--- フォーカスされていないことがまだ示されていない場合 if(m_prev_item_index_focus!=WRONG_VALUE) { m_item_index_focus=WRONG_VALUE; //--- 色を変更する RedrawRow(); m_table.Update(); //--- フォーカスをリセットする m_prev_item_index_focus=WRONG_VALUE; } } //--- フォーカスされている場合 else { //--- 行上のフォーカスを確認する if(m_item_index_focus==WRONG_VALUE) { //--- フォーカスのある行のインデックスを取得する m_item_index_focus=CheckRowFocus(); //--- 行の色を変える RedrawRow(); m_table.Update(); //--- 以前にフォーカスされたインデックスとして保存する m_prev_item_index_focus=m_item_index_focus; return; } //--- マウスカーソルの下の相対Y座標を取得する int y=m_mouse.RelativeY(m_table); //--- フォーカスの検証 bool condition=(y>m_rows[m_item_index_focus].m_y && y<=m_rows[m_item_index_focus].m_y2); //--- フォーカスが変わった場合 if(!condition) { //--- フォーカスのある行のインデックスを取得する m_item_index_focus=CheckRowFocus(); //--- 行の色を変える RedrawRow(); m_table.Update(); //--- 以前にフォーカスされたインデックスとして保存する m_prev_item_index_focus=m_item_index_focus; } } }
テーブル行を高速に再描画するためのCCanvasTable::RedrawRow()メソッドは、次の2つのモードで動作します。
- 行を選択しているとき
- ホバー時に行を強調表示するモードで。
メソッドは、目的のモードを指定するために対応する引数を渡す必要があります。引数はデフォルトではfalseに設定されています。これは、テーブルの行を強調表示するモードでメソッドを使用することを示します。クラスには、現在と以前の選択された/強調表示されたテーブル行を決定するための両方のモードのための特別なフィールドが含まれていますしたがって、別の行をマーキングするには、テーブル全体ではなく、前の行と現在の行のみを再描画する必要があります。
インデックスが定義されていない場合(WRONG_VALUE)はプログラムはメソッドを終了します。次に、定義されているインデックスの数を判断する必要があります。これがテーブルへの最初の入力で、インデックスが1つだけ(現在)定義されている場合、色はそれに応じて現在の行でのみ変更されます。二回目の入力では、2行(現在と前)の色が変更されます。
ここで行の色を変更する順序を決定する必要があります。現在の行のインデックスが以前の行のインデックスよりも大きい場合は、カーソルが下に移動したことになります。次に、初めに以前のインデックスで色を変更しから現在のインデックスの色を変更します。逆の状況では、この反対をします。また、このメソッドは、現在の行のインデックスが定義されていないときに、以前の行のインデックスが存在している間にテーブル領域を離れる瞬間を考慮します。
操作に使われるローカル変数が全て初期化されると、行、グリッド、アイコン、およびテキストの背景が厳密な順序で描画されます。
class CCanvasTable : public CElement { private: //--- 指定されたテーブル行を指定されたモードで再描画する void RedrawRow(const bool is_selected_row=false); }; //+------------------------------------------------------------------+ //| 指定されたテーブル行を指定されたモードで再描画する | //+------------------------------------------------------------------+ void CCanvasTable::RedrawRow(const bool is_selected_row=false) { //--- 現在と1つ前の行インデックス int item_index =WRONG_VALUE; int prev_item_index =WRONG_VALUE; //--- 指定されたモードに対する行インデックスの初期化 if(is_selected_row) { item_index =m_selected_item; prev_item_index =m_prev_selected_item; } else { item_index =m_item_index_focus; prev_item_index =m_prev_item_index_focus; } //--- インデックスが定義されていない場合は終了する if(prev_item_index==WRONG_VALUE && item_index==WRONG_VALUE) return; //--- 描画される行と列の数 int rows_total =(item_index!=WRONG_VALUE && prev_item_index!=WRONG_VALUE)?2 : 1; int columns_total =m_columns_total-1; //--- 座標 int x1=1,x2=m_table_x_size; int y1[2]={0},y2[2]={0}; //--- 特定のシーケンス内の値の配列 int indexes[2]; //--- (1)マウスカーソルが下に動いたか (2) 一番初めに入った場合 if(item_index>m_prev_item_index_focus || item_index==WRONG_VALUE) { indexes[0]=(item_index==WRONG_VALUE || prev_item_index!=WRONG_VALUE)?prev_item_index : item_index; indexes[1]=item_index; } //--- マウスカーソルが上に動いた else { indexes[0]=item_index; indexes[1]=prev_item_index; } //--- 行の背景を描画する for(int r=0; r<rows_total; r++) { //--- 行の上限と下限の座標を計算する y1[r]=m_rows[indexes[r]].m_y+1; y2[r]=m_rows[indexes[r]].m_y2-1; //--- 強調表示モードに関する行のフォーカスの決定 bool is_item_focus=false; if(!m_lights_hover) is_item_focus=(indexes[r]==item_index && item_index!=WRONG_VALUE); else is_item_focus=(item_index==WRONG_VALUE)?(indexes[r]==prev_item_index) :(indexes[r]==item_index); //--- 行の背景を描画する m_table.FillRectangle(x1,y1[r],x2,y2[r],RowColorCurrent(indexes[r],is_item_focus)); } //--- グリッドの色 uint clr=::ColorToARGB(m_grid_color); //--- 境界を描画する for(int r=0; r<rows_total; r++) { for(int c=0; c<columns_total; c++) m_table.Line(m_columns[c].m_x2,y1[r],m_columns[c].m_x2,y2[r],clr); } //--- アイコンを描画する for(int r=0; r<rows_total; r++) { for(int c=0; c<m_columns_total; c++) { //--- (1)アイコンがこのセルに存在し (2) tこの列のテキストが左に揃えられている場合はアイコンを描画する if(ImagesTotal(c,r)>0 && m_columns[c].m_text_align==ALIGN_LEFT) DrawImage(c,indexes[r]); } } //--- 座標の計算 int x=0,y=0; //--- テキスト配列モード uint text_align=0; //--- テキストを描画する for(int c=0; c<m_columns_total; c++) { //--- (1)テキストのX座標と (2) テキスト配列モードを取得する x =TextX(c); text_align =TextAlign(c,TA_TOP); //--- for(int r=0; r<rows_total; r++) { //--- (1) 座標を計算して (2)テキストを描画する y=m_rows[indexes[r]].m_y+m_text_y_offset; m_table.TextOut(x,y,m_columns[c].m_rows[indexes[r]].m_short_text,TextColor(c,indexes[r]),text_align); } } }
結果は下記です。
図2 ホバーされたときにテーブルの行を強調表示するデモンストレーション
テーブルのセルを高速に再描画するメソッド
テーブル行を高速に再描画する方法が検討されています。ここでセルを高速に再描画する方法について説明します。たとえば、テーブルセルのテキストや色、アイコンを変更する必要がある場合は、テーブル全体ではなくセルのみを再描画すれば十分です。これにはCCanvasTable::RedrawCell()プライベートメソッドが使われます。フレームは更新されず、セルの内容のみが再描画されます。背景色は、強調表示モードを考慮して決定されます(有効な場合)。値を決定してローカル変数、背景、アイコン(割り当てられていてテキストが左に揃えられている場合)を初期化し、セルにテキストを描画します。
class CCanvasTable : public CElement { private: //--- テーブルの指定された行を再描画する void RedrawCell(const int column_index,const int row_index); }; //+------------------------------------------------------------------+ //| テーブルの指定された行を再描画する | //+------------------------------------------------------------------+ void CCanvasTable::RedrawCell(const int column_index,const int row_index) { //--- 座標 int x1=m_columns[column_index].m_x+1; int x2=m_columns[column_index].m_x2-1; int y1=m_rows[row_index].m_y+1; int y2=m_rows[row_index].m_y2-1; //--- 座標の計算 int x=0,y=0; //--- フォーカスのチェック bool is_row_focus=false; //--- 行の強調表示モードが有効な場合 if(m_lights_hover) { //--- (1) マウスカーソルの相対Y座標と (2) 指定されたテーブル行のフォーカスを取得する y=m_mouse.RelativeY(m_table); is_row_focus=(y>m_rows[row_index].m_y && y<=m_rows[row_index].m_y2); } //--- セルの背景を描画する m_table.FillRectangle(x1,y1,x2,y2,RowColorCurrent(row_index,is_row_focus)); //--- (1)アイコンがこのセルに存在し (2) tこの列のテキストが左に揃えられている場合はアイコンを描画する if(ImagesTotal(column_index,row_index)>0 && m_columns[column_index].m_text_align==ALIGN_LEFT) DrawImage(column_index,row_index); //--- テキスト配列モードを取得する uint text_align=TextAlign(column_index,TA_TOP); //--- テキストを描画する for(int c=0; c<m_columns_total; c++) { //--- テキストのX座標を取得する x=TextX(c); //--- ループを停止する if(c==column_index) break; } //--- (1) Y座標を計算して (2)テキストを描画する y=y1+m_text_y_offset-1; m_table.TextOut(x,y,m_columns[column_index].m_rows[row_index].m_short_text,TextColor(column_index,row_index),text_align); }
次に、セル内のテキスト、テキストの色、アイコン(割り当てられたものからの選択)を変更できるメソッドについて考えてみましょう。テキストとその色の設定にはCCanvasTable::SetValue()及びCCanvasTable::TextColor()パブリックメソッドが使われなければなりません。これらのメソッドには、セルのインデックス(列と行)と設定する値が渡されます。CCanvasTable::SetValue()メソッドでは、これはセルで表示される文字列です。ここでは、渡された完全な文字列と短縮された文字列(完全な文字列がセルの幅に合わない場合)がテーブル構造体( CTCell)の対応するフィールドに格納されます。CCanvasTable::TextColor()メソッドにはテキストの色が渡されるべきです。両方のメソッドで、すぐにセルを再描画する必要があるかどうかは4番目のパラメータで指定できますが、さもなければこれは後にCCanvasTable::UpdateTable() メソッドを呼び出すことでなされます。
class CCanvasTable : public CElement { private: //--- 指定されたテーブルセルの値を設定する void SetValue(const uint column_index,const uint row_index,const string value,const bool redraw=false); //--- 指定されたテーブルセルでテキストの色を設定する void TextColor(const uint column_index,const uint row_index,const color clr,const bool redraw=false); }; //+------------------------------------------------------------------+ //| 指定されたインデックスで配列に書き入れる | //+------------------------------------------------------------------+ void CCanvasTable::SetValue(const uint column_index,const uint row_index,const string value,const bool redraw=false) { //--- 配列の範囲が超えられているかどうかの確認 if(!CheckOutOfRange(column_index,row_index)) return; //--- 配列に値を格納する m_columns[column_index].m_rows[row_index].m_full_text=value; //--- セルにはまらない場合はテキストを調整して格納する m_columns[column_index].m_rows[row_index].m_short_text=CorrectingText(column_index,row_index); //--- 指定された場合はセルを再描画する if(redraw) RedrawCell(column_index,row_index); } //+------------------------------------------------------------------+ //| テキスト色の配列に書き入れる | //+------------------------------------------------------------------+ void CCanvasTable::TextColor(const uint column_index,const uint row_index,const color clr,const bool redraw=false) { //--- 配列の範囲が超えられているかどうかの確認 if(!CheckOutOfRange(column_index,row_index)) return; //--- 共通の配列にテキストの色を格納する m_columns[column_index].m_rows[row_index].m_text_color=clr; //--- 指定された場合はセルを再描画する if(redraw) RedrawCell(column_index,row_index); }
セルのアイコンはCCanvasTable::ChangeImage()メソッドで変更できます。変更後のアイコンのインデックスはここで3番目のパラメータとして指定されます。前述のセルプロパティを変更するためのメソッド同様、セルを直ちに再描画するのか後で再描画するのかの指定ができます.。
class CCanvasTable : public CElement { private: //--- 指定したセルのアイコンを変更する void ChangeImage(const uint column_index,const uint row_index,const uint image_index,const bool redraw=false); }; //+------------------------------------------------------------------+ //| 指定したセルのアイコンを変更する | //+------------------------------------------------------------------+ void CCanvasTable::ChangeImage(const uint column_index,const uint row_index,const uint image_index,const bool redraw=false) { //--- 配列の範囲が超えられているかどうかの確認 if(!CheckOutOfRange(column_index,row_index)) return; //--- セルアイコンの数を取得する int images_total=ImagesTotal(column_index,row_index); //--- (1) アイコンがない場合や (2) 範囲外の場合は終了する if(images_total==WRONG_VALUE || image_index>=(uint)images_total) return; //--- 指定されたアイコンが選択されたアイコンと一致する場合は終了する if(image_index==m_columns[column_index].m_rows[row_index].m_selected_image) return; //--- 選択されたセルのアイコンのインデックスを格納する m_columns[column_index].m_rows[row_index].m_selected_image=(int)image_index; //--- 指定された場合はセルを再描画する if(redraw) RedrawCell(column_index,row_index); }
テーブル全体の再描画にはあと一つのCCanvasTable::UpdateTable()パブリックメソッドが必要です。呼び出しには2つのモードがあります。
- 上記の方法で行われた最近の変更を表示するのに単にテーブルを更新する必要がある場合
- 変更があってテーブルを完全に再描画する必要がある場合
メソッドの唯一の引数はデフォルトでfalseとなっており、再描画なしの更新が意味されます。
class CCanvasTable : public CElement { private: //--- テーブルの更新 void UpdateTable(const bool redraw=false); }; //+------------------------------------------------------------------+ //| テーブルの更新 | //+------------------------------------------------------------------+ void CCanvasTable::UpdateTable(const bool redraw=false) { //--- 指定された場合はテーブルを再描画する if(redraw) DrawTable(); //--- テーブルを更新する m_table.Update(); }
以下はこの作業の結果です。
図3 レンダーテーブルの新機能のデモンストレーション
この結果をデモするエキスパートアドバイザーは、本稿添付のファイルでダウンロードできます。プログラムの実行中、すべてのテーブルセル(5列および30行)のアイコンは100ミリ秒ごとに変化します。以下のスクリーンショットは、ユーザがMQLアプリケーションのグラフィカルインタフェースと対話しない状態でのCPUの負荷を示しています。100ミリ秒の更新頻度を持つCPU負荷は3%を超えません。
図4 テストMQLアプリケーションの実行中のCPU負荷
コントロールを検証するためのアプリケーション
現在のレンダーテーブルバージョンは、例えば、気配値ウィンドウと同じテーブルを作成するのに十分なほどスマートですこれをデモしてみましょう。この例では、5列25行の表を作成します。これらは、MetaQuotes-Demoサーバーで使用可能な25個の銘柄です。テーブルのデータは次のようになります。
- Symbol – 金融製品(貨幣ペア)
- Bid – 売値
- Ask – 買値
- Spread (!) – 買値と売値のサ
- Time – 最終相場の時刻
気配標示ウィンドウのテーブルのように価格の最新の変化を示す同じアイコンを準備しましょう。テーブルセルの最初の初期化は、コントロールを作成するメソッドで直ちに実行され、カスタムクラスの CProgram :: InitializingTable() 補助メソッドを呼び出すことによって実行されます。
//+------------------------------------------------------------------+ //| アプリケーション作成のクラス | //+------------------------------------------------------------------+ class CProgram : public CWndEvents { private: //--- テーブルを初期化する void InitializingTable(void); }; //+------------------------------------------------------------------+ //| テーブルを初期化する | //+------------------------------------------------------------------+ void CProgram::InitializingTable(void) { //--- ヘッダタイトルの配列 string text_headers[COLUMNS1_TOTAL]={"Symbol","Bid","Ask","!","Time"}; //--- 銘柄の配列 string text_array[25]= { "AUDUSD","GBPUSD","EURUSD","USDCAD","USDCHF","USDJPY","NZDUSD","USDSEK","USDHKD","USDMXN", "USDZAR","USDTRY","GBPAUD","AUDCAD","CADCHF","EURAUD","GBPCHF","GBPJPY","NZDJPY","AUDJPY", "EURJPY","EURCHF","EURGBP","AUDCHF","CHFJPY" }; //--- アイコンの配列 string image_array[3]= { "::Images\\EasyAndFastGUI\\Icons\\bmp16\\circle_gray.bmp", "::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_up.bmp", "::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_down.bmp" }; //--- for(int c=0; c<COLUMNS1_TOTAL; c++) { //--- ヘッダタイトルを設定する m_canvas_table.SetHeaderText(c,text_headers[c]); //--- for(int r=0; r<ROWS1_TOTAL; r++) { //--- アイコンを設定する m_canvas_table.SetImages(c,r,image_array); //--- 銘柄名を設定する if(c<1) m_canvas_table.SetValue(c,r,text_array[r]); //--- すべてのセルのデフォルト値 else m_canvas_table.SetValue(c,r,"-"); } } }
これらのテーブルセルの値は実行中に16ミリ秒ごとにタイマーによって更新されます。このためにあと一つのCProgram::UpdateTable()補助メソッドが作成されました。週末(土日)にはここでプログラムがメソッドを終了します。次に、二重ループがテーブルのすべての列と行を反復処理します。この二重ループは各銘柄の最後の2つのティックを取得して価格の変化を分析した後に対応する値を設定します。
class CProgram : public CWndEvents { private: //--- テーブルを初期化する void InitializingTable(void); }; //+------------------------------------------------------------------+ //| テーブル値の更新 | //+------------------------------------------------------------------+ void CProgram::UpdateTable(void) { MqlDateTime check_time; ::TimeToStruct(::TimeTradeServer(),check_time); //--- 土日には終了する if(check_time.day_of_week==0 || check_time.day_of_week==6) return; //--- for(int c=0; c<m_canvas_table.ColumnsTotal(); c++) { for(int r=0; r<m_canvas_table.RowsTotal(); r++) { //--- データを取得する銘柄 string symbol=m_canvas_table.GetValue(0,r); //--- 最後の2つのティックのデータを取得する MqlTick ticks[]; if(::CopyTicks(symbol,ticks,COPY_TICKS_ALL,0,2)<2) continue; //--- 配列を時系列をして設定する ::ArraySetAsSeries(ticks,true); //--- 銘柄の列、価格の方向を決定 if(c==0) { int index=0; //--- 価格が変更されなかった場合 if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid) index=0; //--- 売値が上がった場合 else if(ticks[0].bid>ticks[1].bid) index=1; //--- 売値が下がった場合 else if(ticks[0].bid<ticks[1].bid) index=2; //--- 対応するアイコンを設定する m_canvas_table.ChangeImage(c,r,index,true); } else { //--- 価格差の列 - スプレッド(!) if(c==3) { //--- ポイント単位のスプレッドサイズを取得/設定する int spread=(int)::SymbolInfoInteger(symbol,SYMBOL_SPREAD); m_canvas_table.SetValue(c,r,string(spread),true); continue; } //--- 小数点以下の桁数を取得する int digit=(int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS); //--- 売値の列 if(c==1) { m_canvas_table.SetValue(c,r,::DoubleToString(ticks[0].bid,digit)); //--- 価格が変更された場合は方向に対応する色を設定する if(ticks[0].bid!=ticks[1].bid) m_canvas_table.TextColor(c,r,(ticks[0].bid<ticks[1].bid)?clrRed : clrBlue,true); //--- continue; } //--- 買値の列 if(c==2) { m_canvas_table.SetValue(c,r,::DoubleToString(ticks[0].ask,digit)); //--- 価格が変更された場合は方向に対応する色を設定する if(ticks[0].ask!=ticks[1].ask) m_canvas_table.TextColor(c,r,(ticks[0].ask<ticks[1].ask)?clrRed : clrBlue,true); //--- continue; } //--- 銘柄の価格の最終到着時刻の列 if(c==4) { long time =::SymbolInfoInteger(symbol,SYMBOL_TIME); string time_msc =::IntegerToString(ticks[0].time_msc); int length =::StringLen(time_msc); string msc =::StringSubstr(time_msc,length-3,3); string str =::TimeToString(time,TIME_MINUTES|TIME_SECONDS)+"."+msc; //--- color clr=clrBlack; //--- 価格が変更されなかった場合 if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid) clr=clrBlack; //--- 売値が上がった場合 else if(ticks[0].bid>ticks[1].bid) clr=clrBlue; //--- 売値が下がった場合 else if(ticks[0].bid<ticks[1].bid) clr=clrRed; //--- 値とテキストの色を設定する m_canvas_table.SetValue(c,r,str); m_canvas_table.TextColor(c,r,clr,true); continue; } } } } //--- テーブルを更新する m_canvas_table.UpdateTable(); }
次の結果が得られます。
図5 気配値ウィンドウとカスタムでレンダーテーブルのデータの比較
本稿で紹介されたテストアプリケーションをさらに研究するためには、以下のリンクを使用してダウンロードしてください。
おわりに
グラフィカルインターフェイスを作成するためのライブラリは、開発の現段階では下の図のようになります。
図6 開発の現段階でのライブラリの構造
テスト用のライブラリとファイルの最新バージョンは以下でダウンロードできます。
これらのファイルに含まれている資料の使用についてご質問がある場合は、記事のいずれかでライブラリの開発の詳細をご参照になるか、本稿へのコメント欄でご質問ください。