图形界面 X: 多行文本框中的字词回卷算法 (集成编译 12)

Anatoli Kazharski | 15 五月, 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)


为了配置文本回卷并在一行里添加文本, 每一行都必须有一个结束标记。

这是一个简单的单行示例。打开任何文本编辑器, 其中可以启用/禁用字词回卷模式 — 例如, 记事本。在文档中添加一行:

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

如果禁用字词回卷模式, 根据文本框的宽度, 该行也许不适应文本框宽度。然后, 要阅读该行, 必须使用水平滚动条。

 图例. 1. 字词回卷模式禁用。

图例. 1. 字词回卷模式禁用。


现在, 开启字词回卷模式。该行能够适应编辑器文本框的宽度:

 图例. 2. 字词回卷模式启用。


图例. 2. 字词回卷模式启用。


可从中看出, 初始字符串被切分成三个子字符串, 它们逐一连续排列。此处, 结束标记仅存在于第三个子串中。以编程方式读取文件的第一行并返回至结束标记的整个文本。 

这可以使用简单的脚本进行检查:

//+------------------------------------------------------------------+
//| 脚本程序的 start 函数                                               |
//+------------------------------------------------------------------+
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__," > 错误: ",::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() 方法的代码即执行此任务。

字词计数很简单。它必须遍历指定行的字符数组, 跟踪形态的外观, 其中 当前字符不是空格符 (' '), 而 前一个是。这表示一个新字词即将开始。如果抵达行结束, 计数器也会增加, 以便不会跳过最后一个字词。

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++)
     {
      //--- 计数, 如果 (2) 抵达行结束, 或是 (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() 方法计算从子字符串开头开始的一个或多个字词的宽度。 

为了清晰起见, 考虑一行文本的示例。其字符 (蓝色), 子字符串 (绿色) 和空格 (红色) 已有索引。例如, 可以看出, 第一 (0) 行上的第一个 (0) 空格的字符索引为 6。

 图例. 3. 字符 (蓝色), 子字符串 (绿色) 和空格 (红色) 的索引。

图例. 3. 字符 (蓝色), 子字符串 (绿色) 和空格 (红色) 的索引。


以下是 CTextBox::SymbolIndexBySpaceNumber() 方法的代码。在此, 一切都很简单: 在循环中遍历指定子字符串的所有字符, 每次找到新的空格字符时增加计数器。如果任何遍历示意 计数器等于第二个传递参数所指定的空格索引, 则会保存字符索引值, 并停止循环。这是方法返回的值。

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. 行溢出文本框的情况。

由于行的动态数组目前由单个元素组成, 所以数组需要增加一个元素。新行中的字符数组必须根据移动文本的字符数设置。此后, 应该移动不适合的部分。最终结果:

 图例. 5. 该行的一部分被移动到下面的新行。

图例. 5. 该行的一部分被移动到下面的新行。

现在我们来看看如果文本框的宽度减少大约 30%, 算法将如何工作。在此, 它首先判断第一行 (索引 0) 的哪些部分超出文本框的边界。在这种情况下, 'fox jumped' 的子串不合适。然后, 动态数组的行增加一个元素。接着, 位于下面的所有子字符串向下移动一行, 从而为即将移动的文本让出一个空位。之后, 'fox jumped' 子字符串如上一段所述移动到空出的位置。此步骤如下图所示。

 图例. 6. 将文本移动到第二行 (索引 1)。

图例. 6. 将文本移动到第二行 (索引 1)。


算法在循环的下一次遍历时进到下一行 (索引 1)。在此, 需要检查该行的一部分是否超过文本框的边界。如果检查表明它未超过, 则需要查看这行是否有足够的空间容纳下一行与索引 2 的部分。检查文本从下一行 (索引 2) 的开始到当前行 (索引 1) 结尾的逆卷条件。

除了这种情况之外, 还需要检查当前行是否包含行结束标记。如果是这样, 则不执行字词逆卷。在这个示例中, 没有结束标记, 并且有足够的空间来反转一个字词 — 'over'。在逆卷期间, 字符数组的大小分别按照当前行和下一行所添加/提取字符数量进行修改。在逆卷期间, 在更改字符数组的大小之前, 剩余的字符将移动到行的开头。下图展示了这一步骤。 

 图例. 7. 从第三行(索引 2) 逆卷字词到第二行 (索引 1)。

图例. 7. 从第三行(索引 2) 逆卷字词到第二行 (索引 1)。


从中可看出, 当文本框区域变窄时, 将执行正向和逆向字词回卷。另一方面, 当文本框延伸时, 字词逆卷到释放出的空间就足够了。每次将文本卷绕到下一行时, 动态数组将增加一个元素。并且每次下一行的所有剩余文本都被逆卷时, 行数组减少一个元素。但在此之前, 如果前面有更多的行, 则必须向上移动一行, 以便剩余文本逆卷时清除形成的空行。 

行的所有重新排列步骤, 正向和逆向字词卷绕在循环过程中均无法看到: 下图展示的粗略示例即为操纵图形界面时用户之所见:

 图例. 8. 通过文本编辑器的示例演示字词卷绕算法。

图例. 8. 通过文本编辑器的示例演示字词卷绕算法。


这并非全部。如果一行中只剩下一个字词 (连续的字符序列), 则逐字符执行拆字。这种情况如下图所示:

 图例. 9. 演示当一个字词都不适合时, 单字符智能卷绕。


图例. 9. 演示当一个字词都不适合时, 单字符智能卷绕。

现在来研究移动行和字符的方法。方法 CTextBox::MoveLines() 将会用来移动行。方法所需传递的行索引, 从哪行 以及 至哪行 需要偏移一个位置。第三个参数是移动方向。省缺设置为向下移动。 

以前, 当使用 '回车' 和 '回退' 键控制文本框时, 就已非正式地使用了行移动算法。现在, 相同的代码在 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--)
        {
         //--- 行数组中前一个元素的索引
         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() 方法即用来检查。方法返回三个值, 其中两个值保存在变量里, 并作为引用参数传递给方法。 

在方法的开始, 有必要获取当前行的宽度, 其索引作为第一个参数传递给方法。行宽检查是从文本框左侧到垂直滚动条宽度之间的空间。如果行宽与文本框相适, 方法返回 false, 意味着 "无溢出"。如果该行不适合, 则需要确定文本框右侧第一个可见字符和空格的索引。为达此目的, 从行尾开始循环遍历字符, 并检查该行从开始到该字符是否适合文本框宽度。如果行适合, 则保存字符索引。此外, 每次遍历都会检查当前字符是否为空格。如果是, 保存其索引 且搜索完成。

所有这些检查和搜索之后, 如果找到至少一个所寻找的索引, 方法将返回 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()。 

方法返回的回卷字符数存至引用变量中, 并标记是否尚有剩余文本或仅仅是其一部分。局部变量的值在方法开头计算, 例如以下参数:

  • 当前行中的字符数
  • 行的完整宽度
  • 可用空间
  • 下一行中的字词数
  • 下一行中的字符数

之后, 在一个循环中判断将有多少字词会从下一行移动到当前行。在每次迭代中, 在获得直到指定空格的子字符串宽度之后, 检查子串是否适合当前行的空余区域。

如果适合, 保存字符索引, 并检查是否可以在这里插入另一个字词。如果文本检查已经结束, 则 将在专用的局部变量中标记, 且循环停止。 

如果子字符串不适合, 那么还需要检查它是否是行中的最后一个字符, 放置一个标记, 表示它是中间没有空格的连续字符串, 并停止循环。

然后, 如果下一行包含空格或没有可用空间, 方法将立即返回结果。在检查已通过的情况下, 进一步判断来自下一行的字词的一部分是否可以移动到当前行。仅当 行不适合当前行的可用空间, 同时, 当前行和下一行的最后一个字符不是空格 时, 才会执行字词一部分的逆卷。在这些检查通过的情况下, 下一个循环将判断要移动的字符数。

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)
        {
         //--- ...检查是否可以插入其它字词
         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) 前行末尾没有空格
   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) 强制: 例如, 按 "回车" 键。省缺时, 设定自动字词回卷模式 作为第三个参数。方法的前两个参数是 (1) 移动文本的开始行索引, 和 (2) 字符的索引, 从将要移动到下一行 (新行) 的文本起始位置。 

因回卷要移动的字符数 会在方法开始处进行判断。之后, (1) 将当前行的所需字符数量复制到本地动态数组, (2) 设置当前行和下一行的数组大小, (3) 将复制的字符添加到 下一行的字符数组。之后, 如果回卷的字符是从键盘输入的文本, 则必须 确定文本光标的位置

方法的最后一个操作是检查并正确设置当前和下一行的结束标记, 因为在不同情况下获得的结果应该是唯一的。

1. 如果按下 "回车" 键后调用了 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;
//--- 如果指示此调用是通过按回车键启动
   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() 方法设计用于字词逆卷。它需要传递下一行的索引, 以及要移动到当前行的字符数。第三个参数表示是否为全部剩余文本, 或仅部分被移动。省缺设置为部分文本回卷 (false)。 

在方法的开头, 下一行的指定字符数被复制到本地动态数组。然后, 当前行字符的数组必须按照添加的字符数增加。之后, (1) 先前复制的字符被添加到当前行的字符数组的新元素中; (2) 下一行的剩余字符将移动到数组的开头; (3) 下一行的字符数组按照提取字符数减少。 

稍后, 文本光标的位置必须调整。如果它位于卷回到前一行的字词的相同部分, 那么它也必须与该部分一起移动。

在最末端, 如果所有剩余的文本均回卷, 必须 (1) 将结束标记添加到当前行, (2) 将下层所有行向上移动一个位置, 并 (3) 将行数组减少一个元素。

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);
//--- 获取前一行中字符数组的大小
   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);
//--- 向上移一行
   MoveLines(next_line_index,lines_total-1,false);
//--- 调整行数组
   ::ArrayResize(m_lines,lines_total-1);
  }

终于要研究最后且最重要的方法 — CTextBox::WordWrap()。为了使字词回卷具有可操作性, 必须要在 CTextBox::ChangeTextBoxSize() 方法里放置此方法的调用。 

CTextBox::WordWrap() 方法的开头, 检查是否启用了多行文本框和字词回卷模式。如果其中一种方法被禁用, 程序将离开该方法。如果启用这些模式, 则需要遍历所有行以便激活文本回卷算法。在此, 每次遍历使用 CTextBox::CheckForOverflow() 方法来检查是否有某行溢出文本框宽度。 

  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++;
         //--- 行数组增加一个元素
         ::ArrayResize(m_lines,++lines_total);
         //--- 从当前位置开始向下移动一行
         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 应用程序进行测试。我们将从上一篇多行文本框的文章中取用现成版本, 从应用程序的图形界面中删除单行文本框。所有东西均保留原样。所有这一切在 MetaTrader 5 终端里的图表上是如何工作的:

图例. 10. 在多行文本框控件中演示文字回卷 

图例. 10. 在多行文本框控件中演示文字回卷


文章中的测试应用程序可从以下链接下载, 以便进一步学习。


结论

目前, 用于创建图形界面的函数库的一般原理图如下所示:

 图例. 11. 当前开发阶段的函数库结构。


图例. 11. 当前开发阶段的函数库结构。


您可以下载最新版本的函数库和文件以便进行测试。

如果您在使用这些文件中提供的材料时有任何疑问, 可以参考函数库开发系列文章之一的详细描述, 或在本文的评论中提出您的问题。