グラフィカルインタフェースX: マルチラインテキストボックス内のワードラップアルゴリズム(ビルド12)

Anatoli Kazharski | 15 5月, 2017


コンテンツ

はじめに

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

本稿では、マルチラインテキストボックスの開発を続けていきます。これまでの進捗状況はグラフィカルインターフェイスX:マルチラインテキストボックス(ビルド8)稿で確認できます。今回の課題は、テキストがボックス幅を超えた場合には自動的にワードラップを行い、機会が生じた場合にはワードラップを取り消してテキストを前行に収めることです。

マルチラインテキストボックスでのワードラップモード

テキストエディタとアプリケーションには、テキストがアプリケーションの領域幅を超えた場合にテキスト情報を処理するためのワードラップを備えられています。これによって、常に水平スクロールバーを使用しなければならないという問題が解決します。 

ワードラップはデフォルトでは無効になっています。その有効化にはCTextBox::WordWrapMode()メソッドを使います。このメソッドはワードラップの実装における唯一のパブリックメソッドです。その他のメソッドはすべてプライベートで、その詳細については以下で詳しく説明します。

//+------------------------------------------------------------------+
//| マルチラインテキストボックス作成クラス                                |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- ワードラップモード
   bool m_word_wrap_mode;
   //---
public:
   //--- ワードラップモード
   void WordWrapMode(const bool mode) { m_word_wrap_mode=mode; }
  };
//+------------------------------------------------------------------+
//| コンストラクタ                                                     |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_word_wrap_mode(false)

ワードラップと行へのテキストの追加を設定するには、行末記号が各行に存在する必要があります。

ここでは単純な例を示します。ワードラップを有効または無効にすることができるような任意のテキストエディタ(例:Notepad)を開いてください。その後ドキュメントに下記の1行追加します。

Google is an American multinational technology company specializing in Internet-related services and products.

テキストボックスの幅によっては、ワードラップモードが無効になっている場合この行はテキストボックスに収まらないかもしれません。その場合、行を読むには水平スクロールバーが使用されます。

 図1 ワードラップモードが無効

図1 ワードラップモードが無効

ここでワードラップを有効にします。これで、行はエディタのテキストボックス幅にはまるはずです。

 図2 ワードラップが有効

図2 ワードラップが有効

文字列が3つの部分文字列に分割されて連なることが分かります。ここでは、行末記号は3番目の部分文字列にのみ存在します。プログラムによってこのファイルの最初の行が読み取られると、行末記号までのテキスト全体が返されます。 

これは簡単なスクリプトを使って確認できます。

//+------------------------------------------------------------------+
//| スクリプトプログラム開始関数                                         |
//+------------------------------------------------------------------+
void OnStart(void)
  {
//--- ハンドルを取得する
   int file=::FileOpen("Topic 'Word wrapping'.txt",FILE_READ|FILE_TXT|FILE_ANSI);
//--- ハンドルが取得出来たらファイルを読み取る
   if(file!=INVALID_HANDLE)
      ::Print(__FUNCTION__," > ",::FileReadString(file));
   else
      ::Print(__FUNCTION__," > error: ",::GetLastError());
  }
//+------------------------------------------------------------------+

下記は最初の行(この場合は唯一の行)を読み取ってログに出力した結果です。

OnStart > Google is an American multinational technology company specializing in Internet-related services and products.

開発されたマルチラインテキストボックスからのこのような情報の読み込みを実装するにはCTextBoxクラスでStringOptions構造体(以前はKeySymbolOptions)に行末記号を 保存するための bool プロパティを追加します。

   //--- 文字とそのプロパティ
   struct StringOptions
     {
      string            m_symbol[];    // 文字
      int               m_width[];     // 文字幅
      bool              m_end_of_line; // 行末記号
     };
   StringOptions  m_lines[];

ワードラップを実装するには、いくつかの主要メソッドと補助メソッドが必要です。作業を列挙してみましょう。

主要メソッド:

  • ワードラップ
  • 最初に表示される文字と右側のスペースのインデックスを返す
  • 移動される文字数を返す
  • テキストを次の行に折り返す
  • テキストを次の行から現在の行へ折り返す

補助メソッド:

  • 指定された行の単語数を返す
  • スペース文字のインデックスを数値で返す
  • 行の移動
  • 指定された行の文字の移動
  • 渡された配列に文字をコピーして次の行に移動する
  • 渡された配列から指定された行に文字を貼り付ける

補助メソッドの構造を詳しく見てみましょう。

アルゴリズムと補助メソッドの説明

ワードラップアルゴリズムには、スペース文字のインデックスを数で検索するループを開始する必要がある瞬間があります。このようなループを整えるには、行内の単語数を決定するメソッドが必要です。下記はこの作業を担当するCTextBox::WordsTotal()メソッドのコ―ドです。

単語を数えることは非常に簡単です。1つ前の文字がスペース文字であって現在の文字がスペース文字 (' ')ではない場合、指定された行の文字の配列を表示されるパターンを追跡しながら反復処理する必要があります。これは新しい単語の始まりを示します。最後の単語を数えるために、カウンタは行の終わりに達したときにも増加されます。

class CTextBox : public CElement
  {
private:
   //--- 指定された行の単語数を返す
   uint              WordsTotal(const uint line_index);
  };
//+------------------------------------------------------------------+
//| 指定された行の単語数を返す                                           |
//+------------------------------------------------------------------+
uint CTextBox::WordsTotal(const uint line_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_symbol);
//--- 単語カウンタ
   uint words_counter=0;
//--- 指定されたインデックスでスペースを検索する
   for(uint s=1; s<symbols_total; s++)
     {
      //--- (1)行末に達した場合または(2)スペースが見つかった場合(単語の終わり)はカウントする
      if(s+1==symbols_total || (m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE))
         words_counter++;
     }
//--- 単語数を返す
   return(words_counter);
  }


スペース文字のインデックスの決定にはCTextBox::SymbolIndexBySpaceNumber()メソッドが使われます。この値が取得されると CTextBox::LineWidth() メソッドを使用して、部分文字列の先頭から初めて単語(1つまたは複数)の幅を計算することができます。 

分かりやすくするために、1行のテキストを使った例をみてみましょう。文字(青)、部分文字列(緑)、スペース(赤)は索引付けされています。たとえば、最初の(0)行の最初の(0)スペースは文字インデックス6を持つことがわかります。

 図3 文字(青)、部分文字列(緑)、スペース(赤)のインデックス

図3 文字(青)、部分文字列(緑)、スペース(赤)のインデックス

下記はCTextBox::SymbolIndexBySpaceNumber()メソッドのコ―ドです。ここではすべてがシンプルです。ループは指定された部分文字列のすべての文字を反復処理し、新しいスペース文字が見つかるたびにカウンタを増やします。反復の1つがカウンタと渡された第2引数の値で指定されたスペースインデックスとが等しいことを示すと、文字インデックス値が格納されてループが停止されます。メソッドはこの値を返します。

class CTextBox : public CElement
  {
private:
   //--- スペース文字のインデックスを数で返す 
   uint              SymbolIndexBySpaceNumber(const uint line_index,const uint space_index);
  };
//+------------------------------------------------------------------+
//| スペース文字のインデックスを数で返す                                  |
//+------------------------------------------------------------------+
uint CTextBox::SymbolIndexBySpaceNumber(const uint line_index,const uint space_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_symbol);
//--- (1) スペース文字のインデックスと (2) スペースカウンタの決定
   uint symbol_index  =0;
   uint space_counter =0;
//--- 指定されたインデックスでスペースを検索する
   for(uint s=1; s<symbols_total; s++)
     {
      //--- スペースが見つかった場合
      if(m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE)
        {
         //--- カウンタが指定されたスペースインデックスと等しい場合は、カウンタを格納してループを停止する
         if(space_counter==space_index)
           {
            symbol_index=s;
            break;
           }
         //--- スペースカウンタを増加する
         space_counter++;
        }
     }
//--- スペースインデックスが見つからない場合は行サイズを返す
   return((symbol_index<1)?symbols_total : symbol_index);
  }

ワードラップアルゴリズムの行と文字配列要素の移動に関連する部分を考えてみましょう。これをさまざまな状況で説明していきます。例えば、下記の行があるとします。

The quick brown fox jumped over the lazy dog.

この行はテキストボックスの幅にはまりません。このテキストボックスの領域は、図4に赤い四角で示されています。ラインの「あまった」部分( 'over the lazy dog.' )は次の行に移動されなければいけません。

 図4 行がテキストボックスにはまらない状態

図4 行がテキストボックスにはまらない状態

行の動的配列には現在要素が1つのみなので、配列の要素を1つ増やなければなりません。新しい行の文字の配列は、移動したテキストの文字数に応じて設定します。この後、行のはまらない部分を移動する必要があります。最終的な結果は次の通りです。

 図5 行の一部が次の行に移動

図5 行の一部が次の行に移動

ここで、テキストボックスの幅が約30%減少した場合のアルゴリズムの動作を見てみましょう。ここでは、初めに、最初の行(インデックス0)のどの部分がテキストボックスの境界を超えているかも判断します。この場合収まらなかったのは 'fox jumped'部分文字列です。次に、行の動的配列が1要素だけ増加され、下にあるすべての部分文字列が1行下にシフトされ、移動されたテキストのスロットが解放されます。その後、 'fox jumped'部分文字列は、前の節で説明したように、解放されたスロットに移動されます。この手順は次の図のとおりです。

 図6 2行目(インデックス1)へのテキストの移動

図6 2行目(インデックス1)へのテキストの移動

アルゴリズムは、ループの次の反復で次の行(インデックス1)に進みます。ここで、この行の一部が再度テキストボックスの境界を超えているかどうかを確認する必要があります。境界を超えていないことが確認された場合は、次の行の一部をインデックス2に収めるのに十分な余裕があるかどうかが確認されます。これは、次の行(インデックス2)の先頭にかけたワードラップをなくして現在の行(インデックス1)の終わりに単語を収められるかどうかを確認します。

この条件確認に加えて、現在の行に行末記号が含まれているかどうかの確認も必要です。行末記号が含まれている場合はこの「逆ワードラップ」は行われません。この例では、行末記号はなく、現在の行の終わりには1単語('over')を収めるのに十分な余地があります。「逆ワードラップ」の最中は、文字の配列のサイズは、現在の行および次の行のそれぞれに追加および抽出された文字の数によって変更されます。逆ワードラップの最中は、文字の配列のサイズを変更する前に、残りの文字が行の先頭に移動されます。次の図は、この手順を示しています。 

 図7 3番目(インデックス2)の行から2番目の(インデックス1)の行への逆ワードラップ

図7 3番目(インデックス2)の行から2番目の(インデックス1)の行への逆ワードラップ

テキストボックスの幅が狭くなると、ワードラップや逆ワードラップが行われることがわかります。一方、テキストボックスが大きくなるとと、解放された空白への逆ワードラップが十分です。テキストが次の行に折り返されるたびに、動的配列は1要素だけ増加します。そして、次の行の残りのすべてのテキストが逆方向に折り返されるたびに、行の配列は1つの要素だけ減らされます。しかし、その前に、行がもっとある場合は、残りのテキストが逆方向に折り返されたときに空行が生じないように、行を1行上に移動する必要があります。 

ループの途中で、これらの行の再調整、直接および逆ワードラップのステップは表示されません。下の図は、グラフィカルインタフェースを使用して作業するときのユーザーへの表示例を示しています。

 図8 テキストエディタの例によるワードラップアルゴリズムのデモンストレーション

図8 テキストエディタの例によるワードラップアルゴリズムのデモンストレーション

これで終わりではありません。行に1つの単語(連続する文字の並び)が残っている場合、ハイフネーションは文字単位で実行されます。この状況は下図に示されています。

 図9 単語が収まりきらないときの文字ごとのラッピングのデモンストレーション

図9 単語が収まりきらないときの文字ごとのラッピングのデモンストレーション

ここで、行や文字を移動するメソッドについて考えてみましょう。行の移動には CTextBox::MoveLines()メソッドが使われます。このメソッドには行のインデックスが渡されます。このインデックス以下の行のインデックスは1つの位置だけシフトする必要があります。3番目のパラメータはシフト方向です。デフォルトのシフトは下向きです。 

以前は'Enter' と ' 'Backspace' キーを使用してテキストボックスを制御するとき、ラインシフトアルゴリズムは反復なしで使用されていました。現在、同じコードが CTextBox クラスの複数のメソッドで使用されているため、繰り返し使用するための別個のメソッドを実装することが妥当です。

下記はCTextBox::MoveLines()メソッドのコ―ドです。

class CTextBox : public CElement
  {
private:
   //--- 線を移動する
   void              MoveLines(const uint from_index,const uint to_index,const bool to_down=true);
  };
//+------------------------------------------------------------------+
//| 行を移動する                                                       |
//+------------------------------------------------------------------+
void CTextBox::MoveLines(const uint from_index,const uint to_index,const bool to_down=true)
  {
//--- 行を下にシフトする
   if(to_down)
     {
      for(uint i=from_index; i>to_index; 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==to_index)
           {
            //--- これが最初の行である場合は終了する
            if(to_index<1)
               break;
           }
        }
     }
//--- 行を上にシフトする
   else
     {
      for(uint i=from_index; i<to_index; i++)
        {
         //--- 行の配列の次の要素のインデックス
         uint next_index=i+1;
         //--- 文字の配列のサイズを取得する
         uint symbols_total=::ArraySize(m_lines[next_index].m_symbol);
         //--- 配列のサイズを変更する
         ArraysResize(i,symbols_total);
         //--- 行のコピーを作る
         LineCopy(i,next_index);
        }
     }
  }

行内の文字の移動にはCTextBox::MoveSymbols()メソッドが実装されました。. これは、ワードラップモードに関連する新しいメソッドだけでなく、以前考慮されたCTextBox::AddSymbol()および CTextBox :: DeleteSymbol()メソッドでキーボードを使って文字を追加/削除するさいにも呼び出されます。ここで設定される入力パラメータは次のとおりです。 (1) 文字を移動する行のインデックス、(2)移動のための開始および終了文字インデックス、 (3) 移動方向(デフォルトでは左)。

class CTextBox : public CElement
  {
private:
   //--- 指定された行の文字の移動
   void              MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true);
  };
//+------------------------------------------------------------------+
//| 指定された行の文字の移動                                            |
//+------------------------------------------------------------------+
void CTextBox::MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true)
  {
//--- 文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- 差
   uint offset=from_pos-to_pos;
//--- 文字が左に移動される場合
   if(to_left)
     {
      for(uint s=to_pos; s<symbols_total-offset; s++)
        {
         uint i=s+offset;
         m_lines[line_index].m_symbol[s] =m_lines[line_index].m_symbol[i];
         m_lines[line_index].m_width[s]  =m_lines[line_index].m_width[i];
        }
     }
//--- 文字が右に移動される場合
   else
     {
      for(uint s=symbols_total-1; s>to_pos; s--)
        {
         uint i=s-1;
         m_lines[line_index].m_symbol[s] =m_lines[line_index].m_symbol[i];
         m_lines[line_index].m_width[s]  =m_lines[line_index].m_width[i];
        }
     }
  }

CTextBox :: CopyWrapSymbols()およびCTextBox::PasteWrapSymbols() メソッドなどの、文字をコピーして貼り付けるための補助メソッドのコードも頻繁に使用されます。コピー時にはCTextBox::CopyWrapSymbols() メソッドに空の動的配列が渡されます。また、指定された文字数をコピーするための行と初めの文字も示されます。文字を貼り付けるには、以前にコピーされた文字を含む配列を CTextBox::PasteWrapSymbols() メソッドに渡す必要があります。同時に、挿入が行われる行と文字のインデックスを示します 。

class CTextBox : public CElement
  {
private:
   //--- 渡された配列に文字をコピーして次の行に移動する
   void              CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[]);
   //--- 渡された配列の文字を指定された行に貼り付ける
   void              PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[]);
  };
//+------------------------------------------------------------------+
//| 渡された配列に文字をコピーして移動する                                |
//+------------------------------------------------------------------+
void CTextBox::CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[])
  {
//--- 配列サイズを設定する
   ::ArrayResize(array,symbols_total);
//--- 移動する文字を配列にコピーする
   for(uint i=0; i<symbols_total; i++)
      array[i]=m_lines[line_index].m_symbol[start_pos+i];
  }
//+------------------------------------------------------------------+
//| 指定された行に文字を貼り付ける                                       |
//+------------------------------------------------------------------+
void CTextBox::PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[])
  {
   uint array_size=::ArraySize(array);
//--- 新しい行の構造体の配列にデータを追加する
   for(uint i=0; i<array_size; i++)
     {
      uint s=start_pos+i;
      m_lines[line_index].m_symbol[s] =array[i];
      m_lines[line_index].m_width[s]  =m_canvas.TextWidth(array[i]);
     }
  }

さて、ワードアルゴリズムの主要メソッドについて考えてみましょう。

メインメソッドの説明

アルゴリズムが動作を開始すると、各行のオーバーフローはループでチェックされます。このチェックにはCTextBox::CheckForOverflow()メソッドが実装されました。このメソッドは3つの値を返します。そのうちの2つは、メソッドに参照パラメータとして渡される変数に格納されます。 

メソッドの最初には現在の行の幅を取得する必要があります。この行のインデックスは、最初のパラメータとしてメソッドに渡されます。行幅の確認には、テキストボックスの左端からのインデントと垂直スクロールバーの幅が考慮されます。行幅がテキストボックスに合っている場合はメソッドはfalseを返します。これは 「オーバーフローなし」を意味します。行が収まらない場合は、テキストボックスの右側に表示される最初の文字とスペースのインデックスを決定する必要があります。これを行うには、行の終わりから始めて行の文字をループし、行が最初からその文字までテキストボックスの幅に収まるかどうかを確認します。行が収まる場合は、文字のインデックスが格納されます。さらに、すべての反復では現在の文字がスペースであるかどうかが確認されます。スペースの場合はインデックスが格納されて検索が完了します。

これらのすべての確認と検索の後、検索されたインデックスの少なくとも1つが見つかると、このメソッドはtrueを返します。つまり行が収まらないということです。文字とスペースのインデックスは、後で次のように使用されます。文字のインデックスが見つかってもスペースのインデックスが見つからなければ、行にはスペースが含まれていないわけで、この行の文字の一部を移動する必要があります。スペースが見つかった場合は、このスペースのインデックスから始まる行の一部を移動する必要があります。

class CTextBox : public CElement
  {
private:
   //--- 表示されている最初の文字とスペースのインデックスを返す
   bool              CheckForOverflow(const uint line_index,int &symbol_index,int &space_index);
  };
//+------------------------------------------------------------------+
//| 行のオーバーフローの確認                                            |
//+------------------------------------------------------------------+
bool CTextBox::CheckForOverflow(const uint line_index,int &symbol_index,int &space_index)
  {
//--- 文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- インデント
   uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
//--- 行の全幅を取得する
   uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
//--- 行幅がテキストボックスに収まる場合
   if(full_line_width<(uint)m_area_visible_x_size)
      return(false);
//--- オーバーフローした文字のインデックスを決定する
   for(uint s=symbols_total-1; s>0; s--)
     {
      //--- (1) 部分文字列の最初から現在の文字までの幅と(2)文字を取得する
      uint   line_width =LineWidth(s,line_index)+x_offset_plus;
      string symbol     =m_lines[line_index].m_symbol[s];
      //--- 目に見える文字がまだ見つからない場合
      if(symbol_index==WRONG_VALUE)
        {
         //--- 部分文字列の幅がテキストボックス領域に収まる場合は、文字インデックスを格納する
         if(line_width<(uint)m_area_visible_x_size)
            symbol_index=(int)s;
         //--- 次の文字に移る
         continue;
        }
      //---これがスペースの場合は、インデックスを保存してループを停止する
      if(symbol==SPACE)
        {
         space_index=(int)s;
         break;
        }
     }
//---この条件が満たされていれば、行は収まらない
   bool is_overflow=(symbol_index!=WRONG_VALUE || space_index!=WRONG_VALUE);
//--- 結果を返す
   return(is_overflow);
  }

行が収まりCTextBox::CheckForOverflow()メソッドがfalseを返した場合は、逆ワードラップが可能であるかを確認する必要があります。ラップする文字数を決定するのはCTextBox::WrapSymbolsTotal()メソッドです。 

このメソッドは、参照変数にラップされる文字数と、それが残りのテキスト全部であるかその一部であるかの印を返します。ローカル変数の値は、メソッドの最初に計算されます。たとえば、パラメータには下記があります。

  • 現在の行の文字数
  • 行の全幅
  • 空き領域の幅
  • 次の行の単語数
  • 次の行の文字数

その後、次の行から現在の行に移動できる単語の数がループによって定められます。各反復では、指定されたスペースまでの部分文字列の幅を取得した後に部分文字列が現在の行の空き領域に収まるかどうかを確認します。

部分文字列が収まる場合は、文字のインデックスを格納して、ここにあと1つ単語を挿入できるかどうかを確認します。確認によってテキストが終了したことが示された場合これは専用のローカル変数にマークされ、ループが停止します。 

部分文字列が収まらない場合は、最後の文字かどうかを確認し、スペースなしの連続文字列であることを示して ループを停止することも必要です。

次に、次の行にスペースが含まれているか空き領域がない場合、メソッドはすぐに結果を返します。この条件が満たされた場合は、次の行の単語の一部を現在の行に移動できるかどうかをさらに判定します。単語の一部の逆折り返しは、この行が現在の行の空き領域に収まらなく、同時に、現在の行と次の行の最後の文字はスペースでない場合にのみ実行されます。これらの条件が満たされた場合は、次のループで移動する文字数が決まります。

class CTextBox : public CElement
  {
private:
   //--- ラップされた文字数を返す
   bool              WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total);
  };
//+------------------------------------------------------------------+
//| ボリュームサインでラップされた文字数を返す                            |
//+------------------------------------------------------------------+
bool CTextBox::WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total)
  {
//---  (1) ラップする文字数と (2)空白のない行の印
   bool is_all_text=false,is_solid_row=false;
//--- 文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- インデント
   uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
//--- 行の全幅を取得する
   uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
//--- 空き領域の幅を取得する
   uint free_space=m_area_visible_x_size-full_line_width;
//--- 次の行の単語数を取得する
   uint next_line_index =line_index+1;
   uint words_total     =WordsTotal(next_line_index);
//--- 文字の配列のサイズを取得する
   uint next_line_symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
//--- 次の行から移動する単語数を決定する(スペースで検索)
   for(uint w=0; w<words_total; w++)
     {
      //---  (1) スペースインデックスと (2) 最初の部分文字列から空白文字までの部分幅を取得する
      uint ss_index        =SymbolIndexBySpaceNumber(next_line_index,w);
      uint substring_width =LineWidth(ss_index,next_line_index);
      //--- 部分文字列が現在の行の空き領域に収まる場合
      if(substring_width<free_space)
        {
         //--- .あと1つの単語が挿入できるかどうかを確認する
         wrap_symbols_total=ss_index;
         //--- これが行全体の場合は停止する
         if(next_line_symbols_total==wrap_symbols_total)
           {
            is_all_text=true;
            break;
           }
        }
      else
        {
         //--- スペースなしの連続した行である場合
         if(ss_index==next_line_symbols_total)
            is_solid_row=true;
         //---
         break;
        }
     }
//---  (1) 空白文字を含む行、または (2) 空き領域がない場合はすぐに結果を返す
   if(!is_solid_row || free_space<1)
      return(is_all_text);
//--- 次の行の全幅を取得する
   full_line_width=LineWidth(next_line_symbols_total,next_line_index)+x_offset_plus;
//--- (1)行が収まらず、 (2) 現在の行と (3)1つ前の行の最後にスペースがない場合
   if(full_line_width>free_space && 
      m_lines[line_index].m_symbol[symbols_total-1]!=SPACE && 
      m_lines[next_line_index].m_symbol[next_line_symbols_total-1]!=SPACE)
     {
      //--- 次の行から移動する文字数を決定する
      for(uint s=next_line_symbols_total-1; s>=0; s--)
        {
         //--- 部分文字列の幅を最初から指定された文字まで取得する
         uint substring_width=LineWidth(s,next_line_index);
         //--- 部分文字列が指定されたコンテナの空き領域に収まらない場合は、次の文字に移動する
         if(substring_width>=free_space)
            continue;
         //--- 部分文字列が収まる場合は、値を格納して停止する
         wrap_symbols_total=s;
         break;
        }
     }
//--- テキスト全体を移動する必要がある場合はtrueを返す
   return(is_all_text);
  }

行が収まらない場合、テキストは CTextBox::WrapTextToNewLine() メソッドを使用して現在の行から次の行に移動されます。これは(1)自動ワードラップと(2)強制:例えば、 'Enter'キーを押したときの2つのモードで使用されます。デフォルトでは、3番目のパラメータとして自動ワードラップモードが設定されています。このメソッドの最初の2つのパラメータは、 (1) テキストを移動する行のインデックスと (2)テキストを次の(新しい)行に移動するための文字のインデックスです。 

折り返しのために移動される文字の数は、メソッドの最初に決定されます。次に、 (1) 現在の行の必要な文字数をローカルの動的配列にコピーし、(2) 現在の行と次の行の配列サイズが設定され、 (3) コピーされた文字が次の行の文字の配列に追加されます。その後、キーボードからテキストを入力するときにカーソルが折り返された文字の中にあった場合はテキストカーソルの位置を決定する必要があります。

このメソッドの最後の操作は、異なる状況で得られた結果が一意であるように、現在の行と次の行の終了記号をチェックして正しく設定することです。

1. 'Enter'キーを押した後に CTextBox::WrapTextToNewLine() が呼び出された場合、現在の行に行末記号があれば行末記号も次の行に追加されます 。現在の行に行末記号がない場合は設定し、次の行から削除する必要があります。  

2. メソッドが自動モードで呼び出されると、現在の行に行末記号がある場合は、現在の行から削除して次の行に設定する必要があります。現在の行に終了記号がない場合は、記号がないことを両方の行に設定する必要があります。 

メソッドのコ―ド:

class CTextBox : public CElement
  {
private:
   //--- 次の行にテキストを折り返す
   void              WrapTextToNewLine(const uint curr_line_index,const uint symbol_index,const bool by_pressed_enter=false);
  };
//+------------------------------------------------------------------+
//| 次の行にテキストを折り返す                                          |
//+------------------------------------------------------------------+
void CTextBox::WrapTextToNewLine(const uint line_index,const uint symbol_index,const bool by_pressed_enter=false)
  {
//--- 行内の文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- 最後の文字のインデックス
   uint last_symbol_index=symbols_total-1;
//--- 空行の場合の調整
   uint check_symbol_index=(symbol_index>last_symbol_index && symbol_index!=symbols_total)?last_symbol_index : symbol_index;
//--- 次の行のインデックス
   uint next_line_index=line_index+1;
//--- 次の行に移される文字の数
   uint new_line_size=symbols_total-check_symbol_index;
//--- 移動する文字を配列にコピーする
   string array[];
   CopyWrapSymbols(line_index,check_symbol_index,new_line_size,array);
//--- 行の構造体の配列のサイズを変更する
   ArraysResize(line_index,symbols_total-new_line_size);
//--- 新しい行の構造体の配列のサイズを変更する
   ArraysResize(next_line_index,new_line_size);
//--- 新しい行の構造体の配列にデータを追加する
   PasteWrapSymbols(next_line_index,0,array);
//--- テキストカーソルの新しい位置を決定する
   int x_pos=int(new_line_size-(symbols_total-m_text_cursor_x_pos));
   m_text_cursor_x_pos =(x_pos<0)?(int)m_text_cursor_x_pos : x_pos;
   m_text_cursor_y_pos =(x_pos<0)?(int)line_index : (int)next_line_index;
//--- Enterの押下によって呼び出しが開始されたことが示された場合
   if(by_pressed_enter)
     {
      //--- 行に終了記号がある場合は、終了記号を現在の行と次の行に設定する
      if(m_lines[line_index].m_end_of_line)
        {
         m_lines[line_index].m_end_of_line      =true;
         m_lines[next_line_index].m_end_of_line =true;
        }
      //--- そうでない場合は現在までのみ
      else
        {
         m_lines[line_index].m_end_of_line      =true;
         m_lines[next_line_index].m_end_of_line =false;
        }
     }
   else
     {
      //--- 行に終了記号がある場合は、終了記号を次の行に設定する
      if(m_lines[line_index].m_end_of_line)
        {
         m_lines[line_index].m_end_of_line      =false;
         m_lines[next_line_index].m_end_of_line =true;
        }
      //---行に終了記号がない場合は、両方の行で続行する
      else
        {
         m_lines[line_index].m_end_of_line      =false;
         m_lines[next_line_index].m_end_of_line =false;
        }
     }
  }


CTextBox::WrapTextToPrevLine()メソッドは逆ワードラップのために設定されています。それには、次の行のインデックスと現在の行に移動する文字の数が渡されます。3番目のパラメータは、残りのテキスト全体またはその一部だけを移動するかどうかを示します。デフォルトではテキストの一部の折り返し(false)が設定されています。 

メソッドの始めでは、次の行の指定された文字数がローカルの動的配列にコピーされます。次に、現在の行の文字の配列を追加された文字数だけ増やす必要があります。その後、 (1) 先にコピーされた文字が、現在の行の文字配列の新しい要素に追加され、(2) 次の行の残りの文字が配列の先頭に移動され、 (3)次の行文字の配列を抽出された文字の数だけ減少します。 

その後テキストカーソルの位置が調整されます。テキストカーソルが前の行に折り返された単語と同じ部分にあったならば、それもその部分と共に移動する必要があります。

一番最後にじゃ、残りのテキストがすべて折り返されている場合は、 (1) 現在の行に終了記号を追加し (2)すべての下の行を1つ上にシフトし(3) 、行の配列を1要素減らすことが必要です。

class CTextBox : public CElement
  {
private:
   //--- 指定された行から前の行へのテキストの折り返し
   void              WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false);
  };
//+------------------------------------------------------------------+
//| 次の行から現在の行へのテキストの折り返し                              |
//+------------------------------------------------------------------+
void CTextBox::WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false)
  {
//--- 行内の文字の配列のサイズを取得する
   uint symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
//--- 前の行のインデックス
   uint prev_line_index=next_line_index-1;
//--- 移動する文字を配列にコピーする
   string array[];
   CopyWrapSymbols(next_line_index,0,wrap_symbols_total,array);
//--- 1つ前の行内の文字の配列のサイズを取得する
   uint prev_line_symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol);
//---前の行の配列サイズを追加文字数で増やす
   uint new_prev_line_size=prev_line_symbols_total+wrap_symbols_total;
   ArraysResize(prev_line_index,new_prev_line_size);
//--- 新しい行の構造体の配列にデータを追加する
   PasteWrapSymbols(prev_line_index,new_prev_line_size-wrap_symbols_total,array);
//--- 現在の行の解放された領域に文字をシフトする
   MoveSymbols(next_line_index,wrap_symbols_total,0);
//--- 現在の行の配列サイズを抽出された文字数で減らす
   ArraysResize(next_line_index,symbols_total-wrap_symbols_total);
//--- テキストカーソルを調整する
   if((is_all_text && next_line_index==m_text_cursor_y_pos) || 
      (!is_all_text && next_line_index==m_text_cursor_y_pos && wrap_symbols_total>0))
     {
      m_text_cursor_x_pos=new_prev_line_size-(wrap_symbols_total-m_text_cursor_x_pos);
      m_text_cursor_y_pos--;
     }
//--- これが行の残りのすべてのテキストでない場合は終了する
   if(!is_all_text)
      return;
//--- 現在の行に終了記号がある場合は、前の行に終了記号を追加する
   if(m_lines[next_line_index].m_end_of_line)
      m_lines[next_line_index-1].m_end_of_line=true;
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
//--- 行を1つ上にシフトする
   MoveLines(next_line_index,lines_total-1,false);
//--- 行の配列のサイズを変更する
   ::ArrayResize(m_lines,lines_total-1);
  }

最後に、最も重要な CTextBox::WordWrap()メソッドを検討する時間です。ワードラップを操作するには、このメソッドへの呼び出しをCTextBox::ChangeTextBoxSize() メソッドに配置する必要があります。 

CTextBox::WordWrap()メソッドの初めに、マルチラインテキストボックスモードとワードラップモードが有効になっているかどうかを確認します。いずれかのメソッドが無効になっている場合、プログラムはメソッドを終了します。モードが有効になっている場合、ワードラップアルゴリズムを有効にするには、すべての行を反復処理する必要があります。ここではそれぞれの反復はCTextBox::CheckForOverflow()メソッドを使って行がテキストボックスの幅を超えるかどうかを確認します。 

  1. 行が収まっていない場合はテキストボックスの右端に最も近い空白文字が見つかったかどうかを確認します。現在の行の一部はスペース文字から始まって次の行に移動されます。スペース文字は次の行に移動されません。 したがって、スペースインデックスは1つ減らされます。次に、行の配列を1要素増加させ、下の行を1つ下にシフトします。行の一部を移動するためのインデックスがもう一度検証されます。その後、テキストが折り返されます。 
  2. 行が収まる場合は、逆ワードラップを行う必要があるかどうかを確認します。現在の行の終了記号が存在するかはこのブロックの先頭で確認されます。存在する場合、プログラムは次の反復に進みます。条件が満たされると移動する文字の数が決定され、その後、テキストは前の行に折り返されます。
//+------------------------------------------------------------------+
//| マルチラインテキストボックス作成クラス                                |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- ワードラップ
   void              WordWrap(void);
  };
//+------------------------------------------------------------------+
//| ワードラップ                                                       |
//+------------------------------------------------------------------+
void CTextBox::WordWrap(void)
  {
//--- (1) マルチラインテキストボックスモードと(2) ワードラップモードが無効な場合は終了する
   if(!m_multi_line_mode || !m_word_wrap_mode)
      return;
//--- 行の配列のサイズを取得する
   uint lines_total=::ArraySize(m_lines);
//--- テキストをテキストボックスの幅に合わせる必要があるかどうかを確認する
   for(uint i=0; i<lines_total; i++)
     {
      //--- 一番初めに目に見える(1) 文字と (2) スペースを定める
      int symbol_index =WRONG_VALUE;
      int space_index  =WRONG_VALUE;
      //--- 次の行のインデックス
      uint next_line_index=i+1;
      //--- 行が収まらない場合は、現在の行の一部を新しい行に折り返す
      if(CheckForOverflow(i,symbol_index,space_index))
        {
         //--- スペース文字は見つかった場合も折り返されない
         if(space_index!=WRONG_VALUE)
            space_index++;
         //--- 行の配列を1要素で増やす
         ::ArrayResize(m_lines,++lines_total);
         //--- 現在の位置から始めて行を1要素で下にシフトする
         MoveLines(lines_total-1,next_line_index);
         //--- テキストが移動される文字のインデックスをチェックする
         int check_index=(space_index==WRONG_VALUE && symbol_index!=WRONG_VALUE)?symbol_index : space_index;
         //--- 次の行にテキストを折り返す
         WrapTextToNewLine(i,check_index);
        }
      //--- 行が収まる場合は、逆ワードラップを行う必要があるかどうかを確認する
      else
        {
         //---  (1) この行に行末記号がある、または (2)これが最後の行である場合は抜かす
         if(m_lines[i].m_end_of_line || next_line_index>=lines_total)
            continue;
         //--- 折り返す文字の数を決定する
         uint wrap_symbols_total=0;
         //--- 次の行の残りのテキストを現在の行に折り返す必要がある場合
         if(WrapSymbolsTotal(i,wrap_symbols_total))
           {
            WrapTextToPrevLine(next_line_index,wrap_symbols_total,true);
            //--- ループでさらに使用できるように配列サイズを更新する
            lines_total=::ArraySize(m_lines);
            //--- 次のチェックのために行をスキップしないように戻る
            i--;
           }
         //--- 収まる分だけ折り返す
         else
            WrapTextToPrevLine(next_line_index,wrap_symbols_total);
        }
     }
  }


自動ワードラップのためのすべてのメソッドが考察されてきました。さて、このすべての仕組みを見てみましょう。

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

テスト用のMQLアプリケーションを作成しましょう。前のマルチラインテキストボックスの記事の既存のバージョンを使用し、アプリケーションのグラフィカルインターフェイスから1行のテキストボックスを削除します。残りは全部同じです。これがMetaTrader 5端末チャートで動作です。

図10 マルチラインテキストボックスでのワードラップのデモンストレーション 

図10 マルチラインテキストボックスでのワードラップのデモンストレーション

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

おわりに

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

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

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

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

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