MQL5 多行文本框文本选择:从 Shift 键侦测到选区算法的底层铺垫(基础篇)
(1/3)· 字符级删改太慢?本篇先把文本框选区的键盘侦测与组合键逻辑拆清楚
在 MQL5 自定义图形界面里逐字符删改多行文本,是多数自建面板用户的日常卡点。没有选区机制,编辑体验直接退回命令行时代。本文作为图形界面 X 系列首篇,先把文本选择所需的键盘状态跟踪与组合键体系讲透。
多行文本框里怎么用键盘选文本
在 MT5 自绘图形界面里做多行文本框,光靠鼠标拖选不够用,交易员复盘时更习惯用 Shift+方向键快速圈定一段日志或参数。原文示例构建于集成构建 13(对应 2017-06-29 发布的 MetaTrader 5 客户端),该演示帖当时有 4829 次查看、152 条讨论,说明这类底层控件需求在量化交易者中并不小众。 要捕捉 Shift 键状态,核心是在窗口消息回调里跟踪 WM_KEYDOWN / WM_KEYUP,用全局标志位记录 Shift 是否按住,再配合左右方向键移动选区起点与终点。选择文本的组合键通常映射为 Shift+←、Shift+→、Shift+Home、Shift+End,覆盖字符级与行级选中。 删除已选文本则依赖一个操纵图像数据的类:文本框本质是画在画布上的像素,选中高亮和清除都要重绘对应区域的背景与文字位图,不能直接调系统 Edit 控件。外汇与贵金属 GUI 工具开发属高风险定制,任何重绘逻辑卡顿都可能拖慢盯盘响应,建议先在策略测试器外的独立 EA 面板里验证。
「为什么多行文本框必须先做文本选择」
在前面关于函数库结构准备的文章里已经把整套界面库的作用拆解过,这一节直接落到具体控件:多行文本框。如果只能一次删一个字符,实盘里贴一段新闻或日志进来再编辑会非常反人类,所以文本选择是绕不开的前置功能。 目前多行文本框控件已经迭代到集成构建 8,文本回卷算法在集成构建 12 里单独写过。下一步要支持组合键选段并整段删除,观感应和常见文本编辑器一致,同时把类结构收尾,进入库演变第二阶段——所有控件改为独立画布渲染。 这一版即该控件的最终形态,后续只在出现更优算法时才动。你可以开 MT5 把库文件解到存档目录同级,挂上多行框控件实测选段删除的手感。
◍ 用 TERMINAL_KEYSTATE_SHIFT 探 Shift 键
在 CKeys 类里补一个 KeyShiftState() 方法,就能在 EA 或指标运行时实时知道 Shift 键是否被按住。这个类此前已经封装过键盘操控的基础逻辑,这次只是加一个轻量接口,专门服务于后续文本选择的组合键判断。 判定逻辑只有一行:调用 ::TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT),若返回值小于 0,就认为 Shift 键处于按压状态。MT5 终端把这个标识符的返回设计成负数代表按下,零或正数代表松开,所以直接用 <0 比较即可,不需要额外位运算。 外汇与贵金属交易使用这类键盘状态监听时属于低风险辅助逻辑,但自动化脚本若依赖物理按键做下单组合,仍需注意终端焦点丢失导致状态误判的高概率问题。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 操控键盘的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CKeys { class="kw">public: class=class="str">"cmt">//--- 返回 Shift 键的状态 class="type">bool KeyShiftState(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 Shift 键的状态 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CKeys::KeyShiftState(class="type">void) { class="kw">return(::TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<class="num">0); }
文本框里那些选词选行的键盘组合
在 MT5 自绘文本框里做文本选择,两键组合是最底层的操作。Shift+Left / Shift+Right 让光标按字符左右移动,经过的字符以高亮背景和异色字体标出,配色可由用户自定义。 Shift+Home / Shift+End 则从光标原位直接拉到行首或行尾,整段字符被选中。Shift+Up / Shift+Down 是上下移一行,选择范围自光标延伸到起始行开头或末行行尾,中间若隔了整行则整行吃进。 遇到跨多行或多词的情形,逐字符或逐行选太低效,就得上三键组合。Ctrl+Shift+Left / Right 一次选中光标左侧或右侧的完整单词;Ctrl+Shift+Home / End 则从光标位置一口气选到文档开头或结尾。 外汇与贵金属图表上的自定义输入框若要做编辑功能,这些组合键映射必须提前在事件处理函数里接住,否则用户按了没反应会直接弃用。
「文本框里怎么把一段字符圈出来」
MT5 标准文本框默认选中态是蓝底白字(背景色 C'51,153,255'、文字色 clrWhite),要用 CTextBox::SelectedBackColor() 和 CTextBox::SelectedTextColor() 改配色,直接传 color 值即可,不必动系统默认渲染。 选区靠四个私有索引撑起来:m_selected_line_from / m_selected_line_to 存起止行,m_selected_symbol_from / m_selected_symbol_to 存起止字符位。按下 Shift+方向前先调 SetStartSelectedTextIndexes() 记起点,只在该值被 Reset 后的首次调用才写;随后光标移动,再调 SetEndSelectedTextIndexes() 记终点。 若移动中发现光标跑到了起点右侧,或发生删字、控件失焦,ResetSelectedText() 会把四个索引清零,选区消失。原先散落在各光标移动方法里的滚屏与越界代码,已抽成独立方法供选择逻辑复用。 方向枚举 ENUM_MOVE_TEXT_CURSOR 塞进了 Enums.mqh,含 TO_NEXT_LEFT_SYMBOL、TO_NEXT_RIGHT_WORD、TO_BEGIN_LINE、TO_END_LAST_LINE 等 10 个标识符。通用方法 MoveTextCursor(方向, 模式) 用 1 表纯移动、2 表带选择移动,按键处理器几乎都走这一条,文件重复代码量显著下降。 TextOut() 不能再整行刷:循环内对每个字符跑 CheckSelectedText(),按「起始行≤结束行」或反向两类分支判定是否落入选区,选中就在字符底下先画填充矩形、再绘字,颜色取 m_selected 系列。这样逐字符输出才能正确显出高亮块。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 所选文本的背景和字符颜色 class="type">class="kw">color m_selected_back_color; class="type">class="kw">color m_selected_text_color; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 所选文本的背景和字符颜色 class="type">void SelectedBackColor(class="kw">const class="type">class="kw">color clr) { m_selected_back_color=clr; } class="type">void SelectedTextColor(class="kw">const class="type">class="kw">color clr) { m_selected_text_color=clr; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CTextBox::CTextBox(class="type">void) : m_selected_text_color(clrWhite), m_selected_back_color(C&class="macro">#x27;class="num">51,class="num">153,class="num">255&class="macro">#x27;) { class=class="str">"cmt">//... } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 行和字符 (所选文本的) 的起始行和结束行索引 class="type">int m_selected_line_from; class="type">int m_selected_line_to; class="type">int m_selected_symbol_from; class="type">int m_selected_symbol_to; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 设置所选文本的 (class="num">1) 起始行 (class="num">2) 结束行索引 class="type">void SetStartSelectedTextIndexes(class="type">void); class="type">void SetEndSelectedTextIndexes(class="type">void); class=class="str">"cmt">//--- 重置所选文本 class="type">void ResetSelectedText(class="type">void); };
◍ 文本框选中区域的起止索引怎么管
在自绘文本框控件里,鼠标拖选文字靠两组索引定位:起始行 m_selected_line_from、起始字符 m_selected_symbol_from,以及结束行 m_selected_line_to、结束字符 m_selected_symbol_to。起始索引只在第一次拖拽时写入,若 m_selected_line_from 等于 WRONG_VALUE 才赋值当前光标坐标,避免拖动过程中起点被反复覆盖。 结束索引每次随光标移动实时刷新。当起止行与字符位置完全一致,说明用户只是点了一下没拉出选区,这时直接调 ResetSelectedText 把四个索引全置回 WRONG_VALUE,相当于取消选择。 光标移动拆成四个私有方法:MoveTextCursorToLeft / Right / Up / Down,分别处理左右单字符和上下行跳跃;配合 CorrectingHorizontalScrollThumb 与 CorrectingVerticalScrollThumb 在越界时自动推滚动条。方向控制另用枚举 ENUM_MOVE_TEXT_CURSOR,0=左移一符、1=右移一符、2=左移一词、3=右移一词,方便外部按键盘事件派发。 开 MT5 建个 CTextBox 派生类,把 WRONG_VALUE 定为 -1,断点打在 SetEndSelectedTextIndexes 里,单响与拖拽两种操作下观察四个成员的变化,能快速验证这套选区逻辑是否如预期清零。
class="type">void CTextBox::SetStartSelectedTextIndexes(class="type">void) { class=class="str">"cmt">//--- 如果尚未设置选择文本的起始行索引 if(m_selected_line_from==WRONG_VALUE) { m_selected_line_from =(class="type">int)m_text_cursor_y_pos; m_selected_symbol_from =(class="type">int)m_text_cursor_x_pos; } } class="type">void CTextBox::SetEndSelectedTextIndexes(class="type">void) { class=class="str">"cmt">//--- 设置所选文本的结束行索引 m_selected_line_to =(class="type">int)m_text_cursor_y_pos; m_selected_symbol_to =(class="type">int)m_text_cursor_x_pos; class=class="str">"cmt">//--- 如果所有索引相同, 则清除选择 if(m_selected_line_from==m_selected_line_to && m_selected_symbol_from==m_selected_symbol_to) ResetSelectedText(); } class="type">void CTextBox::ResetSelectedText(class="type">void) { m_selected_line_from =WRONG_VALUE; m_selected_line_to =WRONG_VALUE; m_selected_symbol_from =WRONG_VALUE; m_selected_symbol_to =WRONG_VALUE; } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 将文本光标向左移动一个字符 class="type">void MoveTextCursorToLeft(class="type">void); class=class="str">"cmt">//--- 将文本光标向右移动一个字符 class="type">void MoveTextCursorToRight(class="type">void); class=class="str">"cmt">//--- 将文本光标向上移动一个字符 class="type">void MoveTextCursorToUp(class="type">void); class=class="str">"cmt">//--- 将文本光标向下移动一个字符 class="type">void MoveTextCursorToDown(class="type">void); class=class="str">"cmt">//--- 调整水平滚动条 class="type">void CorrectingHorizontalScrollThumb(class="type">void); class=class="str">"cmt">//--- 调整垂直滚动条 class="type">void CorrectingVerticalScrollThumb(class="type">void); }; enum ENUM_MOVE_TEXT_CURSOR { TO_NEXT_LEFT_SYMBOL =class="num">0, TO_NEXT_RIGHT_SYMBOL =class="num">1, TO_NEXT_LEFT_WORD =class="num">2, TO_NEXT_RIGHT_WORD =class="num">3,