グラフィカルインタフェースX: マルチラインテキストボックス(ビルド8)

Anatoli Kazharski | 21 3月, 2017


コンテンツ

 

はじめに

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

この記事では、新しい、マルチラインテキストボックスについて検討します。ターミナルが備えたOBJ_EDIT型のグラフィカルオブジェクトとは異なり、ここで説明されるバージョンには入力文字数の制限がありません。テキストボックスをシンプルなテキストエディタに切り替えることもできます。つまり、複数の行の入力が可能で、テキストカーソルはマウスとキーの両方で移動できます。行がコントロールの可視領域をオーバーフローすると、スクロールバーが表示されます。マルチラインテキストボックスは完全にレンダリングされ、その品質はオペレーティングシステムの似たようなのコントロールに可能な限り近いものです。


キーグループとキーボードレイアウト

CTextBox(テキストボックス)型のコントロールのコードを説明する前に、データ入力の手段となるキーボードを簡単に説明する必要があります。また、コントロールクラスの最初のバージョンでどちらのキーが処理されることかを示します。 

キーボードのキーはいくつかのグループに分けられます(図1の表記を参照)。

  • コントロールキー(橙色)
  • ファンクションキー(紫)
  • 英数字キー(青)
  • ナビゲーションキー(緑)
  • 数字キーパッド(赤)

 図1 キーグループ(QWERTYキーボードレイアウト)

図1 キーグループ(QWERTYキーボードレイアウト)


英語のためには複数のラテン語キーボードレイアウトがあります。最も一般的なのはQWERTYです。私たちの場合、主な言語はロシア語なので、ロシア語のレイアウトであるЙЦУКЕНを使います。QWERTYは英語のために残され、追加のものとして選択されます。 

MQL言語にはビルド1510から::TranslateKey()関数が含まれています。これは、渡された押されたキーのコードから文字を取得するために使用でき、オペレーティングシステムで設定された言語とレイアウトに対応します。これまでは、各言語の文字配列を手動で生成する必要があり、そのために大量の作業が必要でした。今ではすべてがはるかに簡単です。


キー押下イベントの処理

キーの押下イベントは::OnChartEvent()システム関数でCHARTEVENT_KEYDOWN識別子を使って追跡できます。

//+------------------------------------------------------------------+
//| ChartEvent関数                                                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- キーの押下 
   if(id==CHARTEVENT_KEYDOWN)
     {
      ::Print("id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam,"; symbol: ",::ShortToString(::TranslateKey((int)lparam)));
      return;
     }
  }

次の値は、他の3つのパラメータとして関数に渡されます。

  • long parameter (lparam) – 押下されたキーのコ―ド、つまり文字やコントロールキーの ASCIIコ―ド。 
  • dparam parameter (dparam) – キーが押された状態で保持されている間に生成されたキーの押下の数。この値は常に1に等しいです。キーが押された瞬間から呼び出し回数を取得する必要がある場合は、計算は独立して行われます。
  • sparam parameter (sparam) – ビットマスクの文字列値で、キーボードのキーの状態を記述します。このイベントは、キーを押すとすぐに生成されます。キーが押され、長押しされずにすぐに放されると、スキャンコードの値はここで受信されます。キーが長押しされた場合、値はスキャンコード+ 16384 ビットに基づいて生成されます。

たとえば、以下のリスト(端末ログに出力)は、Escキーを押したままの結果が示します。このキーのコードは27>(lparam)で押された時のスキャンコードは1(sparam)で、約500ミリ秒長押しされると、ターミナルは16385(スキャンコード+ 16384 ビット)の値を生成し始めます。

2017.01.20 17:53:33.240 id: 0; lparam: 27; dparam: 1.0; sparam: 1
2017.01.20 17:53:33.739 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.772 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.805 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.837 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.870 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
...

全てのキーがCHARTEVENT_KEYDOWN識別子のイベントを生成するわけではありません。それらのうちのいくつかはターミナルのニーズに割り当てられ、他のいくつかは単にキー押下イベントを生成しません。これらは下の図で青で強調表示されています。

図2 ターミナルに使用されていてCHARTEVENT_KEYDOWNイベントを生成しないキー 

図2 ターミナルに使用されていてCHARTEVENT_KEYDOWNイベントを生成しないキー


文字とコントロールキーのASCIIコード

ウィキペディアの情報( さらに見る): 

ASCIIはAmerican Standard Code for Information Interchange(情報交換のためのアメリカンスタンダードコード)の略記で、文字エンコーディング標準です。ASCIIコードは、コンピュータ、通信機器、およびその他のデバイスのテキストを表します。標準の最初の版は1963年に出版されました。

下の図は、キーボードキーのASCIIコードを示しています。

 図3 文字とコントロールキーのASCIIコード

図3 キーのASCIIコード


すべてのASCIIコードはマクロ置換(#define )形式でKeyCodes.mqhに格納されています。下のリストは、これらのコードの一部を示しています。

//+------------------------------------------------------------------+
//|                                                     KeyCodes.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| キー押下イベント(イベントのlongパラメータ)の処理のための              |
//| ASCII文字とコントロールキーのコード                                  |
//+------------------------------------------------------------------+
#define KEY_BACKSPACE          8
#define KEY_TAB                9
#define KEY_NUMPAD_5           12
#define KEY_ENTER              13
#define KEY_SHIFT              16
#define KEY_CTRL               17
#define KEY_BREAK              19
#define KEY_CAPS_LOCK          20
#define KEY_ESC                27
#define KEY_SPACE              32
#define KEY_PAGE_UP            33
#define KEY_PAGE_DOWN          34
#define KEY_END                35
#define KEY_HOME               36
#define KEY_LEFT               37
#define KEY_UP                 38
#define KEY_RIGHT              39
#define KEY_DOWN               40
#define KEY_INSERT             45
#define KEY_DELETE             46
...

 


キースキャンコード

ウィキペディアの情報( さらに見る):  

スキャンコードとは、ほとんどのコンピュータキーボードがコンピュータに送信するどのキーが押されたかを報告するためのデータです。キーボードの各キーには数字または一連の数字が割り当てられています。

下の図はキースキャンコードを示しています。

図4 キースキャンコード 

図4 キースキャンコード


ASCIIコードと同様に、スキャンコードはKeyCodes.mqhファイルに含まれています。下のリストはリストの一部を示します。

//+------------------------------------------------------------------+
//|                                                     KeyCodes.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
...
//--- ビット
#define KEYSTATE_ON            16384
//+------------------------------------------------------------------+
//| キースキャンコード(イベントの文字列パラメータ)                       |
//+------------------------------------------------------------------+
//| 1回押下:KEYSTATE_XXX                                             |
//| 長押し:KEYSTATE_XXX + KEYSTATE_ON                                |
//+------------------------------------------------------------------+
#define KEYSTATE_ESC           1
#define KEYSTATE_1             2
#define KEYSTATE_2             3
#define KEYSTATE_3             4
#define KEYSTATE_4             5
#define KEYSTATE_5             6
#define KEYSTATE_6             7
#define KEYSTATE_7             8
#define KEYSTATE_8             9
#define KEYSTATE_9             10
#define KEYSTATE_0             11
//---
#define KEYSTATE_MINUS         12
#define KEYSTATE_EQUALS        13
#define KEYSTATE_BACKSPACE     14
#define KEYSTATE_TAB           15
//---
#define KEYSTATE_Q             16
#define KEYSTATE_W             17
#define KEYSTATE_E             18
#define KEYSTATE_R             19
#define KEYSTATE_T             20
#define KEYSTATE_Y             21
#define KEYSTATE_U             22
#define KEYSTATE_I             23
#define KEYSTATE_O             24
#define KEYSTATE_P             25
...

 


キーボード操作の補助クラス

キーボードの操作をより便利にするためにCKeysクラスが実装されています。これは Keys.mqhクラスに含まれており、すべてのキーと文字コードを含む KeyCodes.mqh ファイルを含みます。 

//+------------------------------------------------------------------+
//|                                                         Keys.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#include <EasyAndFastGUI\KeyCodes.mqh>
//+------------------------------------------------------------------+
//| キーボード操作のためのクラス                                         |
//+------------------------------------------------------------------+
class CKeys
  {
public:
                     CKeys(void);
                    ~CKeys(void);
  };
//+------------------------------------------------------------------+
//| コンストラクタ                                                     |
//+------------------------------------------------------------------+
CKeys::CKeys(void)
  {
  }
//+------------------------------------------------------------------+
//| デストラクタ                                                       |
//+------------------------------------------------------------------+
CKeys::~CKeys(void)
  {
  }

下記のうちどちらのキー押下かを決定するには

(1) 英数字(スペースを含む)

(2) 数字パッド文字

または (3) 特殊文字

CKeys::KeySymbol() メソッドを使用します。 CHARTEVENT_KEYDOWN識別子を持つイベントのlongパラメータの値が渡されると、文字列形式の文字が返されます。 押されたキーが指定された範囲に属していない場合は空の文字列( '')が返されます。 

class CKeys
  {
public:
   //--- 押されたキーの文字を返す
   string            KeySymbol(const long key_code);
  };
//+------------------------------------------------------------------+
//| 押されたキーの文字を返す                                            |
//+------------------------------------------------------------------+
string CKeys::KeySymbol(const long key_code)
  {
   string key_symbol="";
//--- スペースの入力が必要な場合(Spaceキー)
   if(key_code==KEY_SPACE)
     {
      key_symbol=" ";
     }
//---(1)アルファベット文字、(2)数字パッド文字、または(3)特殊文字の入力が必要がな場合
   else if((key_code>=KEY_A && key_code<=KEY_Z) ||
           (key_code>=KEY_0 && key_code<=KEY_9) ||
           (key_code>=KEY_SEMICOLON && key_code<=KEY_SINGLE_QUOTE))
     {
      key_symbol=::ShortToString(::TranslateKey((int)key_code));
     }
//--- 文字を返す
   return(key_symbol);
  }

最後に、Ctrlキーの現在の状態を判断するメソッドが必要になります。これは、テキストボックス内のテキストカーソルを移動するときに2つのキーを同時に押すさまざまな組み合わせで使用されます。

Ctrlキーの現在の状態を取得するには、ターミナルのシステム関数 ::TerminalInfoInteger()を使用します。この関数には、キーの現在の状態を検出するための複数の識別子があります。CtrキーにはTERMINAL_KEYSTATE_CONTROL識別子が使われます。このタイプの他のすべての識別子は、MQL5言語リファレンスにあります。

識別子を使用してキーが押されたかどうかを判断するのは非常に簡単です。キーが押された場合、戻り値はゼロ未満になります。 

class CKeys
  {
public:
   //--- Ctrlキーの状態を返す
   bool              KeyCtrlState(void);
  };
//+------------------------------------------------------------------+
//| Ctrlキーの状態を返す                                               |
//+------------------------------------------------------------------+
bool CKeys::KeyCtrlState(void)
  {
   return(::TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<0);
  }

これで、テキストボックスを作成する準備が整いました。 

 


マルチラインテキストボックス

マルチラインテキストボックスは、複合コントロールでも使用できます。マルチラインテキストボックスはスクロールバーを含むので、複合コントロールのグループに属します。さらに、マルチラインテキストボックスは、テキストの入力および以前に保存したテキストをファイルから表示するのに使用できます。

数値を入力するためのエディットボックス(CSpinEditクラス)やカスタムテキスト(CTextEdit)は、すでに検討されました。これらにはOBJ_EDIT型のグラフィックオブジェクトが使われました。これには、入力できるのは63で入力が1行に収まらなければならないという重大な制限を伴います。したがって、現在の課題は、そのような制限を持たないテキストエディットボックスを作成することです。  


 

図5 マルチラインテキストボックス

このコントロールを作成するためのCTextBoxクラスを詳しく見ていきましょう。

 

コントロール作成のためのCTextBoxクラスの開発

TextBox.mqhファイルを作成してライブラリのすべてのコントロールの標準メソッドを含むCTextBoxクラスを加え、 下記のファイルをインクルードします

  • コントロールの基本クラス — Element.mqh
  • スクロールバークラス — Scrolls.mqh
  • キーボード操作のためのクラス — Keys.mqh
  • タイムカウンタ操作のためのクラス — TimeCounter.mqh
  • MQLが位置するチャート操作のためのクラス — Chart.mqh 
//+------------------------------------------------------------------+
//|                                                      TextBox.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#include "Scrolls.mqh"
#include "..\Keys.mqh"
#include "..\Element.mqh"
#include "..\TimeCounter.mqh"
#include <Charts\Chart.mqh>
//+------------------------------------------------------------------+
//| マルチラインテキストボックス作成クラス                                |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- キーボード操作クラスのインスタンス
   CKeys             m_keys;
   //--- チャートを管理するクラスのインスタンス
   CChart            m_chart;
   //--- タイムカウンタ操作クラスのインスタンス
   CTimeCounter      m_counter;
   //---
public:
                     CTextBox(void);
                    ~CTextBox(void);
   //--- チャートイベントハンドラ
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- タイマー
   virtual void      OnEventTimer(void);
   //--- 要素の移動
   virtual void      Moving(const int x,const int y,const bool moving_mode=false);
   //--- (1)表示 (2)非表示 (3)リセット (4)削除
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- マウスの左クリックの優先順位の(1)設定と(2)リセット
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
   //--- 色をゼロ化する
   virtual void      ResetColors(void) {}
   //---
private:
   //| ウィンドウの右端の幅を変更する              |
   virtual void      ChangeWidthByRightWindowSide(void);
   //--- ウィンドウの下端で高さを変更する
   virtual void      ChangeHeightByBottomWindowSide(void);
  };



プロパティと外観

文字の配列とそのプロパティを持つ構造体が必要なのでKeySymbolOptionsと名付けます。現在のバージョンでは、2つの動的配列が含まれます。 

  • m_symbol[] は文字列のすべての文字を別々に含みます。
  • m_width[] 配列は文字列のすべての文字の幅を別々に含みます。

このクラスのインスタンスもまた動的配列として宣言されます。そのサイズは常にテキストボックス内の行数と等しくなります。

class CTextBox : public CElement
  {
private:
   //--- 文字とそのプロパティ
   struct KeySymbolOptions
     {
      string            m_symbol[]; // 文字
      int               m_width[];  // 文字幅
     };
   KeySymbolOptions  m_lines[];
  };

この最初のバージョンでは、テキストは行全体として出力されます。したがって、行が出力される前にm_symbol[]配列からまとめられる必要があります。この目的にはCTextBox::CollectString() メソッドが役立ちます。それに行インデックスを渡す必要があります。

class CTextBox : public CElement
  {
private:
   //--- 文字列操作のための変数
   string            m_temp_input_string;
   //---
private:
   //--- 文字から文字列を作る
   string            CollectString(const uint line_index);
  };
//+------------------------------------------------------------------+
//| 文字から文字列を作る                                                |
//+------------------------------------------------------------------+
string CTextBox::CollectString(const uint line_index)
  {
   m_temp_input_string="";
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
   for(uint i=0; i<symbols_total; i++)
      ::StringAdd(m_temp_input_string,m_lines[line_index].m_symbol[i]);
//---
   return(m_temp_input_string);
  }

次に、テキストエディットボックスのプロパティを列挙します。これは、このコントロールの外観、状態、動作モードをカスタマイズするために使用できます。

  • 異なる状態での背景色
  • 異なる状態でのテキストの色
  • 異なる状態でのフレームの色
  • デフォルトテキスト
  • デフォルトテキスト色
  • マルチラインモード
  • 読み込み専用モード
class CTextBox : public CElement
  {
private:
   //--- 背景色
   color             m_area_color;
   color             m_area_color_locked;
   //--- テキストの色
   color             m_text_color;
   color             m_text_color_locked;
   //--- フレームの色
   color             m_border_color;
   color             m_border_color_hover;
   color             m_border_color_locked;
   color             m_border_color_activated;
   //--- デフォルトテキスト
   string            m_default_text;
   //--- デフォルトテキスト色
   color             m_default_text_color;
   //--- マルチラインモード
   bool              m_multi_line_mode;
   //--- 読み込み専用モード
   bool              m_read_only_mode;
   //---
public:
   //--- 異なる状態での背景色
   void              AreaColor(const color clr)                { m_area_color=clr;                 }
   void              AreaColorLocked(const color clr)          { m_area_color_locked=clr;          }
   //--- 異なる状態でのテキストの色
   void              TextColor(const color clr)                { m_text_color=clr;                 }
   void              TextColorLocked(const color clr)          { m_text_color_locked=clr;          }
   //--- 異なる状態でのフレームの色
   void              BorderColor(const color clr)              { m_border_color=clr;               }
   void              BorderColorHover(const color clr)         { m_border_color_hover=clr;         }
   void              BorderColorLocked(const color clr)        { m_border_color_locked=clr;        }
   void              BorderColorActivated(const color clr)     { m_border_color_activated=clr;     }
   //--- (1) デフォルトテキスト (2) デフォルトテキスト色
   void              DefaultText(const string text)            { m_default_text=text;              }
   void              DefaultTextColor(const color clr)         { m_default_text_color=clr;         }
   //--- (1) マルチラインモード (2) 読み込み専用モード
   void              MultiLineMode(const bool mode)            { m_multi_line_mode=mode;           }
   bool              ReadOnlyMode(void)                  const { return(m_read_only_mode);         }
   void              ReadOnlyMode(const bool mode)             { m_read_only_mode=mode;            }
  };

テキストボックス自体(背景、テキスト、フレーム、および点滅するテキストカーソル)はOBJ_BITMAP_LABEL型の単一のグラフィックオブジェクトに完全に描画されます。本質的には、これは単なる絵です。再描画は2つのケースでなされます。

  • コントロールとやり取りするとき
  • テキストボックスがアクティブになったときにカーソルが点滅するとき指定された時間間隔で 

マウスカーソルがテキストボックス領域を移動すると、そのフレームの色が変わります。画像をあまり頻繁に再描画しないようにするには、カーソルがテキストボックスフレームを横切る瞬間を追跡する必要があります。これによって、コントロールはテキストボックス領域にカーソルが出入りする瞬間に一度だけ再描画されます。これらの目的のために、コントロールの基本クラスに CElementBase::IsMouseFocus() メソッドが追加されました。これらは、交差点を示すフラグを設定/取得するために使用されます。

//+------------------------------------------------------------------+
//| 基本コントロールクラス                                              |
//+------------------------------------------------------------------+
class CElementBase
  {
protected:
   //--- マウスカーソルがコントロールの境界線を横切る瞬間を判断する
   bool              m_is_mouse_focus;
   //---
public:
   //--- コントロールのフォーカスから出入りする瞬間
   bool              IsMouseFocus(void)                        const { return(m_is_mouse_focus);             }
   void              IsMouseFocus(const bool focus)                  { m_is_mouse_focus=focus;               }
  };

コードを簡潔で読みやすくするために追加の単純なメソッドが実装されています。これらはコントロールの現在の状態に相対したテキストボックスの背景、フレーム、テキストの色を取得するのに役立ちます。 

class CTextBox : public CElement
  {
private:
   //--- 現在の背景色を返す
   uint              AreaColorCurrent(void);
   //--- 現在のテキストの色を返す
   uint              TextColorCurrent(void);
   //--- 現在のフレームの色を返す
   uint              BorderColorCurrent(void);
  };
//+------------------------------------------------------------------+
//| 現在のコントロールの状態に相対した背景色を返す                         |
//+------------------------------------------------------------------+
uint CTextBox::AreaColorCurrent(void)
  {
   uint clr=::ColorToARGB((m_text_box_state)?m_area_color : m_area_color_locked);
//--- 色を返す
   return(clr);
  }
//+------------------------------------------------------------------+
//|現在のコントロールの状態に相対したテキストの色を返す                     |
//+------------------------------------------------------------------+
uint CTextBox::TextColorCurrent(void)
  {
   uint clr=::ColorToARGB((m_text_box_state)?m_text_color : m_text_color_locked);
//--- 色を返す
   return(clr);
  }
//+------------------------------------------------------------------+
//| 現在のコントロールの状態に相対したフレームの色を返す                    |
//+------------------------------------------------------------------+
uint CTextBox::BorderColorCurrent(void)
  {
   uint clr=clrBlack;
//--- 要素がブロックされていない場合
   if(m_text_box_state)
     {
      //--- テキストボックスがアクティブにされている場合
      if(m_text_edit_state)
         clr=m_border_color_activated;
      //--- アクティブ出ない場合はコントロールのフォーカスを確認する
      else
         clr=(CElementBase::IsMouseFocus())?m_border_color_hover : m_border_color;
     }
//--- コントロールがブロックされている場合
   else
      clr=m_border_color_locked;
//--- 色を返す
   return(::ColorToARGB(clr));
  }

このクラスの多くのメソッドでは、テキストボックスの行の高さの値を、指定されたフォントとそのサイズに関連するピクセル単位で取得する必要があります。これらの目的にはCTextBox::LineHeight()メソッドを使用します。

class CTextBox : public CElement
  {
private:
   //--- 行の高さを返す
   uint              LineHeight(void);
  };
//+------------------------------------------------------------------+
//| 行の高さを返す                                                     |
//+------------------------------------------------------------------+
uint CTextBox::LineHeight(void)
  {
//--- キャンバスに表示されるフォントを設定する(行の高さを取得するために必要)
   m_canvas.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//--- 行の高さを返す
   return(m_canvas.TextHeight("|"));
  }

次に、コントロールを描画するメソッドについて考えてみます。テキストボックスの境界線を描画するために設計されたCTextBox::DrawBorder()メソッドから始めます。テキストボックスの合計サイズが可視部分より大きい場合は、表示領域をオフセットすることができます(スクロールバーまたはカーソルを使用)。したがってフレームはこれらのオフセットを考慮して描画される必要があります。 

class CTextBox : public CElement
  {
private:
   //--- フレームを描画する
   void              DrawBorder(void);
  };
//+------------------------------------------------------------------+
//| テキストボックスのフレームを描画する                                  |
//+------------------------------------------------------------------+
void CTextBox::DrawBorder(void)
  {
//--- コントロールの現在の状態に相対したフレームの色を取得する
   uint clr=BorderColorCurrent();
//--- X軸に沿ったオフセットを取得する
   int xo=(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
   int yo=(int)m_canvas.GetInteger(OBJPROP_YOFFSET);
//--- 境界
   int x_size =m_canvas.X_Size()-1;
   int y_size =m_canvas.Y_Size()-1;
//--- 座標:上・右・下・左
   int x1[4]; x1[0]=x;         x1[1]=x_size+xo; x1[2]=xo;        x1[3]=x;
   int y1[4]; y1[0]=y;         y1[1]=y;         y1[2]=y_size+yo; y1[3]=y;
   int x2[4]; x2[0]=x_size+xo; x2[1]=x_size+xo; x2[2]=x_size+xo; x2[3]=x;
   int y2[4]; y2[0]=y;         y2[1]=y_size+yo; y2[2]=y_size+yo; y2[3]=y_size+yo;
//--- 指定された座標でフレームを描画する
   for(int i=0; i<4; i++)
      m_canvas.Line(x1[i],y1[i],x2[i],y2[i],clr);
  }

CTextBox::DrawBorder() メソッドはまたCTextBox::ChangeObjectsColor()メソッドでも使用されます。マウスカーソルでテキストボックスの枠の色を変更するだけです(下のコードを参照)。これを行うには、単に(テキストボックス全体ではなく)フレームを再描画して、画像を更新します。CTextBox::ChangeObjectsColor() はコントロールのイベントハンドラで呼び出されます。あまりにも頻繁な再描画を避けるために、マウスカーソルがコントロールの境界を横切るのを追跡するのはここです。

class CTextBox : public CElement
  {
private:
   //--- オブジェクトの色の変更
   void              ChangeObjectsColor(void);
  };
//+------------------------------------------------------------------+
//| オブジェクトの色の変更                                              |
//+------------------------------------------------------------------+
void CTextBox::ChangeObjectsColor(void)
  {
//--- フォーカスされていない場合
   if(!CElementBase::MouseFocus())
     {
      //--- フォーカスされていないことがまだ示されていない場合
      if(CElementBase::IsMouseFocus())
        {
         //--- フラグを設定する
         CElementBase::IsMouseFocus(false);
         //--- 色を変更する
         DrawBorder();
         m_canvas.Update();
        }
     }
   else
     {
      //--- フォーカスされていることがまだ示されていない場合
      if(!CElementBase::IsMouseFocus())
        {
         //--- フラグを設定する
         CElementBase::IsMouseFocus(true);
         //--- 色を変更する
         DrawBorder();
         m_canvas.Update();
        }
     }
  }

CTextBox::TextOut()メソッドはキャンバスにテキストを出力するために設計されています。ここでは、最初に、キャンバスを指定された色で塗りつぶしてクリアします。次に、プログラムには2方向に迎えます。

  • マルチラインモードが無効で行内に文字がない場合は、デフォルトのテキストが表示されます(指定されている場合)。標示はエディットボックスの中央です。
  • マルチラインモードが無効になっている場合、または行に少なくとも1文字が含まれている場合は、行の高さを取得して、すべての行を文字配列から作成してからループで表示します。テキストボックス領域の左上隅からのテキストオフセットは、デフォルトで定義されています。X軸沿いでは5ピクセル、Y軸沿いでは4 ピクセルです。これらの値はCTextBox::TextXOffset()及びCTextBox::TextYOffset()メソッドを使用してオーバーライドできます。 
class CTextBox : public CElement
  {
private:
   //--- テキストボックスのエッジからのテキストオフセット
   int               m_text_x_offset;
   int               m_text_y_offset;
   //---
public:
   //--- テキストボックスのエッジからのテキストオフセット
   void              TextXOffset(const int x_offset)           { m_text_x_offset=x_offset;         }
   void              TextYOffset(const int y_offset)           { m_text_y_offset=y_offset;         }
   //---
private:
   //--- キャンバスにテキストを出力する
   void              TextOut(void);
  };
//+------------------------------------------------------------------+
//| コンストラクタ                                                     |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_text_x_offset(5),
                           m_text_y_offset(4)
  {
...
  }
//+------------------------------------------------------------------+
//| キャンバスにテキストを出力する                                       |
//+------------------------------------------------------------------+
void CTextBox::TextOut(void)
  {
//--- キャンバスをクリアする
   m_canvas.Erase(AreaColorCurrent());
//--- 文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- マルチラインモードが有効な場合または文字数がゼロより大きい場合
   if(m_multi_line_mode || symbols_total>0)
     {
      //--- 行の高さを取得する
      int line_height=(int)LineHeight();
      //--- 行の配列のサイズを取得する
      uint lines_total=::ArraySize(m_lines);
      //---
      for(uint i=0; i<lines_total; i++)
        {
         //--- テキストの座標を取得する
         int x=m_text_x_offset;
         int y=m_text_y_offset+((int)i*line_height);
         //--- 文字の配列から文字列を作成する
         CollectString(i);
         //--- テキストを描画する
         m_canvas.TextOut(x,y,m_temp_input_string,TextColorCurrent(),TA_LEFT);
        }
     }
//--- マルチラインモードが無効で文字がない場合は、デフォルトのテキストが表示される
   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);
     }
  }

テキストカーソルを描画するには、座標の計算メソッドが必要です。X座標を計算するには、行のインデックスとカーソルが置かれる文字のインデックスを指定する必要があります。これはCTextBox::LineWidth()メソッドを使用して行います。各文字の幅は KeySymbolOptions 構造体の m_width[] 動的配列に格納されているため、することは指定された位置までの文字の幅を合計するだけです。 

class CTextBox : public CElement
  {
private:
   //--- ピクセル単位での行の幅を返す
   uint              LineWidth(const uint line_index,const uint symbol_index);
  };
//+------------------------------------------------------------------+
//| 開始位置から指定位置までの行幅を返す                                  |
//+------------------------------------------------------------------+
uint CTextBox::LineWidth(const uint line_index,const uint symbol_index)
  {
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
//--- 配列サイズ超過の防止
   uint l=(line_index<lines_total)?line_index : lines_total-1;
//--- 指定された行の文字配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[l].m_width);
//--- 配列サイズ超過の防止
   uint s=(symbol_index<symbols_total)?symbol_index : symbols_total;
//--- 全文字の幅を買合計する
   uint width=0;
   for(uint i=0; i<s; i++)
      width+=m_lines[l].m_width[i];
//--- 行の幅を返す
   return(width);
  }

テキストカーソル座標を取得するメソッド は非常に単純な形を取ります(以下のコードを参照)。座標はm_text_cursor_x及びm_text_cursor_yフィールドに格納されています。座標の計算には、カーソルの現在の位置と、カーソルが移動する行および文字のインデックスが使用されます。m_text_cursor_x_pos及びm_text_cursor_y_posフィールドはこれらの値の格納に使われます。

class CTextBox : public CElement
  {
private:
   //--- テキストカーソルの現在の座標
   int               m_text_cursor_x;
   int               m_text_cursor_y;
   //--- テキストカーソルの現在位置
   uint              m_text_cursor_x_pos;
   uint              m_text_cursor_y_pos;
   //---
private:
   //--- テキストカーソルの座標計算
   void              CalculateTextCursorX(void);
   void              CalculateTextCursorY(void);
  };
//+------------------------------------------------------------------+
//| テキストカーソルのX座標の計算                                        |
//+------------------------------------------------------------------+
void CTextBox::CalculateTextCursorX(void)
  {
//--- 行幅を取得する
   int line_width=(int)LineWidth(m_text_cursor_x_pos,m_text_cursor_y_pos);
//--- カーソルのX座標を計算して格納する
   m_text_cursor_x=m_text_x_offset+line_width;
  }
//+------------------------------------------------------------------+
//| カーソルのY座標を計算して格納する                                     |
//+------------------------------------------------------------------+
void CTextBox::CalculateTextCursorY(void)
  {
//--- 行の高さを取得する
   int line_height=(int)LineHeight();
//--- カーソルのY座標を取得する
   m_text_cursor_y=m_text_y_offset+int(line_height*m_text_cursor_y_pos);
  }

テキストカーソルを描画するCTextBox::DrawCursor()メソッドの準備が整いました。多くの他のテキストエディタでは、テキストカーソルが一部の文字のピクセルと部分的に重なっていることがわかります。テキストカーソルは単にそれらを隠すだけではないこともわかります。文字ののカバーされたピクセルは異なる色で描かれます。これは、文字の可読性を維持するために行われます。 

たとえば、次のスクリーンショットは、テキストエディタで 'd'と 'д'文字が重なっていてもカーソルによっては重なっていないことを示しています。

 図6 'd'文字のピクセルをオーバーラップするテキストカーソルの例

図6 'd'文字のピクセルをオーバーラップするテキストカーソルの例

 図7 'д'文字のピクセルをオーバーラップするテキストカーソルの例

図7 'д'文字のピクセルをオーバーラップするテキストカーソルの例


カーソルとオーバーラップした文字がいつでも背景が何色でも見えるようにするには、カーソルによってオーバーラップされたピクセルの色を反転させるだけで十分です。 

ここでテキストカーソルを描画するCTextBox::DrawCursor()メソッドを考察しましょう。カーソルの幅は1ピクセルに等しく、その高さは行の高さに一致します。一番初めに、カーソルを描画するX座標と行の高さを取得します。Y座標はピクセル単位で描画されるためループで計算されます。色を操作するためのCColorsクラスのインスタンスはコントロールのCElementBase基本クラスで事前に宣言されています。したがって、指定された座標における現在のピクセルの色がY座標の計算後に各反復で取得されます。そして、CColors::Negative()メソッドは色を反転し同じ場所に配置します。 

class CTextBox : public CElement
  {
private:
   //--- テキストカーソルを描画する
   void              DrawCursor(void);
  };
//+------------------------------------------------------------------+
//| テキストカーソルを描画する                                          |
//+------------------------------------------------------------------+
void CTextBox::DrawCursor(void)
  {
//--- 行の高さを取得する
   int line_height=(int)LineHeight();
//--- カーソルのX座標を取得する
   CalculateTextCursorX();
//--- テキストカーソルを描画する
   for(int i=0; i<line_height; i++)
     {
      //--- ピクセルのY座標を取得する
      int y=m_text_y_offset+((int)m_text_cursor_y_pos*line_height)+i;
      //--- ピクセルの現在の色を取得する
      uint pixel_color=m_canvas.PixelGet(m_text_cursor_x,y);
      //--- カーソルの色を反転する
      pixel_color=m_clr.Negative((color)pixel_color);
      m_canvas.PixelSet(m_text_cursor_x,y,::ColorToARGB(pixel_color));
     }
  }

テキスト付きのテキストボックスを描画するためにCTextBox::DrawText() 及びCTextBox::DrawTextAndCursor()メソッドが実装されています。 

CTextBox::DrawText() メソッドは非アクティブなテキストボックス内のテキストを更新するだけでよい場合に使用されます。ここではすべて簡単です。コントロールが隠されていない場合は、テキストを表示し、フレームを描画して画像を更新します。  

class CTextBox : public CElement
  {
private:
   //--- テキストを描画する
   void              DrawText(void);
  };
//+------------------------------------------------------------------+
//| テキストを描画する                                                 |
//+------------------------------------------------------------------+
void CTextBox::DrawText(void)
  {
//--- コントロールが隠れている場合は終了する
   if(!CElementBase::IsVisible())
      return;
//--- テキストを出力する
   CTextBox::TextOut();
//--- フレームを描画する
   DrawBorder();
//--- テキストボックスを更新する
   m_canvas.Update();
  }

テキストに加えてテキストボックスがアクティブな場合はCTextBox::DrawTextAndCursor() メソッドの点滅するテキストカーソルを表示する必要があります。点滅するには、カーソルを表示/非表示にする状態を決定する必要があります。このメソッドが呼び出されるたびに状態が反転されます。また、メソッドにtrue値(show_state 引数)が渡されたときに表示を強制する機能も提供されました。テキストボックスがアクティブのときにカーソルを動かすと強制表示が必要になります。実際には、カーソルの点滅は、タイムカウンタのクラスコンストラクタで指定された間隔でコントロールのタイマーで実行されます。ここではその値は200ミリ秒ですCTextBox::DrawTextAndCursor() が呼ばれるたびにカウンタのリセットが必要です。 

class CTextBox : public CElement
  {
private:
   //--- テキストと点滅カーソルを表示する
   void              DrawTextAndCursor(const bool show_state=false);
  };
//+------------------------------------------------------------------+
//| コンストラクタ                                                     |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void)
  {
//--- タイマカウンタパラメータの設定
   m_counter.SetParameters(16,200);
  }
//+------------------------------------------------------------------+
//| タイマー                                                          |
//+------------------------------------------------------------------+
void CTextBox::OnEventTimer(void)
  {
...
//--- テキストカーソルの更新の間に一時停止する
   if(m_counter.CheckTimeCounter())
     {
      //---コントロールが表示され、テキストボックスがアクティブになっている場合は、テキストカーソルを更新する
      if(CElementBase::IsVisible() && m_text_edit_state)
         DrawTextAndCursor();
     }
  }
//+------------------------------------------------------------------+
//| テキストと点滅カーソルを表示する                                     |
//+------------------------------------------------------------------+
void CTextBox::DrawTextAndCursor(const bool show_state=false)
  {
//--- テキストカーソルの状態(表示/非表示)を確認する
   static bool state=false;
   state=(!show_state)?!state : show_state;
//--- テキストを出力する
   CTextBox::TextOut();
//--- テキストカーソルを描画する
   if(state)
      DrawCursor();
//--- フレームを描画する
   DrawBorder();
//--- テキストボックスを更新する
   m_canvas.Update();
//--- カウンタをリセットする
   m_counter.ZeroTimeCounter();
  }

マルチラインテキストボックスの作成には、スクロールバーを作成するための2つのプライベートメソッドと1つのパブリック メソッドが必要になります: 

class CTextBox : public CElement
  {
private:
   //--- 要素作成オブジェクト
   CRectCanvas       m_canvas;
   CScrollV          m_scrollv;
   CScrollH          m_scrollh;
   //---
public:
   //--- コントロール作成メソッド
   bool              CreateTextBox(const long chart_id,const int subwin,const int x_gap,const int y_gap);
   //---
private:
   bool              CreateCanvas(void);
   bool              CreateScrollV(void);
   bool              CreateScrollH(void);
   //---
public:
   //--- スクロールバーへのポインタを返す
   CScrollV         *GetScrollVPointer(void)                   { return(::GetPointer(m_scrollv));  }
   CScrollH         *GetScrollHPointer(void)                   { return(::GetPointer(m_scrollh));  }
  };

CTextBox::CreateCanvas()メソッドを呼び出してテキストボックスを作成する前にはそのサイズの計算が必要です。ここではCCanvasTable型のレンダーテーブルに実装されているのと同様のメソッドが適用されます。簡単に見てみましょう。画像には全体のサイズと可視部分のサイズがあります。コントロールのサイズは、画像の可視部分のサイズと同じです。テキストカーソルやスクロールバーを移動すると、画像の座標が変化し、可視部分の座標(これは制御座標でもあります)は変わりません。

Y軸に沿ったサイズは、単純に行の数に高さを乗算することによって計算できます。テキストボックスの端からのマージンとスクロールバーのサイズもここで考慮されます。 X軸に沿ったサイズを計算するには、配列全体の最大線幅を知る必要があります。これはCTextBox::MaxLineWidth()メソッドで行われます。ここでは、サイクル内のlines配列を反復処理し、前の行より大きい場合は行の全幅を格納し、値を返します。

class CTextBox : public CElement
  {
private:
   //--- 行の最大幅を返す
   uint              MaxLineWidth(void);
  };
//+------------------------------------------------------------------+
//| 行の最大幅を返す                                                   |
//+------------------------------------------------------------------+
uint CTextBox::MaxLineWidth(void)
  {
   uint max_line_width=0;
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
   for(uint i=0; i<lines_total; i++)
     {
      //--- 文字の配列のサイズを取得する
      uint symbols_total=::ArraySize(m_lines[i].m_symbol);
      //--- 行幅を取得する
      uint line_width=LineWidth(symbols_total,i);
      //--- 行の最大幅を格納する
      if(line_width>max_line_width)
         max_line_width=line_width;
     }
//--- 行の最大幅を返す
   return(max_line_width);
  }

コントロールサイズを計算するCTextBox::CalculateTextBoxSize()メソッドのコ―ドが下記に示されています。このメソッドはCTextBox::ChangeWidthByRightWindowSide()及びCTextBox::ChangeHeightByBottomWindowSide()メソッド内でも呼び出されます。これらのメソッドの目的は、フォームサイズに応じてコントロールを自動的にサイズ変更することです(そのようなプロパティが開発者によって定義されている場合)。 

class CTextBox : public CElement
  {
private:
   //--- コントロール全体と可視部分のサイズ
   int               m_area_x_size;
   int               m_area_y_size;
   int               m_area_visible_x_size;
   int               m_area_visible_y_size;
   //---
private:
   //--- テキストボックスの幅を計算する
   void              CalculateTextBoxSize(void);
  };
//+------------------------------------------------------------------+
//| テキストボックスのサイズを計算する                                   |
//+------------------------------------------------------------------+
void CTextBox::CalculateTextBoxSize(void)
  {
//--- テキストボックスの最大行幅を取得する
   int max_line_width=int((m_text_x_offset*2)+MaxLineWidth()+m_scrollv.ScrollWidth());
//--- 全体幅を決定する
   m_area_x_size=(max_line_width>m_x_size)?max_line_width : m_x_size;
//--- 表示される幅を決定する
   m_area_visible_x_size=m_x_size;
//--- 行の高さを取得する
   int line_height=(int)LineHeight();
//--- 行の配列のサイズを取得する
   int lines_total=::ArraySize(m_lines);
//--- コントロール全体の高さを計算する
   int lines_height=int((m_text_y_offset*2)+(line_height*lines_total)+m_scrollh.ScrollWidth());
//--- 全体の高さを決定する
   m_area_y_size=(m_multi_line_mode && lines_height>m_y_size)?lines_height : m_y_size;
//--- 表示される高さを決定する
   m_area_visible_y_size=m_y_size;
  }

サイズが計算されました。これからはそれを適用します。これはCTextBox::ChangeTextBoxSize()メソッドで行います。ここでのメソッドの引数は、可視領域を最初に移動する必要があるかそのままにする必要があるかを指定します。さらに、このメソッドはスクロールバーのサイズを変更し、スクロールバーのサムに対して最終的な可視領域の調整を実行します。同様のケースが以前の記事で既に説明されているので、これらのメソッドのコードについては、ここでは説明しません。 

class CTextBox : public CElement
  {
private:
   //--- テキストボックスのサイズを更新する
   void              ChangeTextBoxSize(const bool x_offset=false,const bool y_offset=false);
  };
//+------------------------------------------------------------------+
//| テキストボックスのサイズを更新する                                   |
//+------------------------------------------------------------------+
void CTextBox::ChangeTextBoxSize(const bool is_x_offset=false,const bool is_y_offset=false)
  {
//--- テーブルのサイズを変える
   m_canvas.XSize(m_area_x_size);
   m_canvas.YSize(m_area_y_size);
   m_canvas.Resize(m_area_x_size,m_area_y_size);
//--- 可視部分のサイズを設定する
   m_canvas.SetInteger(OBJPROP_XSIZE,m_area_visible_x_size);
   m_canvas.SetInteger(OBJPROP_YSIZE,m_area_visible_y_size);
//--- 全幅と可視部分の差
   int x_different=m_area_x_size-m_area_visible_x_size;
   int y_different=m_area_y_size-m_area_visible_y_size;
//--- XおよびY軸に沿って画像内のフレームオフセットを設定する
   int x_offset=(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
   int y_offset=(int)m_canvas.GetInteger(OBJPROP_YOFFSET);
   m_canvas.SetInteger(OBJPROP_XOFFSET,(!is_x_offset)?0 : (x_offset<=x_different)?x_offset : x_different);
   m_canvas.SetInteger(OBJPROP_YOFFSET,(!is_y_offset)?0 : (y_offset<=y_different)?y_offset : y_different);
//--- スクロールバーのサイズを変更する
   ChangeScrollsSize();
//--- データを調整する
   ShiftData();
  }

次のフィールドとメソッドは、コントロールの状態を管理して現在の状態を取得するためのものです。

  • CTextBox::TextEditState()メソッドはコントロールの状態を取得します。 
  • CTextBox::TextBoxState() メソッドの呼び出しはコントロールをブロック/ブロック解除します。ブロックされたコントロールは読み取り専用モードに移行します。対応する色は、背景、フレーム、およびテキストに設定されます(これは、コントロールを作成する前にユーザによって行われることができます)。 
class CTextBox : public CElement
  {
private:
   //--- 読み込み専用モード
   bool              m_read_only_mode;
   //--- テキストエディットボックスの状態
   bool              m_text_edit_state;
   //--- コントロールの状態
   bool              m_text_box_state;
   //---
public:
   //--- (1) テキストエディットボックスの状態 (2) コントロールの状態を取得/設定する
   bool              TextEditState(void)                 const { return(m_text_edit_state);        }
   bool              TextBoxState(void)                  const { return(m_text_box_state);         }
   void              TextBoxState(const bool state);
  };
//+------------------------------------------------------------------+
//| コントロールの利用可能状態を設定する                                  |
//+------------------------------------------------------------------+
void CTextBox::TextBoxState(const bool state)
  {
   m_text_box_state=state;
//--- 現在の状態に対して設定する
   if(!m_text_box_state)
     {
      //--- 優先順位
      m_canvas.Z_Order(-1);
      //--- 読み取り専用モードのエディットボックス
      m_read_only_mode=true;
     }
   else
     {
      //--- 優先順位
      m_canvas.Z_Order(m_text_edit_zorder);
      //--- 編集モードでのエディットコントロール
      m_read_only_mode=false;
     }
//--- テキストボックスを更新する
   DrawText();
  }

 


テキストカーソルの管理

テキストエディットボックスは、クリックするとアクティブ化されます。クリックされた場所の座標が直ちに決定され、そこにテキストカーソルが移動されます。これを行うのはCTextBox::OnClickTextBox()メソッドです。しかし、その説明に移る前に、最初にCTextBoxクラスの他のメソッドと同様に、その中で呼び出されるいくつかの補助メソッドを検討してみましょう。

CTextBox::SetTextCursor()メソッドはテキストカーソル位置の値を更新します。シングルラインモードではY座標に沿った位置は常に0に等しくなります。

class CTextBox : public CElement
  {
private:
   //--- テキストカーソルの現在位置
   uint              m_text_cursor_x_pos;
   uint              m_text_cursor_y_pos;
   //---
private:
   //--- カーソルを指定された位置に設定する
   void              SetTextCursor(const uint x_pos,const uint y_pos);
  };
//+------------------------------------------------------------------+
//| カーソルを指定された位置に設定する                                    |
//+------------------------------------------------------------------+
void CTextBox::SetTextCursor(const uint x_pos,const uint y_pos)
  {
   m_text_cursor_x_pos=x_pos;
   m_text_cursor_y_pos=(!m_multi_line_mode)?0 : y_pos;
  }

スクロールバー制御メソッド。同様の方法については、シリーズの前回の記事ですでに説明しましたので、ここではコードを表示しません。簡単な注意点:パラメータが渡されない場合、サムは最終位置、つまりリスト/テキスト/ドキュメントの最後に移動されます。 

class CTextBox : public CElement
  {
public:
   //--- ーブルスクロール:(1)垂直 および(2)水平
   void              VerticalScrolling(const int pos=WRONG_VALUE);
   void              HorizontalScrolling(const int pos=WRONG_VALUE);
  };

テキストボックスの非アクティブ化には CTextBox::DeactivateTextBox()が必要です。ここで、ターミナルの開発者が開発した新機能を記述する必要があります。もう1つの識別子(CHART_KEYBOARD_CONTROL)がENUM_CHART_PROPERTY列挙体に追加されました。'Left'、 'Right'、 'Home'、'End'、'Page Up'、'Page Down'キーによるチャートの管理、およびチャートの'+' 及び'-'ズームキーを有効化/無効化します。 したがって、テキストボックスがアクティブになっているときは、一覧表示されたキーが傍受されてテキストボックスの操作が中断されないように、チャート管理機能を無効にする必要があります。テキストボックスが非アクティブになったら、キーボードを使用してチャート管理を再度有効にする必要があります。 

ここでテキストボックスを再描画する必要があります。マルチラインモードでない場合は、テキストカーソルとスクロールバーのサムを行の先頭に移動します。 

class CTextBox : public CElement
  {
private:
   //--- テキストボックスを非アクティブ化する
   void              DeactivateTextBox(void);
  };
//+------------------------------------------------------------------+
//| テキストボックスの非アクティブ化                                     |
//+------------------------------------------------------------------+
void CTextBox::DeactivateTextBox(void)
  {
//--- すでに非アクティブの場合は終了する
   if(!m_text_edit_state)
      return;
//--- 非アクティブ化する
   m_text_edit_state=false;
//--- チャート管理を有効にする
   m_chart.SetInteger(CHART_KEYBOARD_CONTROL,true);
//--- テキストを描画する
   DrawText();
//--- マルチラインモードが無効の場合
   if(!m_multi_line_mode)
     {
      //--- カーソルを行の初めに移動する
      SetTextCursor(0,0);
      //--- カーソルを行の初めに移動する
      HorizontalScrolling(0);
     }
  }

テキストカーソルを管理するときは、可視領域の境界を越えているかどうかを確認する必要があります。交差が発生した場合は、カーソルを再度視界領域に戻す必要があります。この目的のために、再利用可能な追加のメソッドが必要です。テキストボックスの許容境界はマルチラインモードとスクロールバーの存在を考慮して計算される必要があります。 

可視領域をどのくらいシフトするかを計算するには最初に現在のオフセット値を見つけなければなりません: 

class CTextBox : public CElement
  {
private:
   //--- テキストボックスの可視領域の境界の計算
   int               m_x_limit;
   int               m_y_limit;
   int               m_x2_limit;
   int               m_y2_limit;
   //---
private:
   //--- テキストボックスの境界の計算
   void              CalculateBoundaries(void);
   void              CalculateXBoundaries(void);
   void              CalculateYBoundaries(void);
  };
//+------------------------------------------------------------------+
//| 2軸に沿ったテキストボックスの境界の計算                               |
//+------------------------------------------------------------------+
void CTextBox::CalculateBoundaries(void)
  {
   CalculateXBoundaries();
   CalculateYBoundaries();
  }
//+------------------------------------------------------------------+
//| X軸に沿ったテキストボックスの境界の計算                               |  
//+------------------------------------------------------------------+
void CTextBox::CalculateXBoundaries(void)
  {
//--- X座標とX軸に沿ったオフセットを取得する
   int x       =(int)m_canvas.GetInteger(OBJPROP_XDISTANCE);
   int xoffset =(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
//--- テキストボックスの可視領域の境界を計算する
   m_x_limit  =(x+xoffset)-x;
   m_x2_limit =(m_multi_line_mode)?(x+xoffset+m_x_size-m_scrollv.ScrollWidth()-m_text_x_offset)-x : (x+xoffset+m_x_size-m_text_x_offset)-x;
  }
//+------------------------------------------------------------------+
//| Y軸に沿ったテキストボックスの境界の計算                               |  
//+------------------------------------------------------------------+
void CTextBox::CalculateYBoundaries(void)
  {
//--- マルチラインモードが無効の場合は終了する
   if(!m_multi_line_mode)
      return;
//--- Y座標とY軸に沿ったオフセットを取得する
   int y       =(int)m_canvas.GetInteger(OBJPROP_YDISTANCE);
   int yoffset =(int)m_canvas.GetInteger(OBJPROP_YOFFSET);
//--- テキストボックスの可視領域の境界を計算する
   m_y_limit  =(y+yoffset)-y;
   m_y2_limit =(y+yoffset+m_y_size-m_scrollh.ScrollWidth())-y;
  }

現在のカーソル位置に対してスクロールバーを正確に配置するには、次のメソッドを使用します。 

class CTextBox : public CElement
  {
private:
   //--- テキストボックスの左端にあるスクロールバーのサムのX位置の計算
   int               CalculateScrollThumbX(void);
   //--- テキストボックスの右端にあるスクロールバーのサムのX位置の計算
   int               CalculateScrollThumbX2(void);
   //--- テキストボックスの上端にあるスクロールバーサムのY位置の計算
   int               CalculateScrollThumbY(void);
   //--- テキストボックスの下端にあるスクロールバーサムのY位置の計算
   int               CalculateScrollThumbY2(void);
  };
//+------------------------------------------------------------------+
//| テキストボックスの左端のスクロールバーのX位置を計算する
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbX(void)
  {
   return(m_text_cursor_x-m_text_x_offset);
  }
//+------------------------------------------------------------------+
//| テキストボックスの右端のスクロールバーのX位置を計算する
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbX2(void)
  {
   return((m_multi_line_mode)?m_text_cursor_x-m_x_size+m_scrollv.ScrollWidth()+m_text_x_offset : m_text_cursor_x-m_x_size+m_text_x_offset*2);
  }
//+------------------------------------------------------------------+
//| テキストボックスの上端のスクロールバーのY 位置を計算する
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbY(void)
  {
   return(m_text_cursor_y-m_text_y_offset);
  }
//+------------------------------------------------------------------+
//| テキストボックスの下端のスクロールバーのY 位置を計算する
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbY2(void)
  {
//--- キャンバスに表示されるフォントを設定する(行の高さを取得するために必要)
   m_canvas.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//--- 行の高さを取得する
   int line_height=m_canvas.TextHeight("|");
//--- 値を計算して返す
   return(m_text_cursor_y-m_y_size+m_scrollh.ScrollWidth()+m_text_y_offset+line_height);
  }

テキストボックスをクリックされるとそれがアクティブ化されたことを明示的に示すイベントが生成されるようにします。また、テキストボックス内のカーソルの移動に対応するイベントを受け取る必要があります。Defines.mqhファイルに新しい識別子を追加します。 

  • テキストボックスのアクティブ化イベントのためのON_CLICK_TEXT_BOX
  • テキストカーソル移動イベントのためのON_MOVE_TEXT_CURSOR 
//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
...
#define ON_CLICK_TEXT_BOX          (31) // テキストボックスのアクティブ化
#define ON_MOVE_TEXT_CURSOR        (32) // テキストカーソルの移動

これらの識別子を使用して、テキストカーソルの現在の位置は文字列パラメータに追加情報として配置されます。これはMetaEditorを含む多くのテキストエディタで実装されています。下のスクリーンショットは、コードエディタのステータスバーに表示する文字列を生成する例を示しています。


 

図8 MetaEditor内のテキストカーソルの位置

下のリストは、上のスクリーンショットのような形式の文字列を返すCTextBox::TextCursorInfo()メソッドのコードを示しています。行数、指定された行の文字数、およびテキストカーソルの現在位置を取得するために使用できる追加のメソッドも示されています。 

class CTextBox : public CElement
  {
private:
   //--- (1)行のインデックス(2)テキストカーソルがおかれている文字
   //    (3) 行数 (4) t指定された行の文字数 を返す
   uint              TextCursorLine(void)                      { return(m_text_cursor_y_pos);      }
   uint              TextCursorColumn(void)                    { return(m_text_cursor_x_pos);      }
   uint              LinesTotal(void)                          { return(::ArraySize(m_lines));     }
   uint              ColumnsTotal(const uint line_index);
   //--- テキストカーソルに関する情報(行/行数、列/列数)
   string            TextCursorInfo(void);
  };
//+------------------------------------------------------------------+
//| 指定された行の文字数を返す                                          |
//+------------------------------------------------------------------+
uint CTextBox::ColumnsTotal(const uint line_index)
  {
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
//--- 配列サイズ超過の防止
   uint check_index=(line_index<lines_total)?line_index : lines_total-1;
//--- 行内の文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[check_index].m_symbol);
//--- 文字数を返す
   return(symbols_total);
  }
//+------------------------------------------------------------------+
//| テキストカーソルの情報                                              |
//+------------------------------------------------------------------+
string CTextBox::TextCursorInfo(void)
  {
//--- 文字列コンポーネント
   string lines_total        =(string)LinesTotal();
   string columns_total      =(string)ColumnsTotal(TextCursorLine());
   string text_cursor_line   =string(TextCursorLine()+1);
   string text_cursor_column =string(TextCursorColumn()+1);
//--- 文字列を作成する
   string text_box_info="Ln "+text_cursor_line+"/"+lines_total+", "+"Col "+text_cursor_column+"/"+columns_total;
//--- 文字列を返す
   return(text_box_info);
  }

これでこのセクションの冒頭で説明された CTextBox::OnClickTextBox()メソッドの説明を準備する準備が整いました(下記のコ―ドを参照)。ここで、いちばん最初に、マウスの左ボタンがクリックされたオブジェクトの名前のチェックがあります。クリックされたのがテキストボックスでないことが判明した場合は、テキストボックスがまだアクティブな場合に備えて、編集が終了したというメッセージ(ON_END_EDITイベント識別子)を送信します。その後、テキストボックスを無効にしてメソッドを終了します。

クリックされたのがこのテキストボックスであった場合、さらに2つのチェックが続きます。読み取り専用モードが有効な場合やコントロールがブロックされている場合にはプログラムはメソッドを終了します。いずれかの条件がfalseの場合は、メソッドのメインコードに移動します。

まず、キーボードによるチャート管理が無効になります。次に(1)コントロールの可視領域の現在のオフセットを取得し(2クリックが発生した)ポイントの相対座標を決定します。このメソッドのメインループでの計算には行の高さも必要です。 

ループではまずクリックが発生した行を検索します。文字の検索は、クリックの計算されたY座標が行の上境界と下境界の間にある場合にのみ開始されます。この行に文字が含まれていないことが判明した場合は、テキストカーソルと水平スクロールバーを行の先頭に移動する必要があります。ループはこれで停止します。

行に文字が含まれている場合は、2回目のループが始まり、クリックが発生した文字が検索されます。ここでの検索原理は、行の場合とほとんど同じです。唯一の違いは、すべてのフォントがすべての文字に対して同じ幅を持つわけではないので、各反復で文字幅が取得されることです。クリックされた文字が見つかった場合は、テキストカーソルを文字位置に設定して検索を完了します。この行で文字が見つからず最後の文字に達した場合は、文字が存在しないままにカーソルを行の最後の位置に移動し、検索を完了します。 

次に、マルチラインモードが有効になっている場合はテキストカーソルを管理するときは、可視領域の境界を越えているかどうかを確認する必要があります。Y軸に沿ったテキストボックスの可視領域の境界をテキストカーソルが(少なくとも部分的に)超えているかどうかを確認する必要があります。超えている場合は、テキストカーソルの位置を基準にして表示領域を調整します。その後、テキストボックスをアクティブとしてフラグしてから再描画します。 

CTextBox::OnClickTextBox()メソッドの最後には、テキストボックスがアクティブになったことを示すイベント(ON_CLICK_TEXT_BOX イベント識別子)を生成します。明確な識別を提供するには、(1)コントロールの識別子(2)コントロールのインデックス、さらに(3)カーソル位置に関する情報を送信します。 

class CTextBox : public CElement
  {
private:
   //--- 要素の押下を処理する
   bool              OnClickTextBox(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| コントロールのクリックの処理                                         |
//+------------------------------------------------------------------+
bool CTextBox::OnClickTextBox(const string clicked_object)
  {
//--- オブジェクト名が異なる場合には終了する
   if(m_canvas.Name()!=clicked_object)
     {
      //--- テキストボックスがアクティブの場合は、テキストボックス内の行編集モードの終了に関するメッセージを送信する
      if(m_text_edit_state)
         ::EventChartCustom(m_chart_id,ON_END_EDIT,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
      //--- テキストボックスを非アクティブ化する
      DeactivateTextBox();
      return(false);
     }
//---  (1) 読み取り専用モードが有効な場合や(2) コントロールがブロックされている場合には終了する
   if(m_read_only_mode || !m_text_box_state)
      return(true);
//--- チャート管理を無効にする
   m_chart.SetInteger(CHART_KEYBOARD_CONTROL,false);
//--- XおよびY軸に沿ってオフセットを取得する
   int xoffset=(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
   int yoffset=(int)m_canvas.GetInteger(OBJPROP_YOFFSET);
//--- マウスカーソルの下にあるテキストエディットボックスの座標を決定する
   int x =m_mouse.X()-m_canvas.X()+xoffset;
   int y =m_mouse.Y()-m_canvas.Y()+yoffset;
//--- 行の高さを取得する
   int line_height=(int)LineHeight();
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
//--- クリックされた文字を決定する
   for(uint l=0; l<lines_total; l++)
     {
      //--- 条件をチェックするための初期座標を設定する
      int x_offset=m_text_x_offset;
      int y_offset=m_text_y_offset+((int)l*line_height);
      //--- Y軸に沿った条件確認
      bool y_pos_check=(l<lines_total-1)?(y>=y_offset && y<y_offset+line_height) : y>=y_offset;
      //--- クリックされたのがこの行ではなかった場合は次に行く
      if(!y_pos_check)
         continue;
      //--- 文字の配列のサイズを取得する
      uint symbols_total=::ArraySize(m_lines[l].m_width);
      //--- これが空行の場合は、指定した位置にカーソルを移動してループを終了する
      if(symbols_total<1)
        {
         SetTextCursor(0,l);
         HorizontalScrolling(0);
         break;
        }
      //--- クリックされた文字を検出する
      for(uint s=0; s<symbols_total; s++)
        {
         //--- 文字が見つかった場合にはカーソルを指定された位置に移動してループを離れる
         if(x>=x_offset && x<x_offset+m_lines[l].m_width[s])
           {
            SetTextCursor(s,l);
            l=lines_total;
            break;
           }
         //--- 次のチェックのために現在の文字の幅を追加する
         x_offset+=m_lines[l].m_width[s];
         //--- これが最後の文字の場合はカーソルを行の終わりに移動させてループを離れる
         if(s==symbols_total-1 && x>x_offset)
           {
            SetTextCursor(s+1,l);
            l=lines_total;
            break;
           }
        }
     }
//--- マルチラインテキストボックスモードが有効な場合
   if(m_multi_line_mode)
     {
      //--- //--- テキストボックスの可視領域の境界を取得する
      CalculateYBoundaries();
      //--- カーソルのY座標を取得する
      CalculateTextCursorY();
      //--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
      if(m_text_cursor_y<=m_y_limit)
         VerticalScrolling(CalculateScrollThumbY());
      else
        {
         if(m_text_cursor_y+(int)LineHeight()>=m_y2_limit)
            VerticalScrolling(CalculateScrollThumbY2());
        }
     }
//--- テキストボックスをアクティブ化する
   m_text_edit_state=true;
//--- テキストとカーソルを更新する
   DrawTextAndCursor(true);
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_CLICK_TEXT_BOX,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }



文字の入力

次にCTextBox::OnPressedKey()メソッドを考察しましょう。それはキーの押下を処理し、押されたキーに文字が含まれていることが判明した場合、テキストカーソルの現在位置で行に追加しなければなりません。KeySymbolOptions 構造体の配列のサイズを増やしてテキストボックスに入力された文字を配列に追加し、追加された配列要素に文字と文字の幅を追加するメソッドが必要になります 。

CTextBoxクラスの多数のメソッドの配列のサイズ変更には、単純なCTextBox::ArraysResize()メソッドが使用されます。

class CTextBox : public CElement
  {
private:
   //--- 指定された行のプロパティ配列のサイズを変更する
   void              ArraysResize(const uint line_index,const uint new_size);
  };
//+------------------------------------------------------------------+
//| 指定された行のプロパティ配列のサイズを変更する                         |
//+------------------------------------------------------------------+
void CTextBox::ArraysResize(const uint line_index,const uint new_size)
  {
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
//--- 配列サイズ超過の防止
   uint l=(line_index<lines_total)?line_index : lines_total-1;
//--- 構造体配列のサイズを設定する
   ::ArrayResize(m_lines[line_index].m_width,new_size);
   ::ArrayResize(m_lines[line_index].m_symbol,new_size);
  }

CTextBox::AddSymbol()メソッドは新たに入力された文字をテキストボックスに追加するためのものです。より慎重に見てみましょう。新しい文字を入力するときは、配列サイズを1つ増やす必要があります。テキストカーソルの現在位置は文字列の任意の文字であることができます。したがって、配列に文字を追加する前に、現在のテキストカーソルの位置の右にあるすべての文字のインデックスを1つシフトする必要があります。その後、入力された文字をテキストカーソルの位置に格納します。メソッドの最後にはテキストカーソルを右に一文字でシフトします。 

class CTextBox : public CElement
  {
private:
   //--- 文字とそのプロパティを構造体の配列に追加する
   void              AddSymbol(const string key_symbol);
  };
//+------------------------------------------------------------------+
//| 文字とそのプロパティを構造体の配列に追加する                           |
//+------------------------------------------------------------------+
void CTextBox::AddSymbol(const string key_symbol)
  {
//--- 文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 配列のサイズを変更する
   ArraysResize(m_text_cursor_y_pos,symbols_total+1);
//--- 配列の末尾からすべての文字を、追加された文字のインデックスにシフトする
   for(uint i=symbols_total; i>m_text_cursor_x_pos; i--)
     {
      m_lines[m_text_cursor_y_pos].m_symbol[i] =m_lines[m_text_cursor_y_pos].m_symbol[i-1];
      m_lines[m_text_cursor_y_pos].m_width[i]  =m_lines[m_text_cursor_y_pos].m_width[i-1];
     }
//--- 文字幅を取得する
   int width=m_canvas.TextWidth(key_symbol);
//--- 空いた要素に文字を追加する
   m_lines[m_text_cursor_y_pos].m_symbol[m_text_cursor_x_pos] =key_symbol;
   m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos]  =width;
//--- カーソル位置カウンタを増加する
   m_text_cursor_x_pos++;
  }

下記はCTextBox::OnPressedKey()メソッドのコ―ドを示します。テキストボックスがアクティブな場合はメソッドに渡されたキーコードによって文字を取得しようとします。押されたキーに文字が含まれていない場合、プログラムはメソッドを終了します。文字がある場合は、そのプロパティと共に配列に追加します。文字を入力するときにテキストボックスのサイズが変更された可能性があるため、新しい値が計算されて設定されます。その後、テキストボックスの境界線とテキストカーソルの現在の座標を取得します。カーソルがテキストボックスの右端を超えている場合は、水平スクロールバーのサムの位置を調整します。その後 テキストボックスは再描画されテキストカーソルの表示は強制( true )されます.。CTextBox::OnPressedKey()メソッドの最後には、コントロール識別子、コントロールインデックス及びテキストカーソルの位置に関する追加情報を持ったテキストカーソル移動イベント(ON_MOVE_TEXT_CURSOR)が生成されます。 

class CTextBox : public CElement
  {
private:
   //--- キー押下の処理
   bool              OnPressedKey(const long key_code);
  };
//+------------------------------------------------------------------+
//| キー押下の処理                                                     |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKey(const long key_code)
  {
//--- テキストボックスが非アクティブの場合は終了する
   if(!m_text_edit_state)
      return(false);
//--- キーの文字を取得する
   string pressed_key=m_keys.KeySymbol(key_code);
//--- 文字がない場合は終了する
   if(pressed_key=="")
      return(false);
//--- 文字とそのプロパティを追加する
   AddSymbol(pressed_key);
//--- テキストボックスのサイズを計算する
   CalculateTextBoxSize();
//--- テキストボックスの新サイズを設定する
   ChangeTextBoxSize(true,true);
//--- //--- テキストボックスの可視領域の境界を取得する
   CalculateXBoundaries();
//--- カーソルのX座標を取得する
   CalculateTextCursorX();
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_x>=m_x2_limit)
      HorizontalScrolling(CalculateScrollThumbX2());
//--- テキストボックスのテキストを更新する
   DrawTextAndCursor(true);
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


Backspaceキーの押下の処理

ここで、Backspaceキーを押して文字を削除した場合を考えてみましょう。この場合、マルチラインテキストボックスのイベントハンドラはCTextBox::OnPressedKeyBackspace()メソッドを呼び出します。その操作には以前は考慮されていなかった追加メソッドが必要です。まず、コードが示されます。

文字はCTextBox::DeleteSymbol()メソッドを使用して削除されます。最初は、現在の行に少なくとも1文字が含まれているかどうかを確認します。これ以上ない場合は、テキストカーソルを行頭に置いてメソッドを終了します。文字がまだいくつかある場合は、前の文字の位置を取得します。これは、すべての文字を1要素だけ右にシフトする基点となるインデックスになります。その後、テキストカーソルも1つ左に移動します。メソッドの最後には配列サイズが1で減少されます

class CTextBox : public CElement
  {
private:
   //--- 文字を削除する
   void              DeleteSymbol(void);
  };
//+------------------------------------------------------------------+
//| 文字を削除する                                                     |
//+------------------------------------------------------------------+
void CTextBox::DeleteSymbol(void)
  {
//--- 文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 配列が空の場合
   if(symbols_total<1)
     {
      //--- カーソルをカーソルラインのゼロ位置に設定する
      SetTextCursor(0,m_text_cursor_y_pos);
      return;
     }
//--- 1つ前の文字の位置を取得する
   int check_pos=(int)m_text_cursor_x_pos-1;
//--- 範囲外であれば終了する
   if(check_pos<0)
      return;
//--- 削除された文字のインデックスから配列の最後までのすべての文字を1要素だけ右にシフトする
   for(uint i=check_pos; i<symbols_total-1; i++)
     {
      m_lines[m_text_cursor_y_pos].m_symbol[i] =m_lines[m_text_cursor_y_pos].m_symbol[i+1];
      m_lines[m_text_cursor_y_pos].m_width[i]  =m_lines[m_text_cursor_y_pos].m_width[i+1];
     }
//--- カーソル位置カウンタを減少する
   m_text_cursor_x_pos--;
//--- 配列のサイズを変更する
   ArraysResize(m_text_cursor_y_pos,symbols_total-1);
  }

テキストカーソルが行の先頭にあって行が最初の行でない場合は、現在の行を削除してすべての下の行を1つ上に移動する必要があります。削除された行に文字が含まれている場合は、1つ上の位置にある行に追加する必要があります。この操作にはもう一つのCTextBox::ShiftOnePositionUp()追加メソッドが使用されます。補助的なCTextBox::LineCopy()メソッドも行のコピーを若干容易にすることために必要です。 

class CTextBox : public CElement
  {
private:
   //--- 指定した行(コピー元)のコピーを新しい場所(コピー先)に作成する
   void              LineCopy(const uint destination,const uint source);
  };
//+------------------------------------------------------------------+
//| 指定された行のプロパティ配列のサイズを変更する                         |
//+------------------------------------------------------------------+
void CTextBox::LineCopy(const uint destination,const uint source)
  {
   ::ArrayCopy(m_lines[destination].m_width,m_lines[source].m_width);
   ::ArrayCopy(m_lines[destination].m_symbol,m_lines[source].m_symbol);
  }

下記はCTextBox::ShiftOnePositionUp()メソッドのコ―ドです。メソッドの最初のループでは、カーソルの現在位置以下のすべての行を1つ上にシフトします。最初の反復では、行に文字が含まれているかどうかをチェックする必要があります。含まれている文字は前の行に追加するように格納します。行がシフトされたら、行の配列は1要素だけ減少します。テキストカーソルは、前の行の最後に移動されます。 

CTextBox::ShiftOnePositionUp()メソッドの最後のブロックは 削除された行の文字を前の行に追加するためのものです。追加する行がある場合は ::StringToCharArray()関数を使用して、文字コードの形でuchar型の一時配列に転送します。次に、現在の行の配列を、追加された文字数だけ増やします。また、仕上げ操作として、文字とそのプロパティを交互に配列に追加します。uchar型の一時配列からの文字コードの変換は:CharToString()関数を使用して実行されます。 

class CTextBox : public CElement
  {
private:
   //--- 行を1つ上にシフトする
   void              ShiftOnePositionUp(void);
  };
//+------------------------------------------------------------------+
//| 行を1つ上にシフトする                                               |
//+------------------------------------------------------------------+
void CTextBox::ShiftOnePositionUp(void)
  {
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
//--- 次の要素から始まって位置を1つ上にシフトする
   for(uint i=m_text_cursor_y_pos; i<lines_total-1; i++)
     {
      //--- 1番目の反復
      if(i==m_text_cursor_y_pos)
        {
         //--- 文字の配列のサイズを取得する
         uint symbols_total=::ArraySize(m_lines[i].m_symbol);
         //---この行に文字がある場合は、前の行に追加するために文字を格納する
         m_temp_input_string=(symbols_total>0)?CollectString(i) : "";
        }
      //--- 行の配列の次の要素のインデックス
      uint next_index=i+1;
      //--- 文字の配列のサイズを取得する
      uint symbols_total=::ArraySize(m_lines[next_index].m_symbol);
      //--- 配列のサイズを変更する
      ArraysResize(i,symbols_total);
      //--- 行のコピーを作る
      LineCopy(i,next_index);
     }
//--- 行の配列のサイズを変更する
   uint new_size=lines_total-1;
   ::ArrayResize(m_lines,new_size);
//--- 行カウンタを減少する
   m_text_cursor_y_pos--;
//--- 文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- カーソルを終わりに動かす
   m_text_cursor_x_pos=symbols_total;
//--- カーソルのX座標を取得する
   CalculateTextCursorX();
//--- 前の行に追加する必要がある行が存在する場合
   if(m_temp_input_string!="")
     {
      //--- 行を配列に移す
      uchar array[];
      int total=::StringToCharArray(m_temp_input_string,array)-1;
      //--- 文字の配列のサイズを取得する
      symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
      //--- 配列のサイズを変更する
      new_size=symbols_total+total;
      ArraysResize(m_text_cursor_y_pos,new_size);
      //--- 構造体の配列にデータを追加する
      for(uint i=m_text_cursor_x_pos; i<new_size; i++)
        {
         m_lines[m_text_cursor_y_pos].m_symbol[i] =::CharToString(array[i-m_text_cursor_x_pos]);
         m_lines[m_text_cursor_y_pos].m_width[i]  =m_canvas.TextWidth(m_lines[m_text_cursor_y_pos].m_symbol[i]);
        }
     }
  }

すべての補助メソッドが準備されると、CTextBox::OnPressedKeyBackspace()主要メソッドのコードは複雑すぎるとは思われません。ここでは最初にBackspaceキーが押されたかとテキストボックスがアクティブになっているかどうかを確認します。チェックを通過したら、テキストカーソルの現在位置を見ます。テキストカーソルが現時点で行頭にない場合は前の文字を削除します。しかし、テキストカーソルが現時点で最初の行以外の行頭にある場合はすべての下の行を1つ上にシフトして、現在の行を削除します。 

その後、テキストボックスの新しいサイズが計算されて設定されます。テキストカーソルの境界と座標が取得されます。テキストカーソルが表示されている領域を離れる場合は、スクロールバーのサムを調整します。最後に、テキストカーソルを強制的に表示してコントロールを再描画し、カーソルの移動に関するメッセージを生成します。 

class CTextBox : public CElement
  {
private:
   //--- Backspaceキーの押下の処理
   bool              OnPressedKeyBackspace(const long key_code);
  };
//+------------------------------------------------------------------+
//| Backspaceキーの押下の処理                                          |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyBackspace(const long key_code)
  {
//--- テキストボックスがアクティブでないかBackspaceキーでない場合は終了する

   if(key_code!=KEY_BACKSPACE || !m_text_edit_state)
      return(false);
//--- 位置がゼロより大きい場合は文字を削除する
   if(m_text_cursor_x_pos>0)
      DeleteSymbol();
//--- 位置がゼロで、最初の行でない場合は、行を削除する
   else if(m_text_cursor_y_pos>0)
     {
      //--- 行を1つ上にシフトする
      ShiftOnePositionUp();
     }
//--- テキストボックスのサイズを計算する
   CalculateTextBoxSize();
//--- テキストボックスの新サイズを設定する
   ChangeTextBoxSize(true,true);
//--- //--- テキストボックスの可視領域の境界を取得する
   CalculateBoundaries();
//--- カーソルのXとY座標を取得する
   CalculateTextCursorX();
   CalculateTextCursorY();  
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
   else
     {
      if(m_text_cursor_x>=m_x2_limit)
         HorizontalScrolling(CalculateScrollThumbX2());
     }
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
   else
      VerticalScrolling(m_scrollv.CurrentPos());
//--- テキストボックスのテキストを更新する
   DrawTextAndCursor(true);
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


Enterキーの押下の処理

マルチラインモードが有効でEnterキーが押されている場合は、新しい行を追加してテキストカーソルの現在の位置より下の行おをすべて1つ下にシフトする必要があります。ここでの行のシフトには、別の CTextBox::ShiftOnePositionDown()補助メソッドと行をクリアするためのCTextBox::ClearLine().追加メソッドが必要です。

class CTextBox : public CElement
  {
private:
   //--- 指定された行をクリアする
   void              ClearLine(const uint line_index);
  };
//+------------------------------------------------------------------+
//| 指定された行をクリアする                                            |
//+------------------------------------------------------------------+
void CTextBox::ClearLine(const uint line_index)
  {
   ::ArrayFree(m_lines[line_index].m_width);
   ::ArrayFree(m_lines[line_index].m_symbol);
  }

次に CTextBox::ShiftOnePositionDown()メソッドのアルゴリズムを詳細に調べてみましょう。まず、Enterキーが押された行の文字数を格納する必要があります。これは、テキストカーソルが位置していた行の位置とともにCTextBox::ShiftOnePositionDown() メソッドのアルゴリズムがどのように処理されるかを定義します。その後、テキストカーソルを新しい行に移動し、行配列のサイズを1要素だけ増やします。現在の行から始まるすべての行は、配列の末尾から始まって1ループでで1つ下にシフトされなければなりません。最後の反復では、Enterキーが押された行に文字が含まれていない場合は、テキストカーソルが現在位置している行をクリアする必要があります。クリアされた行は、行のコピーであり、その内容は1つ下にシフトした結果として次の行に既に存在しています。

このメソッドの始めに、Enterキーが押された行の文字数を格納しました。行に文字が含まれていることが判明した場合は、その時点でテキストカーソルがどこにあったかを知る必要があります。そして、それが行の最後ではないことが判明した場合は、新しい行に移動するテキストカーソルの現在の位置から行の終わりまでの文字数を計算する必要があります。 これらの目的のためにはここでは一時配列が使用され、文字はコピーされた後で新しい行に移動されます行

class CTextBox : public CElement
  {
private:
   //--- 行を1つ下にシフトする
   void              ShiftOnePositionDown(void);
  };
//+------------------------------------------------------------------+
//| 行を1つ下にシフトする                                               |
//+------------------------------------------------------------------+
void CTextBox::ShiftOnePositionDown(void)
  {
//--- Enterキーが押された行の文字の配列のサイズを取得する
   uint pressed_line_symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- 行カウンタを増加する
   m_text_cursor_y_pos++;
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
//--- 配列を1要素で増やす
   uint new_size=lines_total+1;
   ::ArrayResize(m_lines,new_size);
//--- 現在の位置から始て1つ下の行までシフトする(配列の最後から)
   for(uint i=lines_total; i>m_text_cursor_y_pos; i--)
     {
      //--- 行配列の1つ前の要素のインデックス
      uint prev_index=i-1;
      //--- 文字の配列のサイズを取得する
      uint symbols_total=::ArraySize(m_lines[prev_index].m_symbol);
      //--- 配列のサイズを変更する
      ArraysResize(i,symbols_total);
      //--- 行のコピーを作る
      LineCopy(i,prev_index);
      //--- 新しい行をクリアする
      if(prev_index==m_text_cursor_y_pos && pressed_line_symbols_total<1)
         ClearLine(prev_index);
     }
//--- Enterキーが空行で押されたのでない場合
   if(pressed_line_symbols_total>0)
     {
      //--- Enterキーが押された行のインデックス
      uint prev_line_index=m_text_cursor_y_pos-1;
      //--- カーソルの現在位置から行末までの文字のコピーの配列
      string array[];
      //--- 配列のサイズを新しい行に移動される文字数に設定する
      uint new_line_size=pressed_line_symbols_total-m_text_cursor_x_pos;
      ::ArrayResize(array,new_line_size);
      //--- 新しい行に移動する文字を配列にコピーする
      for(uint i=0; i<new_line_size; i++)
         array[i]=m_lines[prev_line_index].m_symbol[m_text_cursor_x_pos+i];
      //--- Enterキーが押された行の構造体の配列のサイズを変更する
      ArraysResize(prev_line_index,pressed_line_symbols_total-new_line_size);
      //--- 新しい行の構造体の配列のサイズを変更する
      ArraysResize(m_text_cursor_y_pos,new_line_size);
      //--- 新しい行の構造体の配列にデータを追加する
      for(uint k=0; k<new_line_size; k++)
        {
         m_lines[m_text_cursor_y_pos].m_symbol[k] =array[k];
         m_lines[m_text_cursor_y_pos].m_width[k]  =m_canvas.TextWidth(array[k]);
        }
     }
  }

Enterキーの押下さを処理する用意が整いました。ここでCTextBox::OnPressedKeyEnter()メソッドを考察します。最初に、Enterキーが押されてテキストボックスがアクティブになっていることを確認します。次に、すべての条件が満たされててテキストボックスが単行であれば、作業を終了します。これにはON_END_EDIT識別子を持つイベントを送信して無効化します。

マルチラインモードが有効になっている場合はテキストカーソルの現在位置から下の行はすべて1つ下にシフトされます。その後、テキストボックスのサイズが調整され テキストカーソルが可視領域の下限を超えたかどうかが確認されます。さらに、テキストカーソルは行の先頭に置かれます。メソッドの最後に、テキストボックスが再描画され、テキストカーソルが移動されたというメッセージが送信されます。 

class CTextBox : public CElement
  {
private:
   //--- Enterキーの押下の処理
   bool              OnPressedKeyEnter(const long key_code);
  };
//+------------------------------------------------------------------+
//| Enterキーの押下の処理                                              |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyEnter(const long key_code)
  {
//--- Enterキーでないかテキストボックスがアクティブでない場合は終了する
   if(key_code!=KEY_ENTER || !m_text_edit_state)
      return(false);
//--- マルチラインモードが無効の場合
   if(!m_multi_line_mode)
     {
      //--- テキストボックスを非アクティブ化する
      DeactivateTextBox();
      //--- 関連したメッセージを送信する
      ::EventChartCustom(m_chart_id,ON_END_EDIT,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
      return(false);
     }
//--- 行を1つ下にシフトする
   ShiftOnePositionDown();
//--- テキストボックスのサイズを計算する
   CalculateTextBoxSize();
//--- テキストボックスの新サイズを設定する
   ChangeTextBoxSize();
//--- //--- テキストボックスの可視領域の境界を取得する
   CalculateYBoundaries();
//--- カーソルのY座標を取得する
   CalculateTextCursorY();
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_y+(int)LineHeight()>=m_y2_limit)
      VerticalScrolling(CalculateScrollThumbY2());
//--- カーソルを行の初めに移動する
   SetTextCursor(0,m_text_cursor_y_pos);
//--- スクロールバーを初めに移動する
   HorizontalScrolling(0);
//--- テキストボックスのテキストを更新する
   DrawTextAndCursor(true);
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


LeftおよびRightキーの押下の処理

LeftまたはRightキーを押すと、テキストカーソルが対応する方向に1文字移動します。これを実現するには、第一に追加のCTextBox::CorrectingTextCursorXPos() メソッドが必要になります。これによってテキストカーソルの位置が調整されます。このメソッドは、クラスの他のメソッドでも使用されます。

class CTextBox : public CElement
  {
private:
   //--- X軸に沿ったテキストカーソルの調整
   void              CorrectingTextCursorXPos(const int x_pos=WRONG_VALUE);
  };
//+------------------------------------------------------------------+
//| X軸に沿ったテキストカーソルの調整                                    |
//+------------------------------------------------------------------+
void CTextBox::CorrectingTextCursorXPos(const int x_pos=WRONG_VALUE)
  {
//--- 文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_width);
//--- カーソル位置の決定
   uint text_cursor_x_pos=0;
//--- 位置が利用できる場合
   if(x_pos!=WRONG_VALUE)
      text_cursor_x_pos=(x_pos>(int)symbols_total-1)?symbols_total : x_pos;
//--- 位置が利用できない場合は、カーソルを行末に設定する
   else
      text_cursor_x_pos=symbols_total;
//--- 行に文字が含まれていない場合は位置をゼロにする
   m_text_cursor_x_pos=(symbols_total<1)?0 : text_cursor_x_pos;
//--- カーソルのX座標を取得する
   CalculateTextCursorX();
  }

以下のコードは、Leftキーの押下を処理するCTextBox::OnPressedKeyLeft() メソッドのコードを示しています。押されたのが別のキーであった場合、テキストボックスがアクティブでない場合、またはCtrlキーも押された場合は、プログラムはメソッドを終了します。Ctrlキーを使用した同時押下キーの処理は、記事の別のセクションで考慮されます。 

最初の条件が満たされた場合はテキストカーソルの位置を確認します。行頭にない,場合は、テキストカーソルを1つ前の文字にシフトします。行頭にあってこの行が一番初めの行でない場合は、テキストカーソルを前の行の末尾に移動する必要があります。その後、水平スクロールバーと垂直スクロールバーのサムを調整し、テキストボックスを再描画して、テキストカーソルの移動に関するメッセージを送信します。

class CTextBox : public CElement
  {
private:
   //---  Leftキーの押下の処理
   bool              OnPressedKeyLeft(const long key_code);
  };
//+------------------------------------------------------------------+
//| Leftキーの押下の処理                                               |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyLeft(const long key_code)
  {
//--- Leftキーでない場合、Ctrlキーが押された場合、またはテキストボックスがアクティブでない場合は終了する
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- テキストカーソルの位置が0より大きい場合
   if(m_text_cursor_x_pos>0)
     {
      //--- 1つ前の文字にシフトする
      m_text_cursor_x-=m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos-1];
      //--- 文字カウンタを減少する
      m_text_cursor_x_pos--;
     }
   else
     {
      //--- これが最初の行でない場合
      if(m_text_cursor_y_pos>0)
        {
         //--- 前の行の最後に移動する
         m_text_cursor_y_pos--;
         CorrectingTextCursorXPos();
        }
     }
//--- //--- テキストボックスの可視領域の境界を取得する
   CalculateBoundaries();
//--- カーソルのY座標を取得する
   CalculateTextCursorY();
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
   else
     {
      //--- 文字の配列のサイズを取得する
      uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
      //---
      if(m_text_cursor_x_pos==symbols_total && m_text_cursor_x>=m_x2_limit)
         HorizontalScrolling(CalculateScrollThumbX2());
     }
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
//--- テキストボックスのテキストを更新する
   DrawTextAndCursor(true);
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

ここでRightキーの押下を処理するための CTextBox::OnPressedKeyRight()メソッドのコードについて考えてみましょう。ここでは、押されたキーのコード、テキストボックスの状態、Ctrlキーのチェックもメソッドの最初に渡されなければなりません。その後テキストカーソルが行末にあるかを確認します。そうでない場合は、テキストカーソルを右に1文字移動します。テキストカーソルが行の最後にある場合は、この行が最後かどうかを確認します。そうでない場合は、テキストカーソルを次の行の先頭に移動します。

次に、(1)テキストカーソルがテキストボックスの可視領域を越える場合にスクロールバーのサムを調整し、(2)コントロールを再描画し、(3)テキストカーソルの移動に関するメッセージを送信します。  

class CTextBox : public CElement
  {
private:
   //--- Rightキーの押下の処理
   bool              OnPressedKeyRight(const long key_code);
  };
//+------------------------------------------------------------------+
//| Rightキーの押下の処理                                              |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyRight(const long key_code)
  {
//--- Rightキーでない場合、Ctrlキーが押された場合、またはテキストボックスがアクティブでない場合は終了する
   if(key_code!=KEY_RIGHT || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- 文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_width);
//--- これが最後の行である場合
   if(m_text_cursor_x_pos<symbols_total)
     {
      //--- テキストカーソルの位置を次の文字にシフトする
      m_text_cursor_x+=m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos];
      //--- 文字カウンタを増加する
      m_text_cursor_x_pos++;
     }
   else
     {
      //--- 行の配列のサイズを取得する
      uint lines_total=::ArraySize(m_lines);
      //--- これが最後の行でない場合
      if(m_text_cursor_y_pos<lines_total-1)
        {
         //--- カーソルを次の行の初めに移動する
         m_text_cursor_x=m_text_x_offset;
         SetTextCursor(0,++m_text_cursor_y_pos);
        }
     }
//--- //--- テキストボックスの可視領域の境界を取得する
   CalculateBoundaries();
//--- カーソルのY座標を取得する
   CalculateTextCursorY();
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_x>=m_x2_limit)
      HorizontalScrolling(CalculateScrollThumbX2());
   else
     {
      if(m_text_cursor_x_pos==0)
         HorizontalScrolling(0);
     }
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_y+(int)LineHeight()>=m_y2_limit)
      VerticalScrolling(CalculateScrollThumbY2());
//--- テキストボックスのテキストを更新する
   DrawTextAndCursor(true);
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


UpおよびDownキーの押下の処理

UpキーとDownキーを押すと、テキストカーソルが上下に移動します。CTextBox::OnPressedKeyUp()及びCTextBox::OnPressedKeyDown() メソッドは、これらのキーの押下を処理するように設計されています。それらの間の唯一の違いは単に2行のコードにあるので、ここではそのうちの1つだけのコードを提供します。

コードの初めでは3つの条件が満たされる必要があります。(1)単一行のテキストボックスである場合、(2)別のキーが押された場合、または(3)テキストボックスがアクティブでない場合にはプログラムはメソッドを終了します。テキストカーソルの現在位置が最初の行にない場合は、テキストカーソルを前の行に移動し( CTextBox :: OnPressedKeyDown() メソッドの次の行まで)行配列の範囲を超えた場合は文字数が調整されます。

その後、テキストカーソルがテキストボックスの可視領域外にあるかどうかを確認し、必要に応じてスクロールバーのサムを調整します。両メソッドの唯一の違いはここにあり、CTextBox::OnPressedKeyUp() メソッドが上部境界の超過を確認するのに比べてCTextBox::OnPressedKeyDown() メソッドは下部境界の超過を確認します。最後に、テキストボックスが再描画され、テキストカーソルの移動に関するメッセージが送信されます。

class CTextBox : public CElement
  {
private:
   //--- Upキーの押下の処理
   bool              OnPressedKeyUp(const long key_code);
   //--- Downキーの押下の処理
   bool              OnPressedKeyDown(const long key_code);
  };
//+------------------------------------------------------------------+
//| Upキーの押下の処理                                                 |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyUp(const long key_code)
  {
//--- マルチラインモードが無効の場合は終了する
   if(!m_multi_line_mode)
      return(false);
//--- Upキーでないかテキストボックスがアクティブでない場合は終了する
   if(key_code!=KEY_UP || !m_text_edit_state)
      return(false);
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
//--- 配列の範囲が超えられていない場合
   if(m_text_cursor_y_pos-1<lines_total)
     {
      //--- 前の行に移動する
      m_text_cursor_y_pos--;
      //--- X軸に沿ったテキストカーソルの調整
      CorrectingTextCursorXPos(m_text_cursor_x_pos);
     }
//--- //--- テキストボックスの可視領域の境界を取得する
   CalculateBoundaries();
//--- カーソルのY座標を取得する
   CalculateTextCursorY();
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
//--- テキストボックスのテキストを更新する
   DrawTextAndCursor(true);
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }



HomeおよびEndキーの押下の処理

HomeキーとEndキーを 押すと、テキストカーソルが行の初めと終わりに移動します。CTextBox::OnPressedKeyHome() 及びCTextBox::OnPressedKeyEnd() メソッドは、これらのイベントを処理するように設計されています。コードは以下に示されています。それは非常に簡単で詳細なコメントが書かれているので、これ以上の説明は必要ありません。 

class CTextBox : public CElement
  {
private:
   //--- Homeキーの押下の処理
   bool              OnPressedKeyHome(const long key_code);
   //--- Endキーの押下の処理
   bool              OnPressedKeyEnd(const long key_code);
  };
//+------------------------------------------------------------------+
//| Homeキーの押下の処理                                               |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyHome(const long key_code)
  {
//--- Homeキーでない場合、Ctrlキーが押された場合、またはテキストボックスがアクティブでない場合は終了する
   if(key_code!=KEY_HOME || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- カーソルを現在の行の初めに移動する
   SetTextCursor(0,m_text_cursor_y_pos);
//--- スクロールバーを最初の位置に移動する
   HorizontalScrolling(0);
//--- テキストボックスのテキストを更新する
   DrawTextAndCursor(true);
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }
//+------------------------------------------------------------------+
//| Endキーの押下の処理                                                |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyEnd(const long key_code)
  {
//--- Endキーでない場合、Ctrlキーが押された場合、またはテキストボックスがアクティブでない場合は終了する
   if(key_code!=KEY_END || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- 現在の行の文字数を返す
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- カーソルを現在の行の終わりに移動する
   SetTextCursor(symbols_total,m_text_cursor_y_pos);
//--- カーソルのX座標を取得する
   CalculateTextCursorX();
//--- //--- テキストボックスの可視領域の境界を取得する
   CalculateXBoundaries();
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_x>=m_x2_limit)
      HorizontalScrolling(CalculateScrollThumbX2());
//--- テキストボックスのテキストを更新する
   DrawTextAndCursor(true);
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


キーのCtrlキーとの同時の押下の処理

次に、以下のキーの組み合わせを処理する方法について考えてみましょう。

  • 'Ctrl' + 'Left' – テキストカーソルを単語の左側の単語に移動します。
  • 'Ctrl' + 'Right' – テキストカーソルを単語の右側の単語に移動します。
  • 'Ctrl' + 'Home' – テキストカーソルを最初の行の先頭に移動します。 
  • 'Ctrl' + 'End' – テキストカーソルを最後の行の最後に移動します。

これらのメソッドのうちで、例として、テキストカーソルを単語から左の単語に移動するCTextBox::OnPressedKeyCtrlAndLeft()メソッドのみが考慮されます。この方法の初めにはCtrlキーとLeftキーの同時押下が確認されます。これらのキーのいずれかが押されていない場合、プログラムはメソッドを終了します。また、テキストボックスをアクティブ化する必要があります。

現在のテキストカーソルの位置が行頭にあって行が最初の行ではない場合には、前の行の末尾に移動します。テキストカーソルが現在の行の先頭にない場合、途切れることのない文字列の先頭を見つける必要があります。スペース文字(' ') は割り込み文字として機能します。ここでは、ループで、現在の行に沿って右から左に移動し、組み合わせが見つかると、次の文字がスペースで現在の文字がその他の任意の文字であって、それが始点でない場合は、テキストカーソルをその位置に設定します。

その後、テキストカーソルがテキストボックスの可視領域外にあるかどうかを確認し、必要に応じてスクロールバーのサムを調整します。最後には、テキストボックスが再描画され、テキストカーソルが移動されたというメッセージが送信されます。

class CTextBox : public CElement
  {
private:
   //---  Ctrl + Leftキーの押下の処理
   bool              OnPressedKeyCtrlAndLeft(const long key_code);
   //---  Ctrl + Rightキーの押下の処理
   bool              OnPressedKeyCtrlAndRight(const long key_code);
   //---  Ctrl + Homeキーの押下の処理
   bool              OnPressedKeyCtrlAndHome(const long key_code);
   //---  Ctrl + Endキーの押下の処理
   bool              OnPressedKeyCtrlAndEnd(const long key_code);
  };
//+------------------------------------------------------------------+
//| Ctrl + Leftキーの押下の処理                                        |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyCtrlAndLeft(const long key_code)
  {
//---  (1) Leftキーでない場合、(2) Ctrlキーが押されていない場合、または(3) テキストボックスがアクティブでない場合は終了する
   if(!(key_code==KEY_LEFT && m_keys.KeyCtrlState()) || !m_text_edit_state)
      return(false);
//--- スペース
   string SPACE=" ";
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
//--- 現在の行の文字数を返す
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- カーソルが現在の行の先頭にあってこれが最初の行でない場合は
//--- カーソルを前の行の終わりに移動する
   if(m_text_cursor_x_pos==0 && m_text_cursor_y_pos>0)
     {
      //--- 前の行のインデックスを取得する
      uint prev_line_index=m_text_cursor_y_pos-1;
      //--- 前の行の文字数を取得する
      symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol);
      //--- カーソルを前の行の終わりに移動する
      SetTextCursor(symbols_total,prev_line_index);
     }
//--- カーソルが現在の行の先頭にあるか最初の行にある場合
   else
     {
      //--- 文字の連続シーケンスの開始点を見つける(右から左へ)
      for(uint i=m_text_cursor_x_pos; i<=symbols_total; i--)
        {
         //--- カーソルが行末にある場合は、次の行に移動する
         if(i==symbols_total)
            continue;
         //--- これが行の最初の文字である場合
         if(i==0)
           {
            //--- カーソルを行の初めに設定する
            SetTextCursor(0,m_text_cursor_y_pos);
            break;
           }
         //--- これが行の最初の文字でない場合
         else
           {
            //--- 最初の連続シーケンスの開始が見つかった場合
            //    先頭は次のインデックスでスペースとみなされる
            if(i!=m_text_cursor_x_pos &&  
               m_lines[m_text_cursor_y_pos].m_symbol[i]!=SPACE &&  
               m_lines[m_text_cursor_y_pos].m_symbol[i-1]==SPACE)
              {
               //--- カーソルを新しい連続シーケンスの先頭に設定する
               SetTextCursor(i,m_text_cursor_y_pos);
               break;
              }
           }
        }
     }
//--- //--- テキストボックスの可視領域の境界を取得する
   CalculateBoundaries();
//--- カーソルのX座標を取得する
   CalculateTextCursorX();
//--- カーソルのY座標を取得する
   CalculateTextCursorY();
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
   else
     {
      //--- 文字の配列のサイズを取得する
      symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
      //---
      if(m_text_cursor_x_pos==symbols_total && m_text_cursor_x>=m_x2_limit)
         HorizontalScrolling(CalculateScrollThumbX2());
     }
//--- テキストカーソルが表示領域<分節から離れた場合はスクロールバーを移動する
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
//--- テキストボックスのテキストを更新する
   DrawTextAndCursor(true);
//--- 関連したメッセージを送信する
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

このセクションの冒頭で一覧できる他のすべてのメソッドの研究は、読者に任されています。 

 


コントロールのライブラリエンジンへの統合

マルチラインテキストボックスが正しく動作するためには CWndContainerクラスの WindowElements構造体にプライベート配列が必要です。CTextBoxクラスを持つファイルをWndContainer.mqhファイルにインクルードします

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#include "Controls\TextBox.mqh"

新しいコントロールのためのプライベート配列WindowElements構造体に追加します。 

//+------------------------------------------------------------------+
//| インターフェースオブジェクト格納クラス                                |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
   //--- 要素配列の構造体
   struct WindowElements
     {
      //--- マルチラインテキストボックス
      CTextBox         *m_text_boxes[];
     };
   //--- 各ウィンドウの要素配列の配列
   WindowElements    m_wnd[];
  };

CTextBox 型のコントロールはコンポジットであり、他のコントロール(この場合はスクロールバー)のコントロールを含んでいるため、これらのコントロールへのポインタが対応するプライベート配列に配布されるメソッドが必要です。以下のリストは、この目的のために設計されたCWndContainer::AddTextBoxElements() メソッドのコードを示しています。このメソッドは、他の同様のメソッドと同じ場所、つまり CWndContainer::AddToElementsArray()で呼び出されます。 

class CWndContainer
  {
private:
   //--- マルチラインテキストボックスのオブジェクトへのポインタを格納する
   bool              AddTextBoxElements(const int window_index,CElementBase &object);
  };
//+------------------------------------------------------------------+
//| マルチラインテキストボックスのオブジェクトへのポインタを格納する         |
//+------------------------------------------------------------------+
bool CWndContainer::AddTextBoxElements(const int window_index,CElementBase &object)
  {
//--- これえがマルチラインテキストボックスでない場合は終了する
   if(dynamic_cast<CTextBox *>(&object)==NULL)
      return(false);
//--- コントールへのポインタを取得する
   CTextBox *tb=::GetPointer(object);
   for(int i=0; i<2; i++)
     {
      int size=::ArraySize(m_wnd[window_index].m_elements);
      ::ArrayResize(m_wnd[window_index].m_elements,size+1);
      if(i==0)
        {
         //--- スクロールバーポインタを取得する
         CScrollV *sv=tb.GetScrollVPointer();
         m_wnd[window_index].m_elements[size]=sv;
         AddToObjectsArray(window_index,sv);
         //--- ポインタをプライベート配列に追加する
         AddToRefArray(sv,m_wnd[window_index].m_scrolls);
        }
      else if(i==1)
        {
         CScrollH *sh=tb.GetScrollHPointer();
         m_wnd[window_index].m_elements[size]=sh;
         AddToObjectsArray(window_index,sh);
         //--- ポインタをプライベート配列に追加する
         AddToRefArray(sh,m_wnd[window_index].m_scrolls);
        }
     }
//--- ポインタをプライベート配列に追加する
   AddToRefArray(tb,m_wnd[window_index].m_text_boxes);
   return(true);
  }

ここでCWndEvents::OnTimerEvent() メソッドに特定の追加を行う必要があります。グラフィカルインターフェイスはマウスカーソルが移動したときにのみ再描画され、マウスカーソルの移動が停止してから一定時間停止することを覚えておいてください。CTextBoxのコントロールは例外です。そうしないと、テキストボックスがアクティブになったときにテキストカーソルが点滅しません。 

//+------------------------------------------------------------------+
//| タイマー                                                          |
//+------------------------------------------------------------------+
void CWndEvents::OnTimerEvent(void)
  {
//--- マウスカーソルが静止していて(呼び出しの差が300ミリ秒を超える)マウスの左ボタンが離された場合は終了する
   if(m_mouse.GapBetweenCalls()>300 && !m_mouse.LeftButtonState())
     {
      int text_boxes_total=CWndContainer::TextBoxesTotal(m_active_window_index);
      for(int e=0; e<text_boxes_total; e++)
         m_wnd[m_active_window_index].m_text_boxes[e].OnEventTimer();
      //---
      return;
     }
//--- 配列が空の場合終了する  
   if(CWndContainer::WindowsTotal()<1)
      return;
//--- 全てのコントロールのイベントのタイマーでのチェック
   CheckElementsEventsTimer();
//--- チャートを再描画する
   m_chart.Redraw();
  }

さて、マルチラインテキストボックスの検証を可能にするテストMQLアプリケーションを作成しましょう。 

 


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

検証をのために、2つのテキストボックスを含むグラフィカルインタフェースを持つMQLアプリケーションを作成します。 。そのうちの1つはシングルライン、もう1つはマルチラインになります。この例でのグラフィカルインタフェースには、これらのテキストボックスに加えてコンテキストメニューとステータスバーを持つメインメニューが含まれています。ステータスバーの2番目の項目は、マルチラインテキストボックスのテキストカーソルの位置をブロードキャストします。

CTextBox クラスインスタンスを2つ作成し、テキストボックスを作成するための2つのメソッドを宣言します。

class CProgram : public CWndEvents
  {
protected:
   //--- 編集
   CTextBox          m_text_box1;
   CTextBox          m_text_box2;
   //---
protected:
   //--- 編集
   bool              CreateTextBox1(const int x_gap,const int y_gap);
   bool              CreateTextBox2(const int x_gap,const int y_gap);
  };

下のリストは、マルチラインテキストボックスを作成する2番目のメソッドのコードを示しています。マルチラインモードの有効化にはCTextBox::MultiLineMode()メソッドを使います。領域の自動的なフォームサイズに合わせた調整はCElementBase::AutoXResizeXXX()メソッドを使用して行う必要があります。例として、本稿の内容をマルチラインテキストボックスに追加しましょう。これを行うには CTextBoxクラスの特別なメソッドを使用して後でループで追加できる行の配列を準備します。 

//+------------------------------------------------------------------+
//| マルチラインテキストボックスを作成する                                |
//+------------------------------------------------------------------+
bool CProgram::CreateTextBox2(const int x_gap,const int y_gap)
  {
//--- ウィンドウポインタを格納する
   m_text_box2.WindowPointer(m_window);
//--- 作成前にプロパティを設定する
   m_text_box2.FontSize(8);
   m_text_box2.Font("Calibri"); // Consolas|Calibri|Tahoma
   m_text_box2.AreaColor(clrWhite);
   m_text_box2.TextColor(clrBlack);
   m_text_box2.MultiLineMode(true);
   m_text_box2.AutoXResizeMode(true);
   m_text_box2.AutoXResizeRightOffset(2);
   m_text_box2.AutoYResizeMode(true);
   m_text_box2.AutoYResizeBottomOffset(24);
//--- 行の配列
   string lines_array[]=
     {
      "Introduction",
      "Key groups and keyboard layouts",
      "Handling the keypress event",
      "ASCII codes of characters and control keys",
      "Key Scan Codes",
      "Auxiliary class for working with the keyboard",
      "The Multiline Text box control",
      "Developing the CTextBox class for creating the control",
      "Properties and appearance",
      "Managing the text cursor",
      "Entering a character",
      "Handling the pressing of the Backspace key",
      "Handling the pressing of the Enter key",
      "Handling the pressing of the Left and Right keys",
      "Handling the pressing of the Up and Down keys",
      "Handling the pressing of the Home and End keys",
      "Handling the simultaneous pressing of keys in combination with the Ctrl key",
      "Integration of the control in the library engine",
      "Application for testing the control",
      "Conclusion"
     };
//--- テキストボックスにテキストを追加する
   int lines_total=::ArraySize(lines_array);
   for(int i=0; i<lines_total; i++)
     {
      //--- 最初の行にテキストを追加する
      if(i==0)
         m_text_box2.AddText(0,lines_array[i]);
      //--- テキストボックスに行を追加する
      else
         m_text_box2.AddLine(lines_array[i]);
     }
//--- コントロールを作成する
   if(!m_text_box2.CreateTextBox(m_chart_id,m_subwin,x_gap,y_gap))
      return(false);
//--- オブジェクトをオブジェクトグループの共通配列に追加する
   CWndContainer::AddToElementsArray(0,m_text_box2);
//--- ステータスバーの項目にテキストを設定する
   m_status_bar.ValueToItem(1,m_text_box2.TextCursorInfo());
   return(true);
  }

テキストボックスからメッセージを受け取るために、MQLアプリケーションのイベントハンドラに次のコードを追加します。

//+------------------------------------------------------------------+
//| チャートイベントハンドラ                                            |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- (1)値を入力する(2)テキストボックスをアクティブ化する、または(3)テキストカーソルを移動するイベント
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT ||
      id==CHARTEVENT_CUSTOM+ON_CLICK_TEXT_BOX ||
      id==CHARTEVENT_CUSTOM+ON_MOVE_TEXT_CURSOR)
     {
      ::Print(__FUNCTION__," > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);

      //---識別子が一致する場合(マルチラインテキストボックスからのメッセージ)
      if(lparam==m_text_box2.Id())
        {
         //--- ステータスバーの2番目の項目の更新
         m_status_bar.ValueToItem(1,sparam);
        }
      //--- チャートを再描画する
      m_chart.Redraw();
      return;
     }
  }


アプリケーションをコンパイルしてチャートに読み込むと次のことがわかります。

 図9 テキストボックスのデモンストレーションによるグラフィカルインターフェイス

図9 テキストボックスのデモンストレーションによるグラフィカルインターフェイス

 

本稿で紹介されたテストアプリケーションをさらに研究するためには、以下のリンクを使用してダウンロードしてください。 

 


おわりに

このグラフィカルインタフェース作成ライブラリの概略は現在以下の通りに見えます。

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

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

 

次のライブラリバージョンはさらに開発され、既に実装されているコントロールには新しい機能が追加されます。テスト用のライブラリとファイルの最新バージョンは以下でダウンロードできます。

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