图形界面 X: 在多行文本框中选择文本 (集成构建 13)·进阶篇
(2/3)·逐个删字符太反人类?本篇把 Shift 系组合键和图像类封装进控件,一次框选整段
「文本框光标的九向位移枚举」
在 MT5 自定义文本框控件里,文本光标的移动方向被固化成一个枚举,数值从 4 到 9 分别对应向上一行、向下一行、行首、行尾、首行开头、末行末尾,前面 1~3 通常留给左右字符与单词级移动。 CTextBox 私有方法 MoveTextCursor 接收一个 ENUM_MOVE_TEXT_CURSOR 参数,用 switch 分发到具体位移函数。例如 TO_END_LINE 会先取当前行 m_lines[y].m_symbol 的数组大小,再 SetTextCursor 到该长度;TO_END_LAST_LINE 则取 m_lines 总行数减一作为末行索引,同样用 ArraySize 拿字符数落光标。 开 MT5 新建一个继承 CElement 的面板类,把下面代码贴进私有段,就能在调试时直观看到光标跳行逻辑。外汇与贵金属图表挂这类控件属高风险操作,参数误用可能卡死界面,建议先在策略测试器里隔离验证。
TO_NEXT_UP_LINE =class="num">4, TO_NEXT_DOWN_LINE =class="num">5, TO_BEGIN_LINE =class="num">6, TO_END_LINE =class="num">7, TO_BEGIN_FIRST_LINE =class="num">8, TO_END_LAST_LINE =class="num">9 }; class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 按指定的方向移动文本光标 class="type">void MoveTextCursor(class="kw">const ENUM_MOVE_TEXT_CURSOR direction); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按指定的方向移动文本光标 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::MoveTextCursor(class="kw">const ENUM_MOVE_TEXT_CURSOR direction) { class="kw">switch(direction) { class=class="str">"cmt">//--- 光标向左移动一个字符 case TO_NEXT_LEFT_SYMBOL : MoveTextCursorToLeft(); class="kw">break; class=class="str">"cmt">//--- 光标向右移动一个字符 case TO_NEXT_RIGHT_SYMBOL : MoveTextCursorToRight(); class="kw">break; class=class="str">"cmt">//--- 光标向左移动一个单词 case TO_NEXT_LEFT_WORD : MoveTextCursorToLeft(true); class="kw">break; class=class="str">"cmt">//--- 光标向右移动一个单词 case TO_NEXT_RIGHT_WORD : MoveTextCursorToRight(true); class="kw">break; class=class="str">"cmt">//--- 光标向上移动一行 case TO_NEXT_UP_LINE : MoveTextCursorToUp(); class="kw">break; class=class="str">"cmt">//--- 光标向下移动一行 case TO_NEXT_DOWN_LINE : MoveTextCursorToDown(); class="kw">break; class=class="str">"cmt">//--- 光标移动到当前行的开头 case TO_BEGIN_LINE : SetTextCursor(class="num">0,m_text_cursor_y_pos); class="kw">break; class=class="str">"cmt">//--- 光标移动到当前行的末尾 case TO_END_LINE : { class=class="str">"cmt">//--- 获取当前行中的字符数 class="type">uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class=class="str">"cmt">//--- 移动光标 SetTextCursor(symbols_total,m_text_cursor_y_pos); class="kw">break; } class=class="str">"cmt">//--- 光标移动到首行的开头 case TO_BEGIN_FIRST_LINE : SetTextCursor(class="num">0,class="num">0); class="kw">break; class=class="str">"cmt">//--- 光标移动到最后一行的末尾 case TO_END_LAST_LINE : { class=class="str">"cmt">//--- 获取最后一行的行号和字符数 class="type">uint lines_total =::ArraySize(m_lines); class="type">uint symbols_total =::ArraySize(m_lines[lines_total-class="num">1].m_symbol); class=class="str">"cmt">//--- 移动光标 SetTextCursor(symbols_total,lines_total-class="num">1); class="kw">break; } } }
◍ 文本框里左方向键的两种走法
在 MT5 自定义文本框控件里,单独按 Left 和按住 Shift 再按 Left,走的是两套完全不同的内部逻辑。前者只挪光标、清掉已有选区;后者在移动的同时持续拉长选区,方便交易者框选一段日志或参数串。
| 看 OnPressedKeyLeft:一进来先用 `key_code!=KEY_LEFT | m_keys.KeyCtrlState() | m_keys.KeyShiftState()` 把非左键、带 Ctrl、带 Shift 三种情况全部挡掉,返回 false 不处理。过了这关才 ResetSelectedText 清选区,再 MoveTextCursor(TO_NEXT_LEFT_SYMBOL) 左移一个字符,随后校正横竖滚动条并重绘。 |
|---|
OnPressedKeyShiftAndLeft 的拦截条件正好反过来——要求 Shift 必须处于按下状态(!m_keys.KeyShiftState() 时退出)。通过后先 SetStartSelectedTextIndexes 记选区起点,移完光标再 SetEndSelectedTextIndexes 补终点,等于每按一次就往选区右界扩一个字符。
底层 MoveTextCursor 有两个重载:带 with_highlighted_text 参数的版本里,false 分支会先 ResetSelectedText 再单纯移动;true 分支则走设置起止索引的选区流程。你在 EA 面板里做可编辑输入框时,照这套事件分发写,左键行为就不会和系统的文本选择打架。外汇与贵金属图表挂这类控件属高风险环境,实盘前请在策略测试器里把按键响应跑够 200 次以上。
class="type">bool CTextBox::OnPressedKeyLeft(class="kw">const class="type">long key_code) { if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || m_keys.KeyShiftState()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 重置选择 ResetSelectedText(); class=class="str">"cmt">//--- 文本光标向左移动一个字符 MoveTextCursor(TO_NEXT_LEFT_SYMBOL); class=class="str">"cmt">//--- 调整滚动条 CorrectingHorizontalScrollThumb(); CorrectingVerticalScrollThumb(); class=class="str">"cmt">//--- 更新文本框中的文本 DrawTextAndCursor(true); class=class="str">"cmt">//--- 发送有关它的消息 ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); class="kw">return(true); } class="type">bool CTextBox::OnPressedKeyShiftAndLeft(class="kw">const class="type">long key_code) { if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || !m_keys.KeyShiftState()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 设置选择文本的起始索引 SetStartSelectedTextIndexes(); class=class="str">"cmt">//--- 文本光标向左移动一个字符 MoveTextCursor(TO_NEXT_LEFT_SYMBOL); class=class="str">"cmt">//--- 设置所选文本的结束行索引 SetEndSelectedTextIndexes(); class=class="str">"cmt">//--- 调整滚动条 CorrectingHorizontalScrollThumb(); CorrectingVerticalScrollThumb(); class=class="str">"cmt">//--- 更新文本框中的文本 DrawTextAndCursor(true); class=class="str">"cmt">//--- 发送有关它的消息 ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); class="kw">return(true); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 按指定的方向移动文本光标 class="type">void MoveTextCursor(class="kw">const ENUM_MOVE_TEXT_CURSOR direction,class="kw">const class="type">bool with_highlighted_text); }; class="type">void CTextBox::MoveTextCursor(class="kw">const ENUM_MOVE_TEXT_CURSOR direction,class="kw">const class="type">bool with_highlighted_text) { if(!with_highlighted_text) { class=class="str">"cmt">//--- 重置选择 ResetSelectedText(); class=class="str">"cmt">//--- 光标移动到首行的开头 MoveTextCursor(direction); } else { class=class="str">"cmt">//--- 设置选择文本的起始索引 SetStartSelectedTextIndexes(); class=class="str">"cmt">//--- 文本光标向左移动一个字符 MoveTextCursor(direction); class=class="str">"cmt">//--- 设置所选文本的结束行索引 SetEndSelectedTextIndexes(); } }
文本框选区判定的代码落点
在 MT5 自定义文本框控件里,Shift 系组合键(Shift+Left/Right/Up/Down、Shift+Home/End,以及 Ctrl+Shift 变体)全部被拆成独立的私有方法挂在 CTextBox 类下,说明选区扩展逻辑是按按键原子化处理的,而不是在一个大分支里硬塞。 真正决定“某字符是否处于选中态”的,是 CheckSelectedText() 这个函数。它先以 m_selected_line_from==WRONG_VALUE 作为早期出口——没起选就直接返回 false,避免后续无意义的行列比较。 当起始行反而大于结束行(即向上或向左反选)时,判定规则翻转:末行且字符位置 >= m_selected_symbol_to 才标蓝。这种从“用户拖拽方向”反推索引边界的做法,开 MT5 把这段抄进自己的面板类,调 m_selected_line_from / m_selected_line_to 两个字段就能复现选区高亮。外汇与贵金属图表挂这类自定义控件需注意,高频重绘可能拖慢 tick 响应,属高风险定制。
class="type">bool CTextBox::CheckSelectedText(class="kw">const class="type">uint line_index,class="kw">const class="type">uint symbol_index) { class="type">bool is_selected_text=class="kw">false; class=class="str">"cmt">//--- 如果没有选择文本, 离开 if(m_selected_line_from==WRONG_VALUE) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果初始索引在下面的行 if(m_selected_line_from>m_selected_line_to) { class=class="str">"cmt">//--- 最后一行和字符在最后选择的右侧 if((class="type">int)line_index==m_selected_line_to && (class="type">int)symbol_index>=m_selected_symbol_to) { is_selected_text=true; } class=class="str">"cmt">//--- 初始行和字符在最初选择的左侧
「文本框选区判定的三种拖拽方向」
在自绘文本框控件里,选中态的判定依赖起始行/列与结束行/列的相对位置。代码用 m_selected_line_from / m_selected_symbol_from 记录锚点,m_selected_line_to / m_selected_symbol_to 记录松开点,is_selected_text 作为逐字符的命中标记。 当锚点行在结束行下方(反向向下拖),若当前行等于锚点行且列号小于锚点列,或处于两行之间,该字符标记为选中。反之锚点在上、向下拖时,末行左侧与首行右侧及中间行全选,逻辑对称。 同一行内的选区看列区间:光标右移时命中 to 与 from 之间,左移时命中 from 与 to 之间,最终 return(is_selected_text) 交还布尔结果。 CTextBox 继承 CElement,私有方法 TextOut() 负责落盘:先 m_canvas.Erase(AreaColorCurrent()) 清画布,再取 m_lines 大小做越界钳制(m_text_cursor_y_pos 超过 lines_total 则归到末行减1),之后才逐字符绘制。外汇与贵金属图表叠加自绘控件属高风险操作,参数越界可能引发闪烁或崩溃,建议在 MT5 策略测试器里单步验证。
else if((class="type">int)line_index==m_selected_line_from && (class="type">int)symbol_index<m_selected_symbol_from) { is_selected_text=true; } class=class="str">"cmt">//--- 中间行 (选择所有字符) else if((class="type">int)line_index>m_selected_line_to && (class="type">int)line_index<m_selected_line_from) { is_selected_text=true; } } class=class="str">"cmt">//--- 如果初始索引在上面 else if(m_selected_line_from<m_selected_line_to) { class=class="str">"cmt">//--- 最后一行和字符在最后选择的左侧 if((class="type">int)line_index==m_selected_line_to && (class="type">int)symbol_index<m_selected_symbol_to) { is_selected_text=true; } class=class="str">"cmt">//--- 初始行和字符在初始选择的右侧 else if((class="type">int)line_index==m_selected_line_from && (class="type">int)symbol_index>=m_selected_symbol_from) { is_selected_text=true; } class=class="str">"cmt">//--- 中间行 (选择所有字符) else if((class="type">int)line_index<m_selected_line_to && (class="type">int)line_index>m_selected_line_from) { is_selected_text=true; } } class=class="str">"cmt">//--- 如果初始和最终索引在同一行 else { class=class="str">"cmt">//--- 查找已检查的行 if((class="type">int)line_index>=m_selected_line_to && (class="type">int)line_index<=m_selected_line_from) { class=class="str">"cmt">//--- 如果光标向右移动, 且字符在所选范围内 if(m_selected_symbol_from>m_selected_symbol_to) { if((class="type">int)symbol_index>=m_selected_symbol_to && (class="type">int)symbol_index<m_selected_symbol_from) is_selected_text=true; } class=class="str">"cmt">//--- 如果光标向左移动, 且字符在所选范围内 else { if((class="type">int)symbol_index>=m_selected_symbol_from && (class="type">int)symbol_index<m_selected_symbol_to) is_selected_text=true; } } } class=class="str">"cmt">//--- 返回结果 class="kw">return(is_selected_text); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 将文本输出到画布 class="type">void TextOut(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将文本输出到画布 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::TextOut(class="type">void) { class=class="str">"cmt">//--- 清洁画布 m_canvas.Erase(AreaColorCurrent()); class=class="str">"cmt">//--- 获取行数组的大小 class="type">uint lines_total=::ArraySize(m_lines); class=class="str">"cmt">//--- 在超界的情况下, 调整大小 m_text_cursor_y_pos=(m_text_cursor_y_pos>=lines_total)? lines_total-class="num">1 : m_text_cursor_y_pos; class=class="str">"cmt">//--- 获取字符数组的大小
◍ 文本框里逐字符绘制的底层循环
在 MT5 的 Canvas 自定义控件里,文本不是一句 TextOut 就完事。多行模式开启、或当前行已有字符时,引擎会先取行宽 LineWidth 与行高 LineHeight,再按 lines_total 逐行扫描,每行内部再按字符数组 m_lines[i].m_symbol 逐个落笔。 逐字符绘制时,x 坐标从 m_text_x_offset 起算,每画一个字符就累加 m_lines[i].m_width[s],y 则按 i*line_height 向下偏移。若 CheckSelectedText(i,s) 命中,该字符前景色切到 m_selected_text_color,并用 FillRectangle 以 m_selected_back_color 填出选中背景——背景矩形右下角取 x+m_lines[i].m_width[s] 与 y+line_height-1,比字符略收 1 像素边距。 当多行模式关闭且首行无字符,分支走 else:仅当 m_default_text 非空,才在控件中心 (m_area_x_size/2, m_area_y_size/2) 以 TA_CENTER|TA_VCENTER 绘制省缺提示文字。外汇与贵金属图表挂这类自定义面板属高风险操作,参数误写可能导致 EA 在实时账户上绘制异常。 把下面这段直接贴进你的 Canvas 类方法,能在 MT5 策略测试器的可视化模式里验证选中高亮逻辑是否按预期走。
class="type">uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class=class="str">"cmt">//--- 如果启用了多行模式, 或是字符数大于零 if(m_multi_line_mode || symbols_total>class="num">0) { class=class="str">"cmt">//--- 获取行宽 class="type">int line_width=(class="type">int)LineWidth(m_text_cursor_x_pos,m_text_cursor_y_pos); class=class="str">"cmt">//--- 获取行高, 并循环遍历所有行 class="type">int line_height=(class="type">int)LineHeight(); for(class="type">uint i=class="num">0; i<lines_total; i++) { class=class="str">"cmt">//--- 获取文本坐标 class="type">int x=m_text_x_offset; class="type">int y=m_text_y_offset+((class="type">int)i*line_height); class=class="str">"cmt">//--- 获取字符串大小 class="type">uint string_length=::ArraySize(m_lines[i].m_symbol); class=class="str">"cmt">//--- 绘制文本 for(class="type">uint s=class="num">0; s<string_length; s++) { class="type">uint text_color=TextColorCurrent(); class=class="str">"cmt">//--- 如果有已选择的文本, 确定其颜色和当前字符的背景颜色 if(CheckSelectedText(i,s)) { class=class="str">"cmt">//--- 所选文字的颜色 text_color=::ColorToARGB(m_selected_text_color); class=class="str">"cmt">//--- 计算绘制背景的坐标 class="type">int x2=x+m_lines[i].m_width[s]; class="type">int y2=y+line_height-class="num">1; class=class="str">"cmt">//--- 绘制字符的背景颜色 m_canvas.FillRectangle(x,y,x2,y2,::ColorToARGB(m_selected_back_color)); } class=class="str">"cmt">//--- 绘制字符 m_canvas.TextOut(x,y,m_lines[i].m_symbol[s],text_color,TA_LEFT); class=class="str">"cmt">//--- 下一个字符的 X 坐标 x+=m_lines[i].m_width[s]; } } } class=class="str">"cmt">//--- 如果多行模式被禁用且没有字符, 则显示省缺文本 else { class=class="str">"cmt">//--- 绘制文本, 如果指定 if(m_default_text!="") m_canvas.TextOut(m_area_x_size/class="num">2,m_area_y_size/class="num">2,m_default_text,::ColorToARGB(m_default_text_color),TA_CENTER|TA_VCENTER); }
单行与多行选中文本的清除逻辑
文本框里删掉高亮文本不是一刀切,核心要看选区落在单行还是跨行。单行场景走 DeleteTextOnOneLine,先算要删几个字符:用 fabs(selected_symbol_from - selected_symbol_to) 取绝对值,这就是待清除长度。 若选区起点索引在右(to < from),从起点位置向右把后续字符前移覆盖,行字符数组同步缩减同样数量;若起点在左,文本光标先左移 symbols_to_delete 再搬字符。注意光标左移这步容易漏,漏了会导致下一次输入错位。 跨行删除交给 DeleteTextOnMultipleLines,算法重很多。先统计首末行字符总数、中间行数、首末行各自要删的字符数;再把跨行后保留的字符塞进临时动态数组,调接收行结构、按删除行数平移行、压缩行数组。选择方向向下(首行高于末行)时,光标要回跳到选区初始索引。 总入口 DeleteSelectedText 负责判断调哪个分支,删完重置首尾索引、重算文本框尺寸与最大行宽,并发光标移动消息;有选区返回 true,无选区 false。它不止响应 Backspace,输入新字符和回车时也先删选区再执行键动作。 别把正态当圣经:上面代码里 fabs 取字符差,若你改造成支持双向 RTL 文本,得先确认 m_selected_symbol_* 的语义是否仍成立,否则跨行复制会丢尾字符。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 删除一行中选择的文本 class="type">void DeleteTextOnOneLine(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 删除一行中选择的文本 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::DeleteTextOnOneLine(class="type">void) { class="type">int symbols_total =::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class="type">int symbols_to_delete =::fabs(m_selected_symbol_from-m_selected_symbol_to); class=class="str">"cmt">//--- 如果字符的初始索引在右侧 if(m_selected_symbol_to<m_selected_symbol_from) { class=class="str">"cmt">//--- 将字符移动到当前行中的空余区域 MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_from,m_selected_symbol_to); } class=class="str">"cmt">//--- 如果字符的初始索引在左侧 else { class=class="str">"cmt">//--- 将文本光标向左移动要删除数量的字符 m_text_cursor_x_pos-=symbols_to_delete; class=class="str">"cmt">//--- 将字符移动到当前行中的空余区域 MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_to,m_selected_symbol_from); } class=class="str">"cmt">//--- 当前行的数组大小减少已提取的字符数 ArraysResize(m_text_cursor_y_pos,symbols_total-symbols_to_delete); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 删除多行中选择的文本 class="type">void DeleteTextOnMultipleLines(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 删除多行中选择的文本 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::DeleteTextOnMultipleLines(class="type">void) { class=class="str">"cmt">//--- 初始和最后一行的字符总数 class="type">uint symbols_total_line_from =::ArraySize(m_lines[m_selected_line_from].m_symbol); class="type">uint symbols_total_line_to =::ArraySize(m_lines[m_selected_line_to].m_symbol); class=class="str">"cmt">//--- 要被删除的中间行数 class="type">uint lines_to_delete =::fabs(m_selected_line_from-m_selected_line_to); class=class="str">"cmt">//--- 初始和最后一行要删除的字符数 class="type">uint symbols_to_delete_in_line_from =::fabs(symbols_total_line_from-m_selected_symbol_from); class="type">uint symbols_to_delete_in_line_to =::fabs(symbols_total_line_to-m_selected_symbol_to); class=class="str">"cmt">//--- 如果初始行低于最后一行 if(m_selected_line_from>m_selected_line_to) { class=class="str">"cmt">//--- 将要移动的字符复制到数组中 class="type">class="kw">string array[]; CopyWrapSymbols(m_selected_line_from,m_selected_symbol_from,symbols_to_delete_in_line_from,array); class=class="str">"cmt">//--- 调整接收行数组大小