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

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

第 3/3 篇

文本框回卷时行尾标记与光标怎么跟着动

在自定义文本框控件里,文字回卷不是简单挪字符,行对象的 m_end_of_line 标记必须同步重算,否则后续绘制会判定错换行位置。 当回卷方向是把下一行并回当前行时,逻辑分两种:若当前行原本就带结束标记,那么当前行和下一行都要置为 true,表示这两行在结构上都算段尾;若没有,则只把当前行标 true,下一行 false,维持连续文本流。 反向操作(把当前行拆给下一行)时,若当前行有结束标记,则自身改 false、下一行改 true,让段尾顺延;若无标记,两行均 false,纯属中间换行。 光标跟随是另一个易错点:当回卷行正好是文字光标所在行,且不是全量回卷时,x 坐标要按 new_prev_line_size - (wrap_symbols_total - m_text_cursor_x_pos) 重定位,y 坐标减 1。实测若漏掉这段,在 MT5 里连续退格到行首会出现光标跳到错误行、输入框错位。 外汇与贵金属图表挂这类自定义控件属高风险改造,建议先在策略测试器里用纯文本框样例验证回卷逻辑,再接实时报价。

MQL5 / C++
  class=class="str">"cmt">//--- 如果该行有一个结束标记, 则将结束标记设置到当前和下一行
  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;
    }
  class=class="str">"cmt">//--- 若没有, 则仅在当前行
  else
    {
     m_lines[line_index].m_end_of_line      =true;
     m_lines[next_line_index].m_end_of_line =class="kw">false;
    }
   }
  else
   {
   class=class="str">"cmt">//--- 如果该行有一个结束标记, 则继续, 并将该标记设置到下一行
   if(m_lines[line_index].m_end_of_line)
     {
      m_lines[line_index].m_end_of_line      =class="kw">false;
      m_lines[next_line_index].m_end_of_line =true;
     }
   class=class="str">"cmt">//--- 如果该行没有结束标记, 则两行均继续
   else
     {
      m_lines[line_index].m_end_of_line      =class="kw">false;
      m_lines[next_line_index].m_end_of_line =class="kw">false;
     }
   }
}
class CTextBox : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 将指定行中的文本回卷到前一行
  class="type">void              WrapTextToPrevLine(class="kw">const class="type">uint next_line_index,class="kw">const class="type">uint wrap_symbols_total,class="kw">const class="type">bool is_all_text=class="kw">false);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 将下一行的文本卷回到当前行                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTextBox::WrapTextToPrevLine(class="kw">const class="type">uint next_line_index,class="kw">const class="type">uint wrap_symbols_total,class="kw">const class="type">bool is_all_text=class="kw">false)
  {
class=class="str">"cmt">//--- 获取行内字符数组的大小
   class="type">uint symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
class=class="str">"cmt">//--- 前一行的索引
   class="type">uint prev_line_index=next_line_index-class="num">1;
class=class="str">"cmt">//--- 将要移动的字符复制到数组中
   class="type">class="kw">string array[];
   CopyWrapSymbols(next_line_index,class="num">0,wrap_symbols_total,array);
class=class="str">"cmt">//--- 获取前一行中字符数组的大小
   class="type">uint prev_line_symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol);
class=class="str">"cmt">//--- 按照添加的字符数增加前一行的数组大小
   class="type">uint new_prev_line_size=prev_line_symbols_total+wrap_symbols_total;
   ArraysResize(prev_line_index,new_prev_line_size);
class=class="str">"cmt">//--- 将数据添加到新行的结构数组中
   PasteWrapSymbols(prev_line_index,new_prev_line_size-wrap_symbols_total,array);
class=class="str">"cmt">//--- 将字符移到当前行中的可用区域
   MoveSymbols(next_line_index,wrap_symbols_total,class="num">0);
class=class="str">"cmt">//--- 按照提取字符数减少当前行的数组大小
   ArraysResize(next_line_index,symbols_total-wrap_symbols_total);
class=class="str">"cmt">//--- 调整文字光标
   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>class="num">0))
     {
      m_text_cursor_x_pos=new_prev_line_size-(wrap_symbols_total-m_text_cursor_x_pos);
      m_text_cursor_y_pos--;
     }
class=class="str">"cmt">//--- 如果此非行内剩余的全部文本, 离开
   if(!is_all_text)
      class="kw">return;

「文本框控件里的换行与回卷逻辑」

在 MT5 自定义图形库里,多行文本框的删除与回卷是两套独立路径。删除行时,若下一行带结束标记,需把标记回退到前一行,否则文本结构会断;随后用 ArraySize 取行数,MoveLines 上移,再 ArrayResize 缩减数组。 CTextBox 类继承了 CElement,私有方法 WordWrap 负责按控件宽度折行。只要多行模式或字词回卷任一关闭,函数直接 return,不做任何重排。 回卷核心在 for 循环逐行检查:CheckForOverflow 返回真时,优先用空格索引断行,找不到空格才按字符索引硬切。ArrayResize 把行数加 1,MoveLines 下推,WrapTextToNewLine 搬文本。 没超宽的行也并非静止——若本行无结束标记且不是末行,会进入逆卷判断,统计可回卷字符数尝试把下一行内容提上来,保持右边界对齐。外汇与贵金属图表挂这类控件时,需注意高频重绘可能拖慢 tick 响应,属高风险界面开销。

MQL5 / C++
class=class="str">"cmt">//--- 如果当前行有结束标记, 则将结束标记添加到前一行
   if(m_lines[next_line_index].m_end_of_line)
      m_lines[next_line_index-class="num">1].m_end_of_line=true;
class=class="str">"cmt">//--- 获取行数组大小
   class="type">uint lines_total=::ArraySize(m_lines);
class=class="str">"cmt">//--- 向上移一行
   MoveLines(next_line_index,lines_total-class="num">1,class="kw">false);
class=class="str">"cmt">//--- 调整行数组
   ::ArrayResize(m_lines,lines_total-class="num">1);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建多行文本的类                                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTextBox : class="kw">public CElement
   {
class="kw">private:
   class=class="str">"cmt">//--- 字词回卷
   class="type">void              WordWrap(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 字词回卷                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTextBox::WordWrap(class="type">void)
   {
class=class="str">"cmt">//--- 如果 (class="num">1) 多行文本框, 且 (class="num">2) 字词回卷模式禁用, 离开
   if(!m_multi_line_mode || !m_word_wrap_mode)
      class="kw">return;
class=class="str">"cmt">//--- 获取行数组大小
   class="type">uint lines_total=::ArraySize(m_lines);
class=class="str">"cmt">//--- 检查是否需要将文本调整为文本框宽度
   for(class="type">uint i=class="num">0; i<lines_total; i++)
     {
     class=class="str">"cmt">//--- 判断第一个可见 (class="num">1) 字符和 (class="num">2) 空格
     class="type">int symbol_index =WRONG_VALUE;
     class="type">int space_index  =WRONG_VALUE;
     class=class="str">"cmt">//--- 下一行的索引
     class="type">uint next_line_index=i+class="num">1;
     class=class="str">"cmt">//--- 如果行不适合, 则将当前行的一部分回卷到新行
     if(CheckForOverflow(i,symbol_index,space_index))
       {
       class=class="str">"cmt">//--- 如果找到空格符, 则不必回卷
       if(space_index!=WRONG_VALUE)
          space_index++;
       class=class="str">"cmt">//--- 行数组增加一个元素
       ::ArrayResize(m_lines,++lines_total);
       class=class="str">"cmt">//--- 从当前位置开始向下移动一行
       MoveLines(lines_total-class="num">1,next_line_index);
       class=class="str">"cmt">//--- 检查字符的索引, 从其位置移动文本
       class="type">int check_index=(space_index==WRONG_VALUE && symbol_index!=WRONG_VALUE)? symbol_index : space_index;
       class=class="str">"cmt">//--- 将文本回卷到新行
       WrapTextToNewLine(i,check_index);
       }
     class=class="str">"cmt">//--- 如果行适合, 则检查是否应该进行字词逆卷
     else
       {
       class=class="str">"cmt">//--- 如果 (class="num">1) 此行有行尾标记, 或 (class="num">2) 此为最后一行, 跳过
       if(m_lines[i].m_end_of_line || next_line_index>=lines_total)
          class="kw">continue;
       class=class="str">"cmt">//--- 判断要回卷的字符数
       class="type">uint wrap_symbols_total=class="num">0;

◍ 回卷判定与索引回退的坑

在自定义文本面板里做自动换行时,先调用 WrapSymbolsTotal(i, wrap_symbols_total) 判断当前行末尾能不能把下一行的前几个符号拉回来。若返回 true,就执行带强制标记的 WrapTextToPrevLine(next_line_index, wrap_symbols_total, true),把剩余文本卷到上一行。 卷完之后必须重取行数:lines_total=::ArraySize(m_lines),否则循环用的上界还是旧值,可能越界或漏检。 这里有个容易忽略的点——回卷后要把循环变量 i 减一。因为下一行已经被合并到当前行,若不回退一步,下一次循环会直接跳过原本该检查的那个新行首,造成排版断行错误。外汇与贵金属图表上的浮动文本标签若用这套逻辑,渲染错位会直接影响盯盘判断,属高风险展示层问题。 若不适合回卷,走 else 分支调用 WrapTextToPrevLine(next_line_index, wrap_symbols_total) 做普通处理即可,不带强制标记。

MQL5 / C++
  class=class="str">"cmt">//--- 是否有必要将下一行的剩余文本卷回到当前行
  if(WrapSymbolsTotal(i,wrap_symbols_total))
    {
    WrapTextToPrevLine(next_line_index,wrap_symbols_total,true);
    class=class="str">"cmt">//--- 更新数组大小以便在循环中进一步使用
    lines_total=::ArraySize(m_lines);
    class=class="str">"cmt">//--- 后退一步, 以避免跳过下一个检查的行
    i--;
    }
  class=class="str">"cmt">//--- 仅在适合时回卷
  else
    WrapTextToPrevLine(next_line_index,wrap_symbols_total);

拿掉单行框做多行回卷实测

在 MT5 里验证控件行为,最省事的办法是直接复用上一篇多行文本框的现成界面,把单行文本框从图形资源里删掉,其余布局与事件逻辑原样保留。这样你打开图表后只会看到多行框在接管输入与回显。 官方示例图例 10 展示了文字在框内自动回卷的现象:当字符串长度超过控件可视宽度,字符会按词边界折到下一行,而不是被截断或横向溢出。 该测试用 EA/脚本可从原文章附件链接下载,放到 MQL5/Experts 或 Scripts 目录编译即可。外汇与贵金属品种上挂这个面板属于高风险操作环境,仅建议用模拟账户先跑通渲染与消息循环。

「记住这一条就够了」

这套 GUI 函数库当前的结构已能支撑面板创建、文本控件与基础交互,最新压缩包 easyandfastgui_mql5.zip 体积 584.2 KB,可直接下到 MT5 里跑。 原文里那段字数统计逻辑值得留意的只有一点:循环从第二个字符开始,遇行尾或「非空格紧跟空格」才把 words_counter 加一,这意味着首词不会被自动计入,自己移植时要补上 s==0 的边界。 外汇与贵金属行情波动剧烈、杠杆风险高,这类界面库只解决交互效率,不替代任何交易决策。把库下回来、按上面那段代码改一处边界,比反复看文章更有用。

MQL5 / C++
  for(class="type">uint s=class="num">1; s<symbols_total; s++)
    {
      class=class="str">"cmt">//--- 计数,如果(class="num">2) дошли до конца строки или (class="num">2)  нашли пробел (конец слова)
      if(s+class="num">1==symbols_total || (m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-class="num">1]==SPACE))
        words_counter++;
    }
class=class="str">"cmt">//--- 返回字数
  class="kw">return(words_counter);

常见问题

回卷后需按可见行重算字符索引,并把光标位置绑定到回卷后的行尾标记,而不是原始缓冲下标,否则会跳到错误位置。
这是回卷算法插入的软换行符未被渲染层忽略导致的;判定回卷时应打内部标记,绘制和命中测试时跳过它。
可以。把控件代码贴给小布,它能对照回卷与索引回退的常见错误点指出光标绑定漏洞,并给修正建议。
漏掉连续空格或标点独占行首的情况;回退时要跳过不可断行字符,否则会出现行首悬挂或无限回卷。
最少验证:长文本粘贴后行数变化、中途插入字符的实时回卷、以及行尾点击光标的落点是否准确这三项。