内容
- 概述
- 多行文本框中的字词回卷
- 算法和辅助方法的描述
- 主要方法的描述
- 用于测试控件的应用程序
- 结论
概述
第一篇文章 图形界面 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. 字词回卷模式禁用。
现在, 开启字词回卷模式。该行能够适应编辑器文本框的宽度:
图例. 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. 字符 (蓝色), 子字符串 (绿色) 和空格 (红色) 的索引。
以下是 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. 行溢出文本框的情况。
由于行的动态数组目前由单个元素组成, 所以数组需要增加一个元素。新行中的字符数组必须根据移动文本的字符数设置。此后, 应该移动不适合的部分。最终结果:
图例. 5. 该行的一部分被移动到下面的新行。
现在我们来看看如果文本框的宽度减少大约 30%, 算法将如何工作。在此, 它首先判断第一行 (索引 0) 的哪些部分超出文本框的边界。在这种情况下, 'fox jumped' 的子串不合适。然后, 动态数组的行增加一个元素。接着, 位于下面的所有子字符串向下移动一行, 从而为即将移动的文本让出一个空位。之后, 'fox jumped' 子字符串如上一段所述移动到空出的位置。此步骤如下图所示。
图例. 6. 将文本移动到第二行 (索引 1)。
算法在循环的下一次遍历时进到下一行 (索引 1)。在此, 需要检查该行的一部分是否超过文本框的边界。如果检查表明它未超过, 则需要查看这行是否有足够的空间容纳下一行与索引 2 的部分。检查文本从下一行 (索引 2) 的开始到当前行 (索引 1) 结尾的逆卷条件。
除了这种情况之外, 还需要检查当前行是否包含行结束标记。如果是这样, 则不执行字词逆卷。在这个示例中, 没有结束标记, 并且有足够的空间来反转一个字词 — 'over'。在逆卷期间, 字符数组的大小分别按照当前行和下一行所添加/提取字符数量进行修改。在逆卷期间, 在更改字符数组的大小之前, 剩余的字符将移动到行的开头。下图展示了这一步骤。
图例. 7. 从第三行(索引 2) 逆卷字词到第二行 (索引 1)。
从中可看出, 当文本框区域变窄时, 将执行正向和逆向字词回卷。另一方面, 当文本框延伸时, 字词逆卷到释放出的空间就足够了。每次将文本卷绕到下一行时, 动态数组将增加一个元素。并且每次下一行的所有剩余文本都被逆卷时, 行数组减少一个元素。但在此之前, 如果前面有更多的行, 则必须向上移动一行, 以便剩余文本逆卷时清除形成的空行。
行的所有重新排列步骤, 正向和逆向字词卷绕在循环过程中均无法看到: 下图展示的粗略示例即为操纵图形界面时用户之所见:
图例. 8. 通过文本编辑器的示例演示字词卷绕算法。
这并非全部。如果一行中只剩下一个字词 (连续的字符序列), 则逐字符执行拆字。这种情况如下图所示:
图例. 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(), 那么如果当前行有一个行结束标记, 则行结束标记也会添加到 下一行。如果当前行没有行结束标记, 则必须在当前行中设置并从下一行中删除。
方法的代码:
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() 方法来检查是否有某行溢出文本框宽度。
- 如果某行不适合, 则查看是否找到最靠近文本框右侧的空格。当前行从该空格符开始的一部分将被移到下一行。空格符不会移到下一行; 所以, 空格索引递增。然后, 行数组增加一个元素, 并且下层的行会被向下移动一个位置。移动部分行的索引被再次验证。之后, 文本被回卷。
- 如果行适合, 则检查是否应该进行字词逆卷。在此模块的开始处检查当前行的结束标记。如果存在, 则程序进行下一次迭代。如果检查通过, 判断要移动的字符数, 之后文字被卷回到前一行。
//+------------------------------------------------------------------+ //| 创建多行文本的类 | //+------------------------------------------------------------------+ 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. 在多行文本框控件中演示文字回卷
文章中的测试应用程序可从以下链接下载, 以便进一步学习。
结论
目前, 用于创建图形界面的函数库的一般原理图如下所示:
图例. 11. 当前开发阶段的函数库结构。
您可以下载最新版本的函数库和文件以便进行测试。
如果您在使用这些文件中提供的材料时有任何疑问, 可以参考函数库开发系列文章之一的详细描述, 或在本文的评论中提出您的问题。