图形界面 X: 在多行文本框中选择文本 (集成构建 13)·综合运用
◍ 文本框多选删除的底层搬运逻辑
在 MT5 自定义文本框控件里,跨行删除选中文本不是简单抹掉字符,而是先把待删区段拷出来、再重组接收行结构。下面这段分支处理的是「初始行高于最后一行」的反向选择场景,实际跑起来和正向选择的区别只在于目标行数组与光标落点。 uint new_size=m_selected_symbol_from+symbols_to_delete_in_line_to; ArraysResize(m_selected_line_from,new_size); 这里先按「起始列 + 待删列数」算出新行长度并扩容,避免粘贴回写时越界。PasteWrapSymbols 负责把刚才 CopyWrapSymbols 抽出的字符塞回接收行,行数组总尺寸则由 ArraySize(m_lines) 实时取,再靠 MoveLines 把下方行整体上移 lines_to_delete 个位置。 DeleteSelectedText 的收口动作很固定:ResetSelectedText 清掉选区状态,CalculateTextBoxSize 与 ChangeTextBoxSize 重算边框,CorrectingHorizontalScrollThumb / CorrectingVerticalScrollThumb 重校双向滚动条。最后 DrawTextAndCursor(true) 重绘并经由 EventChartCustom 抛出 ON_MOVE_TEXT_CURSOR 自定义事件,图表端监听此 ID 就能同步外部状态。 验证方式直接:在 MT5 策略测试器的指标面板里挂一个带 CTextBox 的 EA,选中跨 3 行文本按删除,观察 m_lines 数组大小是否恰好减少 2(中间整行被吞),以及光标是否落在 m_selected_symbol_from 所指列。外汇与贵金属图表嵌入此类控件时需注意,高频重绘可能拖慢 tick 响应,属高风险定制方案。
else { class=class="str">"cmt">//--- 将要移动的字符复制到数组中 class="type">class="kw">string array[]; CopyWrapSymbols(m_selected_line_to,m_selected_symbol_to,symbols_to_delete_in_line_to,array); class=class="str">"cmt">//--- 调整接收行数组大小 class="type">uint new_size=m_selected_symbol_from+symbols_to_delete_in_line_to; ArraysResize(m_selected_line_from,new_size); class=class="str">"cmt">//--- 将数据添加到接收行结构的数组 PasteWrapSymbols(m_selected_line_from,m_selected_symbol_from,array); class=class="str">"cmt">//--- 获取行数组的大小 class="type">uint lines_total=::ArraySize(m_lines); class=class="str">"cmt">//--- 根据要删除的行数向上移动行 MoveLines(m_selected_line_from+class="num">1,lines_total-lines_to_delete,lines_to_delete,class="kw">false); class=class="str">"cmt">//--- 调整行数组大小 ::ArrayResize(m_lines,lines_total-lines_to_delete); class=class="str">"cmt">//--- 将光标移动到选择的初始位置 SetTextCursor(m_selected_symbol_from,m_selected_line_from); } } class CTextBox : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 删除所选文本 class="type">void DeleteSelectedText(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 删除所选文本 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTextBox::DeleteSelectedText(class="type">void) { class=class="str">"cmt">//--- 如果未选择文本, 离开 if(m_selected_line_from==WRONG_VALUE) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果从一行中删除字符 if(m_selected_line_from==m_selected_line_to) DeleteTextOnOneLine(); class=class="str">"cmt">//--- 如果从多行中删除字符 else DeleteTextOnMultipleLines(); class=class="str">"cmt">//--- 重置所选文本 ResetSelectedText(); class=class="str">"cmt">//--- 计算文本框的大小 CalculateTextBoxSize(); class=class="str">"cmt">//--- 设置文本框的新尺寸 ChangeTextBoxSize(); class=class="str">"cmt">//--- 调整滚动条 CorrectingHorizontalScrollThumb(); CorrectingVerticalScrollThumb(); class=class="str">"cmt">//--- 更新文本框中的文本 DrawTextAndCursor(true); class=class="str">"cmt">//--- 发送有关它的消息 ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo()); class="kw">return(true); }
用 CImage 接管图像像素的存取
在 MT5 标准库里, 图像数据以前散落在 CCanvasTable 这类控件用的结构体中, 现在统一收进 Objects.mqh 里的 CImage 类。它把像素数组、宽、高、文件路径封成四个受保护成员, 对外只暴露读写接口, 复用起来比旧结构干净。 CImage 的核心是两个动作: ReadImageData 按路径把图像读进 m_image_data 并记好宽高; CopyImageData 则先按源数组大小重设接收数组, 再循环逐元素拷贝。需要批量贴图或做图层缓存时, 直接调 CopyImageData 比手写循环省事。 下面这段是类骨架, 注意 Data() 做了重载——单参返回某索引像素颜色, 双参写入, 索引越界会直接抛数组错误, 调用前用 DataTotal() 确认长度。 别在多线程里共享同一个 CImage 实例。m_image_data 是普通 uint 数组, 没有加锁, 两个线程同时写 Data() 大概率丢像素或崩 EA。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 保存图像数据的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CImage { class="kw">protected: class="type">uint m_image_data[]; class=class="str">"cmt">// 图像像素数组 class="type">uint m_image_width; class=class="str">"cmt">// 图像宽度 class="type">uint m_image_height; class=class="str">"cmt">// 图像高度 class="type">class="kw">string m_bmp_path; class=class="str">"cmt">// 图像文件路径 class="kw">public: class=class="str">"cmt">//--- (class="num">1) 数据数组大小, (class="num">2) 设置/返回数据 (像素颜色) class="type">uint DataTotal(class="type">void) { class="kw">return(::ArraySize(m_image_data)); } class="type">uint Data(class="kw">const class="type">uint data_index) { class="kw">return(m_image_data[data_index]); } class="type">void Data(class="kw">const class="type">uint data_index,class="kw">const class="type">uint data) { m_image_data[data_index]=data; } class=class="str">"cmt">//--- 设置/返回图像宽度 class="type">void Width(class="kw">const class="type">uint width) { m_image_width=width; } class="type">uint Width(class="type">void) { class="kw">return(m_image_width); } class=class="str">"cmt">//--- 设置/返回图像高度 class="type">void Height(class="kw">const class="type">uint height) { m_image_height=height; } class="type">uint Height(class="type">void) { class="kw">return(m_image_height); } class=class="str">"cmt">//--- 设置/返回图像路径 class="type">void BmpPath(class="kw">const class="type">class="kw">string bmp_file_path) { m_bmp_path=bmp_file_path; } class="type">class="kw">string BmpPath(class="type">void) { class="kw">return(m_bmp_path); } };
「图像类的构造读取与数据拷贝」
在 MT5 里做自定义指标或面板时,经常要把 BMP/PNG 资源读进内存数组再逐像素处理。下面这个 CImage 类的骨架,把图像宽高和路径初始化留给了构造器和析构器,二者目前都是空实现,仅把 m_image_width、m_image_height 置 0,m_bmp_path 置空串。 真正干活的是 ReadImageData()。它先调用 ResetLastError() 清掉之前的错误码,再把传入路径存进成员变量,随后用 ResourceReadImage() 把资源文件读入 m_image_data,同时拿到宽高。若返回 false,就打印函数名和 GetLastError() 后退出,调用方据此判断加载是否成功。 CopyImageData() 负责把另一个 CImage 实例的像素数组整段搬过来:先取源数组长度 source_data_total,用 ArrayResize() 把目标数组拉到同样大小,再跑一个 uint 循环逐元素赋值。这种写法在复制 1920×1080 的位图时,数组长度会到 207 万级,循环开销不可忽视,实盘加载大图建议先测耗时。 别把空构造器当累赘。留空析构意味着类不自动释放 m_image_data,如果你在 EA 里频繁 new/delete CImage,要自己管数组内存,否则 MT5 终端跑久了可能吃内存。
CImage::CImage(class="type">void) : m_image_width(class="num">0), m_image_height(class="num">0), m_bmp_path("") { } CImage::~CImage(class="type">void) { } class CImage { class="kw">public: class=class="str">"cmt">//--- 读取并存储所传递的图像数据 class="type">bool ReadImageData(class="kw">const class="type">class="kw">string bmp_file_path); }; class="type">bool CImage::ReadImageData(class="kw">const class="type">class="kw">string bmp_file_path) { class=class="str">"cmt">//--- 清除最后的错误 ::ResetLastError(); class=class="str">"cmt">//--- 保存图像路径 m_bmp_file_path=bmp_file_path; class=class="str">"cmt">//--- 读取并保存图像数据 if(!::ResourceReadImage(m_bmp_file_path,m_image_data,m_image_width,m_image_height)) { ::Print(__FUNCTION__," > 错误: ",::GetLastError()); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- class="kw">return(true); } class CImage { class="kw">public: class=class="str">"cmt">//--- 复制所传递的图像数据 class="type">void CopyImageData(CImage &array_source); }; class="type">void CImage::CopyImageData(CImage &array_source) { class=class="str">"cmt">//--- 获取接收数组和源数组的大小 class="type">uint data_total =DataTotal(); class="type">uint source_data_total=::GetPointer(array_source).DataTotal(); class=class="str">"cmt">//--- 调整接收数组的大小 ::ArrayResize(m_image_data,source_data_total); class=class="str">"cmt">//--- 复制数据 for(class="type">uint i=class="num">0; i<source_data_total; i++) m_image_data[i]=::GetPointer(array_source).Data(i); }
◍ 把这条线请下神坛
多行文本框控件的价值已经说清:它绕开了 OBJ_EDIT 只能单行输入的硬限制,允许在面板里直接敲多行内容,这对做日志框、备注框一类交互有意义。当前函数库结构图显示,界面层已拆出独立控件模块,后续会把这些控件塞进表格单元,并改用渲染模式替代堆标准对象。 垂直缩放那段讨论里,作者给的落法是三个属性:CHART_SCALEFIX 开固定缩放,CHART_FIXED_MAX 和 CHART_FIXED_MIN 锁上下界,思路和现有水平滚动一致。下面这段代码就是直接对子图表 ID 设值,把范围压在 1.0 到 2.0 之间。 外汇与贵金属图表嵌入这类自绘控件时,需注意终端事件捕获的边界限制,缩放逻辑可能在某些布局下失效,属于高风险定制。跑通后你可以改那两个 double 参数,看子图纵向压缩是否符合预期。
class=class="str">"cmt">//--- 固定变焦模式 ::ChartSetInteger(m_sub_chart_id[i],CHART_SCALEFIX,true); class=class="str">"cmt">//-- 最大值和最小值 ::ChartSetDouble(m_sub_chart_id[i],CHART_FIXED_MAX,class="num">2.0); ::ChartSetDouble(m_sub_chart_id[i],CHART_FIXED_MIN,class="num">1.0);