信号计算器·进阶篇
(2/3)· 搞懂信号兼容限制与复制比例算法,才能把订阅决策从盲猜变成可计算
「信号计算器面板的控件骨架与画布挂载」
做信号计算器第一件事是把控件铺到对话框客户区。面板按五行排布:第一行放标签与下拉框组合,第二行单标签,第三行八个功能按钮,第四行是自研的表格控件,第五行用 CCanvas 画位图按钮。所有控件对象创建后都必须调用 CDialog::Add 塞进客户区,否则界面上根本看不到。 以 Label4 为例,先 new 出对象、设好属性,退出函数前一句 Add(label4) 决定它能不能渲染。这套顺序可概括为:Create 面板 → 逐个 Create 元件 → 改属性 → Add 到面板,顺序乱了 MT5 会报空指针或控件失踪。 画布走的是 CoSDialog::CreateBmpButton1。先 m_canvas1.Create 建独立资源,注意传入的 x2、y2 就是图标像素尺寸,必须一致;接着 FontSet、Erase 填面板色、Update 推显。第二步建 10×10 的 CBmpButton,锚点 x1、y1 才是关键。第三步把 ResourceName 喂给 BmpOnName,控件按画布尺寸拉伸;第四步照样 Add 进面板。 表格数据排序在 InitStructurs 里用 CArrayObj 线性表完成,方案来自 Vasiliy Sokolov。原始信号复制比例乱序如 Siksikawa 8770.0、Hedge 1.0 混排,插入时按 rate 字段实时重排——日志里 row #0 到 row #5 每次 Insert 后顺序都变,证明是插入即排序。想看完整过程,把该方法里打印块注释去掉重编译即可。 新表格 CTableListView 基于 CListView 改,分可见与不可见两层。可见部分由 x1,y1,x2,y2 矩形加列元件限定,内部用 m_arr_rows(CArrayObj)管 CEdit 指针,像两层俄套娃;不可见部分用 m_arr_rows_str(CArrayString)和 m_arr_rows_val(CArrayLong)存文字与数值。外汇贵金属信号订阅涉及跟单,杠杆与汇率波动属高风险,排序结果仅作参考而非收益保证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定义 | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- 缩进和间隙 class="macro">#define INDENT_LEFT(class="num">11) class=class="str">"cmt">// 左边的缩进 (包含边框宽度)
◍ 面板控件尺寸与标签创建的实现细节
在 MT5 自定义对话框里,先靠一组宏把控件尺寸钉死,能避免不同 DPI 下布局错位。顶端缩进 INDENT_TOP 取 11 像素(含边框),纵向间隙 CONTROLS_GAP_Y 为 5 像素;下拉框宽 60、高 20,列表高 102,按钮宽 72、高 20,编辑框宽 60、高 20。 CoSDialog::Create 先置 m_error=true,再调用基类 CAppDialog::Create 建壳,若失败直接返回 false,这是标准的资源申请失败短路写法。 CreateLabel4 里标签坐标用 x1=INDENT_LEFT、y1=INDENT_TOP+COMBOBOX_HEIGHT+CONTROLS_GAP_Y 算出来,也就是顶缩进 11 加下拉高 20 加间隙 5,y1 实际为 36(假设 INDENT_LEFT 为 0 时);标签宽固定 100、高复用下拉框 20。文本取自多语言对象的第 3 号索引,最后 Add 进容器,任一步失败都返回 false。 开 MT5 把这段宏和 CreateLabel4 抄进你的面板类,改 m_languages.GetText(3) 的索引就能换标签文案,验证布局是否如数落位。
class="macro">#define INDENT_TOP(class="num">11) class=class="str">"cmt">// 顶端的缩进 (包含边框宽度) class="macro">#define CONTROLS_GAP_Y(class="num">5) class=class="str">"cmt">// 纵坐标上的间隙 class=class="str">"cmt">//--- for combo boxes class="macro">#define COMBOBOX_WIDTH(class="num">60) class=class="str">"cmt">// 横坐标上的大小 class="macro">#define COMBOBOX_HEIGHT(class="num">20) class=class="str">"cmt">// 纵坐标上的大小 class=class="str">"cmt">//--- for list view class="macro">#define LIST_HEIGHT(class="num">102) class=class="str">"cmt">// 纵坐标上的大小 class=class="str">"cmt">//--- for buttons class="macro">#define BUTTON_WIDTH(class="num">72) class=class="str">"cmt">// 横坐标上的大小 class="macro">#define BUTTON_HEIGHT(class="num">20) class=class="str">"cmt">// 纵坐标上的大小 class=class="str">"cmt">//--- for the indication area class="macro">#define EDIT_WIDTH(class="num">60) class=class="str">"cmt">// 横坐标上的大小 class="macro">#define EDIT_HEIGHT(class="num">20) class=class="str">"cmt">// 纵坐标上的大小 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CoSDialog::Create(const class="type">long chart,const class="type">class="kw">string name,const class="type">int subwin,const class="type">int x1,const class="type">int y1,const class="type">int x2,const class="type">int y2) { class=class="str">"cmt">//--- m_error=true; if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2)) class="kw">return(false); class=class="str">"cmt">//--- class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建 "Signals" 标签 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CoSDialog::CreateLabel4(class="type">void) { class=class="str">"cmt">//--- 坐标 class="type">int x1=INDENT_LEFT; class="type">int y1=INDENT_TOP+COMBOBOX_HEIGHT+CONTROLS_GAP_Y; class="type">int x2=x1+class="num">100; class="type">int y2=y1+COMBOBOX_HEIGHT; class=class="str">"cmt">//--- 创建 if(!m_label4.Create(m_chart_id,m_name+"Label4",m_subwin,x1,y1,x2,y2)) class="kw">return(false); if(!m_label4.Text(m_languages.GetText(class="num">3))) class="kw">return(false); if(!Add(m_label4)) class="kw">return(false); class=class="str">"cmt">//--- 成功 class="kw">return(true); }
在面板里铺画布和位图按钮
往 MT5 自定义控件面板里塞元素时,先建 Label 再建 Canvas 是常见顺序。下面这段逻辑里,Label4 创建失败就直接 return(false),不往下走,避免空指针把整块面板拖崩。 画布用 COLOR_FORMAT_XRGB_NOALPHA 建,尺寸取 x2、y2 两点坐标。字体设成 Trebuchet MS、高度 -100、FW_THIN,背景擦成 C'0xF7,0xF7,0xF7' 的浅灰并立即 Update(true) 推到图表,肉眼能看到面板底色瞬间刷出来。 位图按钮 BmpButton1 占地只有 10×10 像素(x1+10, y1+10),它的图案直接绑刚建好的画布资源名 m_canvas1.ResourceName()。若 BmpOnName 失败同样 return(false),否则 Add 进容器。 插入排序那行值得盯:min_deposit = AccInfo.balance / rate * 100.0,用账户余额除以某汇率再乘 100 算出最小入金,塞进 m_table.InsertSort(new CLineTable(...))。外汇和贵金属杠杆品种汇率跳动大,这套算法算出的数值仅供参考,实际爆仓风险仍高。 几个容器数组也得一并声明:m_arr_rows 装 CEdit 行对象,m_arr_rows_str 装 CArrayString,m_arr_rows_val 装 CArrayLong,三者都靠 CArrayObj 管指针,少一个都可能让后续表格刷新漏行。
if(!m_label4.Create(m_chart_id,m_name+"Label4",m_subwin,x1,y1,x2,y2)) class="kw">return(false); if(!Add(m_label4)) class="kw">return(false); class=class="str">"cmt">//--- 创建画布 if(!m_canvas1.Create("Canvas1",x2,y2,COLOR_FORMAT_XRGB_NOALPHA)) { Print("Error creating canvas: ",GetLastError()); class="kw">return(false); } m_canvas1.FontSet("Trebuchet MS",-class="num">100,FW_THIN); m_canvas1.Erase(ColorToARGB(C&class="macro">#x27;0xF7,0xF7,0xF7&class="macro">#x27;,class="num">255)); m_canvas1.Update(true); class=class="str">"cmt">//--- 创建 if(!m_bmp_button1.Create(m_chart_id,m_name+"BmpButton1",m_subwin,x1,y1,x1+class="num">10,y1+class="num">10)) class="kw">return(false); class=class="str">"cmt">//--- 设置 CBmpButton 控件位图文件的名称 if(!m_bmp_button1.BmpOnName(m_canvas1.ResourceName())) class="kw">return(false); if(!Add(m_bmp_button1)) class="kw">return(false); else { min_deposit=AccInfo.balance/rate*class="num">100.0; } class=class="str">"cmt">//Print("Insert : ",name,"; ",rate,"; ",min_deposit); m_table.InsertSort(new CLineTable(name,rate,min_deposit)); class=class="str">"cmt">//for(class="type">int m=class="num">0;m<m_table.Total();m++) class=class="str">"cmt">// { class=class="str">"cmt">// CLineTable *line=m_table.At(m); class=class="str">"cmt">// Print("row #",m,": ",line.Text(),"; ",line.Number(),"; ",line.Number1()); class=class="str">"cmt">// } } else PrintFormat("Error in call of SignalBaseSelect. Error code=%d",GetLastError()); } class=class="str">"cmt">//--- class="kw">return(true); } class="kw">virtual class="type">bool Create(const class="type">long chart,const class="type">class="kw">string name,const class="type">int subwin,const class="type">int x1, const class="type">int y1,const class="type">int x2,const class="type">int y2,const class="type">uchar columns,const class="type">class="kw">ushort &columns_size[]); CArrayObj m_arr_rows; class=class="str">"cmt">// objects-rows(CEdit) 指针数组 CArrayObj m_arr_rows_str; class=class="str">"cmt">// objects-rows(CArrayString) 的指针数组 CArrayObj m_arr_rows_val; class=class="str">"cmt">// objects-rows(CArrayLong) 的指针数组
「表格视图的构造与行列内存分配」
CTableListView::Create 是列表控件初始化的核心入口,先把列数 m_columns 和每列宽度数组 columns_size 拷进成员,这里有个隐蔽点:首列和末列的宽度会各减 1,用来抵消边框占位,若你传入的数组长度和列数不一致函数直接返回 false,调用方拿不到任何报错提示只能自己查。 可见行数由 (y2-y1)/m_item_height 推算,如果 TotalView 失败就回退到单行高度加两倍边框的保底值;m_total_view 小于 1 同样直接退出,意味着控件区域高度不够一行就别想画出来。 真正吃内存的是双重循环:外层按可见行数 new 出 CArrayObj 行容器,内层按列数 new 出 CEdit 单元格塞进去。假设 10 行 5 列就是 50 个 CEdit 对象,实盘面板若开多图表要算好句柄占用。 下面这段是 Create 的真实代码,逐行拆给你看关键分支落在哪。
class="type">bool CTableListView::Create(const class="type">long chart,const class="type">class="kw">string name,const class="type">int subwin,const class="type">int x1, const class="type">int y1,const class="type">int x2,const class="type">int y2,const class="type">uchar columns,const class="type">class="kw">ushort &columns_size[]) { m_columns=columns; ArrayResize(m_columns_size,m_columns); if(ArraySize(columns_size)!=m_columns) class="kw">return(false); ArrayCopy(m_columns_size,columns_size,class="num">0,class="num">0,WHOLE_ARRAY); m_columns_size[class="num">0]-=class="num">1; m_columns_size[m_columns-class="num">1]-=class="num">1; class="type">int y=y2; class=class="str">"cmt">//--- 如果之前确定了可见的行数,调整垂直大小 if(!TotalView((y2-y1)/m_item_height)) y=m_item_height+y1+class="num">2*CONTROLS_BORDER_WIDTH; class=class="str">"cmt">//--- 检查可见行数 if(m_total_view<class="num">1) class="kw">return(false); class=class="str">"cmt">//--- 调用父类方法 if(!CWndClient::Create(chart,name,subwin,x1,y1,x2,y)) class="kw">return(false); class=class="str">"cmt">//--- 设置 if(!m_background.ColorBackground(CONTROLS_LIST_COLOR_BG)) class="kw">return(false); if(!m_background.ColorBorder(CONTROLS_LIST_COLOR_BORDER)) class="kw">return(false); class=class="str">"cmt">//--- 创建依赖的控件 CArrayObj *m_arr_cells; for(class="type">int i=class="num">0;i<m_total_view;i++) { m_arr_cells=new CArrayObj; if(CheckPointer(m_arr_cells)==POINTER_INVALID) class="kw">return(false); for(class="type">int j=class="num">0;j<m_columns;j++) { CEdit *m_cell; m_cell=new CEdit; if(CheckPointer(m_cell)==POINTER_INVALID) class="kw">return(false); m_arr_cells.Add(m_cell); } m_arr_rows.Add(m_arr_cells); } class=class="str">"cmt">//--- for(class="type">int i=class="num">0;i<m_total_view;i++) { if(!CreateRow(i)) class="kw">return(false); if(m_height_variable && i>class="num">0) { class=class="str">"cmt">// m_rows[i].Hide(); /// CArrayObj *m_arr_cells_i=m_arr_rows.At(i); if(CheckPointer(m_arr_cells_i)==POINTER_INVALID) class="kw">return(false); for(class="type">int j=class="num">0;j<m_arr_cells_i.Total();j++) { CEdit *m_cell=m_arr_cells_i.At(j); if(CheckPointer(m_cell)==POINTER_INVALID)
◍ 行与单元格的实例化收尾
表格可视区域在确认了行数之后,会按 (y2-y1)/m_item_height 的比值去申请行对象;若 TotalView 调用失败,则回退到 y=m_item_height+y1+2*CONTROLS_BORDER_WIDTH 的兜底坐标,避免控件叠在边框上。 紧接着用双层循环把每一行的单元格数组建出来:外层 i 跑 m_total_view 次,内层 j 跑 m_columns 次,每次 new 一个 CEdit 并做 CheckPointer 校验,指针无效立即 return(false)。这一步决定了你之后在 MT5 里看到的行×列网格规模。 CreateRow 内部则按索引取回该行单元格数组,再遍历 m_arr_cells.Total() 逐个创建 CEdit,名称拼成 name_index_i,并强制 Text("") 与 ReadOnly(true)。想改可编辑表格,把 ReadOnly 的参数翻成 false 即可,但外汇与贵金属行情波动剧烈、GUI 误触可能带来高风险,建议先用模拟账户验证。
if(!TotalView((y2-y1)/m_item_height)) y=m_item_height+y1+class="num">2*CONTROLS_BORDER_WIDTH; CArrayObj *m_arr_cells; for(class="type">int i=class="num">0;i<m_total_view;i++) { m_arr_cells=new CArrayObj; if(CheckPointer(m_arr_cells)==POINTER_INVALID) class="kw">return(false); for(class="type">int j=class="num">0;j<m_columns;j++) { CEdit *m_cell; m_cell=new CEdit; if(CheckPointer(m_cell)==POINTER_INVALID) class="kw">return(false); m_arr_cells.Add(m_cell); } m_arr_rows.Add(m_arr_cells); } for(class="type">int i=class="num">0;i<m_total_view;i++) { if(!CreateRow(i)) class="kw">return(false); } class="kw">return(true); class="type">bool CTableListView::CreateRow(const class="type">int index) { CArrayObj *m_arr_cells=m_arr_rows.At(index); if(CheckPointer(m_arr_cells)==POINTER_INVALID) class="kw">return(false); for(class="type">int i=class="num">0;i<m_arr_cells.Total();i++) { CEdit *m_cell=m_arr_cells.At(i); if(CheckPointer(m_cell)==POINTER_INVALID) class="kw">return(false); x1+=x2; x2=m_columns_size[i]; if(!m_cell.Create(m_chart_id,m_name+"_"+IntegerToString(index)+"_"+IntegerToString(i), m_subwin,x1,y1,x1+x2,y2)) class="kw">return(false); if(!m_cell.Text("")) class="kw">return(false); if(!m_cell.ReadOnly(true)) class="kw">return(false); if(!Add(m_cell)) class="kw">return(false); } class="kw">return(true); }