
グラフィカルインターフェイスX:マルチラインテキストボックスでのテキスト選択(ビルド13)
目次
はじめに
シリーズ第一弾のグラフィカルインタフェース I:ライブラリストラクチャの準備(チャプター 1)ではライブラリの目的が詳しく考察されました。第一部の記事へのリンクの完全なリストは各章の末尾でみられます。開発の現段階でのライブラリの完全版をダウンロードすることができます。ファイルはアーカイブと同じディレクトリに配置される必要があります。
下記で検討されているマルチラインテキストボックスを最大限に活用するためにはテキスト選択を実装する必要があります。1文字ごとに削除することは不便だからです。
キーの組み合わせを使用してテキストを選択して選択したテキストを削除することは、他のテキストエディタとまったく同じです。さらに、引き続きコードを最適化し、ライブラリの進化の第2段階の最終プロセスではすべてのコントロールが別々の画像(キャンバス)としてレンダリングされるため、これに向かってクラスを準備します。
ここで示されるのはこのコントロールの最終版です。この後の修正は、特定のアルゴリズムに関するより効率的な解決法が見つかった場合にのみ行われます。
Shiftキー押下の追跡
まず、以前に考察されたCKeysキーボード操作クラスに、Shiftキーの現在の状態を特定するCKeys::KeyShiftState()メソッドを追加します。このキーは、テキストを選択するためのさまざまな組み合わせで使用されます。下記は、この単純なメソッドのコードを示しています。Shiftキーは、TERMINAL_KEYSTATE_SHIFT識別子を持つ::TerminalInfoInteger()関数がゼロより小さい値を返した場合に押下されたとみなされます。
//+------------------------------------------------------------------+ //| キーボード操作のためのクラス | //+------------------------------------------------------------------+ class CKeys { public: //--- Shiftキーの状態を返す bool KeyShiftState(void); }; //+------------------------------------------------------------------+ //| Shiftキーの状態を返す | //+------------------------------------------------------------------+ bool CKeys::KeyShiftState(void) { return(::TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<0); }
テキストを選択するためのキーの組み合わせ
テキストを選択するためのキーの組み合わせをすべて考えてみましょう。これはテキストボックスに実装されます。2つのキーの組み合わせから始めます。
- 'Shift + Left'と'Shift + Right'の組み合わせは、テキストカーソルをそれぞれ左右に1文字ずつ移動します。テキストは、異なる色の背景と文字で強調表示されます(ユーザーによってもカスタマイズ可能)。
図1 左右へ1文字ずつ移動してテキストを選択する
- 'Shift + Home'と'Shift + End'の組み合わせは、テキストカーソルをそれぞれ行の先頭と末尾に移動して、カーソルの開始位置からのすべての文字を選択します。
図2 カーソルを開始位置から行の始めと終わりに移動してテキストを選択する
- 'Shift + Up'と'Shift + Down'の組み合わせはテキストカーソルをそれぞれ上下に1行ずつ移動します。カーソルから最初の行の始めまでのテキストとカーソルから最後の行の終わりまでのテキストが選択されます。選択された最初と最後の行の間にさらに行がある場合、それらのテキストは完全に選択されます。
図3 上下に1行ずつ移動してテキストを選択する
テキストの選択には3つのキーの組み合わせが使用されることがあります。たとえば、1行で複数の単語をすばやく選択する必要がある場合に文字ごとに選択するのは面倒で、複数の行からなるテキストを選択する必要がある場合には行単位の選択でさえ便利ではありません。
3つのキーの組み合わせではShiftの他にCtrlが使われます。この組み合わせについて考えて、後に本稿内で実装します。
- 'Ctrl + Shift + Left'と'Ctrl + Shift + Right'の組み合わせは、テキストカーソルの現在の位置の左/右にある単語全体の選択を意図しています。
図4 左右へ1語ずつ移動してテキストを選択する
- 'Ctrl + Shift + Home'と'Ctrl + Shift + End'の組み合わせは、テキストカーソルの現在の位置から始まって、それぞれ最初の行の初めと最後の行の終わりまでのすべてのテキストを選択することができます。
図5 カーソルを文書の先頭と最後に移動してテキストを選択する
次のセクションでは、テキストを選択するのに使用されるメソッドについて検討します。
テキスト選択メソッド
選択されたテキストはデフォルトでは青い背景に白文字で表示されますが、必要に応じて、CTextBox:: SelectedBackColor()および CTextBox:: SelectedTextColor()メソッドを使用して変更できます。
class CTextBox : public CElement { private: //--- 選択されたテキストの背景色とテキストの色 color m_selected_back_color; color m_selected_text_color; //--- private: //--- 選択されたテキストの背景色とテキストの色 void SelectedBackColor(const color clr) { m_selected_back_color=clr; } void SelectedTextColor(const color clr) { m_selected_text_color=clr; } }; //+------------------------------------------------------------------+ //| コンストラクタ | //+------------------------------------------------------------------+ CTextBox::CTextBox(void) : m_selected_text_color(clrWhite), m_selected_back_color(C'51,153,255') { //... }
テキストを選択するには、選択されたテキストの行と文字の最初と最後のインデックスを指定するフィールドとメソッドが必要です。さらに、選択が解除されたときにこれらの値をリセットするメソッドが必要になります。
テキストを選択するためのキーの組み合わせが押下されるたびに、テキストカーソルを動かす前にCTextBox::SetStartSelectedTextIndexes()メソッドが呼び出されます。これは、テキストカーソルが最初にあった行のインデックスと文字のインデックスの値を設定します。値は、それらの値が最後にリセットされた後の初のメソッドへの呼び出しである場合にのみ設定されます。このメソッドが呼び出されるとカーソルが移動します。次に CTextBox::SetEndSelectedTextIndexes()メソッドが呼び出され、行インデックスと文字インデックスの最終値(つまり、テキストカーソルの位置)が設定されます。テキスト選択モードでのテキストカーソルの移動中にカーソルが開始位置にあることが判明した場合、CTextBox::ResetSelectedText() メソッドが呼び出されて値がリセットされます。 値は、テキストカーソルの移動、選択したテキストの削除、またはテキストボックスの非アクティブ化でもリセットされます。
class CTextBox : public CElement { private: //--- (選択されたテキストの)行と文字の始めと終わりのインデックス int m_selected_line_from; int m_selected_line_to; int m_selected_symbol_from; int m_selected_symbol_to; //--- private: //--- 選択されたテキストの (1) 始めと (2) 終わりのインデックスを設定する void SetStartSelectedTextIndexes(void); void SetEndSelectedTextIndexes(void); //--- 選択されたテキストをリセットする void ResetSelectedText(void); }; //+------------------------------------------------------------------+ //| 選択されたテキストのインデックスを設定する | //+------------------------------------------------------------------+ void CTextBox::SetStartSelectedTextIndexes(void) { //--- テキスト選択の開始インデックスがまだ設定されていない場合 if(m_selected_line_from==WRONG_VALUE) { m_selected_line_from =(int)m_text_cursor_y_pos; m_selected_symbol_from =(int)m_text_cursor_x_pos; } } //+------------------------------------------------------------------+ //| 選択するテキストの終わりのインデックスを設定する | //+------------------------------------------------------------------+ void CTextBox::SetEndSelectedTextIndexes(void) { //--- 選択するテキストの終わりのインデックスを設定する m_selected_line_to =(int)m_text_cursor_y_pos; m_selected_symbol_to =(int)m_text_cursor_x_pos; //---すべてのインデックスが同じ場合は選択範囲をクリアする if(m_selected_line_from==m_selected_line_to && m_selected_symbol_from==m_selected_symbol_to) ResetSelectedText(); } //+------------------------------------------------------------------+ //| 選択されたテキストをリセットする | //+------------------------------------------------------------------+ void CTextBox::ResetSelectedText(void) { m_selected_line_from =WRONG_VALUE; m_selected_line_to =WRONG_VALUE; m_selected_symbol_from =WRONG_VALUE; m_selected_symbol_to =WRONG_VALUE; }
以前にカーソルを動かすメソッドで使用されたコードブロックは、テキスト選択メソッドで繰り返し使用されるため、別々のメソッドとして実装されました。テキストカーソルが可視領域を超えた場合のスクロールバーの調整コードも同様です。
class CTextBox : public CElement { private: //--- テキストカーソルを左に1文字動かす void MoveTextCursorToLeft(void); //--- テキストカーソルを右に1文字動かす void MoveTextCursorToRight(void); //--- テキストカーソルを上に1文字動かす void MoveTextCursorToUp(void); //--- テキストカーソルを下に1文字動かす void MoveTextCursorToDown(void); //--- 水平スクロールバーの調整 void CorrectingHorizontalScrollThumb(void); //--- 垂直スクロールバーの調整 void CorrectingVerticalScrollThumb(void); };
テキストカーソル移動メソッドの呼び出しを除いて、Shiftとのキーの組み合わせの押下の処理にはすべて同じコードが含まれているので、単にテキストカーソルを動かす方向を渡すことができる追加のメソッドを作成するのが理にかなっています。Enums.mqhファイルには、いくつかの識別子を持つENUM_MOVE_TEXT_CURSOR列挙型(下のリストを参照)が追加され、テキストカーソルの移動先を示すために使用できます。
- TO_NEXT_LEFT_SYMBOL — 左へ一文字
- TO_NEXT_RIGHT_SYMBOL — 右へ一文字
- TO_NEXT_LEFT_WORD — 左へ一単語
- TO_NEXT_RIGHT_WORD — 右へ一単語
- TO_NEXT_UP_LINE — 一行上
- TO_NEXT_DOWN_LINE — 一行下
- TO_BEGIN_LINE — 現在の行の最初
- TO_END_LINE — 現在の行の最後
- TO_BEGIN_FIRST_LINE — 一行目の最初
- TO_END_LAST_LINE — 最終行の最後
//+---------------------------------+ //| カーソル移動先の列挙 | //+---------------------------------+ enum ENUM_MOVE_TEXT_CURSOR { TO_NEXT_LEFT_SYMBOL =0, TO_NEXT_RIGHT_SYMBOL =1, TO_NEXT_LEFT_WORD =2, TO_NEXT_RIGHT_WORD =3, TO_NEXT_UP_LINE =4, TO_NEXT_DOWN_LINE =5, TO_BEGIN_LINE =6, TO_END_LINE =7, TO_BEGIN_FIRST_LINE =8, TO_END_LAST_LINE =9 };
ここで、テキストカーソルを動かす一般的なメソッドであるCTextBox::MoveTextCursor()を作成することができます。ここでは、上記の識別子の1つを渡すだけで十分です。CTextBoxコントロールのキー入力イベントを処理するすべてのメソッドで、同じメソッドが使用されます。
class CTextBox : public CElement { private: //--- テキストカーソルの指定された方向への移動 void MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR方向); }; //+----------------------------------------------------+ //| テキストカーソルの指定された方向への移動 | //+----------------------------------------------------+ void CTextBox::MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR方向) { switch(方向) { //--- カーソルを1文字左に動かす case TO_NEXT_LEFT_SYMBOL : MoveTextCursorToLeft(); break; //--- カーソルを1文字右に動かす case TO_NEXT_RIGHT_SYMBOL : MoveTextCursorToRight(); break; //--- カーソルを1単語左に動かす case TO_NEXT_LEFT_WORD : MoveTextCursorToLeft(true); break; //--- カーソルを1単語右に動かす case TO_NEXT_RIGHT_WORD : MoveTextCursorToRight(true); break; //--- カーソルを1行上に動かす case TO_NEXT_UP_LINE : MoveTextCursorToUp(); break; //--- カーソルを1行下に動かす case TO_NEXT_DOWN_LINE : MoveTextCursorToDown(); break; //--- カーソルを現在の行の初めに動かす case TO_BEGIN_LINE : SetTextCursor(0,m_text_cursor_y_pos); break; //--- カーソルを現在の行の終わりに動かす case TO_END_LINE : { //--- 現在の行の文字数を返す uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); //--- カーソルを動かす SetTextCursor(symbols_total,m_text_cursor_y_pos); break; } //--- カーソルを1行目の初めに動かす case TO_BEGIN_FIRST_LINE : SetTextCursor(0,0); break; //--- カーソルを最後の行の終わりに動かす case TO_END_LAST_LINE : { //--- 行の数と最後の行の文字数を取得する uint lines_total =::ArraySize(m_lines); uint symbols_total =::ArraySize(m_lines[lines_total-1].m_symbol); //--- カーソルを動かす SetTextCursor(symbols_total,lines_total-1); break; } } }
テキストカーソル移動のハンドラメソッドとテキスト選択イベントに繰り返しコードブロックが多数存在するため、このファイルのコードは、大幅に削減できます。
下記は、テキストカーソル移動メソッドの繰り返されたコードブロックの例です。
//+-----------------------------------------------+ //| Leftキーの押下の処理 | //+-----------------------------------------------+ bool CTextBox::OnPressedKeyLeft(const long key_code) { //--- (1) Leftキーでない場合、 (2) Ctrlキーが押された場合、または (3) Shiftキーが押された場合は終了する if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || m_keys.KeyShiftState()) return(false); //--- 選択をリセットする ResetSelectedText(); //--- テキストカーソルを左に1文字シフトする MoveTextCursor(TO_NEXT_LEFT_SYMBOL); //--- スクロールバーを調整する CorrectingHorizontalScrollThumb(); CorrectingVerticalScrollThumb(); //--- テキストボックスのテキストを更新する DrawTextAndCursor(true); //--- 関連したメッセージを送信する ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); return(true); }
下記は テキストを選択するメソッドでの繰り返されたコードブロックの例です。
//+----------------------------------------------+ //| Shift + Left キーの押下の処理 | //+----------------------------------------------+ bool CTextBox::OnPressedKeyShiftAndLeft(const long key_code) { //--- (1) Leftキーでない場合、 (2) Ctrlキーが押された場合、または (3) Shiftキーが押されていない場合は終了する if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || !m_keys.KeyShiftState()) return(false); //--- 選択されたテキストのインデックスを設定する SetStartSelectedTextIndexes(); //--- テキストカーソルを左に1文字シフトする MoveTextCursor(TO_NEXT_LEFT_SYMBOL); //--- 選択するテキストの終わりのインデックスを設定する SetEndSelectedTextIndexes(); //--- スクロールバーを調整する CorrectingHorizontalScrollThumb(); CorrectingVerticalScrollThumb(); //--- テキストボックスのテキストを更新する DrawTextAndCursor(true); //--- 関連したメッセージを送信する ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); return(true); }
追加の(オーバーロードされた)CTextBox::MoveTextCursor()メソッドを実装しましょう。このメソッドには、移動方向の識別子と、 (1) テキストカーソルの移動であるか (2)テキストカーソルの移動であるかのフラグを渡す必要があります。
class CTextBox : public CElement { private: //--- テキストカーソルの指定された方向への移動 void MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction,const bool with_highlighted_text); }; //+----------------------------------------------------+ //| 条件付きでテキストカーソルを指定された方向へ | //| 動かす | //+----------------------------------------------------+ void CTextBox::MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction,const bool with_highlighted_text) { //---これが単にテキストカーソル移動の場合 if(!with_highlighted_text) { //--- 選択をリセットする ResetSelectedText(); //--- カーソルを1行目の初めに動かす MoveTextCursor(direction); } //--- テキストの選択が有効な場合 else { //--- 選択されたテキストのインデックスを設定する SetStartSelectedTextIndexes(); //--- テキストカーソルを左に1文字シフトする MoveTextCursor(direction); //--- 選択するテキストの終わりのインデックスを設定する SetEndSelectedTextIndexes(); } //--- スクロールバーを調整する CorrectingHorizontalScrollThumb(); CorrectingVerticalScrollThumb(); //--- テキストボックスのテキストを更新する DrawTextAndCursor(true); //--- 関連したメッセージを送信する ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); }
以下にはテキストを選択するためのキーの組み合わせを処理するメソッドが示されます。これらのコードはほぼ同じなので(パラメータが異なるのみ)本稿に添付されているファイルでコードを勉強することができます。
class CTextBox : public CElement { private: //--- Shift + Left キーの押下の処理 bool OnPressedKeyShiftAndLeft(const long key_code); //--- Shift + Right キーの押下の処理 bool OnPressedKeyShiftAndRight(const long key_code); //--- Shift + Up キーの押下の処理 bool OnPressedKeyShiftAndUp(const long key_code); //--- Shift + Down キーの押下の処理 bool OnPressedKeyShiftAndDown(const long key_code); //--- Shift + Home キーの押下の処理 bool OnPressedKeyShiftAndHome(const long key_code); //--- Shift + End キーの押下の処理 bool OnPressedKeyShiftAndEnd(const long key_code); //--- Ctrl + Shift + Left キーの押下の処理 bool OnPressedKeyCtrlShiftAndLeft(const long key_code); //--- Ctrl + Shift + Right キーの押下の処理 bool OnPressedKeyCtrlShiftAndRight(const long key_code); //--- Ctrl + Shift + Home キーの押下の処理 bool OnPressedKeyCtrlShiftAndHome(const long key_code); //--- Ctrl + Shift + End キーの押下の処理 bool OnPressedKeyCtrlShiftAndEnd(const long key_code); };
これまでのところ、テキストはキャンバス全体に適用されました。しかし、選択された文字とその下の背景が色を変えるので、文字を文字単位で出力する必要があります。これを行うにはCTextBox::TextOut()メソッドを少し変更してみましょう。
また、選択された文字を確認するためには追加のCTextBox::CheckSelectedText()メソッドが必要になります。テキストの選択中に、テキストカーソルの最初と最後の行と文字のインデックスが保存されていることはすでに分かっています。 したがって、ループ内の文字を反復処理することで、行内の文字が選択されているかどうかを簡単に判断できます。そのロジックはシンプルです。
- 文字は、行の最初のインデックスが最後のインデックスよりも小さい場合に選択されています。
- これが最後の行で、文字が最後に選択されたものの右にある場合
- これが最初の行で、文字が最初に選択されたものの左にある場合
- すべての文字が中間の行で選択されている場合
- 文字は、行の最初のインデックスが最後のインデックスよりも大きい場合に選択されています。
- これが最後の行で、文字が最後に選択された文字の左側にある場合
- これが最初の行で、文字が最初に選択されたものの右にある場合
- すべての文字が中間の行で選択されている場合
- テキストが1行のみで選択されている場合、文字は、最初の文字インデックスと最後の文字インデックスの間の指定された範囲内にある場合に選択されます。
class CTextBox : public CElement { private: //--- 選択されたテキストの存在を確認する bool CheckSelectedText(const uint line_index,const uint symbol_index); }; //+------------------------------------------------------------+ //| 選択されたテキストの存在を確認する | //+------------------------------------------------------------+ bool CTextBox::CheckSelectedText(const uint line_index,const uint symbol_index) { bool is_selected_text=false; //--- 選択されたテキストがない場合は終了する if(m_selected_line_from==WRONG_VALUE) return(false); //--- 初めのインデックスが下の行にある場合 if(m_selected_line_from>m_selected_line_to) { //--- 最後の行で、文字で最終的な選択された文字の右 if((int)line_index==m_selected_line_to && (int)symbol_index>=m_selected_symbol_to) { is_selected_text=true; } //--- 最初の行で、文字が最初に選択されたものの左 else if((int)line_index==m_selected_line_from && (int)symbol_index<m_selected_symbol_from) { is_selected_text=true; } //--- 中間の行(すべての文字が選択されている) else if((int)line_index>m_selected_line_to && (int)line_index<m_selected_line_from) { is_selected_text=true; } } //--- 初めのインデックスが上の行にある場合 else if(m_selected_line_from<m_selected_line_to) { //--- 最後の行で、文字が最後に選択された文字の左 if((int)line_index==m_selected_line_to && (int)symbol_index<m_selected_symbol_to) { is_selected_text=true; } //--- 最初の行で、文字が最初に選択された文字の右 else if((int)line_index==m_selected_line_from && (int)symbol_index>=m_selected_symbol_from) { is_selected_text=true; } //--- 中間の行(すべての文字が選択されている) else if((int)line_index<m_selected_line_to && (int)line_index>m_selected_line_from) { is_selected_text=true; } } //--- 最初と最後のインデックスが同じ行にある場合 else { //--- チェックされた行を見つける if((int)line_index>=m_selected_line_to && (int)line_index<=m_selected_line_from) { //--- カーソルが右に動いて、文字が選択範囲内にある場合 if(m_selected_symbol_from>m_selected_symbol_to) { if((int)symbol_index>=m_selected_symbol_to && (int)symbol_index<m_selected_symbol_from) is_selected_text=true; } //--- カーソルが左に動いて、文字が選択範囲内にある場合 else { if((int)symbol_index>=m_selected_symbol_from && (int)symbol_index<m_selected_symbol_to) is_selected_text=true; } } } //--- 結果を返す return(is_selected_text); }
テキストを出力するために設計されたCTextBox::TextOut() メソッドでは、行全体を出力するのではなく、行の文字での繰り返しを含む内部ループを追加する必要があります。このループはチェックされた文字が選択されているかどうかを判断します。文字が選択されている場合、その色が決定され、文字の下に塗りつぶされた矩形が描画されます。その後文字が出力されます。
class CTextBox : public CElement { private: //--- キャンバスにテキストを出力する void TextOut(void); }; //+----------------------------------+ //| キャンバスにテキストを出力する | //+----------------------------------+ void CTextBox::TextOut(void) { //--- キャンバスを消去する m_canvas.Erase(AreaColorCurrent()); //--- 行の配列のサイズを取得する uint lines_total=::ArraySize(m_lines); //--- サイズが超過した場合の修正 m_text_cursor_y_pos=(m_text_cursor_y_pos>=lines_total)?lines_total-1 : m_text_cursor_y_pos; //--- 文字の配列のサイズを取得する uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); //--- マルチラインモードが有効な場合または文字数がゼロより大きい場合 if(m_multi_line_mode || symbols_total>0) { //--- 行幅を取得する int line_width=(int)LineWidth(m_text_cursor_x_pos,m_text_cursor_y_pos); //--- Get the line height and iterate over all lines in a loop int line_height=(int)LineHeight(); for(uint i=0; i<lines_total; i++) { //--- テキストの座標を取得する int x=m_text_x_offset; int y=m_text_y_offset+((int)i*line_height); //--- 文字列のサイズを取得する uint string_length=::ArraySize(m_lines[i].m_symbol); //--- テキストを描画する for(uint s=0; s<string_length; s++) { uint text_color=TextColorCurrent(); //--- 選択されたテキストがある場合は、現在の文字の色と背景色を決定する if(CheckSelectedText(i,s)) { //--- 選択されたテキストの色 text_color=::ColorToARGB(m_selected_text_color); //--- 背景を描画するための座標を計算する int x2=x+m_lines[i].m_width[s]; int y2=y+line_height-1; //--- 文字の背景色を描く m_canvas.FillRectangle(x,y,x2,y2,::ColorToARGB(m_selected_back_color)); } //--- 文字を描く m_canvas.TextOut(x,y,m_lines[i].m_symbol[s],text_color,TA_LEFT); //--- 次の文字のX座標 x+=m_lines[i].m_width[s]; } } } //--- マルチラインモードが無効で文字がない場合は、デフォルトのテキストが表示される else { //--- 指定された場合テキストを描画する if(m_default_text!="") m_canvas.TextOut(m_area_x_size/2,m_area_y_size/2,m_default_text,::ColorToARGB(m_default_text_color),TA_CENTER|TA_VCENTER); } }
テキストを選択する方法が実装されました。これが完成したアプリケーションの外観です。
図6 MQLアプリケーションの実装されたテキストボックスでのテキスト選択のデモ
選択したテキストを削除する方法
次に、選択したテキストを削除するメソッドを考えてみましょう。ここでは、選択したテキストを1行または複数行で選択するかどうかによって、選択したテキストを削除するときに異なるメソッドが適用されることに注意することが重要です。
1行で選択されたテキストを削除するためにはCTextBox::DeleteTextOnOneLine()メソッドが呼び出されます。削除する文字の数は、メソッドの先頭で決定されます。次に、選択されたテキストの文字の最初のインデックスが右にある場合、文字はこの初期位置から削除する文字数だけ右にシフトされます。その後、行の文字の配列は同じ量だけ減少します。
選択されたテキストの文字の最初のインデックスが左にある場合、テキストカーソルも削除する文字数だけ右にシフトする必要があります。
class CTextBox : public CElement { private: //--- 1行で選択されたテキストを削除する void DeleteTextOnOneLine(void); }; //+--------------------------------------------------------------+ //| 1行で選択されたテキストを削除する | //+--------------------------------------------------------------+ void CTextBox::DeleteTextOnOneLine(void) { int symbols_total =::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); int symbols_to_delete =::fabs(m_selected_symbol_from-m_selected_symbol_to); //--- 初めのインデックスが右にある場合 if(m_selected_symbol_to<m_selected_symbol_from) { //--- 現在の行の解放された領域に文字をシフトする MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_from,m_selected_symbol_to); } //--- 初めのインデックスが左にある場合 else { //--- テキストカーソルを削除される文字数だけ左にシフトする m_text_cursor_x_pos-=symbols_to_delete; //--- 現在の行の解放された領域に文字をシフトする MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_to,m_selected_symbol_from); } //--- 現在の行の配列サイズを抽出された文字数で減らす ArraysResize(m_text_cursor_y_pos,symbols_total-symbols_to_delete); }
複数行で選択されたテキストを削除するためにはCTextBox::DeleteTextOnMultipleLines()メソッドが呼び出されます。アルゴリズムはここではもっと複雑です。まず、以下を決定する必要があります。
- 最初と最後の行の合計文字数
- 選択されたテキストの中間行の数(最初と最後の行を除く)
- 最初と最後の行で削除される文字数
さらなるアクションのシーケンスを以下に示します。テキストが選択された方向(上または下)に応じて、初期および最終インデックスは他のメソッドに渡されます。
- ある行から別の行に移動する文字は削除後も残っており、一時的な動的配列にコピーされます。
- 受信側の配列(行)のサイズが変更されます。
- データが受信側の行の構造体の配列に追加されます。
- 行は、削除された行の数だけシフトされます。
- 行の配列のサイズが変更されます(削除された行の数だけ減少します)。
- 最初の行が最終行より上にある場合(テキスト選択が下向き)、テキストカーソルは選択されたテキストの最初のインデックス(行と文字)に移動します。
class CTextBox : public CElement { private: //--- 複数行で選択されたテキストを削除する void DeleteTextOnMultipleLines(void); }; //+----------------------------------------------------------+ //| 複数行で選択されたテキストを削除する | //+----------------------------------------------------------+ void CTextBox::DeleteTextOnMultipleLines(void) { //--- 最初と最後の行の合計文字数 uint symbols_total_line_from =::ArraySize(m_lines[m_selected_line_from].m_symbol); uint symbols_total_line_to =::ArraySize(m_lines[m_selected_line_to].m_symbol); //--- 削除される中間行の数 uint lines_to_delete =::fabs(m_selected_line_from-m_selected_line_to); //--- 最初と最後の行で削除される文字数 uint symbols_to_delete_in_line_from =::fabs(symbols_total_line_from-m_selected_symbol_from); uint symbols_to_delete_in_line_to =::fabs(symbols_total_line_to-m_selected_symbol_to); //--- 最初の行が最後の行より下の場合 if(m_selected_line_from>m_selected_line_to) { //--- 移動する文字を配列にコピーする string array[]; CopyWrapSymbols(m_selected_line_from,m_selected_symbol_from,symbols_to_delete_in_line_from,array); //--- 受信側の行のサイズを変更する uint new_size=m_selected_symbol_to+symbols_to_delete_in_line_from; ArraysResize(m_selected_line_to,new_size); //--- データを受信側の行の構造体の配列に追加する PasteWrapSymbols(m_selected_line_to,m_selected_symbol_to,array); //--- 行の配列のサイズを取得する uint lines_total=::ArraySize(m_lines); //--- 行を削除する行数で上にシフトする MoveLines(m_selected_line_to+1,lines_total-lines_to_delete,lines_to_delete,false); //--- 行の配列のサイズを変更する ::ArrayResize(m_lines,lines_total-lines_to_delete); } //--- 最初の行が最後の行より上の場合 else { //--- 移動する文字を配列にコピーする string array[]; CopyWrapSymbols(m_selected_line_to,m_selected_symbol_to,symbols_to_delete_in_line_to,array); //--- 受信側の行のサイズを変更する uint new_size=m_selected_symbol_from+symbols_to_delete_in_line_to; ArraysResize(m_selected_line_from,new_size); //--- データを受信側の行の構造体の配列に追加する PasteWrapSymbols(m_selected_line_from,m_selected_symbol_from,array); //--- 行の配列のサイズを取得する uint lines_total=::ArraySize(m_lines); //--- 行を削除する行数で上にシフトする MoveLines(m_selected_line_from+1,lines_total-lines_to_delete,lines_to_delete,false); //--- 行の配列のサイズを変更する ::ArrayResize(m_lines,lines_total-lines_to_delete); //--- カーソルを選択範囲の最初の位置に動かす SetTextCursor(m_selected_symbol_from,m_selected_line_from); } }
上記のどのメソッドを呼び出すかは、テキストを削除する主なメソッドであるCTextBox::DeleteSelectedText()で決定されます。選択されたテキストが削除されると、最初のインデックスと最後のインデックスの値がリセットされます。その後、行数が変更されている可能性があるので、テキストボックスの寸法を再計算する必要があります。また、行の最大幅が変更されている可能性があり、これは、テキストボックスの計算に使用されます。最後に、メソッドはテキストカーソルが移動したというメッセージを送信します。このメソッドは、テキストが選択され削除された場合はtrueを返し、メソッドが呼び出された時点で選択されたテキストがないと判明した場合はfalseを返します。
class CTextBox : public CElement { private: //--- 選択されたテキストを削除する void DeleteSelectedText(void); }; //+-----------------------------------------------------------------+ //| 選択されたテキストを削除する | //+-----------------------------------------------------------------+ bool CTextBox::DeleteSelectedText(void) { //--- テキストが選択されていない場合は終了する if(m_selected_line_from==WRONG_VALUE) return(false); //--- 文字が1行から削除された場合 if(m_selected_line_from==m_selected_line_to) DeleteTextOnOneLine(); //--- 文字が複数行から削除された場合 else DeleteTextOnMultipleLines(); //--- 選択されたテキストをリセットする ResetSelectedText(); //--- テキストボックスのサイズを計算する CalculateTextBoxSize(); //--- テキストボックスの新サイズを設定する ChangeTextBoxSize(); //--- スクロールバーを調整する CorrectingHorizontalScrollThumb(); CorrectingVerticalScrollThumb(); //--- テキストボックスのテキストを更新する DrawTextAndCursor(true); //--- 関連したメッセージを送信する ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); return(true); }
CTextBox::DeleteSelectedText()メソッドはBackspaceキーが押下されたときだけではなく、 (1) 新しい文字を入力するときと (2) Enterキーが押下されたときにも呼ばれます。このような場合、最初にテキストが削除され、次に押されたキーに対応するアクションが実行されます。
完成したアプリケーションは次のようになります。
図7 選択したテキストを削除するデモンストレーション
画像データを扱うクラス
本稿の補足として、画像データを扱うための新しいクラス (CImage) を考えてみましょう。このクラスは、ライブラリの画像を描画する必要があるコントロールの多くのクラスで繰り返し使用され、Objects.mqhファイルに含まれています。
下記はクラスプロパティです。- 画像画素の配列
- 画像幅
- 画像の高さ
- 画像ファイルへのパス
//+---------------------------------------------------+ //| 画像データ格納クラス | //+---------------------------------------------------+ class CImage { protected: uint m_image_data[]; // 画像画素の配列 uint m_image_width; // 画像幅 uint m_image_height; // 画像の高さ string m_bmp_path; // 画像ファイルへのパス public: //--- (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::CImage(void) : m_image_width(0), m_image_height(0), m_bmp_path("") { } //+------------------------------------------------------------------+ //| デストラクタ | //+------------------------------------------------------------------+ CImage::~CImage(void) { }
CImage::ReadImageData()メソッドは、画像とそのプロパティを保存するためのものです。このメソッドは、指定されたパスの画像を読み込んでそのデータを格納します。
class CImage { public: //--- 渡された画像のデータを読み込んで保存する bool ReadImageData(const string bmp_file_path); }; //+-------------------------------------------------------+ //| 渡された画像を配列に保存する | //+-------------------------------------------------------+ bool CImage::ReadImageData(const string bmp_file_path) { //--- 最後のエラーをリセットする ::ResetLastError(); //--- 画像へのパスを格納する m_bmp_file_path=bmp_file_path; //--- 画像データを読み込んで格納する if(!::ResourceReadImage(m_bmp_file_path,m_image_data,m_image_width,m_image_height)) { ::Print(__FUNCTION__," > error: ",::GetLastError()); return(false); } //--- return(true); }
場合によっては、同じ種類(CImage)の画像のコピーを作る必要があるかもしれません。このために CImage::CopyImageData() メソッドが実装されています。このメソッドの開始時には、受信側の配列のサイズは送信側の配列のサイズに設定されます。次に、ループでデータがソース配列から受信側の配列にコピーされます。
class CImage { public: //--- 渡された画像のデータをコピーする void CopyImageData(CImage &array_source); }; //+-----------------------------------------------------------+ //| 渡された画像のデータをコピーする | //+-----------------------------------------------------------+ void CImage::CopyImageData(CImage &array_source) { //--- 受信側配列と送信側配列のサイズを取得する uint data_total =DataTotal(); uint source_data_total =::GetPointer(array_source).DataTotal(); //--- 受信側の行のサイズを変更する ::ArrayResize(m_image_data,source_data_total); //--- データをコピーする for(uint i=0; i<source_data_total; i++) m_image_data[i]=::GetPointer(array_source).Data(i); }
CCanvasTable クラスは今回の更新までは画像データを格納する構造体を使用していましたが、これはCImageクラスの導入によって変わりました。
おわりに
マルチラインテキストボックスの開発は本稿で終了します。その主な特徴は、入力する文字数に制限がなく複数の行を入力できるということです。これはOBJ_EDIT型の標準的なグラフィックオブジェクトに欠けていて所望されていたものです。次回の記事では、「テーブルセルのコントロール」のトピックの開発を続け、その記事で説明したコントロールを使用してテーブルセルの値を変更する機能を追加します。さらに、コントロールのいくつかは新しいモードに切り替わります。複数のコントロールが複数の標準グラフィックオブジェクトから構築されずにレンダーされることになります。
このグラフィカルインタフェース作成ライブラリの概略は現在以下の通りに見えます。
図8 開発の現段階でのライブラリの構造
以下は、本稿でデモされている、テスト用のライブラリとファイルの最新バージョンです。
これらのファイルに含まれている資料の使用についてご質問がある場合は、記事のいずれかでライブラリの開発の詳細をご参照になるか、本稿へのコメント欄でご質問ください。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/3197





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索