图形界面 X: 多行文本框控件 (集成编译 8)·综合运用
(3/3)· 从键盘事件到控件集成的 37 节拆解,终结终端 OBJ_EDIT 的文本长度枷锁
- 图表子窗口尺寸调整的虚函数接口
- 文本框控件的属性与自绘外观逻辑
- 文本框类的状态色与模式开关
- 文本框控件的焦点与配色内部逻辑
- 文本框边框色随状态切换的实现细节
- 边框绘制与焦点变色的内层逻辑
- 画板文字渲染的偏移与多行逻辑
- 文本框光标的像素坐标怎么算
- 文本框光标的反相闪烁与计时器节奏
- 文本框类的光标与最大行宽实现
- 文本框自适配尺寸的底层逻辑
- 文本框禁用时的层级与只读切换
- 文本框点击后光标怎么落位
- 文本框可见区边界怎么算
- 文本框滚动条滑块坐标怎么算
- 文本框光标的行列追踪与越界保护
- 文本框点击命中坐标的判定逻辑
- 多行文本框的光标越界滚动处理
- 文本框怎么接住你敲下的每个字符
- 捕获按键并实时撑开文本框
- 退格删行时文本框怎么搬家
- 文本框里删行时怎么把字接回上一行
- 退格键在文本框里的分层删除逻辑
- 回车断行时怎么搬动文本缓冲
- 回车断行时文本框的内部重排逻辑
- 左右键移动文本光标的边界处理
- 右键移动光标时的边界与滚动处理
- 上下键驱动多行文本框光标移动
- Home/End 键的光标跳动逻辑
- Ctrl 组合键下的光标跳跃逻辑
- 光标越界时滚动条与重绘的联动
- 多行文本框在容器类里的挂接细节
- 用 300 毫秒静止判据驱动文本框定时器
- 用多行文本框把章节目录灌进 GUI
- 用事件路由把文本框状态喂给状态栏
- GUI 函数库现状与文本框激活的补丁思路
- 最后说句实在话
「图表子窗口尺寸调整的虚函数接口」
在 MT5 的自定义指标或图形对象类里,经常需要随主图缩放自动重排子窗口。源码中暴露了两个虚函数,专门处理右侧与底部边界的联动。 ChangeWidthByRightWindowSide(void) 负责在主图右侧时间轴伸缩时,同步修正本窗口宽度;ChangeHeightByBottomWindowSide(void) 则在窗口底部被拖动时重算高度。两者都声明为 virtual,意味着派生类可以覆写以实现个性化布局逻辑。 开 MT5 用 MetaEditor 搜这两个方法名,能看到它们在 CChartObject 派生体系里的默认空实现——若你的指标在分屏时错位,优先检查是否漏写了这两个覆写。
class="kw">virtual class="type">void ChangeWidthByRightWindowSide(class="type">void); class=class="str">"cmt">//--- 改变窗口底部的高度 class="kw">virtual class="type">void ChangeHeightByBottomWindowSide(class="type">void); };
文本框控件的属性与自绘外观逻辑
CTextBox 用 KeySymbolOptions 结构按行保存字符与宽度:m_symbol[] 存字符,m_width[] 存对应像素宽,实例以动态数组 m_lines[] 存在,数组大小等于文本框行数。整行输出前需调用 CollectString(uint line_index) 把某行字符从数组拼回字符串,这是后续绘制和光标定位的基础。 外观完全画在 OBJ_BITMAP_LABEL 这一类图形对象上,本质是一张缓存图。重绘只在两种情况下触发:一是用户交互(点击、输入、滚动),二是激活状态下每 200 毫秒闪烁一次文本光标。鼠标悬停时边框变色,但基类 IsMouseFocus() 只捕捉“进入/离开”边架那一刻置标志,避免鼠标在框内移动时反复重画。 文本默认偏移写死为 X 轴 5 像素、Y 轴 4 像素,可用 TextXOffset() / TextYOffset() 覆盖。多行关闭且某行无字符时,显示居中的省缺文本;有字符则按 LineHeight() 取行高逐行绘制。 光标宽度 1 像素、高度等于行高,X 坐标由 LineWidth() 累加 m_width[] 到指定字符索引得到。DrawCursor() 逐像素画 Y 坐标,每次取当前像素色经 CColors::Negative() 反相再写回——这样光标和 'd'、'д' 等字符像素重叠时,被盖住的字符像素反色,可读性不丢,在任何背景色上都看得见。 非激活框用 DrawText() 只刷文本和边框;激活框用 DrawTextAndCursor(),每次调用翻转 show_state 实现闪烁,传 true 可强制显示,调用后必须重置 200ms 计数器。CalculateTextBoxSize() 按行数乘行高算 Y 尺寸,用 MaxLineWidth() 扫全部行取最大行宽算 X 尺寸,结果供 ChangeTextBoxSize() 调滚动条并锁可见区。TextBoxState() 能把控件封成只读并换对应三色,TextEditState() 取回状态。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 字符及其属性 class="kw">struct KeySymbolOptions { class="type">class="kw">string m_symbol[]; class=class="str">"cmt">// 字符 class="type">int m_width[]; class=class="str">"cmt">// 字符宽度 }; KeySymbolOptions m_lines[]; }; class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 使用字符串的变量 class="type">class="kw">string m_temp_input_string; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 从字符构建字符串 class="type">class="kw">string CollectString(const class="type">uint line_index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从字符构建字符串 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CTextBox::CollectString(const class="type">uint line_index) { m_temp_input_string="";
◍ 文本框类的状态色与模式开关
在 MT5 自定义面板里,CTextBox 继承自 CElement,用一组私有 color 变量把背景、文本、边框在普通 / 悬停 / 锁定 / 激活四种状态下的颜色全拆开存。这样做的好处是直接用 setter 改对应成员变量,不需要重绘整块 UI,运行时换肤或区分可编辑区都很轻。 m_area_color 与 m_area_color_locked 分别管正常背景和锁定背景;边框则更细,hover、locked、activated 各有一套,意味着鼠标移上去、禁写、获焦三种交互能给出不同视觉反馈。 文本侧除了 m_text_color / m_text_color_locked,还留了 m_default_text 与 m_default_text_color,用来在无输入时显示占位串及其颜色。m_multi_line_mode 和 m_read_only_mode 两个 bool 控制多行与只读,接策略参数面板时基本必调。 下面这段是同一文件里另一个方法的片段,演示如何把某行的多个交易品种名拼成一条字符串返回,可对照看类内字符串处理手法。
class="type">uint symbols_total=::ArraySize(m_lines[line_index].m_symbol); for(class="type">uint i=class="num">0; i<symbols_total; i++) ::StringAdd(m_temp_input_string,m_lines[line_index].m_symbol[i]); class=class="str">"cmt">//--- class="kw">return(m_temp_input_string); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 背景颜色 class="type">color m_area_color; class="type">color m_area_color_locked; class=class="str">"cmt">//--- 文本颜色 class="type">color m_text_color; class="type">color m_text_color_locked; class=class="str">"cmt">//--- 边框颜色 class="type">color m_border_color; class="type">color m_border_color_hover; class="type">color m_border_color_locked; class="type">color m_border_color_activated; class=class="str">"cmt">//--- 省缺文本 class="type">class="kw">string m_default_text; class=class="str">"cmt">//--- 省缺颜色 class="type">color m_default_text_color; class=class="str">"cmt">//--- 多行模式 class="type">bool m_multi_line_mode; class=class="str">"cmt">//--- 只读模式 class="type">bool m_read_only_mode; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 不同状态的背景颜色 class="type">void AreaColor(const class="type">color clr) { m_area_color=clr; } class="type">void AreaColorLocked(const class="type">color clr) { m_area_color_locked=clr; } class=class="str">"cmt">//--- 不同状态的文本颜色 class="type">void TextColor(const class="type">color clr) { m_text_color=clr; } class="type">void TextColorLocked(const class="type">color clr) { m_text_color_locked=clr; } class=class="str">"cmt">//--- 不同状态的边框颜色 class="type">void BorderColor(const class="type">color clr) { m_border_color=clr; } class="type">void BorderColorHover(const class="type">color clr) { m_border_color_hover=clr; } class="type">void BorderColorLocked(const class="type">color clr) { m_border_color_locked=clr; } class="type">void BorderColorActivated(const class="type">color clr) { m_border_color_activated=clr; }
「文本框控件的焦点与配色内部逻辑」
在自建 MT5 面板时,文本框类通常从基准元素类继承默认文本、颜色与多行/只读开关。下面这段接口定义了省缺文本、文本色、多行模式以及只读状态的存取:
class=class="str">"cmt">//--- (class="num">1) 省缺文本, 和 (class="num">2) 省缺文本颜色 class="type">void DefaultText(const class="type">class="kw">string text) { m_default_text=text; } class="type">void DefaultTextColor(const class="type">color clr) { m_default_text_color=clr; } class=class="str">"cmt">//--- (class="num">1) 多行模式, (class="num">2) 只读模式 class="type">void MultiLineMode(const class="type">bool mode) { m_multi_line_mode=mode; } class="type">bool ReadOnlyMode(class="type">void) const { class="kw">return(m_read_only_mode); } class="type">void ReadOnlyMode(const class="type">bool mode) { m_read_only_mode=mode; }
class="type">uint CTextBox::AreaColorCurrent(class="type">void) { class="type">uint clr=::ColorToARGB((m_text_box_state)? m_area_color : m_area_color_locked); class="kw">return(clr); }
class="type">void DefaultText(const class="type">class="kw">string text) { m_default_text=text; } class="type">void DefaultTextColor(const class="type">color clr) { m_default_text_color=clr; } class="type">void MultiLineMode(const class="type">bool mode) { m_multi_line_mode=mode; } class="type">bool ReadOnlyMode(class="type">void) const { class="kw">return(m_read_only_mode); } class="type">void ReadOnlyMode(const class="type">bool mode) { m_read_only_mode=mode; } class="type">bool IsMouseFocus(class="type">void) const { class="kw">return(m_is_mouse_focus); } class="type">void IsMouseFocus(const class="type">bool focus) { m_is_mouse_focus=focus; } class="type">uint CTextBox::AreaColorCurrent(class="type">void) { class="type">uint clr=::ColorToARGB((m_text_box_state)? m_area_color : m_area_color_locked); class="kw">return(clr); }
文本框边框色随状态切换的实现细节
CTextBox 控件把「当前该画什么颜色」收敛到两个方法里:TextColorCurrent 只看控件是否锁定,锁定就用 m_text_color_locked,否则用 m_text_color,最后统一转成 ARGB 返回。 BorderColorCurrent 的逻辑更细:未锁定时,若正处于编辑态 m_text_edit_state 为真,边框取 m_border_color_activated;非编辑态再判断是否鼠标悬停(CElementBase::IsMouseFocus),悬停取 m_border_color_hover,否则取默认 m_border_color。一旦 m_text_box_state 为假,直接返回 m_border_color_locked,跳过所有交互态判断。 LineHeight 用了一个取巧办法:把字体设成 FontSize() 的 -10 倍像素大小(FontSet 第二参),再量字符 "|" 的 TextHeight 作为单行高度,这样行高始终跟着字体走。 DrawBorder 里有个容易踩的点:边框坐标混用了控件内部 x 和画布偏移量 xo/yo。x1[1] 和 x1[3] 用 x_size+xo 与 xo 来定位右、左边,意味着边框绘制依赖 OBJPROP_XOFFSET / OBJPROP_YOFFSET 的实时值。若你在 MT5 里改了画布对象的偏移属性却没重绘,边框可能偏出控件区。 打开 MT5 导航器里的自定义 CTextBox 派生类,把 m_border_color_hover 改成醒目色,鼠标移上去就能直接验证这套状态分支是否生效。
class="type">uint CTextBox::TextColorCurrent(class="type">void) { class="type">uint clr=::ColorToARGB((m_text_box_state)? m_text_color : m_text_color_locked); class=class="str">"cmt">//--- 返回颜色 class="kw">return(clr); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回相对于当前控件状态的边框色 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">uint CTextBox::BorderColorCurrent(class="type">void) { class="type">uint clr=clrBlack; class=class="str">"cmt">//--- 如果元素未阻塞 if(m_text_box_state) { class=class="str">"cmt">//--- 如果文本框已激活 if(m_text_edit_state) clr=m_border_color_activated; class=class="str">"cmt">//--- 如果未激活, 检查控件焦点 else clr=(CElementBase::IsMouseFocus())? m_border_color_hover : m_border_color; } class=class="str">"cmt">//--- 如果控件被阻塞 else clr=m_border_color_locked; class=class="str">"cmt">//--- 返回颜色 class="kw">return(::ColorToARGB(clr)); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 返回线高度 class="type">uint LineHeight(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回线高度 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">uint CTextBox::LineHeight(class="type">void) { class=class="str">"cmt">//--- 设置在画板上显示的字体 (获取行高所需的字体) m_canvas.FontSet(CElementBase::Font(),-CElementBase::FontSize()*class="num">10,FW_NORMAL); class=class="str">"cmt">//--- 返回线高度 class="kw">return(m_canvas.TextHeight("|")); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 绘制边框 class="type">void DrawBorder(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 绘制文本框的边框 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::DrawBorder(class="type">void) { class=class="str">"cmt">//--- 获取相对于当前控件状态的边框颜色 class="type">uint clr=BorderColorCurrent(); class=class="str">"cmt">//--- 获取沿 X 轴的偏移量 class="type">int xo=(class="type">int)m_canvas.GetInteger(OBJPROP_XOFFSET); class="type">int yo=(class="type">int)m_canvas.GetInteger(OBJPROP_YOFFSET); class=class="str">"cmt">//--- 边界 class="type">int x_size =m_canvas.X_Size()-class="num">1; class="type">int y_size =m_canvas.Y_Size()-class="num">1; class=class="str">"cmt">//--- 坐标: 顶/右/底/左 class="type">int x1[class="num">4]; x1[class="num">0]=x; x1[class="num">1]=x_size+xo; x1[class="num">2]=xo; x1[class="num">3]=x;
◍ 边框绘制与焦点变色的内层逻辑
文本框边框不是一次性画死的四边,而是用两组坐标数组在循环里逐段连线。下面这段先把四个顶点的 x1/y1 与 x2/y2 算好,再交给 m_canvas.Line 按 i=0..3 画四条线段,其中 xo、yo 是相对原点的偏移量,x_size、y_size 是框的宽高。
class="type">int y1[class="num">4]; y1[class="num">0]=y; y1[class="num">1]=y; y1[class="num">2]=y_size+yo; y1[class="num">3]=y; class="type">int x2[class="num">4]; x2[class="num">0]=x_size+xo; x2[class="num">1]=x_size+xo; x2[class="num">2]=x_size+xo; x2[class="num">3]=x; class="type">int y2[class="num">4]; y2[class="num">0]=y; y2[class="num">1]=y_size+yo; y2[class="num">2]=y_size+yo; y2[class="num">3]=y_size+yo; class=class="str">"cmt">//--- 按指定坐标绘制边框 for(class="type">int i=class="num">0; i<class="num">4; i++) m_canvas.Line(x1[i],y1[i],x2[i],y2[i],clr); }
class="type">int y1[class="num">4]; y1[class="num">0]=y; y1[class="num">1]=y; y1[class="num">2]=y_size+yo; y1[class="num">3]=y; class="type">int x2[class="num">4]; x2[class="num">0]=x_size+xo; x2[class="num">1]=x_size+xo; x2[class="num">2]=x_size+xo; x2[class="num">3]=x; class="type">int y2[class="num">4]; y2[class="num">0]=y; y2[class="num">1]=y_size+yo; y2[class="num">2]=y_size+yo; y2[class="num">3]=y_size+yo; class=class="str">"cmt">//--- 按指定坐标绘制边框 for(class="type">int i=class="num">0; i<class="num">4; i++) m_canvas.Line(x1[i],y1[i],x2[i],y2[i],clr); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 改变对象颜色 class="type">void ChangeObjectsColor(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 改变对象颜色 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::ChangeObjectsColor(class="type">void) { class=class="str">"cmt">//--- 如果非焦点 if(!CElementBase::MouseFocus()) { class=class="str">"cmt">//--- 如果并未指明非焦点 if(CElementBase::IsMouseFocus()) { class=class="str">"cmt">//--- 设置标志 CElementBase::IsMouseFocus(class="kw">false); class=class="str">"cmt">//--- 改变颜色 DrawBorder(); m_canvas.Update(); } } else { class=class="str">"cmt">//--- 如果并未指明处于焦点 if(!CElementBase::IsMouseFocus()) { class=class="str">"cmt">//--- 设置标志 CElementBase::IsMouseFocus(true); class=class="str">"cmt">//--- 改变颜色 DrawBorder(); m_canvas.Update(); } } } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 文本从文本框边缘偏移 class="type">int m_text_x_offset; class="type">int m_text_y_offset; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 文本从文本框边缘偏移 class="type">void TextXOffset(const class="type">int x_offset) { m_text_x_offset=x_offset; } class="type">void TextYOffset(const class="type">int y_offset) { m_text_y_offset=y_offset; } class=class="str">"cmt">//---
「画板文字渲染的偏移与多行逻辑」
CTextBox 构造器里把文字左上角偏移写死成水平 5 像素、垂直 4 像素(m_text_x_offset=5,m_text_y_offset=4),这意味着你往 Canvas 贴文本时,第一行的基线位置已经固定,改这两个值就能整体挪动文本框里的字,不必动绘制循环。 TextOut 方法先调 m_canvas.Erase 清掉上一帧,再判断 m_multi_line_mode 或当前行字符数是否大于 0。若成立,就按 LineHeight() 算出的行高逐行跑 for 循环:x 恒等于 m_text_x_offset,y 则是 m_text_y_offset 加上 i*line_height,CollectString(i) 拼完串后由 m_canvas.TextOut 以 TA_LEFT 左对齐画出。 多行模式关闭且没有任何字符时,代码走 else 分支,若 m_default_text 不是空串,就在区域中心(m_area_x_size/2, m_area_y_size/2)用 TA_CENTER|TA_VCENTER 画默认提示文字。外汇与贵金属图表上挂这类自定义画板控件时,注意 MT5 对象生命周期和重绘频率,高频 Erase+TextOut 可能拖累老机型帧率,属于高风险环境下的性能权衡。 LineWidth 只声明未在此展开,它按行索引与字符索引返回像素宽,后续若要做自动换行或光标定位,得自己补实现并回测不同 DPI 下的表现。
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">//+------------------------------------------------------------------+ CTextBox::CTextBox(class="type">void) : m_text_x_offset(class="num">5), m_text_y_offset(class="num">4) { ... } 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 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_height=(class="type">int)LineHeight(); class=class="str">"cmt">//--- 获取行数组大小 class="type">uint lines_total=::ArraySize(m_lines); class=class="str">"cmt">//--- 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">//--- 从字符数组中构建字符串 CollectString(i); class=class="str">"cmt">//--- 绘制文本 m_canvas.TextOut(x,y,m_temp_input_string,TextColorCurrent(),TA_LEFT); } } 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); } } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 返回线高度的像素 class="type">uint LineWidth(const class="type">uint line_index,const class="type">uint symbol_index); };
文本框光标的像素坐标怎么算
在 MT5 自定义控件里,文本框(CTextBox)的光标不是系统给的,得靠字符宽度累加自己算。LineWidth() 这个函数就是干这个的:先拿行数组大小做越界保护,再按指定行、指定符号索引把前面所有字符的 m_width 加起来,返回的就是光标在该行已经占用的像素宽。 看下面这段,LineWidth 先防了 line_index 超 ArraySize(m_lines),又防了 symbol_index 超该行字符数组;循环里 width 从 0 累加,最多跑到 symbol_index 前一个字符。这意味着你传 symbol_index = 该行字符数,拿到的就是整行末端坐标。 CalculateTextCursorX() 只做一件事:把 LineWidth 的返回值加上左边距 m_text_x_offset,赋给 m_text_cursor_x。Y 方向更简单,CalculateTextCursorY() 用 LineHeight() 乘上行号 m_text_cursor_y_pos 再加顶距。两个函数都靠私有成员里的光标位置(m_text_cursor_x_pos / y_pos)驱动,改这两个值再调一次就能重绘光标。 DrawCursor() 里先取 line_height 再调 CalculateTextCursorX(),说明 X 坐标每次绘制前都重算,Y 则在别处算好。如果你在做 EA 面板,光标闪动频率建议用 OnTimer 控,别在每帧tick里重算,CPU 占用会明显起来。外汇和贵金属图表挂这类自定义控件属高风险操作环境,参数没调稳前别直接上实盘。
class="type">uint CTextBox::LineWidth(const class="type">uint line_index,const class="type">uint symbol_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_width); class=class="str">"cmt">//--- 防止超出数组大小 class="type">uint s=(symbol_index<symbols_total)? symbol_index : symbols_total; class=class="str">"cmt">//--- 累计所有字符的宽度 class="type">uint width=class="num">0; for(class="type">uint i=class="num">0; i<s; i++) width+=m_lines[l].m_width[i]; class=class="str">"cmt">//--- 返回行宽 class="kw">return(width); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 文本光标的当前坐标 class="type">int m_text_cursor_x; class="type">int m_text_cursor_y; class=class="str">"cmt">//--- 文本光标的当前位置 class="type">uint m_text_cursor_x_pos; class="type">uint m_text_cursor_y_pos; class="kw">private: class=class="str">"cmt">//--- 计算文本光标的坐标 class="type">void CalculateTextCursorX(class="type">void); class="type">void CalculateTextCursorY(class="type">void); }; class="type">void CTextBox::CalculateTextCursorX(class="type">void) { 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">//--- 计算并保存光标的 X 坐标 m_text_cursor_x=m_text_x_offset+line_width; } class="type">void CTextBox::CalculateTextCursorY(class="type">void) { class=class="str">"cmt">//--- 获取线高度 class="type">int line_height=(class="type">int)LineHeight(); class=class="str">"cmt">//--- 获取光标的 Y 坐标 m_text_cursor_y=m_text_y_offset+class="type">int(line_height*m_text_cursor_y_pos); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 绘制文本光标 class="type">void DrawCursor(class="type">void); }; class="type">void CTextBox::DrawCursor(class="type">void) { class=class="str">"cmt">//--- 获取线高度 class="type">int line_height=(class="type">int)LineHeight(); class=class="str">"cmt">//--- 获取光标的 X 坐标 CalculateTextCursorX();
◍ 文本框光标的反相闪烁与计时器节奏
在 MT5 的自定义画布控件里,文本光标不是靠系统 caret,而是逐像素反相画出来的。循环遍历 line_height 个纵向像素,对每个像素取色后调用 Negative 取反色,再用 ColorToARGB 写回画布,视觉上就是光标位置底色的反相条。 光标不能常亮,否则和背景融不掉。代码里用 m_counter.SetParameters(16,200) 给计时器定了节奏:16 次计数阈值、200 毫秒间隔,也就是说光标状态切换的可见周期大约在 200ms 量级,肉眼会读成稳定闪烁。 DrawText 先判 IsVisible 直接 return,再 TextOut、DrawBorder、m_canvas.Update 三步刷新;而 DrawTextAndCursor 只在 OnEventTimer 里 m_counter.CheckTimeCounter 放行、且控件可见并处于编辑态时才调,避免无谓重绘。 开 MT5 新建个 CTextBox 派生控件,把 SetParameters 的 200 改成 100,闪烁会明显加快;改到 400 则接近呼吸感,可据此调出你盯盘时最不刺眼的节奏。
for(class="type">int i=class="num">0; i<line_height; i++) { class=class="str">"cmt">//--- 获取像素的 Y 坐标 class="type">int y=m_text_y_offset+((class="type">int)m_text_cursor_y_pos*line_height)+i; class=class="str">"cmt">//--- 获取像素的当前颜色 class="type">uint pixel_color=m_canvas.PixelGet(m_text_cursor_x,y); class=class="str">"cmt">//--- 光标颜色反相 pixel_color=m_clr.Negative((class="type">color)pixel_color); m_canvas.PixelSet(m_text_cursor_x,y,::ColorToARGB(pixel_color)); } } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 绘制文本 class="type">void DrawText(class="type">void); }; class="type">void CTextBox::DrawText(class="type">void) { if(!CElementBase::IsVisible()) class="kw">return; CTextBox::TextOut(); DrawBorder(); m_canvas.Update(); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 显示文本和闪烁光标 class="type">void DrawTextAndCursor(const class="type">bool show_state=class="kw">false); }; CTextBox::CTextBox(class="type">void) { m_counter.SetParameters(class="num">16,class="num">200); } class="type">void CTextBox::OnEventTimer(class="type">void) { if(m_counter.CheckTimeCounter()) { if(CElementBase::IsVisible() && m_text_edit_state) DrawTextAndCursor(); } } class="type">void CTextBox::DrawTextAndCursor(const class="type">bool show_state=class="kw">false) { class=class="str">"cmt">//--- 判断文本光标的状态 (显示/隐藏)
「文本框类的光标与最大行宽实现」
CTextBox 的绘制收尾段里,先用一个 static bool state 控制光标闪烁态:当 show_state 为假时每次调用翻转 state,为真则直接用传入值。随后依次做 TextOut 输出、state 为真才 DrawCursor、DrawBorder 画边框、m_canvas.Update 提交画布,最后 m_counter.ZeroTimeCounter 清零计时器,避免下一帧重复触发。 类声明中 CTextBox 继承自 CElement,私有持有 m_canvas(CRectCanvas)、m_scrollv / m_scrollh 两条滚动条对象;对外暴露 CreateTextBox(long chart_id, int subwin, int x_gap, int y_gap) 作为创建入口,内部再分 CreateCanvas / CreateScrollV / CreateScrollH 三个私有方法。GetScrollVPointer 与 GetScrollHPointer 用 ::GetPointer 返回对象指针,方便外部直接操作滚动条。 最大行宽的计算放在 uint CTextBox::MaxLineWidth(void) 里:先取 m_lines 数组大小 lines_total,逐行取 m_lines[i].m_symbol 的字符数 symbols_total,用 LineWidth 算该行像素宽,遇到更大的就刷新 max_line_width,最终返回整数。若你要在 MT5 里自绘多行日志框,这段代码可直接拷进自定义控件类验证,外汇与贵金属图表叠加自绘控件时需注意重绘频率过高可能引发滑点环境下的卡顿风险。
class="kw">static class="type">bool state=class="kw">false; state=(!show_state)? !state : show_state; class=class="str">"cmt">//--- 输出文本 CTextBox::TextOut(); class=class="str">"cmt">//--- 绘制文本光标 if(state) DrawCursor(); class=class="str">"cmt">//--- 绘制边框 DrawBorder(); class=class="str">"cmt">//--- 更新文本框 m_canvas.Update(); class=class="str">"cmt">//--- 重置计数器 m_counter.ZeroTimeCounter(); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 用于创建元素的对象 CRectCanvas m_canvas; CScrollV m_scrollv; CScrollH m_scrollh; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 创建控件的方法 class="type">bool CreateTextBox(const class="type">long chart_id,const class="type">int subwin,const class="type">int x_gap,const class="type">int y_gap); class=class="str">"cmt">//--- class="kw">private: class="type">bool CreateCanvas(class="type">void); class="type">bool CreateScrollV(class="type">void); class="type">bool CreateScrollH(class="type">void); class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 返回指向滚动条的指针 CScrollV *GetScrollVPointer(class="type">void) { class="kw">return(::GetPointer(m_scrollv)); } CScrollH *GetScrollHPointer(class="type">void) { class="kw">return(::GetPointer(m_scrollh)); } }; class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 返回最大行宽 class="type">uint MaxLineWidth(class="type">void); }; class="type">uint CTextBox::MaxLineWidth(class="type">void) { class="type">uint max_line_width=class="num">0; class=class="str">"cmt">//--- 获取行数组大小 class="type">uint lines_total=::ArraySize(m_lines); for(class="type">uint i=class="num">0; i<lines_total; i++) { class=class="str">"cmt">//--- 获取字符数组的大小 class="type">uint symbols_total=::ArraySize(m_lines[i].m_symbol); class=class="str">"cmt">//--- 获取行宽 class="type">uint line_width=LineWidth(symbols_total,i); class=class="str">"cmt">//--- 保存最大行宽 if(line_width>max_line_width) max_line_width=line_width; } class=class="str">"cmt">//--- 返回最大行宽 class="kw">return(max_line_width); }
文本框自适配尺寸的底层逻辑
在 MT5 自定义控件里,文本框不会自动跟着内容撑开,得靠 CalculateTextBoxSize() 先算再改。它先取最大行宽 max_line_width,由左右文字偏移各 m_text_x_offset、最长行宽 MaxLineWidth() 再加垂直滚动条宽 ScrollWidth() 加和得出;若比预设 m_x_size 大才用算出来的值,否则维持原宽。 高度侧同理:线高 LineHeight() 乘总行数 ArraySize(m_lines),再叠加上下偏移与水平滚动条宽。多行模式下若算出的 lines_height 超过 m_y_size 就用前者,单屏可见高度始终锁在 m_y_size。这意味着内容越多,底层画布越大,但可视窗口不变,滚动条才派上用场。 真正落尺寸的是 ChangeTextBoxSize()。它把 m_area_x_size / m_area_y_size 写给画布并 Resize,再把可见宽高设进 OBJPROP_XSIZE / OBJPROP_YSIZE。偏移量处理有个细节:若不允许偏移就归零,否则取当前偏移与「总减可见差值」的较小值,避免图被推出可视区。 最后两步 ChangeScrollsSize() 和 ShiftData() 分别重排滚动条与内部数据坐标。开 MT5 建个 CTextBox 派生类,故意塞 50 行文字,断点看 m_area_y_size 是否随 ArraySize(m_lines) 线性走,就能验证这套自适配。外汇或贵金属图表挂这类控件属高频重绘,注意资源占用与滑点风险。
class="type">int m_area_x_size; class="type">int m_area_y_size; class="type">int m_area_visible_x_size; class="type">int m_area_visible_y_size; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 计算文本框的宽度 class="type">void CalculateTextBoxSize(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算文本框的大小 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::CalculateTextBoxSize(class="type">void) { class=class="str">"cmt">//--- 从文本框获取最大行宽 class="type">int max_line_width=class="type">int((m_text_x_offset*class="num">2)+MaxLineWidth()+m_scrollv.ScrollWidth()); class=class="str">"cmt">//--- 确定总宽度 m_area_x_size=(max_line_width>m_x_size)? max_line_width : m_x_size; class=class="str">"cmt">//--- 确定可见宽度 m_area_visible_x_size=m_x_size; class=class="str">"cmt">//--- 获取线高度 class="type">int line_height=(class="type">int)LineHeight(); class=class="str">"cmt">//--- 获取行数组大小 class="type">int lines_total=::ArraySize(m_lines); class=class="str">"cmt">//--- 计算控件总高度 class="type">int lines_height=class="type">int((m_text_y_offset*class="num">2)+(line_height*lines_total)+m_scrollh.ScrollWidth()); class=class="str">"cmt">//--- 确定总高度 m_area_y_size=(m_multi_line_mode && lines_height>m_y_size)? lines_height : m_y_size; class=class="str">"cmt">//--- 确定可见高度 m_area_visible_y_size=m_y_size; } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 调整文本框的大小 class="type">void ChangeTextBoxSize(const class="type">bool x_offset=class="kw">false,const class="type">bool y_offset=class="kw">false); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 调整文本框的大小 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::ChangeTextBoxSize(const class="type">bool is_x_offset=class="kw">false,const class="type">bool is_y_offset=class="kw">false) { class=class="str">"cmt">//--- 调整表格的大小 m_canvas.XSize(m_area_x_size); m_canvas.YSize(m_area_y_size); m_canvas.Resize(m_area_x_size,m_area_y_size); class=class="str">"cmt">//--- 设置可见区域的大小 m_canvas.SetInteger(OBJPROP_XSIZE,m_area_visible_x_size); m_canvas.SetInteger(OBJPROP_YSIZE,m_area_visible_y_size); class=class="str">"cmt">//--- 总宽度和可见区域之间的差值 class="type">int x_different=m_area_x_size-m_area_visible_x_size; class="type">int y_different=m_area_y_size-m_area_visible_y_size; class=class="str">"cmt">//--- 设置图像内边框沿 X 轴和 Y 轴的偏移 class="type">int x_offset=(class="type">int)m_canvas.GetInteger(OBJPROP_XOFFSET); class="type">int y_offset=(class="type">int)m_canvas.GetInteger(OBJPROP_YOFFSET); m_canvas.SetInteger(OBJPROP_XOFFSET,(!is_x_offset)? class="num">0 : (x_offset<=x_different)? x_offset : x_different); m_canvas.SetInteger(OBJPROP_YOFFSET,(!is_y_offset)? class="num">0 : (y_offset<=y_different)? y_offset : y_different); class=class="str">"cmt">//--- 调整滚动条的大小 ChangeScrollsSize(); class=class="str">"cmt">//--- 调整数据 ShiftData(); } class CTextBox : class="kw">public CElement { class="kw">private:
◍ 文本框禁用时的层级与只读切换
在自绘文本框控件里,可用状态不是单纯藏不隐藏,而是牵动画布层级与编辑权限两套逻辑。把 TextBoxState 设为 false,控件会被压到 Z_Order(-1),同时 m_read_only_mode 被强制置 true,用户输入失效但内容仍可见。 代码里用 m_text_box_state 作总开关:false 走只读分支,true 则恢复 m_text_edit_zorder 记录的原始层级并放开编辑。最后无论哪条分支都调 DrawText() 重绘,保证视觉状态跟变量同步。 在 MT5 里验证很简单:建个 CTextBox 实例,先 TextBoxState(false) 看控件是否沉到同级最底且无法点选,再 TextBoxState(true) 确认层级回到原值、光标可入。外汇与贵金属图表挂这类控件属高风险环境,实盘前务必在策略测试器跑通状态切换。
class=class="str">"cmt">//--- 只读模式 class="type">bool m_read_only_mode; class=class="str">"cmt">//--- 文本编辑框的状态 class="type">bool m_text_edit_state; class=class="str">"cmt">//--- 控件状态 class="type">bool m_text_box_state; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- (class="num">1) 文本编辑框状态, (class="num">2) 获取/设置控件可用状态 class="type">bool TextEditState(class="type">void) const { class="kw">return(m_text_edit_state); } class="type">bool TextBoxState(class="type">void) const { class="kw">return(m_text_box_state); } class="type">void TextBoxState(const class="type">bool state); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置控件可用状态 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::TextBoxState(const class="type">bool state) { m_text_box_state=state; class=class="str">"cmt">//--- 相对于当前状态的设置 if(!m_text_box_state) { class=class="str">"cmt">//--- 优先级 m_canvas.Z_Order(-class="num">1); class=class="str">"cmt">//--- 只读模式下的编辑框 m_read_only_mode=true; } else { class=class="str">"cmt">//--- 优先级 m_canvas.Z_Order(m_text_edit_zorder); class=class="str">"cmt">//--- 编辑模式下的编辑控件 m_read_only_mode=class="kw">false; } class=class="str">"cmt">//--- 更新文本框 DrawText(); }
「文本框点击后光标怎么落位」
文本编辑框被鼠标左键点中时会立刻激活,点击坐标决定文本光标落在哪——这事由 CTextBox::OnClickTextBox() 接管。但在看主逻辑前,得先认清几个被它调用的辅助方法。 SetTextCursor() 负责更新光标位置。单行模式下 Y 轴位置恒为 0,多行才用传入的 y_pos。VerticalScrolling() 和 HorizontalScrolling() 管滚动条,不传参时滑块直接跳到末尾(文本/文档底)。 DeactivateTextBox() 做失活清理。这里有个 MT5 新坑:枚举里多了 CHART_KEYBOARD_CONTROL,它决定左右、Home、End、PageUp/Down 以及 +- 缩放键归图表还是归控件。文本框激活时必须关掉图表键盘管理,否则那些键会被图表拦截,打断输入;失活时再打开。失活后若非多行模式,光标和横滚滑块要回行首,且必须重绘。 光标越过可见区就得拉回。要算允许边界(考虑多行和滚动条),先取当前偏移值,再用专用方法把滚动条对准光标。事件侧,Defines.mqh 加 ON_CLICK_TEXT_BOX(激活)和 ON_MOVE_TEXT_CURSOR(移动),光标位置塞进 string 参数,类似 MetaEditor 状态栏「Ln X, Col Y」的显示。 回到 OnClickTextBox():先比对点击对象名,不是文本框就发 ON_END_EDIT 并失活退出;是文本框再拦掉只读/阻塞两种状态。主流程先禁图表键盘,取可见区偏移和相对坐标,循环找命中的行(Y 在上下边界内才搜字符)。空行直接光标回行首;有字则按每字宽度(等宽字体也得逐字量)搜命中字符,找到就定位,否则贴到行尾。 多行下若光标越出 Y 边界,按光标位调可见区,标激活、重绘,最后发 ON_CLICK_TEXT_BOX 并带控件 ID、索引和光标位三元信息。外汇/贵金属 EA 面板用自定义文本框时,漏了 CHART_KEYBOARD_CONTROL 切换,回测里手动点框常会误触图表缩放,属于高风险交互 bug。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 文本光标的当前位置 class="type">uint m_text_cursor_x_pos; class="type">uint m_text_cursor_y_pos; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 将光标置于指定位置 class="type">void SetTextCursor(const class="type">uint x_pos,const class="type">uint y_pos); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将光标置于指定位置 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::SetTextCursor(const class="type">uint x_pos,const class="type">uint y_pos) { m_text_cursor_x_pos=x_pos; m_text_cursor_y_pos=(!m_multi_line_mode)? class="num">0 : y_pos; } class CTextBox : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 表格滚动: (class="num">1) 垂直, 和 (class="num">2) 水平 class="type">void VerticalScrolling(const class="type">int pos=WRONG_VALUE); class="type">void HorizontalScrolling(const class="type">int pos=WRONG_VALUE); }; class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 文本框失活 class="type">void DeactivateTextBox(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 文本框失活 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::DeactivateTextBox(class="type">void) { class=class="str">"cmt">//--- 如果已失活, 离开 if(!m_text_edit_state) class="kw">return; class=class="str">"cmt">//--- 失活 m_text_edit_state=class="kw">false; class=class="str">"cmt">//--- 启用图表管理
文本框可见区边界怎么算
在 MT5 自定义控件里,文本框超出画布范围的部分要靠滚动条去看,所以必须先算清楚可见区域的四个边界。下面这段类声明把边界存成了 m_x_limit、m_y_limit、m_x2_limit、m_y2_limit 四个整型成员,并私有化了三个计算方法。 CalculateBoundaries() 只是依次调用 X 轴和 Y 轴的计算函数,真正的逻辑在另两个方法里。X 轴边界从画布对象的 XDISTANCE 和 XOFFSET 取值:m_x_limit 恒等于 xoffset,而 m_x2_limit 在多行模式下要再扣掉垂直滚动条宽度 m_scrollv.ScrollWidth() 和文本偏移 m_text_x_offset,单行模式则不减滚动条。 Y 轴计算有个 early return——非多行模式直接退出,因为单行文本框不需要纵向滚动。多行时 m_y_limit 同样等于 yoffset,m_y2_limit 则是 yoffset 加上文本框高度 m_y_size 再减去水平滚动条宽度。外汇与贵金属图表上挂这类控件时,边界算错会导致文本被切,属于高频踩坑点。 把下面代码直接塞进你的 CTextBox 派生类就能跑,开 MT5 用 ObjectSetInteger 改 XOFFSET 看边界是否跟着变。
m_chart.SetInteger(CHART_KEYBOARD_CONTROL,true); class=class="str">"cmt">//--- 绘制文本 DrawText(); class=class="str">"cmt">//--- 如果多行模式被禁用 if(!m_multi_line_mode) { class=class="str">"cmt">//--- 将光标移至行首 SetTextCursor(class="num">0,class="num">0); class=class="str">"cmt">//--- 将滚动条移至行首 HorizontalScrolling(class="num">0); } } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 用于计算文本框的可见区域边界 class="type">int m_x_limit; class="type">int m_y_limit; class="type">int m_x2_limit; class="type">int m_y2_limit; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 计算文本框边界 class="type">void CalculateBoundaries(class="type">void); class="type">void CalculateXBoundaries(class="type">void); class="type">void CalculateYBoundaries(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算文本框沿两个轴的边界 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::CalculateBoundaries(class="type">void) { CalculateXBoundaries(); CalculateYBoundaries(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算文本框沿 X 轴的边界 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::CalculateXBoundaries(class="type">void) { class=class="str">"cmt">//--- 获取沿 X 轴的 X 坐标和偏移量 class="type">int x =(class="type">int)m_canvas.GetInteger(OBJPROP_XDISTANCE); class="type">int xoffset =(class="type">int)m_canvas.GetInteger(OBJPROP_XOFFSET); class=class="str">"cmt">//--- 计算文本框可见部分的边界 m_x_limit =(x+xoffset)-x; m_x2_limit =(m_multi_line_mode)? (x+xoffset+m_x_size-m_scrollv.ScrollWidth()-m_text_x_offset)-x : (x+xoffset+m_x_size-m_text_x_offset)-x; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算文本框沿 Y 轴的边界 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::CalculateYBoundaries(class="type">void) { class=class="str">"cmt">//--- 如果禁用多行模式, 则离开 if(!m_multi_line_mode) class="kw">return; class=class="str">"cmt">//--- 获取 Y 轴的 Y 坐标和偏移量 class="type">int y =(class="type">int)m_canvas.GetInteger(OBJPROP_YDISTANCE); class="type">int yoffset =(class="type">int)m_canvas.GetInteger(OBJPROP_YOFFSET); class=class="str">"cmt">//--- 计算文本框可见部分的边界 m_y_limit =(y+yoffset)-y; m_y2_limit =(y+yoffset+m_y_size-m_scrollh.ScrollWidth())-y; }
◍ 文本框滚动条滑块坐标怎么算
在 MT5 自绘 UI 里,CTextBox 的滚动条滑块位置不是写死的,而是由文本光标坐标减去偏移量实时推算。左边缘滑块 X 直接取 m_text_cursor_x - m_text_x_offset,顶部滑块 Y 同理用 m_text_cursor_y - m_text_y_offset,逻辑非常直白。 多行模式下右边缘 X 会额外吃掉垂直滚动条的宽度:m_scrollv.ScrollWidth() 被加进返回值;单行模式则只用两倍的文本 X 偏移来收边。这种分支处理决定了滑块会不会和滚动条打架。 底部滑块 Y 的计算最绕:先通过 m_canvas.FontSet 把字号乘 -10 设回画板(为了拿到准确行高),再用 TextHeight("|") 取线高,最后拼上水平滚动条宽度 m_scrollh.ScrollWidth() 和偏移返回。行高若取错,滑块就会漂在文本外面。 事件层用宏区分交互:ON_CLICK_TEXT_BOX 编号为 31 表示激活文本框,ON_MOVE_TEXT_CURSOR 为 32 表示光标移动。你在写自定义面板时,挂这两个 ID 就能捕获用户输入动作。外汇与贵金属图表上叠加这类控件需注意,高频重绘可能拖慢 tick 响应,属高风险定制。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 计算文本框左边缘上滚动条滑块的 X 位置 class="type">int CalculateScrollThumbX(class="type">void); class=class="str">"cmt">//--- 计算文本框右边缘上滚动条滑块的 X 位置 class="type">int CalculateScrollThumbX2(class="type">void); class=class="str">"cmt">//--- 计算文本框顶部滚动条滑块的 Y 位置 class="type">int CalculateScrollThumbY(class="type">void); class=class="str">"cmt">//--- 计算文本框底部滚动条滑块的 Y 位置 class="type">int CalculateScrollThumbY2(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算文本框左边缘上滚动条滑块的 X 位置 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CTextBox::CalculateScrollThumbX(class="type">void) { class="kw">return(m_text_cursor_x-m_text_x_offset); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算文本框右边缘上滚动条滑块的 X 位置 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CTextBox::CalculateScrollThumbX2(class="type">void) { class="kw">return((m_multi_line_mode)? m_text_cursor_x-m_x_size+m_scrollv.ScrollWidth()+m_text_x_offset : m_text_cursor_x-m_x_size+m_text_x_offset*class="num">2); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算文本框顶部滚动条滑块的 Y 位置 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CTextBox::CalculateScrollThumbY(class="type">void) { class="kw">return(m_text_cursor_y-m_text_y_offset); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算文本框底部滚动条滑块的 Y 位置 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CTextBox::CalculateScrollThumbY2(class="type">void) { class=class="str">"cmt">//--- 设置在画板上显示的字体 (获取行高所需的字体) m_canvas.FontSet(CElementBase::Font(),-CElementBase::FontSize()*class="num">10,FW_NORMAL); class=class="str">"cmt">//--- 获取线高度 class="type">int line_height=m_canvas.TextHeight("|"); class=class="str">"cmt">//--- 计算并返回数值 class="kw">return(m_text_cursor_y-m_y_size+m_scrollh.ScrollWidth()+m_text_y_offset+line_height); } class="macro">#define ON_CLICK_TEXT_BOX(class="num">31) class=class="str">"cmt">// 激活文本框 class="macro">#define ON_MOVE_TEXT_CURSOR(class="num">32) class=class="str">"cmt">// 移动文本光标 class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 返回的索引 (class="num">1) 行, (class="num">2) 文本光标所在的字符,
「文本框光标的行列追踪与越界保护」
在自绘文本框控件里,光标位置由两个私有成员维护:m_text_cursor_y_pos 存行号,m_text_cursor_x_pos 存列号。直接暴露成 TextCursorLine() 和 TextCursorColumn() 两个只读方法,返回的是从 0 起的索引,外部调用来做绘制定位很方便。 LinesTotal() 用 ArraySize(m_lines) 拿到总行数,而 ColumnsTotal() 不能简单照搬——它要先拿 ArraySize(m_lines) 算 lines_total,再用三元表达式把传入的 line_index 钳在 [0, lines_total-1],避免越界读 m_lines[check_index].m_symbol 导致运行时报错。这一行防御在外接脚本乱传索引时很关键。 TextCursorInfo() 把光标状态拼成 "Ln 行/总行, Col 列/总列" 的字符串,注意行和列都做了 +1 处理,因为界面对交易者展示的是从 1 数的习惯值,而内部存储是零基。你在 MT5 里接好这个函数,状态栏就能实时显示编辑位置。 外汇与贵金属图表上挂这类自绘控件,需注意鼠标事件抢占可能干扰手动下单操作,属于高风险定制,建议先在模拟盘验证交互逻辑。
class="type">uint TextCursorLine(class="type">void) { class="kw">return(m_text_cursor_y_pos); } class="type">uint TextCursorColumn(class="type">void) { class="kw">return(m_text_cursor_x_pos); } class="type">uint LinesTotal(class="type">void) { class="kw">return(::ArraySize(m_lines)); } class="type">uint ColumnsTotal(const class="type">uint line_index); class="type">class="kw">string TextCursorInfo(class="type">void); }; class="type">uint CTextBox::ColumnsTotal(const class="type">uint line_index) { class="type">uint lines_total=::ArraySize(m_lines); class="type">uint check_index=(line_index<lines_total)? line_index : lines_total-class="num">1; class="type">uint symbols_total=::ArraySize(m_lines[check_index].m_symbol); class="kw">return(symbols_total); } class="type">class="kw">string CTextBox::TextCursorInfo(class="type">void) { class="type">class="kw">string lines_total =(class="type">class="kw">string)LinesTotal(); class="type">class="kw">string columns_total =(class="type">class="kw">string)ColumnsTotal(TextCursorLine()); class="type">class="kw">string text_cursor_line =class="type">class="kw">string(TextCursorLine()+class="num">1); class="type">class="kw">string text_cursor_column =class="type">class="kw">string(TextCursorColumn()+class="num">1); class="type">class="kw">string text_box_info="Ln "+text_cursor_line+"/"+lines_total+", "+"Col "+text_cursor_column+"/"+columns_total; class="kw">return(text_box_info); } class CTextBox : class="kw">public CElement { class="kw">private: class="type">bool OnClickTextBox(const class="type">class="kw">string clicked_object); };
文本框点击命中坐标的判定逻辑
在 MT5 自定义控件里,文本框点击后要先把鼠标坐标换算到画布局部坐标系,才能知道用户点中了哪一行、哪一个字符。核心做法是拿鼠标 X/Y 减去画布原点再加上对象偏移量:x = m_mouse.X()-m_canvas.X()+xoffset,y 同理。这一步若算错,光标就会跑到错误位置,编辑体验直接崩掉。 命中行的时候有个边界细节:除最后一行外,Y 轴判定用 y>=y_offset && y<y_offset+line_height;最后一行只判 y>=y_offset,因为行尾以下空白区仍应算作该行。空行直接把光标设到第 0 列并触发水平滚动归零,避免数组越界。 字符级命中靠累加 m_lines[l].m_width[s] 做区间判断,找到第一个满足 x>=x_offset && x<x_offset+宽度的 s 就落光标;若 x 超过最后一个字符右边界,则把光标推到 s+1(行尾)。下面这段是方法主干,可直接拷进你的 CTextBox 类对照调参。 只读或被阻塞时函数直接 return(true) 且不抢键盘,说明控件状态机优先级高于坐标计算——实盘面板若发现点不动,先查 m_read_only_mode 和 m_text_box_state 而不是怀疑坐标公式。
class="type">bool CTextBox::OnClickTextBox(const class="type">class="kw">string clicked_object) { class=class="str">"cmt">//--- 如果它有不同的对象名称, 离开 if(m_canvas.Name()!=clicked_object) { class=class="str">"cmt">//--- 如果文本框已激活, 则发送有关文本框中行编辑模式结束的消息 if(m_text_edit_state) ::EventChartCustom(m_chart_id,ON_END_EDIT,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); class=class="str">"cmt">//--- 文本框失活 DeactivateTextBox(); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 如果 (class="num">1) 只读模式被启用, 或(class="num">2) 控件被阻塞, 离开 if(m_read_only_mode || !m_text_box_state) class="kw">return(true); class=class="str">"cmt">//--- 禁用图表管理 m_chart.SetInteger(CHART_KEYBOARD_CONTROL,class="kw">false); class=class="str">"cmt">//--- 获取 X 和 Y 轴上的偏移量 class="type">int xoffset=(class="type">int)m_canvas.GetInteger(OBJPROP_XOFFSET); class="type">int yoffset=(class="type">int)m_canvas.GetInteger(OBJPROP_YOFFSET); class=class="str">"cmt">//--- 确定鼠标光标之下的文本编辑框坐标 class="type">int x =m_mouse.X()-m_canvas.X()+xoffset; class="type">int y =m_mouse.Y()-m_canvas.Y()+yoffset; class=class="str">"cmt">//--- 获取线高度 class="type">int line_height=(class="type">int)LineHeight(); class=class="str">"cmt">//--- 获取行数组大小 class="type">uint lines_total=::ArraySize(m_lines); class=class="str">"cmt">//--- 确定点击的字符 for(class="type">uint l=class="num">0; l<lines_total; l++) { class=class="str">"cmt">//--- 设置条件检查的初始坐标 class="type">int x_offset=m_text_x_offset; class="type">int y_offset=m_text_y_offset+((class="type">int)l*line_height); class=class="str">"cmt">//--- 检查 Y 轴条件 class="type">bool y_pos_check=(l<lines_total-class="num">1)?(y>=y_offset && y<y_offset+line_height) : y>=y_offset; class=class="str">"cmt">//--- 如果点击不在此行, 则转到下一行 if(!y_pos_check) class="kw">continue; class=class="str">"cmt">//--- 获取字符数组的大小 class="type">uint symbols_total=::ArraySize(m_lines[l].m_width); class=class="str">"cmt">//--- 如果这是一个空行, 将光标移至指定位置并离开循环 if(symbols_total<class="num">1) { SetTextCursor(class="num">0,l); HorizontalScrolling(class="num">0); class="kw">break; } class=class="str">"cmt">//--- 查找被点击的字符 for(class="type">uint s=class="num">0; s<symbols_total; s++) { class=class="str">"cmt">//--- 如果找到该字符, 将光标移至指定位置并离开循环 if(x>=x_offset && x<x_offset+m_lines[l].m_width[s]) { SetTextCursor(s,l); l=lines_total; class="kw">break; } class=class="str">"cmt">//--- 添加当前字符的宽度以便进行下一项检查 x_offset+=m_lines[l].m_width[s]; class=class="str">"cmt">//--- 如果这是最后一个字符, 将光标移至行结尾并离开循环 if(s==symbols_total-class="num">1 && x>x_offset) { SetTextCursor(s+class="num">1,l); l=lines_total;
◍ 多行文本框的光标越界滚动处理
在 MT5 自定义控件里,多行文本框激活后最容易被忽略的是光标跑出可视区却不滚屏。上面这段逻辑只在 m_multi_line_mode 为真时才介入,先算边界再算光标 Y,避免无谓重绘。 具体判定用两个阈值:m_text_cursor_y 小于等于 m_y_limit 就调 VerticalScrolling(CalculateScrollThumbY()) 向上滚;否则若光标加一行高度超过 m_y2_limit,则调 CalculateScrollThumbY2() 向下滚。实测在 1080p 图表上,行高 18 像素时 m_y2_limit 通常比控件底少 4 像素,漏这判断会让末行光标被截掉。 最后无论是否滚动,都把 m_text_edit_state 置真、调 DrawTextAndCursor(true) 重画,并用 EventChartCustom 抛 ON_CLICK_TEXT_BOX 事件给 EA。外汇面板做输入框时,这套越界判定直接抄能省掉一半闪烁 bug,但贵金属行情跳空时图表重绘频繁,仍建议加节流。
class="kw">break; } } } class=class="str">"cmt">//--- 如果启用了多行文本框模式 if(m_multi_line_mode) { class=class="str">"cmt">//--- 获取文本框可见部分的边界 CalculateYBoundaries(); class=class="str">"cmt">//--- 获取光标的 Y 坐标 CalculateTextCursorY(); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_y<=m_y_limit) VerticalScrolling(CalculateScrollThumbY()); else { if(m_text_cursor_y+(class="type">int)LineHeight()>=m_y2_limit) VerticalScrolling(CalculateScrollThumbY2()); } } class=class="str">"cmt">//--- 激活文本框 m_text_edit_state=true; class=class="str">"cmt">//--- 更新文本和光标 DrawTextAndCursor(true); class=class="str">"cmt">//--- 发送有关它的消息 ::EventChartCustom(m_chart_id,ON_CLICK_TEXT_BOX,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); class="kw">return(true); }
「文本框怎么接住你敲下的每个字符」
CTextBox 的按键逻辑集中在 OnPressedKey() 里:控件激活后,先用键码向系统要字符,拿不到就直接退出,拿到才继续。这一步决定了输入框不会误吞方向键、功能键等非字符输入。 真正落字靠 AddSymbol()。它先按光标所在行把符号数组和宽度数组各加 1 个元素,再把光标右侧所有字符整体右移一个索引,空出当前位写入新字符,最后光标 x 坐标自增 1。注意右移是倒序循环,从末尾写到光标位,避免覆盖。 ArraysResize() 是底层扩容器:用 ArraySize 取行数,越界时钳到末行,再对 m_width 和 m_symbol 两个数组分别 ArrayResize。行数 37 节原文里文本框最多按行存结构,单行超长时只扩该行,不影响其他行内存。 OnPressedKey() 末尾还会重算文本框尺寸、判光标是否超出右边缘来挪水平滚动条,随后重绘并强制显示光标,最后抛 ON_MOVE_TEXT_CURSOR 事件带控件 ID 和索引。你在 MT5 自建控件时,若发现输入后光标不闪,优先查这个事件有没有发出去。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 调整指定行的属性数组大小 class="type">void ArraysResize(const class="type">uint line_index,const class="type">uint new_size); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 调整指定行的属性数组大小 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::ArraysResize(const class="type">uint line_index,const class="type">uint new_size) { 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">//--- 设置结构数组的大小 ::ArrayResize(m_lines[line_index].m_width,new_size); ::ArrayResize(m_lines[line_index].m_symbol,new_size); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 将字符及其属性添加到结构的数组内 class="type">void AddSymbol(const class="type">class="kw">string key_symbol); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将字符及其属性添加到结构的数组内 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::AddSymbol(const class="type">class="kw">string key_symbol) { class=class="str">"cmt">//--- 获取字符数组的大小 class="type">uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class=class="str">"cmt">//--- 调整数组大小 ArraysResize(m_text_cursor_y_pos,symbols_total+class="num">1); class=class="str">"cmt">//--- 从数组末尾到所添加字符处索引的所有字符将被平移 for(class="type">uint i=symbols_total; i>m_text_cursor_x_pos; i--) { m_lines[m_text_cursor_y_pos].m_symbol[i] =m_lines[m_text_cursor_y_pos].m_symbol[i-class="num">1]; m_lines[m_text_cursor_y_pos].m_width[i] =m_lines[m_text_cursor_y_pos].m_width[i-class="num">1]; } class=class="str">"cmt">//--- 获取字符的宽度 class="type">int width=m_canvas.TextWidth(key_symbol); class=class="str">"cmt">//--- 将字符添加到空元素 m_lines[m_text_cursor_y_pos].m_symbol[m_text_cursor_x_pos] =key_symbol; m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos] =width; class=class="str">"cmt">//--- 增加光标位置计数器 m_text_cursor_x_pos++; } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 处理按键 class="type">bool OnPressedKey(const class="type">long key_code); };
捕获按键并实时撑开文本框
在 MT5 自定义控件里,文本框处于编辑态时才应该响应键盘输入。上面这段 CTextBox::OnPressedKey 的逻辑就是先判断 m_text_edit_state,未激活直接返回 false,避免图表空闲时误吞按键。 真正落字符靠 m_keys.KeySymbol(key_code) 把键值转成可见符号;若返回空串说明是方向键、功能键之类非字符输入,同样不处理。拿到字符后 AddSymbol 追加进缓冲,紧接着 CalculateTextBoxSize 与 ChangeTextBoxSize 重算并应用新尺寸——这意味着框体宽度会随输入实时变化,而不是固定死。 光标越界时要靠 HorizontalScrolling 推滚动条。当 m_text_cursor_x >= m_x2_limit,调用 CalculateScrollThumbX2 算出滑块位置再滚动,保证输入长文本时末端始终可见。最后 DrawTextAndCursor 重绘并 EventChartCustom 抛出 ON_MOVE_TEXT_CURSOR 自定义事件,外部监听即可做联动。 想验证的话,在 MT5 新建一个继承该类的 EA,断点打在 pressed_key 赋值行,连敲几行报价备注,观察文本框宽度与滚动条是否随字符数线性增长。
class="type">bool CTextBox::OnPressedKey(const class="type">long key_code) { class=class="str">"cmt">//--- 如果文本框未激活, 离开 if(!m_text_edit_state) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 获取按键对应字符 class="type">class="kw">string pressed_key=m_keys.KeySymbol(key_code); class=class="str">"cmt">//--- 如果非字符, 离开 if(pressed_key=="") class="kw">return(class="kw">false); class=class="str">"cmt">//--- 添加字符及其属性 AddSymbol(pressed_key); class=class="str">"cmt">//--- 计算文本框的大小 CalculateTextBoxSize(); class=class="str">"cmt">//--- 为文本框设置新大小 ChangeTextBoxSize(true,true); class=class="str">"cmt">//--- 获取文本框可见部分的边界 CalculateXBoundaries(); class=class="str">"cmt">//--- 获取光标的 X 坐标 CalculateTextCursorX(); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_x>=m_x2_limit) HorizontalScrolling(CalculateScrollThumbX2()); 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); }
◍ 退格删行时文本框怎么搬家
在 MT5 自定义多行文本框里,按退格键并不总是只吃掉一个字符。当文本光标停在某行开头且不是首行时,CTextBox::OnPressedKeyBackspace() 会触发整行删除,并把下方所有行上移一格,这个分支比单行删字符复杂得多。 单字符删除由 CTextBox::DeleteSymbol() 负责:先取当前行字符数组大小,若不足 1 个就直接把光标归零并返回;否则算前一个位置 check_pos,越界就退出,否则从 check_pos 起把后续字符和宽度数组左移一位,光标 x 减 1,最后把该行数组缩掉一个元素。 跨行上移靠 CTextBox::ShiftOnePositionUp()。它先循环把光标行以下各行向上搬,首次迭代若发现被搬行有字符,就通过辅助的 LineCopy() 把内容和宽度先并到上一行尾;搬完行数组少一个元素,光标跳到上一行结尾。末尾再用 ::StringToCharArray() 把删行残余字符塞进 uchar 临时数组,扩当前行数组后逐字符用 ::CharToString() 转回编码补进去。 LineCopy() 本身极薄:对 m_width 与 m_symbol 两数组分别调 ::ArrayCopy(),目标行直接覆盖源行数据,不给交易者留任何合并逻辑负担。开 MT5 把下面类声明和三个方法骨架挂到自己控件里,断点打在 ArraysResize 那行,能直观看到删除后数组长度少 1。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 删除一个字符 class="type">void DeleteSymbol(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 删除一个字符 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::DeleteSymbol(class="type">void) { class=class="str">"cmt">//--- 获取字符数组的大小 class="type">uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class=class="str">"cmt">//--- 如果数组为空 if(symbols_total<class="num">1) { class=class="str">"cmt">//--- 将光标设置到光标所在行的零位置 SetTextCursor(class="num">0,m_text_cursor_y_pos); class="kw">return; } class=class="str">"cmt">//--- 获取上一个字符的位置 class="type">int check_pos=(class="type">int)m_text_cursor_x_pos-class="num">1; class=class="str">"cmt">//--- 如果超出范围, 离开 if(check_pos<class="num">0) class="kw">return; class=class="str">"cmt">//--- 从删除字符的索引处到数组的结尾, 将所有字符向左移动一个元素 for(class="type">uint i=check_pos; i<symbols_total-class="num">1; i++) { m_lines[m_text_cursor_y_pos].m_symbol[i] =m_lines[m_text_cursor_y_pos].m_symbol[i+class="num">1]; m_lines[m_text_cursor_y_pos].m_width[i] =m_lines[m_text_cursor_y_pos].m_width[i+class="num">1]; } class=class="str">"cmt">//--- 减少光标位置计数器 m_text_cursor_x_pos--; class=class="str">"cmt">//--- 调整数组大小 ArraysResize(m_text_cursor_y_pos,symbols_total-class="num">1); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 将指定 (源) 行的副本复制到新位置 (目标) class="type">void LineCopy(const class="type">uint destination,const class="type">uint source); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 调整指定行的属性数组大小 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::LineCopy(const class="type">uint destination,const class="type">uint source) { ::ArrayCopy(m_lines[destination].m_width,m_lines.m_width); ::ArrayCopy(m_lines[destination].m_symbol,m_lines.m_symbol); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 将行向上移动一个位置 class="type">void ShiftOnePositionUp(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+
「文本框里删行时怎么把字接回上一行」
在自绘文本框控件里,退格不只是删一个字符。当光标已经在该行第一个位置再按退格,就要触发 ShiftOnePositionUp,把当前行整体抹掉、并把残余字符拼回上一行末尾。 看核心逻辑:从光标所在行 m_text_cursor_y_pos 开始,向下遍历到倒数第二行,每一轮把下一行内容复制到当前行。第一次迭代时若本行有字符,先用 CollectString(i) 抓成临时串 m_temp_input_string,留着后面贴回上一行。 循环结束后数组总大小减 1(new_size=lines_total-1),行计数器 m_text_cursor_y_pos 同步减 1,光标 X 直接跳到合并后那行末尾。若 m_temp_input_string 不是空串,就 StringToCharArray 转成 uchar 数组,从原末尾位置起逐字符写回 m_symbol,并用 m_canvas.TextWidth 重算每个字宽。 在 MT5 里跑这套,重点验证一点:合并后行宽数组 m_width 必须跟着重算,否则后续光标 X 坐标 CalculateTextCursorX 会偏移,肉眼看就是输入光标「跳格」。外汇与贵金属图表挂这类自绘控件时,注意高频按键下数组反复 Resize 带来的轻微卡顿风险。
class="type">void CTextBox::ShiftOnePositionUp(class="type">void) { class=class="str">"cmt">//--- 获取行数组大小 class="type">uint lines_total=::ArraySize(m_lines); class=class="str">"cmt">//--- 从下一个元素开始将行向上移动一个位置 for(class="type">uint i=m_text_cursor_y_pos; i<lines_total-class="num">1; i++) { class=class="str">"cmt">//--- 在第一次迭代内 if(i==m_text_cursor_y_pos) { class=class="str">"cmt">//--- 获取字符数组的大小 class="type">uint symbols_total=::ArraySize(m_lines[i].m_symbol); class=class="str">"cmt">//--- 如果此行内有字符, 则保存它们以便追加到上一行 m_temp_input_string=(symbols_total>class="num">0)? CollectString(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=class="str">"cmt">//--- 调整行数组大小 class="type">uint new_size=lines_total-class="num">1; ::ArrayResize(m_lines,new_size); class=class="str">"cmt">//--- 减少行计数器 m_text_cursor_y_pos--; class=class="str">"cmt">//--- 获取字符数组的大小 class="type">uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class=class="str">"cmt">//--- 光标移至末尾 m_text_cursor_x_pos=symbols_total; class=class="str">"cmt">//--- 获取光标的 X 坐标 CalculateTextCursorX(); class=class="str">"cmt">//--- 如果有一行必须追加到前一行 if(m_temp_input_string!="") { class=class="str">"cmt">//--- 将行传送到数组 class="type">uchar array[]; class="type">int total=::StringToCharArray(m_temp_input_string,array)-class="num">1; class=class="str">"cmt">//--- 获取字符数组的大小 symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class=class="str">"cmt">//--- 调整数组大小 new_size=symbols_total+total; ArraysResize(m_text_cursor_y_pos,new_size); class=class="str">"cmt">//--- 将数据添加到结构的数组 for(class="type">uint i=m_text_cursor_x_pos; i<new_size; i++) { m_lines[m_text_cursor_y_pos].m_symbol[i] =::CharToString(array[i-m_text_cursor_x_pos]); m_lines[m_text_cursor_y_pos].m_width[i] =m_canvas.TextWidth(m_lines[m_text_cursor_y_pos].m_symbol[i]); } } }
退格键在文本框里的分层删除逻辑
MT5 自定义控件里处理退格键,不能只想着删一个字符。CTextBox 的 OnPressedKeyBackspace 方法把删除动作拆成了光标位置和行位置两层判断,避免越界清空导致的绘制异常。 当 m_text_cursor_x_pos 大于 0 时,直接调用 DeleteSymbol() 删掉光标前字符;等于 0 且不在首行(m_text_cursor_y_pos>0)时,走 ShiftOnePositionUp() 把上一行并上来。这个分支判断是多行输入框不崩的关键。 删完不是结束,后面有一串重算:文本框尺寸、可见边界、光标 XY 坐标依次刷新,光标跑出可视区就触发水平或垂直滚动。最后用 EventChartCustom 把光标变动推给图表,方便外部 EA 监听输入状态。 在 MT5 里跑这套,重点看两个数据点:光标 x 为 0 且 y 大于 0 时会触发整行上移,以及删除后必然重算尺寸并可能调用 HorizontalScrolling/VerticalScrolling。改参数时别动 m_text_edit_state 的前置校验,否则未激活状态下退格会静默返回 false。
class="type">bool CTextBox::OnPressedKeyBackspace(const class="type">long key_code) { class=class="str">"cmt">//--- 如果不是删除键, 或如果文本框没有激活, 离开 if(key_code!=KEY_BACKSPACE || !m_text_edit_state) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果位置大于零, 则删除字符 if(m_text_cursor_x_pos>class="num">0) DeleteSymbol(); class=class="str">"cmt">//--- 如果位置为零, 并且不是第一行, 删除行 else if(m_text_cursor_y_pos>class="num">0) { class=class="str">"cmt">//--- 将行向上移动一个位置 ShiftOnePositionUp(); } class=class="str">"cmt">//--- 计算文本框的大小 CalculateTextBoxSize(); class=class="str">"cmt">//--- 为文本框设置新大小 ChangeTextBoxSize(true,true); class=class="str">"cmt">//--- 获取文本框可见部分的边界 CalculateBoundaries(); class=class="str">"cmt">//--- 获取光标的 X 和 Y 坐标 CalculateTextCursorX(); CalculateTextCursorY(); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_x<=m_x_limit) HorizontalScrolling(CalculateScrollThumbX()); else { if(m_text_cursor_x>=m_x2_limit) HorizontalScrolling(CalculateScrollThumbX2()); } class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_y<=m_y_limit) VerticalScrolling(CalculateScrollThumbY()); else VerticalScrolling(m_scrollv.CurrentPos()); 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); }
◍ 回车断行时怎么搬动文本缓冲
多行文本框里按下回车,本质是在行数组里插出一个空位,再把光标下方的所有行整体下沉一格。这件事拆成两个私有方法最干净:ClearLine() 负责释放某一行挂着的宽度与符号数组,ShiftOnePositionDown() 负责真正的下沉循环。 ShiftOnePositionDown() 一进来先记下回车所在行的字符数(ArraySize 取 m_lines[y].m_symbol),随后光标行号 +1,原行数 lines_total 基础上 ArrayResize 把 m_lines 扩出一个元素。循环从数组末尾倒着写到新光标位置,每次把上一行整行 LineCopy 到当前行,若上一行正是刚按回车那行且原本无任何字符,就顺手 ClearLine 清掉副本。 如果回车行本身有字符,还要准备一个临时 string array[],专门承接从光标位置到行尾、等待挪去新行的那截内容——这部分代码在原文里只声明了数组,未贴完复制逻辑,开 MT5 跟到 LineCopy 与后续粘贴就能补全。 OnPressedKeyEnter() 的上层逻辑很直白:先判文本框激活且多行模式开,单行情况直接发 ON_END_EDIT 事件解除激活;多行则调 ShiftOnePositionDown() 下沉后,检查光标是否跌出可视下边界,把光标归到新行行首,重绘并广播光标移动消息。外汇/贵金属 EA 里做日志输入框时,这种断行处理不当会吃掉字符,建议挂个样例面板实测回车边界。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 清除指定行 class="type">void ClearLine(const class="type">uint line_index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 清除指定行 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::ClearLine(const class="type">uint line_index) { ::ArrayFree(m_lines[line_index].m_width); ::ArrayFree(m_lines[line_index].m_symbol); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 将行向下移动一个位置 class="type">void ShiftOnePositionDown(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将行向下移动一个位置 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::ShiftOnePositionDown(class="type">void) { class=class="str">"cmt">//--- 从按下回车键之处开始, 获取行中字符数组的大小 class="type">uint pressed_line_symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class=class="str">"cmt">//--- 增加行计数器 m_text_cursor_y_pos++; class=class="str">"cmt">//--- 获取行数组大小 class="type">uint lines_total=::ArraySize(m_lines); class=class="str">"cmt">//--- 将数组增加一个元素 class="type">uint new_size=lines_total+class="num">1; ::ArrayResize(m_lines,new_size); class=class="str">"cmt">//--- 从当前位置开始向下移动一个项目 (从数组的末尾开始) for(class="type">uint i=lines_total; i>m_text_cursor_y_pos; 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">//--- 清除新行 if(prev_index==m_text_cursor_y_pos && pressed_line_symbols_total<class="num">1) ClearLine(prev_index); } class=class="str">"cmt">//--- 如果回车键并非在空行上按下 if(pressed_line_symbols_total>class="num">0) { class=class="str">"cmt">//--- 行的索引, 按回车键的位置 class="type">uint prev_line_index=m_text_cursor_y_pos-class="num">1; class=class="str">"cmt">//--- 从光标的当前位置开始到行尾复制字符用的数组 class="type">class="kw">string array[]; class=class="str">"cmt">//--- 设置数组的大小等于必须移动到新行的字符数
「回车断行时文本框的内部重排逻辑」
在 MT5 自定义文本框控件里,按回车不是简单插入换行符,而是触发一次结构数组的切割与下移。先按光标列位置算出当前行剩余字符数 new_line_size = pressed_line_symbols_total - m_text_cursor_x_pos,再把后半段字符拷进临时数组,原有行与新行分别用 ArraysResize 重设长度。 下面的片段展示了字符迁移与新行宽度重算的过程:先循环把字符塞进新行结构,紧接着用 m_canvas.TextWidth 逐个量出显示宽度,这步决定了后续横向滚动条是否该出现。
class="type">uint new_line_size=pressed_line_symbols_total-m_text_cursor_x_pos; ::ArrayResize(array,new_line_size); class=class="str">"cmt">//--- 将要移动到新行的字符复制到数组中 for(class="type">uint i=class="num">0; i<new_line_size; i++) array[i]=m_lines[prev_line_index].m_symbol[m_text_cursor_x_pos+i]; class=class="str">"cmt">//--- 为按下回车键的行调整结构数组的大小 ArraysResize(prev_line_index,pressed_line_symbols_total-new_line_size); class=class="str">"cmt">//--- 调整新行的结构数组大小 ArraysResize(m_text_cursor_y_pos,new_line_size); class=class="str">"cmt">//--- 将数据添加到新行的结构数组 for(class="type">uint k=class="num">0; k<new_line_size; k++) { m_lines[m_text_cursor_y_pos].m_symbol[k] =array[k]; m_lines[m_text_cursor_y_pos].m_width[k] =m_canvas.TextWidth(array[k]); } } } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 处理按下回车键 class="type">bool OnPressedKeyEnter(const class="type">long key_code); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理按下回车键 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTextBox::OnPressedKeyEnter(const class="type">long key_code) { class=class="str">"cmt">//--- 如果不是回车键, 或如果文本框未激活, 离开 if(key_code!=KEY_ENTER || !m_text_edit_state) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果多行模式被禁用 if(!m_multi_line_mode) { class=class="str">"cmt">//--- 文本框失活 DeactivateTextBox(); class=class="str">"cmt">//--- 发送有关它的消息 ::EventChartCustom(m_chart_id,ON_END_EDIT,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 将行向下移动一个位置 ShiftOnePositionDown(); class=class="str">"cmt">//--- 计算文本框的大小 CalculateTextBoxSize(); class=class="str">"cmt">//--- 为文本框设置新大小 ChangeTextBoxSize(); class=class="str">"cmt">//--- 获取文本框可见部分的边界 CalculateYBoundaries(); class=class="str">"cmt">//--- 获取光标的 Y 坐标 CalculateTextCursorY(); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_y+(class="type">int)LineHeight()>=m_y2_limit) VerticalScrolling(CalculateScrollThumbY2()); class=class="str">"cmt">//--- 将光标移至行首 SetTextCursor(class="num">0,m_text_cursor_y_pos); class=class="str">"cmt">//--- 将滚动条移至开头 HorizontalScrolling(class="num">0); 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">uint new_line_size=pressed_line_symbols_total-m_text_cursor_x_pos; ::ArrayResize(array,new_line_size); class=class="str">"cmt">//--- 将要移动到新行的字符复制到数组中 for(class="type">uint i=class="num">0; i<new_line_size; i++) array[i]=m_lines[prev_line_index].m_symbol[m_text_cursor_x_pos+i]; class=class="str">"cmt">//--- 为按下回车键的行调整结构数组的大小 ArraysResize(prev_line_index,pressed_line_symbols_total-new_line_size); class=class="str">"cmt">//--- 调整新行的结构数组大小 ArraysResize(m_text_cursor_y_pos,new_line_size); class=class="str">"cmt">//--- 将数据添加到新行的结构数组 for(class="type">uint k=class="num">0; k<new_line_size; k++) { m_lines[m_text_cursor_y_pos].m_symbol[k] =array[k]; m_lines[m_text_cursor_y_pos].m_width[k] =m_canvas.TextWidth(array[k]); } } } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 处理按下回车键 class="type">bool OnPressedKeyEnter(const class="type">long key_code); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理按下回车键 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTextBox::OnPressedKeyEnter(const class="type">long key_code) { class=class="str">"cmt">//--- 如果不是回车键, 或如果文本框未激活, 离开 if(key_code!=KEY_ENTER || !m_text_edit_state) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果多行模式被禁用 if(!m_multi_line_mode) { class=class="str">"cmt">//--- 文本框失活 DeactivateTextBox(); class=class="str">"cmt">//--- 发送有关它的消息 ::EventChartCustom(m_chart_id,ON_END_EDIT,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 将行向下移动一个位置 ShiftOnePositionDown(); class=class="str">"cmt">//--- 计算文本框的大小 CalculateTextBoxSize(); class=class="str">"cmt">//--- 为文本框设置新大小 ChangeTextBoxSize(); class=class="str">"cmt">//--- 获取文本框可见部分的边界 CalculateYBoundaries(); class=class="str">"cmt">//--- 获取光标的 Y 坐标 CalculateTextCursorY(); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_y+(class="type">int)LineHeight()>=m_y2_limit) VerticalScrolling(CalculateScrollThumbY2()); class=class="str">"cmt">//--- 将光标移至行首 SetTextCursor(class="num">0,m_text_cursor_y_pos); class=class="str">"cmt">//--- 将滚动条移至开头 HorizontalScrolling(class="num">0); 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); }
左右键移动文本光标的边界处理
在 MT5 自定义文本框控件里,左右方向键的职责不只是挪动一个字符,还要兼顾行首行尾的跨行跳转。按下左键时,先过三道闸:键码必须是 KEY_LEFT、不能处于 Ctrl 组合态、文本框得是激活状态,任一不满足直接 return false,不碰内部状态。 若光标不在行首(m_text_cursor_x_pos>0),就沿 X 轴减掉上一个字符的像素宽度 m_lines[行号].m_width[位置-1],同时字符计数器减一。若已在行首但不在第一行,则行号减一,调用 CorrectingTextCursorXPos() 把光标 X 位置拉到该行末尾——这个方法会按 ArraySize 算出的字符数把越界位置夹回合法区间。 右键逻辑镜像对称:不在行尾就右移一个字符宽度;在行尾且非末行时跳到下一行开头。两种按键收尾都要重算可见边界、光标坐标,并在光标越出 m_x_limit 时调用 HorizontalScrolling 推滑块。实际验证时可在 OnPressedKeyLeft 的高亮 if 块打 Print,能看到行首左移会触发 m_text_cursor_y_pos-- 而非 x 变化。 CorrectingTextCursorXPos 的入参 x_pos 若传 WRONG_VALUE,光标直接落到行尾;传具体值则做上限截断(x_pos>符号数-1 时取符号数)。空行时 symbols_total<1,光标 X 强制归零,避免负下标。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 沿 X 轴调整文本光标 class="type">void CorrectingTextCursorXPos(const class="type">int x_pos=WRONG_VALUE); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 沿 X 轴调整文本光标 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTextBox::CorrectingTextCursorXPos(const class="type">int x_pos=WRONG_VALUE) { class=class="str">"cmt">//--- 获取字符数组的大小 class="type">uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_width); class=class="str">"cmt">//--- 确定光标位置 class="type">uint text_cursor_x_pos=class="num">0; class=class="str">"cmt">//--- 如果位置可用 if(x_pos!=WRONG_VALUE) text_cursor_x_pos=(x_pos>(class="type">int)symbols_total-class="num">1)? symbols_total : x_pos; class=class="str">"cmt">//--- 如果位置不可用, 将光标设置为行的结尾 else text_cursor_x_pos=symbols_total; class=class="str">"cmt">//--- 零位置, 如果该行不包含字符 m_text_cursor_x_pos=(symbols_total<class="num">1)? class="num">0 : text_cursor_x_pos; class=class="str">"cmt">//--- 获取光标的 X 坐标 CalculateTextCursorX(); } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 处理按下左键 class="type">bool OnPressedKeyLeft(const class="type">long key_code); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理按下左键 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTextBox::OnPressedKeyLeft(const class="type">long key_code) { class=class="str">"cmt">//--- 如果不是左键, 或如果按 Ctrl 键, 或如果文本框未激活, 离开 if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || !m_text_edit_state) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果文本光标位置大于零 if(m_text_cursor_x_pos>class="num">0) { class=class="str">"cmt">//--- 将其移动到上一个字符 m_text_cursor_x-=m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos-class="num">1]; class=class="str">"cmt">//--- 减少字符计数器 m_text_cursor_x_pos--; } else { class=class="str">"cmt">//--- 如果这不是第一行 if(m_text_cursor_y_pos>class="num">0) { class=class="str">"cmt">//--- 移动到上一行的末尾 m_text_cursor_y_pos--; CorrectingTextCursorXPos(); } } class=class="str">"cmt">//--- 获取文本框可见部分的边界 CalculateBoundaries(); class=class="str">"cmt">//--- 获取光标的 Y 坐标 CalculateTextCursorY(); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_x<=m_x_limit) HorizontalScrolling(CalculateScrollThumbX());
◍ 右键移动光标时的边界与滚动处理
在 MT5 自定义文本框控件里,右键(KEY_RIGHT)的逻辑首先要排除干扰:函数一开始就判断 key_code 不等于 KEY_RIGHT、Ctrl 被按住、或文本框未处于编辑态时直接返回 false,避免误触发。 光标在行内移动时,靠 m_lines[y].m_width[x_pos] 累加像素位移,而不是简单 +1,因为等宽字体之外每个字符渲染宽度不同;行尾再按右键则跳到下一行首,条件是 m_text_cursor_y_pos < 行数-1,最后一行不会越界。 离开可见区才滚视图:x 方向超 m_x2_limit 调 HorizontalScrolling(CalculateScrollThumbX2()),若回到行首(x_pos==0)则归零滚动;y 方向用 m_text_cursor_y+LineHeight() >= m_y2_limit 判断,触发 VerticalScrolling。实测若 LineHeight 返回 18 像素、可视高 180,则第 10 行底沿就会拉垂直条。 每次移动末尾都 DrawTextAndCursor(true) 重绘并 EventChartCustom 发 ON_MOVE_TEXT_CURSOR 事件,方便外部 EA 监听光标位置做联动。
else { class=class="str">"cmt">//--- 获取字符数组的大小 class="type">uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class=class="str">"cmt">//--- if(m_text_cursor_x_pos==symbols_total && m_text_cursor_x>=m_x2_limit) HorizontalScrolling(CalculateScrollThumbX2()); } class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_y<=m_y_limit) VerticalScrolling(CalculateScrollThumbY()); 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">bool OnPressedKeyRight(const class="type">long key_code); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理按下右键 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTextBox::OnPressedKeyRight(const class="type">long key_code) { class=class="str">"cmt">//--- 如果不是右键, 或如果按 Ctrl 键, 或如果文本框未激活, 离开 if(key_code!=KEY_RIGHT || m_keys.KeyCtrlState() || !m_text_edit_state) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 获取字符数组的大小 class="type">uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_width); class=class="str">"cmt">//--- 如果这是行的末尾 if(m_text_cursor_x_pos<symbols_total) { class=class="str">"cmt">//--- 将文本光标的位置移动到下一个字符 m_text_cursor_x+=m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos]; class=class="str">"cmt">//--- 增加字符计数器 m_text_cursor_x_pos++; } else { class=class="str">"cmt">//--- 获取行数组大小 class="type">uint lines_total=::ArraySize(m_lines); class=class="str">"cmt">//--- 如果这不是最后一行 if(m_text_cursor_y_pos<lines_total-class="num">1) { class=class="str">"cmt">//--- 将光标移动到下一行的开始处 m_text_cursor_x=m_text_x_offset; SetTextCursor(class="num">0,++m_text_cursor_y_pos); } } class=class="str">"cmt">//--- 获取文本框可见部分的边界 CalculateBoundaries(); class=class="str">"cmt">//--- 获取光标的 Y 坐标 CalculateTextCursorY(); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_x>=m_x2_limit) HorizontalScrolling(CalculateScrollThumbX2()); else { if(m_text_cursor_x_pos==class="num">0) HorizontalScrolling(class="num">0); } class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_y+(class="type">int)LineHeight()>=m_y2_limit) VerticalScrolling(CalculateScrollThumbY2()); 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); }
「上下键驱动多行文本框光标移动」
在自绘 MT5 控件里,多行文本框靠上、下方向键切换行位置,核心落在 CTextBox::OnPressedKeyUp() 与 OnPressedKeyDown() 两个方法。两者逻辑几乎对称,差异仅两行:一个判上边界越界、一个判下边界越界,所以官方示例只给其一。 进入方法先过三道闸:单行模式直接 false 退出;按键码不是对应方向或控件未处于编辑激活态也退出;这能避免无关键盘事件污染光标状态。随后若光标 Y 坐标减 1 仍在 m_lines 数组范围内(ArraySize 拿到的行数作上限),才执行 m_text_cursor_y_pos-- 并调用 CorrectingTextCursorXPos 重算 X 偏移,防止上一行更短时光标悬空。 移行后必须重算可见边界与光标像素坐标。代码里 m_text_cursor_y<=m_y_limit 那支(示例中高亮行)专门处理向上键时光标滑出顶部可视区,此时 VerticalScrolling(CalculateScrollThumbY()) 推动纵向滚动条滑块。最后 DrawTextAndCursor(true) 重绘并 EventChartCustom 抛出 ON_MOVE_TEXT_CURSOR 自定义事件,方便父窗口联动。 想验证这套机制,把下面类声明与方法贴进 MT5 自定义指标工程,给文本框设 m_multi_line_mode=true 后按方向键,观察滚动条滑块 thumb 位置随行移动的概率性跳变即可。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 处理按向上键 class="type">bool OnPressedKeyUp(const class="type">long key_code); class=class="str">"cmt">//--- 处理按向下键 class="type">bool OnPressedKeyDown(const class="type">long key_code); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理按向上键 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTextBox::OnPressedKeyUp(const class="type">long key_code) { class=class="str">"cmt">//--- 如果禁用多行模式, 则离开 if(!m_multi_line_mode) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果不是向上键, 或如果文本框未激活, 离开 if(key_code!=KEY_UP || !m_text_edit_state) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 获取行数组大小 class="type">uint lines_total=::ArraySize(m_lines); class=class="str">"cmt">//--- 如果未超出数组范围 if(m_text_cursor_y_pos-class="num">1<lines_total) { class=class="str">"cmt">//--- 移动到上一行 m_text_cursor_y_pos--; class=class="str">"cmt">//--- 沿 X 轴调整文本光标 CorrectingTextCursorXPos(m_text_cursor_x_pos); } class=class="str">"cmt">//--- 获取文本框可见部分的边界 CalculateBoundaries(); class=class="str">"cmt">//--- 获取光标的 Y 坐标 CalculateTextCursorY(); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_x<=m_x_limit) HorizontalScrolling(CalculateScrollThumbX()); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_y<=m_y_limit) VerticalScrolling(CalculateScrollThumbY()); 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); }
Home/End 键的光标跳动逻辑
在自定义文本框控件里,Home 和 End 键的职责很单纯:把文本光标瞬间甩到当前行的头或尾。CTextBox 里用 OnPressedKeyHome() 和 OnPressedKeyEnd() 两个私有方法接住这两个按键事件,逻辑不绕,但有几个隐藏约定值得注意。 Home 的处理最直白——只要 key_code 不是 KEY_HOME、或者 Ctrl 被按住、或者文本框没处在编辑态,直接返回 false 不接管。否则光标 x 坐标置 0、横向滚动条归位到 0,再重绘并抛一个 ON_MOVE_TEXT_CURSOR 的自定义图表事件。 End 稍麻烦一点:它先拿当前行 m_lines[y].m_symbol 的数组长度当结尾下标,调 SetTextCursor 落位后还要重算光标 X 和可见边界。若光标 X 超出 m_x2_limit,滚动条会跟着 CalculateScrollThumbX2() 走,避免光标跑出可视区。下面把两段源码逐行拆开看。 留意一个细节:两个方法都显式排除了 Ctrl 组合键。也就是说 Ctrl+Home / Ctrl+End 这套「跳到整篇头尾」的惯用法在这里默认不生效,若你的面板想要全文跳转,得自己改判断条件。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 处理按下起始键 class="type">bool OnPressedKeyHome(const class="type">long key_code); class=class="str">"cmt">//--- 处理按下结尾键 class="type">bool OnPressedKeyEnd(const class="type">long key_code); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理按下起始键 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTextBox::OnPressedKeyHome(const class="type">long key_code) { class=class="str">"cmt">//--- 如果不是起始键, 或如果按 Ctrl 键, 或如果文本框未激活, 离开 if(key_code!=KEY_HOME || m_keys.KeyCtrlState() || !m_text_edit_state) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 将光标移动到当前行的开始处 SetTextCursor(class="num">0,m_text_cursor_y_pos); class=class="str">"cmt">//--- 将滚动条移至第一个位置 HorizontalScrolling(class="num">0); 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理按下结尾键 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTextBox::OnPressedKeyEnd(const class="type">long key_code) { class=class="str">"cmt">//--- 如果不是结尾键, 或如果按 Ctrl 键, 或如果文本框未激活, 离开 if(key_code!=KEY_END || m_keys.KeyCtrlState() || !m_text_edit_state) class="kw">return(class="kw">false); 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=class="str">"cmt">//--- 获取光标的 X 坐标 CalculateTextCursorX(); class=class="str">"cmt">//--- 获取文本框可见部分的边界 CalculateXBoundaries(); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_x>=m_x2_limit) HorizontalScrolling(CalculateScrollThumbX2()); 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); }
◍ Ctrl 组合键下的光标跳跃逻辑
在自绘文本框里,Ctrl 配合方向键不是逐字符移动,而是按单词边界跳。典型映射有四种:Ctrl+左跳到左侧单词头,Ctrl+右跳到右侧单词尾,Ctrl+Home 去首行首,Ctrl+End 去末行尾。 以 OnPressedKeyCtrlAndLeft 为例,方法入口先判键:若按下的不是 KEY_LEFT、或 Ctrl 没按住、或文本框未激活,直接 return false 不处理。 若光标已在某行行首且不是第一行(m_text_cursor_x_pos==0 && m_text_cursor_y_pos>0),就跳到上一行末尾,用 SetTextCursor(symbols_total, prev_line_index) 落位。若不在行首,则从当前位置向右往左扫,以空格 ' ' 作分隔符,遇到“前一个非空格+后一个是空格”的组合就把光标停在那里,循环里 i 从 m_text_cursor_x_pos 递减。 跳出循环后照例做可见区校验:若光标跑出文本框可视范围,调滚动条滑块;最后重绘并发消息通知光标已移动。剩下的 Ctrl+右 / Home / End 三个方法结构同构,读者可在 MT5 里照葫芦画瓢补完。
class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 处理按下 Ctrl + 左键 class="type">bool OnPressedKeyCtrlAndLeft(const class="type">long key_code); class=class="str">"cmt">//--- 处理按下 Ctrl + 右键 class="type">bool OnPressedKeyCtrlAndRight(const class="type">long key_code); class=class="str">"cmt">//--- 处理同时按下 Ctrl + 起始键 class="type">bool OnPressedKeyCtrlAndHome(const class="type">long key_code); class=class="str">"cmt">//--- 处理同时按下 Ctrl + 结尾键 class="type">bool OnPressedKeyCtrlAndEnd(const class="type">long key_code); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理同时按下 Ctrl + 左键 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTextBox::OnPressedKeyCtrlAndLeft(const class="type">long key_code) { class=class="str">"cmt">//--- 如果(class="num">1) 不是左键, 或 (class="num">2) 如果未按 Ctrl 键, 或 (class="num">3) 如果文本框未激活, 离开 if(!(key_code==KEY_LEFT && m_keys.KeyCtrlState()) || !m_text_edit_state) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 空格符 class="type">class="kw">string SPACE=" "; class=class="str">"cmt">//--- 获取行数组大小 class="type">uint lines_total=::ArraySize(m_lines); class=class="str">"cmt">//--- 获取当前行中的字符数 class="type">uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class=class="str">"cmt">//--- 如果光标在当前行的开头, 并且这不是第一行, class=class="str">"cmt">// 将光标移动到上一行的末尾 if(m_text_cursor_x_pos==class="num">0 && m_text_cursor_y_pos>class="num">0) { class=class="str">"cmt">//--- 获取上一行的索引 class="type">uint prev_line_index=m_text_cursor_y_pos-class="num">1; class=class="str">"cmt">//--- 获取上一行中的字符数 symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol); class=class="str">"cmt">//--- 将光标移动到上一行的末尾 SetTextCursor(symbols_total,prev_line_index); } class=class="str">"cmt">// --- 如果光标在当前行的开头或光标在第一行 else { class=class="str">"cmt">//--- 找到连续字符序列的开始 (从右到左) for(class="type">uint i=m_text_cursor_x_pos; i<=symbols_total; i--) { class=class="str">"cmt">//--- 如果光标在行的末尾, 转至下一个 if(i==symbols_total) class="kw">continue; class=class="str">"cmt">//--- 如果这是行的第一个字符 if(i==class="num">0) { class=class="str">"cmt">//--- 将光标设置在行首 SetTextCursor(class="num">0,m_text_cursor_y_pos); class="kw">break; } class=class="str">"cmt">//--- 如果这不是行的第一个字符 else {
「光标越界时滚动条与重绘的联动」
在文本框控件里,文本光标移动后第一件事是重算可见边界:CalculateBoundaries() 拿到视窗范围,随后用 CalculateTextCursorX() 和 CalculateTextCursorY() 把光标逻辑坐标落进像素空间。 若光标 X 坐标 m_text_cursor_x 小于等于左边界 m_x_limit,直接调 HorizontalScrolling(CalculateScrollThumbX()) 把横向滚动条拇指推到对应位置;否则判断当前行字符总数 symbols_total,当光标在末位且坐标触到右边界 m_x2_limit,才用 CalculateScrollThumbX2() 右移。纵向同理,m_text_cursor_y 越上界就 VerticalScrolling(CalculateScrollThumbY())。 代码片段里有一段连续序列检测:当索引 i 不等于记录位、当前符号非空格且前一符号是空格时,说明碰到了新词开头,SetTextCursor 重置光标并 break。这套逻辑保证了长文本输入时视窗始终跟随光标,读者可在 MT5 自建 CTextBox 派生类里打断点验证 m_text_cursor_x 的跳变阈值。 最后 DrawTextAndCursor(true) 重绘文本与光标,并用 EventChartCustom 抛出 ON_MOVE_TEXT_CURSOR 自定义事件,外部监听即可做联动高亮。外汇与贵金属图表挂这类自定义控件需注意高频重绘带来的 CPU 占用风险,可能拖累报价刷新。
class=class="str">"cmt">//--- 如果第一次找到连续序列的开始。 class=class="str">"cmt">// 下一索引开始应考虑是空格。 if(i!=m_text_cursor_x_pos && m_lines[m_text_cursor_y_pos].m_symbol[i]!=SPACE && m_lines[m_text_cursor_y_pos].m_symbol[i-class="num">1]==SPACE) { class=class="str">"cmt">//--- 将光标设置到新连续序列的开头 SetTextCursor(i,m_text_cursor_y_pos); class="kw">break; } } } } class=class="str">"cmt">//--- 获取文本框可见部分的边界 CalculateBoundaries(); class=class="str">"cmt">//--- 获取光标的 X 坐标 CalculateTextCursorX(); class=class="str">"cmt">//--- 获取光标的 Y 坐标 CalculateTextCursorY(); class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_x<=m_x_limit) HorizontalScrolling(CalculateScrollThumbX()); else { class=class="str">"cmt">//--- 获取字符数组的大小 symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol); class=class="str">"cmt">//--- if(m_text_cursor_x_pos==symbols_total && m_text_cursor_x>=m_x2_limit) HorizontalScrolling(CalculateScrollThumbX2()); } class=class="str">"cmt">//--- 如果文本光标离开可见区域, 则移动滚动条 if(m_text_cursor_y<=m_y_limit) VerticalScrolling(CalculateScrollThumbY()); 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); }
多行文本框在容器类里的挂接细节
要让多行文本框(CTextBox)在自定义界面容器里正常跑,CWndContainer 的 WindowElements 结构里必须单独开一个 CTextBox* 数组,否则复合控件里的子元素(垂直/水平滚动条)没法被事件系统遍历到。 CTextBox 本身是复合控件,内嵌了滚动条,所以光存指针不够,还得有个分发方法把内部子控件拆出来塞进对应专用数组。下面这段 AddTextBoxElements() 就是在 AddToElementsArray() 的同位置被调用的,逻辑是先 dynamic_cast 判类型,再循环两次分别抓垂直和水平滚动条指针挂到 m_elements 和 m_scrolls。 一个容易踩的坑:界面默认只在鼠标移动且停顿后才重绘,但文本框激活时文本光标要持续闪烁,所以 OnTimerEvent() 里必须把 CTextBox 当成例外强制周期重绘,否则光标看起来像死掉了。 实盘写 EA 面板时,外汇和贵金属波动快、点差跳变频繁,这类自绘控件若卡顿会直接影响下单反应,集成完建议在 MT5 策略测试器里开一个空图表 EA 专门点文本框验证闪烁与滚动。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| WndContainer.mqh | class=class="str">"cmt">//| 版权所有 class="num">2015, MetaQuotes 软件公司| class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Controls\TextBox.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于存储所有接口对象的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CWndContainer { class="kw">protected: class=class="str">"cmt">//--- 元素数组的结构 class="kw">struct WindowElements { class=class="str">"cmt">//--- 多行文本框 CTextBox *m_text_boxes[]; }; class=class="str">"cmt">//--- 每个窗口的元素数组 WindowElements m_wnd[]; }; class CWndContainer { class="kw">private: class=class="str">"cmt">//--- 保存指向多行文本框对象的指针 class="type">bool AddTextBoxElements(const class="type">int window_index,CElementBase &object); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 保存指向多行文本框对象的指针 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWndContainer::AddTextBoxElements(const class="type">int window_index,CElementBase &object) { class=class="str">"cmt">//--- 如果此非多行文本框, 离开 if(class="kw">dynamic_cast<CTextBox *>(&object)==NULL) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 获取控件的指针 CTextBox *tb=::GetPointer(object); for(class="type">int i=class="num">0; i<class="num">2; i++) { class="type">int size=::ArraySize(m_wnd[window_index].m_elements); ::ArrayResize(m_wnd[window_index].m_elements,size+class="num">1); if(i==class="num">0) { class=class="str">"cmt">//--- 获取滚动条指针 CScrollV *sv=tb.GetScrollVPointer(); m_wnd[window_index].m_elements[size]=sv; AddToObjectsArray(window_index,sv); class=class="str">"cmt">//--- 将指针添加到专用数组 AddToRefArray(sv,m_wnd[window_index].m_scrolls); } else if(i==class="num">1) { CScrollH *sh=tb.GetScrollHPointer(); m_wnd[window_index].m_elements[size]=sh;
◍ 用 300 毫秒静止判据驱动文本框定时器
在自定义窗口的事件类里,OnTimerEvent 用了一个很实在的节流逻辑:只有当鼠标光标静止超过 300 毫秒且左键已释放,才遍历当前激活窗口里的文本框去跑各自的 OnEventTimer。 这段判据直接写在函数开头,m_mouse.GapBetweenCalls()>300 配合 !m_mouse.LeftButtonState() 同时成立就提前返回,避免拖拽或连点过程中无谓刷新。文本框总数由 CWndContainer::TextBoxesTotal(m_active_window_index) 取回,循环里逐个调用 m_text_boxes[e].OnEventTimer()。 若窗口数组为空(WindowsTotal()<1)也直接离开,不往下走。否则统一交给 CheckElementsEventsTimer() 扫全部控件事件,最后 m_chart.Redraw() 重绘。开 MT5 把 300 这个值改成 150 或 500,能明显感觉到文本框响应节奏变快或变钝,外汇与贵金属图表多窗口下高频重绘有卡顿风险,调参前先在模拟环境验证。
AddToObjectsArray(window_index,sh); class=class="str">"cmt">//--- 将指针添加到专用数组 AddToRefArray(sh,m_wnd[window_index].m_scrolls); } } class=class="str">"cmt">//--- 将指针添加到专用数组 AddToRefArray(tb,m_wnd[window_index].m_text_boxes); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计时器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndEvents::OnTimerEvent(class="type">void) { class=class="str">"cmt">//--- 如果鼠标光标处于静止 (调用之间的时间差 >class="num">300 毫秒), 并释放鼠标左键, 离开 if(m_mouse.GapBetweenCalls()>class="num">300 && !m_mouse.LeftButtonState()) { class="type">int text_boxes_total=CWndContainer::TextBoxesTotal(m_active_window_index); for(class="type">int e=class="num">0; e<text_boxes_total; e++) m_wnd[m_active_window_index].m_text_boxes[e].OnEventTimer(); class=class="str">"cmt">//--- class="kw">return; } class=class="str">"cmt">//--- 如果数组为空,则离开 if(CWndContainer::WindowsTotal()<class="num">1) class="kw">return; class=class="str">"cmt">//--- 通过定时器检查所有控件的事件 CheckElementsEventsTimer(); class=class="str">"cmt">//--- 重绘图表 m_chart.Redraw(); }
「用多行文本框把章节目录灌进 GUI」
想验证 CTextBox 的多行能力,最直白的办法是建一个带主菜单和状态栏的 MQL 应用:一个单行框、一个多行框,状态栏第二项实时显示多行框里文本光标的位置。 多行模式不是靠构造函数开关,而是创建前调用 CTextBox::MultiLineMode(true)。若表单区域要跟着窗口拉伸,得配合 CElementBase::AutoXResizeXXX 系列方法,否则框体不会自适应。 下面这段是创建第二个多行框的方法,把本文 23 个章节名塞进 string 数组,再用 AddText 写首行、AddLine 补后续行。编译挂到图表后,能看到图例 9 那样的演示界面;外汇与贵金属行情高波动,这类 GUI 仅作交互验证,不涉及任何交易信号。
class CProgram : class="kw">public CWndEvents { class="kw">protected: class=class="str">"cmt">//--- 编辑 CTextBox m_text_box1; CTextBox m_text_box2; class=class="str">"cmt">//--- class="kw">protected: class=class="str">"cmt">//--- 编辑 class="type">bool CreateTextBox1(const class="type">int x_gap,const class="type">int y_gap); class="type">bool CreateTextBox2(const class="type">int x_gap,const class="type">int y_gap); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建一个多行文本框 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateTextBox2(const class="type">int x_gap,const class="type">int y_gap) { class=class="str">"cmt">//--- 保存窗口指针 m_text_box2.WindowPointer(m_window); class=class="str">"cmt">//--- 在创建之前设置属性 m_text_box2.FontSize(class="num">8); m_text_box2.Font("Calibri"); class=class="str">"cmt">// Consolas|Calibri|Tahoma m_text_box2.AreaColor(clrWhite); m_text_box2.TextColor(clrBlack); m_text_box2.MultiLineMode(true); m_text_box2.AutoXResizeMode(true); m_text_box2.AutoXResizeRightOffset(class="num">2); m_text_box2.AutoYResizeMode(true); m_text_box2.AutoYResizeBottomOffset(class="num">24); class=class="str">"cmt">//--- 行数组 class="type">class="kw">string lines_array[]= { "概论", "按键组和键盘布局", "处理按键事件", "字符的 ASCII 编码和控制键", "键位扫描码", "操控键盘的辅助类", "多行文本框控件", "开发 CTextBox 类用于创建控件", "属性和外观", "管理文本光标", "输入一个字符", "处理按退格键", "处理按回车键", "处理按左、右键", "处理按上、下键", "处理按起始和结尾键", "处理并发按键和 Ctrl 键组合", "函数库引擎中的控件集成", "测试控件的应用", "结论" }; class=class="str">"cmt">//--- 在文本框中添加文本 class="type">int lines_total=::ArraySize(lines_array); for(class="type">int i=class="num">0; i<lines_total; i++) { class=class="str">"cmt">//--- 在第一行添加文本 if(i==class="num">0) m_text_box2.AddText(class="num">0,lines_array[i]); class=class="str">"cmt">//--- 在文本框中添加一行 else m_text_box2.AddLine(lines_array[i]); } class=class="str">"cmt">//--- 创建控件 if(!m_text_box2.CreateTextBox(m_chart_id,m_subwin,x_gap,y_gap))
用事件路由把文本框状态喂给状态栏
在 MT5 自定义面板里,多行文本框的光标位置、输入完成、点击事件都不会自动反映到状态栏。上面这段把三类自定义事件归并到 OnEvent,统一拦截后再决定更新哪一块界面。 判断核心是 lparam==m_text_box2.Id(),只有来自目标文本框的消息才写进状态栏第二项(索引 1)。其余事件直接跳过,避免无谓重绘。 实际验证时,你在文本框里敲字、点选、移动光标,终端日志会打印 id / lparam / dparam / sparam 四个值,方便确认事件确实触发。外汇与贵金属图表加载这类 EA 属高风险操作,事件频繁重绘可能拖慢老机器,建议先在模拟盘测。
class="kw">return(class="kw">false); class=class="str">"cmt">//--- 将对象添加到对象组的公用数组 CWndContainer::AddToElementsArray(class="num">0,m_text_box2); class=class="str">"cmt">//--- 在状态栏设置文本项目 m_status_bar.ValueToItem(class="num">1,m_text_box2.TextCursorInfo()); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 图表事件处理器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- 事件 (class="num">1) 输入值, 或 (class="num">2) 激活文本框, 或(class="num">3) 移动文本光标 if(id==CHARTEVENT_CUSTOM+ON_END_EDIT || id==CHARTEVENT_CUSTOM+ON_CLICK_TEXT_BOX || id==CHARTEVENT_CUSTOM+ON_MOVE_TEXT_CURSOR) { ::Print(__FUNCTION__," > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam); class=class="str">"cmt">//--- 如果标识符匹配 (来自多行文本框的消息) if(lparam==m_text_box2.Id()) { class=class="str">"cmt">//--- 更新状态栏的第二项 m_status_bar.ValueToItem(class="num">1,sparam); } class=class="str">"cmt">//--- 重绘图表 m_chart.Redraw(); class="kw">return; } }
◍ GUI 函数库现状与文本框激活的补丁思路
这套 EasyAndFast GUI 函数库当前版本的结构示意图已迭代到图例 10,核心由 CWndContainer 管理图表窗口与控件对象,CTextBox 负责输入框交互。下一版计划往已有控件里加新功能,读者可直接下载 easyandfastgui_mql5.zip(558.17 KB)在 MT5 里跑一遍验证。 评论区里 Christian 提到,双击表格单元格弹出输入框后,为了避免多余点击来激活它,作者补了一个公开方法 CTextBox::ActivateTextBox()。这个方法内部调用 OnClickTextBox() 并传入控件图形对象名,等于模拟了一次点击,文本选中也在这一步完成。 leonerd 在 2022 年 4 月指出 CTimeCounter::CheckTimeCounter() 只比数字不绑时间,怀疑应改用 GetTickCount() 才对——这类细节如果你在回测里发现计时器行为异常,值得去源码里核一下。 外汇与贵金属品种波动剧烈、杠杆风险高,任何 GUI 封装都只是辅助观察,实盘前务必在策略测试器里用小资金验证。
Nach dem Doppelklick auf die Tabellenzelle erscheint das Eingabefeld, aber um einen weiteren Klick auszuschließen, um das Eingabefeld zu aktivieren, wurde eine zusätzliche öffentliche Methode CTextBox :: ActivateTextBox() benötigt. Sein Anruf simuliert einen Klick auf das Eingabefeld. Um dies zu tun, rufen Sie einfach die Methode CTextBox :: OnClickTextBox() auf und übergeben Sie den Namen des Grafikobjekts des Elements. Die Auswahl des Textes erfolgt bereits bei dieser Methode class=class="str">"cmt">//--- 如果数组为空,则退出。 if(CWndContainer::WindowsTotal()<class="num">1) class="kw">return; CWndContainer class="kw">protected: CChart m_chart; class=class="str">"cmt">//-- 图表窗口 ID 和编号 class="type">long m_chart_id; class="type">int m_subwin; class=class="str">"cmt">//--- 计划名称 class="type">class="kw">string m_program_name; class=class="str">"cmt">//--- 指标简称 class="type">class="kw">string m_indicator_shortname; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 事件参数 class="type">int m_id; class="type">long m_lparam; class="type">class="kw">double m_dparam; class="type">class="kw">string m_sparam; class=class="str">"cmt">//--- class="kw">protected: CWndEvents(class="type">void); ~CWndEvents(class="type">void); class=class="str">"cmt">//--- 虚拟图形事件处理程序 class="kw">virtual class="type">void OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam) {} class=class="str">"cmt">//--- 定时器 class="type">void OnTimerEvent(class="type">void); class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//-- 图表事件处理程序
「最后说句实在话」
上面这组 CChart 类的私有与保护方法,把 MT5 图表事件从底层回调一直拆到控件级轮询,本质是把「用户动了什么」变成可程序化响应的数据流。 你在 EA 里接 ChartEvent 时,若只重写 public 的入口而忽略 CheckElementsEventsTimer 这类私有轮询,控件状态就可能漏事件——尤其外汇和贵金属行情跳空时鼠标与对象点击并发,漏判概率会明显上升。 开 MT5 新建个面板类继承这套结构,把 ChartEventMouseMove 打点输出到 Experts 日志,跑一晚上 EURUSD 的 M15,就能看清子窗口编号判定在哪根 K 线之后才稳定。 高风险品种上任何事件驱动逻辑都先过小样本验证,别直接挂实盘。
class="type">void ChartEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam); class="kw">private: class="type">void ChartEventCustom(class="type">void); class="type">void ChartEventClick(class="type">void); class="type">void ChartEventMouseMove(class="type">void); class="type">void ChartEventObjectClick(class="type">void); class="type">void ChartEventEndEdit(class="type">void); class="type">void ChartEventChartChange(class="type">void); class=class="str">"cmt">//--- 检查控件中的事件 class="type">void CheckElementsEvents(class="type">void); class=class="str">"cmt">//-- 确定子窗口编号 class="type">void DetermineSubwindow(class="type">void); class=class="str">"cmt">//--- 检查控件的事件 class="type">void CheckSubwindowNumber(class="type">void); class=class="str">"cmt">//--- 事件参数初始化 class="type">void InitChartEventsParams(const class="type">int id,const class="type">long lparam,const class="type">class="kw">double dparam,const class="type">class="kw">string sparam); class=class="str">"cmt">//--- 移动窗口 class="type">void MovingWindow(class="type">void); class=class="str">"cmt">//--- 通过计时器检查所有元素的事件 class="type">void CheckElementsEventsTimer(class="type">void); class=class="str">"cmt">//--- class="kw">protected: class=class="str">"cmt">//--- 删除接口 class="type">void Destroy(class="type">void); class="kw">public: class="kw">protected: class="kw">private: