MQL5 图形界面多行文本框字词回卷:从结构铺垫到算法落地的基础篇(基础篇)
(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 来存行尾状态,这是回卷可读写的底层支点。
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)) 打一行带空格的字符串,就能验证词数统计是否符合预期。
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 主线程的概率,建议仅在文本结构变更时调用。
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">//--- 如果这是最后一次遍历