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

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

第 2/3 篇

文本框里字符左右搬家的实现

在自定义文本框控件里,编辑一行文字往往不是简单覆盖,而是要把某个位置之后的字符整体左移或右移。MQL5 里这段 MoveSymbols 方法就专门干这个:给定行号、源位置和目标位置,再把 to_left 开关打开,就能把字符往左挤。 左移时循环从 to_pos 跑到 symbols_total-offset,把后面的字符和宽度逐个往前填;右移则反向从尾部遍历,把字符往后挪。offset 就是 from_pos 减 to_pos 的差值,决定挪动跨度。 与行级移动配合的还有上层逻辑:当文本行需要向上移动时,代码会从 from_index 循环到 to_index,对每一行取下一行大小、调 ArraysResize 再 LineCopy,等于把下一行内容整体抄上来。 在 MT5 里建个 CTextBox 派生类,挂上这段 MoveSymbols,改 from_pos 和 to_pos 两个参数就能观察光标插入或删除时字符重排的实际表现,外汇或贵金属图表上叠加此类控件需注意高频重绘带来的性能与滑点风险。

MQL5 / C++
if(prev_index==to_index)
  {
  class=class="str">"cmt">//--- 如果这是第一行, 离开
  if(to_index<class="num">1)
    class="kw">break;
  }
  }
class=class="str">"cmt">//--- 向上移动行
 else
  {
  for(class="type">uint i=from_index; i<to_index; i++)
   {
   class=class="str">"cmt">//--- 行数组中下一个元素的索引
   class="type">uint next_index=i+class="num">1;
   class=class="str">"cmt">//--- 获取字符数组的大小
   class="type">uint symbols_total=::ArraySize(m_lines[next_index].m_symbol);
   class=class="str">"cmt">//--- 调整数组大小
   ArraysResize(i,symbols_total);
   class=class="str">"cmt">//--- 制作行副本
   LineCopy(i,next_index);
   }
  }
class CTextBox : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 移动指定行中的字符
  class="type">void                MoveSymbols(class="kw">const class="type">uint line_index,class="kw">const class="type">uint from_pos,class="kw">const class="type">uint to_pos,class="kw">const class="type">bool to_left=true);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 移动指定行中的字符                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTextBox::MoveSymbols(class="kw">const class="type">uint line_index,class="kw">const class="type">uint from_pos,class="kw">const class="type">uint to_pos,class="kw">const class="type">bool to_left=true)
  {
class=class="str">"cmt">//--- 获取字符数组的大小
  class="type">uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
class=class="str">"cmt">//--- 差值
  class="type">uint offset=from_pos-to_pos;
class=class="str">"cmt">//--- 如果字符要向左移动
  if(to_left)
    {
    for(class="type">uint s=to_pos; s<symbols_total-offset; s++)
      {
      class="type">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];
      }
    }
class=class="str">"cmt">//--- 如果字符要向右移动
  else
    {
    for(class="type">uint s=symbols_total-class="num">1; s>to_pos; s--)
      {
      class="type">uint i=s-class="num">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];
      }
    }
  }
class CTextBox : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 将字符复制到传递数组以便移动到下一行

◍ 文本框换行时的字符搬运逻辑

在自绘文本框做自动换行时,核心动作是把某一行尾部多余字符先抠出来、再贴到下一行开头。下面这段 CTextBox 的成员函数就是干这个的,直接在 MT5 里建个脚本挂上就能验证字符数组的搬运结果。 CopyWrapSymbols 负责把指定行从 start_pos 开始的 symbols_total 个字符塞进外部传进来的 array,先 ArrayResize 定长再逐个赋值。PasteWrapSymbols 则反向操作:按 array 的长度,把字符写到目标行 start_pos 起的位置,同时用 TextWidth 把每个符号的像素宽存进 m_width,供后续绘制对齐用。 注意两个函数都靠 uint 下标遍历,symbols_total 或 array 尺寸算错就会越界写崩 m_lines。外汇与贵金属图表上跑这类自绘控件时行情跳动频繁,数组越界可能引发指标卡死,属于高风险操作,建议先在 EURUSD 1 分钟图上单字符压测。

MQL5 / C++
class="type">void CopyWrapSymbols(class="kw">const class="type">uint line_index,class="kw">const class="type">uint start_pos,class="kw">const class="type">uint symbols_total,class="type">class="kw">string &array[]);
class=class="str">"cmt">//--- 将字符从传递数组粘贴到指定的行
class="type">void PasteWrapSymbols(class="kw">const class="type">uint line_index,class="kw">const class="type">uint start_pos,class="type">class="kw">string &array[]);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 将字符复制到传递数组以便移动                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTextBox::CopyWrapSymbols(class="kw">const class="type">uint line_index,class="kw">const class="type">uint start_pos,class="kw">const class="type">uint symbols_total,class="type">class="kw">string &array[])
  {
class=class="str">"cmt">//--- 设置数组大小
   ::ArrayResize(array,symbols_total);
class=class="str">"cmt">//--- 将要移动的字符复制到数组中
   for(class="type">uint i=class="num">0; i<symbols_total; i++)
      array[i]=m_lines[line_index].m_symbol[start_pos+i];
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 将字符粘贴到指定的行                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTextBox::PasteWrapSymbols(class="kw">const class="type">uint line_index,class="kw">const class="type">uint start_pos,class="type">class="kw">string &array[])
  {
   class="type">uint array_size=::ArraySize(array);
class=class="str">"cmt">//--- 将数据添加到新行的结构数组中
   for(class="type">uint i=class="num">0; i<array_size; i++)
     {
      class="type">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]);
     }
  }

「文本框自动换行的溢出判定与回卷逻辑」

多行文本框在 MT5 自定义控件里最麻烦的不是画字,而是行宽超出可见区域后怎么断行。核心入口是 CheckForOverflow(),它拿行索引、字符索引和空格索引三个参数,后两个以引用传出,调用方据此决定是整段后移还是按词回卷。 方法先算当前行从文本框左缘到垂直滚动条之间的可用宽度:full_line_width = LineWidth(symbols_total, line_index) + 文本左缩进 + 滚动条宽。若 full_line_width < m_area_visible_x_size,直接 return false,表示无溢出,这一行不用动。 一旦溢出,就从行尾反向遍历字符,每步重算 LineWidth(s, line_index) + 偏移。首个令子串宽度小于可见区域的字符索引存入 symbol_index;若途经空格则把空格索引也存下并中止搜索。返回 true 即代表该行放不下。 两种结果导向不同动作:只拿到 symbol_index 没空格,说明是无空格长串,只能硬切一部分字符去下一行;拿到空格索引就从该空格后截断,空格本身不随移。 行若未溢出,还要查上一行有没有剩余词可逆卷回来,WrapSymbolsTotal() 会算下一行能搬回几个词。只有当前行有空位、下一行末尾非空格且子串放得下时才回卷,否则保持原样。 真正执行搬移的是 WrapTextToNewLine(),自动模式(省缺)与回车强制模式对行结束标记处理不同:强制模式下当前行有结束符则下一行也补,无则当前行设、下一行删;自动模式当前行有结束符就移到下一行,无则两行都不设。这些标记一致性直接决定后续绘制不重不漏。

MQL5 / C++
<span class="keyword">class</span> CTextBox : <span class="keyword">class="kw">public</span> CElement
&nbsp;&nbsp;{
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 返回第一个可见字符和空格的索引</span>
&nbsp;&nbsp; <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CheckForOverflow(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">uint</span> line_index,<span class="keyword">class="type">int</span> &amp;symbol_index,<span class="keyword">class="type">int</span> &amp;space_index);
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| 检查行溢出&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">bool</span> CTextBox::CheckForOverflow(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">uint</span> line_index,<span class="keyword">class="type">int</span> &amp;symbol_index,<span class="keyword">class="type">int</span> &amp;space_index)
&nbsp;&nbsp;{
<span class="comment">class=class="str">"cmt">//--- 获取字符数组的大小</span>
&nbsp;&nbsp; <span class="keyword">class="type">uint</span> symbols_total=::<span class="functions">ArraySize</span>(m_lines[line_index].m_symbol);
<span class="comment">class=class="str">"cmt">//--- 缩进</span>
&nbsp;&nbsp; <span class="keyword">class="type">uint</span> x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
<span class="comment">class=class="str">"cmt">//--- 获取行的完整宽度</span>
&nbsp;&nbsp; <span class="keyword">class="type">uint</span> full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
<span class="comment">class=class="str">"cmt">//--- 如果行宽符合文本框</span>
&nbsp;&nbsp; <span class="keyword">if</span>(full_line_width&lt;(<span class="keyword">class="type">uint</span>)m_area_visible_x_size)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="macro">class="kw">false</span>);
<span class="comment">class=class="str">"cmt">//--- 确定溢出字符的索引</span>
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">uint</span> s=symbols_total-<span class="number">class="num">1</span>; s&gt;<span class="number">class="num">0</span>; s--)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- 获取 (class="num">1) 从开始至当前字符的子字符串宽度, 以及 (class="num">2) 字符</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span>&nbsp;&nbsp; line_width =LineWidth(s,line_index)+x_offset_plus;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span> symbol&nbsp;&nbsp;&nbsp;&nbsp; =m_lines[line_index].m_symbol[s];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- 如果未找到可见字符</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(symbol_index==<span class="macro">WRONG_VALUE</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 如果子字符串宽度适合文本框区域, 则存储字符索引</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(line_width&lt;(<span class="keyword">class="type">uint</span>)m_area_visible_x_size)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;symbol_index=(<span class="keyword">class="type">int</span>)s;

文本框回卷时怎么算该挪多少字符

在自绘文本框控件里,换行不是简单按固定列数切,而是看当前行还剩多少可见宽度。WrapSymbolsTotal 这个函数就是干这事:先拿 full_line_width 和 m_area_visible_x_size 算出 free_space,再拿下一行的词逐个试,能塞进剩余宽度的就标记为可回卷。 代码里用 WordsTotal 拿到下一行词数,循环里调 SymbolIndexBySpaceNumber 取第 w 个空格的索引,再用 LineWidth 算从行首到该空格的子串宽度。只要 substring_width < free_space,就把 ss_index 记进 wrap_symbols_total,继续看能不能再塞一个词。 如果下一行整行都被搬完(next_line_symbols_total == wrap_symbols_total),置 is_all_text 并 break;要是遇到没有空格的连续串(ss_index == next_line_symbols_total),标 is_solid_row 也直接 break。这两处短路判断决定回卷是「整行搬」还是「硬截断」。 实际在 MT5 里调文本框宽度参数时,free_space 变小会直接让回卷点前移,连续无空格行会触发 is_solid_row 走截断逻辑,长单词可能被劈开。外汇贵金属面板做自绘 HUD 时,这种细节容易导致文字溢出,建议开 MT5 用不同 m_area_visible_x_size 跑一遍验证。

MQL5 / C++
class="type">bool CTextBox::WrapSymbolsTotal(class="kw">const class="type">uint line_index,class="type">uint &wrap_symbols_total)
  {
class=class="str">"cmt">//--- 标记为 (class="num">1) 回卷的字符数, 以及 (class="num">2) 无空格的行
  class="type">bool is_all_text=class="kw">false,is_solid_row=class="kw">false;
class=class="str">"cmt">//--- 获取字符数组的大小
  class="type">uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
class=class="str">"cmt">//--- 缩进
  class="type">uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
class=class="str">"cmt">//--- 获取行的完整宽度
  class="type">uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
class=class="str">"cmt">//--- 获取可用空间的宽度
  class="type">uint free_space=m_area_visible_x_size-full_line_width;
class=class="str">"cmt">//--- 获取下一行中的字词数
  class="type">uint next_line_index =line_index+class="num">1;
  class="type">uint words_total     =WordsTotal(next_line_index);
class=class="str">"cmt">//--- 获取字符数组的大小
  class="type">uint next_line_symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
class=class="str">"cmt">//--- 确定从下一行移动的字词数 (按空格搜索)
  for(class="type">uint w=class="num">0; w<words_total; w++)
    {
    class=class="str">"cmt">//--- 获取 (class="num">1) 空格索引, 以及 (class="num">2) 宽度, 如果子字符串从起始到空格
    class="type">uint ss_index           =SymbolIndexBySpaceNumber(next_line_index,w);
    class="type">uint substring_width    =LineWidth(ss_index,next_line_index);
    class=class="str">"cmt">//--- 如果子串适合当前行的空余空间
    if(substring_width<free_space)
      {
      class=class="str">"cmt">//--- ...检查是否可以插入其它字词
      wrap_symbols_total=ss_index;
      class=class="str">"cmt">//--- 如果是整行, 停止
      if(next_line_symbols_total==wrap_symbols_total)
        {
        is_all_text=true;
        class="kw">break;
        }
      }
    else
      {
      class=class="str">"cmt">//--- 如果此为没有空格的连续行
      if(ss_index==next_line_symbols_total)
        is_solid_row=true;
      class=class="str">"cmt">//---
      class="kw">break;
      }
    }

◍ 文本回卷时怎样重算光标坐标

在 CTextBox 的 WrapTextToNewLine 里,回卷不是简单把字符搬去下一行,还要同步修正文本光标的位置,否则后续输入会错位。 核心算法先算出要移动的字符数 new_line_size = symbols_total - check_symbol_index,再把这部分从当前行切出、贴到下一行结构数组。 光标重算的逻辑在 x_pos = new_line_size - (symbols_total - m_text_cursor_x_pos) 这一句:若 x_pos 为负,说明光标原本在保留区,位置不变;否则落到下一行对应偏移。y 坐标随之在 line_index 与 next_line_index 之间切换。 按回车触发的分支(by_pressed_enter=true)会走另一套换行处理,上面这段坐标修正对普通自动回卷和手动回车都生效。开 MT5 把 CTextBox 派生出来打个日志,就能看到光标索引在长文本回卷时的跳变。

MQL5 / C++
class="type">void CTextBox::WrapTextToNewLine(class="kw">const class="type">uint line_index,class="kw">const class="type">uint symbol_index,class="kw">const class="type">bool by_pressed_enter=class="kw">false)
  {
class=class="str">"cmt">//--- 获取行内字符数组的大小
   class="type">uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
class=class="str">"cmt">//--- 最后一个字符的索引
   class="type">uint last_symbol_index=symbols_total-class="num">1;
class=class="str">"cmt">//--- 空行的情况则调整
   class="type">uint check_symbol_index=(symbol_index>last_symbol_index && symbol_index!=symbols_total)? last_symbol_index : symbol_index;
class=class="str">"cmt">//--- 下一行的索引
   class="type">uint next_line_index=line_index+class="num">1;
class=class="str">"cmt">//--- 要移动到新行的字符数
   class="type">uint new_line_size=symbols_total-check_symbol_index;
class=class="str">"cmt">//--- 将要移动的字符复制到数组中
   class="type">class="kw">string array[];
   CopyWrapSymbols(line_index,check_symbol_index,new_line_size,array);
class=class="str">"cmt">//--- 调整行结构的数组大小
   ArraysResize(line_index,symbols_total-new_line_size);
class=class="str">"cmt">//--- 调整新行结构的数组大小
   ArraysResize(next_line_index,new_line_size);
class=class="str">"cmt">//--- 将数据添加到新行的结构数组中
   PasteWrapSymbols(next_line_index,class="num">0,array);
class=class="str">"cmt">//--- 判断文本光标的新位置
   class="type">int x_pos=class="type">int(new_line_size-(symbols_total-m_text_cursor_x_pos));
   m_text_cursor_x_pos =(x_pos<class="num">0)? (class="type">int)m_text_cursor_x_pos : x_pos;
   m_text_cursor_y_pos =(x_pos<class="num">0)? (class="type">int)line_index : (class="type">int)next_line_index;
class=class="str">"cmt">//--- 如果指示此调用是通过按回车键启动
   if(by_pressed_enter)
     {

常见问题

按字符累积像素宽度与文本框内宽比较,超宽则把末尾若干字符回卷到下一行行首,逐字符试算直到不溢出。
回卷后依据字符在新行的索引重算基线坐标,用行高乘行号加字符偏移,别直接沿用旧行列值。
可以,把你的换行与坐标计算代码贴给小布,它会指出溢出判定和光标重算里的常见疏漏。
多因按固定字符数而非像素宽度断行,应逐字测宽,遇空格或标点才优先断,避免中途截断。
重算光标行号后,若行号低于顶部或高于底部可视行,偏移滚动基准使该行进入视图即可。