MQL5 图形界面多行文本框字词回卷:从结构铺垫到算法落地的基础篇(基础篇)
📐

MQL5 图形界面多行文本框字词回卷:从结构铺垫到算法落地的基础篇(基础篇)

(1/3)· 文本框宽度溢出只能拖滚动条?这篇先把回卷控件的结构和私有方法讲透

案例拆解 第 1/3 篇

在 MT5 自定义面板里塞进长文本,宽度一超就逼用户狂拉水平滚动条,是很多自制控件的通病。字词回卷不是记事本专属,控件自己不处理换行标记,读取逻辑就会断在半行。本文是图形界面 X 系列三部分的第一节,先把多行文本框回卷所需的类结构和辅助方法铺开。

◍ 多行文本框里的字词回卷怎么算

在 MT5 自定义图形界面里,多行文本框(Edit 控件的多行模式)不会自动按像素宽度断词,必须自己实现字词回卷算法,否则长英文单词或连续数字会撑破控件边界。 核心思路是:先把整段文本按空格拆成 token 列表,再逐个 token 累加测量宽度(用 TextGetSize 或 CCanvas 文本测量),当当前行累加宽度超过控件可视宽度时,把上一个 token 之后的位置作为回卷点,换行继续排。 辅助方法通常包含:按空格切词、测量单 token 宽度、判断当前行是否越界、以及把排好的多行结果写回控件。MetaTrader 5 build 12 的集成编译环境已支持相关文本测量 API,可直接在 EA 的面板代码里调用。 实际验证时,开 MT5 新建一个空 EA,挂一个 200×100 的多行 Edit,塞入一段不含空格的长字符串(如 64 位哈希),不回卷会只显示开头被截断;接上回卷算法后,相同字符串会被按字符宽度折成多行。

「多行文本框的回卷与逆卷任务」

函数库结构在首篇已铺开,每章末尾都挂了文章索引与当前阶段的完整库下载包,解压后目录层级必须保持原样,否则 MT5 加载时会找不到依赖文件。 本次接着做多行文本框控件,前序进度可查「图形界面 X:多行文本框控件(集成编译 8)」。 这阶段的硬指标是:文本框宽度被撑破时,要自动把溢出的词折到下一行;若某行后面腾出了空间,还得把下一行的词逆卷回上一行,避免右侧留白过大。外汇与贵金属图表上的面板空间有限,这类回卷逻辑直接决定信息密度,属于高风险环境下的实用细节。

文本框里的字词回卷到底怎么切行

MT5 自建多行文本框默认不开启字词回卷,m_word_wrap_mode 初始为 false。想让长文本自动折行,只能走 CTextBox::WordWrapMode(true) 这一个公有入口,其余回卷逻辑全在私有方法里。 回卷不是简单按像素砍断:每一行必须带一个行结束标记 m_end_of_line,只有真正逻辑行的末尾才置 true。比如把 'Google is an American multinational technology company specializing in Internet-related services and products.' 塞进窄文本框,禁用回卷时整行溢出要靠水平滚动条;开启后原串被切成三个可视子串,但结束标记只挂在第三段的末尾。 用一段脚本读文件能直接验证这种行为。下面代码打开 ANSI 文本并打印首行,日志输出整句而非截断片段,说明 FileReadString 读到的是带结束标记的完整逻辑行。 辅助方法里有一组私有函数专门算切分位置:返回右侧首个可见字符与空格索引、统计某行字词数、把越界字符复制到暂存数组再贴到下一行。结构里的 StringOptions 新增了 bool m_end_of_line 来存行尾状态,这是回卷可读写的底层支点。

MQL5 / C++
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">bool m_word_wrap_mode;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 字词回卷模式
  class="type">void WordWrapMode(class="kw">const class="type">bool mode) { m_word_wrap_mode=mode; }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造器                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTextBox::CTextBox(class="type">void) : m_word_wrap_mode(class="kw">false)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序的 start 函数                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart(class="type">void)
  {
class=class="str">"cmt">//--- 获取文件句柄
  class="type">int file=::FileOpen("Topic &class="macro">#x27;Word wrapping&class="macro">#x27;.txt",FILE_READ|FILE_TXT|FILE_ANSI);
class=class="str">"cmt">//--- 如果得到句柄, 则读取文件
  if(file!=INVALID_HANDLE)
    ::Print(__FUNCTION__," > ",::FileReadString(file));
  else
    ::Print(__FUNCTION__," > 错误: ",::GetLastError());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
OnStart > Google is an American multinational technology company specializing in Internet-related services and products.
  class=class="str">"cmt">//--- 字符及其属性
  class="kw">struct StringOptions
    {
      class="type">class="kw">string          m_symbol[];   class=class="str">"cmt">// 字符
      class="type">int             m_width[];    class=class="str">"cmt">// 字符宽度
      class="type">bool            m_end_of_line; class=class="str">"cmt">// 行结束标记
    };
  StringOptions  m_lines[];

◍ 字词回卷怎么数、怎么搬

回卷算法在某个时刻必须启动循环去定位空格字符的索引,在此之前得先知道一行里到底有几个词。CTextBox::WordsTotal() 就是干这个的:它遍历指定行的字符数组,只要出现“当前字符不是空格、前一个却是空格”的形态,就判定一个新词要开始;若已走到行尾,计数器同样加一,避免漏掉最后一个词。 拿到词数后,CTextBox::SymbolIndexBySpaceNumber() 负责给出具体空格符的索引。举例来说,第一行第 0 个空格的字符索引是 6,拿到这个值就能用 CTextBox::LineWidth() 算出从子串开头起一个或多个词的像素宽度。 实际回流时,如果一行像 'The quick brown fox jumped over the lazy dog.' 超出文本框红框区域,超出的 'over the lazy dog.' 要搬去下一行。动态数组原本只有 1 个元素,这时需扩容 1 个,并按搬走的字符数重设新行字符数组,再执行迁移。 若文本框宽度骤减约 30%,算法先判定第 0 行里 'fox jumped' 子串放不下,数组再增一行,其下所有子串整体下移一行腾位,再把 'fox jumped' 塞进去。循环进到第 1 行后会查是否还能从索引 2 逆卷一个词回来——本例无行结束标记且有空位,于是 'over' 被逆卷回第 1 行,字符数组大小按增减字符数同步修改。 宽度变窄会同时触发正逆向回卷;宽度延伸时只需把下文逆卷进释放空间。每次下压一行数组加 1,每次下一行全逆卷数组减 1,减之前若上方还有行则先上移清掉空行。 行级搬运交给 CTextBox::MoveLines(),入参为源行、目标行和方向移动(默认向下);字符级搬运由 CTextBox::MoveSymbols() 负责,入参含行索引、起止字符索引和方向(默认向左)。CopyWrapSymbols() 与 PasteWrapSymbols() 则在复制粘贴时频繁复用这两套逻辑。 下面这段是 WordsTotal 的核心实现,注意循环从 s=1 起步,用 m_lines[l].m_symbol[s]!=SPACE 且前一项 ==SPACE 来抓词首: [CODE] 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); } class CTextBox : public CElement { private: //--- 返回空格符的索引号码 uint SymbolIndexBySpaceNumber(const uint line_index,const uint space_index); }; //+------------------------------------------------------------------+ [/CODE] 在 MT5 里建个 CTextBox 派生类,把这段 WordsTotal 原样丢进去,用 Print(WordsTotal(0)) 打一行带空格的字符串,就能验证词数统计是否符合预期。

MQL5 / C++
class CTextBox : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 返回指定行中的字词数量
   class="type">uint               WordsTotal(class="kw">const class="type">uint line_index);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回指定行中的字词数量                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">uint CTextBox::WordsTotal(class="kw">const class="type">uint line_index)
  {
class=class="str">"cmt">//--- 获取行数组大小
   class="type">uint lines_total=::ArraySize(m_lines);
class=class="str">"cmt">//--- 防止超出数组大小
   class="type">uint l=(line_index<lines_total)? line_index : lines_total-class="num">1;
class=class="str">"cmt">//--- 获取指定行的字符数组大小
   class="type">uint symbols_total=::ArraySize(m_lines[l].m_symbol);
class=class="str">"cmt">//--- 字词计数器
   class="type">uint words_counter=class="num">0;
class=class="str">"cmt">//--- 在指定行搜索空格符
   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);
  }
class CTextBox : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 返回空格符的索引号码 
   class="type">uint               SymbolIndexBySpaceNumber(class="kw">const class="type">uint line_index,class="kw">const class="type">uint space_index);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+

「按空格序号定位字符与行块下移的实现」

在自定义文本框控件里,经常需要按「第几个空格」去反查字符位置。SymbolIndexBySpaceNumber 的思路是:先夹紧行号避免越界,再从该行的第 2 个字符开始扫,遇到「前一个字符是空格、当前不是空格」就记一次空格计数,计数等于目标 space_index 时把当前下标存下来并 break。 若整行没匹配到指定空格序号,函数返回该行字符总数(symbols_total),调用方据此可判定「已超出本行空格数量」。注意循环从 s=1 起跳,因此首字符前导空格不会被计入空格序号,第 1 个空格序号对应的是「第一个非空格紧跟在空格后」的位置。 MoveLines 负责行块重排。to_down=true 时,从 from_index 向下(索引递减)一路把前一行 prev_index 的内容拷贝到当前 i,并调用 ArraysResize 按源行字符数扩当前行缓冲。这一机制在外汇/贵金属 EA 的日志面板重绘时可能用到,但 GUI 频繁重排有拖慢 MT5 主线程的概率,建议仅在文本结构变更时调用。

MQL5 / C++
class="type">uint CTextBox::SymbolIndexBySpaceNumber(class="kw">const class="type">uint line_index,class="kw">const class="type">uint space_index)
  {
class=class="str">"cmt">//--- 获取行数组大小
   class="type">uint lines_total=::ArraySize(m_lines);
class=class="str">"cmt">//--- 防止超出数组大小
   class="type">uint l=(line_index<lines_total)? line_index : lines_total-class="num">1;
class=class="str">"cmt">//--- 获取指定行的字符数组大小
   class="type">uint symbols_total=::ArraySize(m_lines[l].m_symbol);
class=class="str">"cmt">//--- (class="num">1) 用来判断空格符索引, 以及 (class="num">2) 空格符的计数
   class="type">uint symbol_index  =class="num">0;
   class="type">uint space_counter =class="num">0;
class=class="str">"cmt">//--- 在指定行搜索空格符
   for(class="type">uint s=class="num">1; s<symbols_total; s++)
     {
      class=class="str">"cmt">//--- 如果发现空格符
      if(m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-class="num">1]==SPACE)
        {
         class=class="str">"cmt">//--- 如果计数器等于指定的空格符索引, 则将其保存并停止循环
         if(space_counter==space_index)
           {
            symbol_index=s;
            class="kw">break;
           }
         class=class="str">"cmt">//--- 增加空格符的计数器
         space_counter++;
        }
     }
class=class="str">"cmt">//--- 如果未发现空格符的索引, 返回行大小
   class="kw">return((symbol_index<class="num">1)? symbols_total : symbol_index);
  }
class CTextBox : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 移动行
   class="type">void              MoveLines(class="kw">const class="type">uint from_index,class="kw">const class="type">uint to_index,class="kw">const class="type">bool to_down=true);
  };
class="type">void CTextBox::MoveLines(class="kw">const class="type">uint from_index,class="kw">const class="type">uint to_index,class="kw">const class="type">bool to_down=true)
  {
class=class="str">"cmt">//--- 向下移动行
   if(to_down)
     {
      for(class="type">uint i=from_index; i>to_index; i--)
        {
         class=class="str">"cmt">//--- 行数组中前一个元素的索引
         class="type">uint prev_index=i-class="num">1;
         class=class="str">"cmt">//--- 获取字符数组的大小
         class="type">uint symbols_total=::ArraySize(m_lines[prev_index].m_symbol);
         class=class="str">"cmt">//--- 调整数组大小
         ArraysResize(i,symbols_total);
         class=class="str">"cmt">//--- 制作行副本
         LineCopy(i,prev_index);
         class=class="str">"cmt">//--- 如果这是最后一次遍历
把控件自检交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到控件渲染与文本溢出的实时状态,你专注决策逻辑而不是反复手测换行。

常见问题

它是实现字词回卷的唯一公有方法,其余参与回卷逻辑的方法均为私有,在类内部调度。
结束标记用于界定一行文本的读取边界,编程方式读行时返回至结束标记的整段内容,否则会跨行误读。
默认禁用,需要显式调用 WordWrapMode() 激活,控件才不会在宽度溢出时依赖水平滚动条。
可以,小布盯盘的 AIGC 模块能载入面板快照并标注文本溢出区域,省去你手动拖宽窗体排查的重复劳动。
它保存行结束标记状态,使 CTextBox 能正确识别哪一行真正结束,支撑回卷与逆卷的读取。