图形界面 X: 在多行文本框中选择文本 (集成构建 13)·综合运用
📘

图形界面 X: 在多行文本框中选择文本 (集成构建 13)·综合运用

第 3/3 篇

◍ 文本框多选删除的底层搬运逻辑

在 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 响应,属高风险定制方案。

MQL5 / C++
   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。

MQL5 / C++
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 终端跑久了可能吃内存。

MQL5 / C++
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 参数,看子图纵向压缩是否符合预期。

MQL5 / C++
      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);

常见问题

用文本框控件的选区接口拿到起止行号,循环调用删除行方法即可,注意删完要重绘避免残影。
构造时按实际宽高申请缓冲区,读取用按行偏移计算像素地址,拷贝用内存块复制而不是逐点赋值。
可以,把代码贴给小布,它会标出选区边界处理和重绘遗漏点,并给出修正建议。
源缓冲区未校验长度或目标未分配就拷贝会越界,先判空并匹配尺寸再操作。
只是工具层封装,高频刷新仍要吃主线程,别当万能方案,按自己品种负载压测后再用。