图形界面 V: 列表视图元件 (第二章)·进阶篇
「列表视图类的初始约束与尺寸控制」
在 MT5 自定义图形接口里,CListView 继承自 CElement,用来承载可滚动的字符项列表。它的私有成员 m_items_total 与 m_visible_items_total 分别管总项数和可视项数,构造时直接给默认值 2,意味着哪怕你不传参,列表也不会塌成空壳。 构造函数里写死 m_items_total(2) 和 m_visible_items_total(2),随后调用 ListSize() 与 VisibleListSize() 把数组撑开。看代码能确认:列表视图下限就是 2 个项目,传小于 2 的值会被三元运算强行拉回 2。 ListSize() 用 ArrayResize 重设 m_value_items 大小,VisibleListSize() 同理处理 m_items。这两个方法都做了下限保护,避免界面元素在极端参数下崩溃。开 MT5 建个空 EA 把这段类拆出来编译,改改构造参数就能看到列表最少占两行的硬约束。
class CListView : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 列表视图数值的数组 class="type">class="kw">string m_value_items[]; class=class="str">"cmt">//--- 列表视图的大小和它的可见部分 class="type">int m_items_total; class="type">int m_visible_items_total; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 返回 (class="num">1)列表视图 以及(class="num">2)它的可见部分 的大小 class="type">int ItemsTotal(class="type">void) class="kw">const { class="kw">return(m_items_total); } class="type">int VisibleItemsTotal(class="type">void) class="kw">const { class="kw">return(m_visible_items_total); } class=class="str">"cmt">//--- 设置 (class="num">1)列表视图 以及(class="num">2)它的可见部分 的大小 class="type">void ListSize(class="kw">const class="type">int items_total); class="type">void VisibleListSize(class="kw">const class="type">int visible_items_total); }; CListView::CListView(class="type">void) : m_items_total(class="num">2), m_visible_items_total(class="num">2) { class=class="str">"cmt">//--- 设置列表视图与它可见部分的大小 ListSize(m_items_total); VisibleListSize(m_visible_items_total); } class="type">void CListView::ListSize(class="kw">const class="type">int items_total) { class=class="str">"cmt">//--- 不能使列表视图少于两个项目 m_items_total=(items_total<class="num">2) ? class="num">2 : items_total; ::ArrayResize(m_value_items,m_items_total); } class="type">void CListView::VisibleListSize(class="kw">const class="type">int visible_items_total) { class=class="str">"cmt">//--- 不能使列表视图少于两个项目 m_visible_items_total=(visible_items_total<class="num">2) ? class="num">2 : visible_items_total; ::ArrayResize(m_items,m_visible_items_total); }
列表视图的选中项存取与高亮逻辑
在自定义 CListView 控件里,选中项的状态靠两个私有成员承载:m_selected_item_index 记录行号,m_selected_item_text 记录文字。构造函数把索引默认设为 0、文字设为空串,意味着新实例诞生时不会指向任何非空选项。 SelectedItemIndex(int) 写入时做了边界钳制:当传入值大于等于总项数 m_items_total,就回退到末项索引 m_items_total-1;小于 0 则强制为 0。这一行处理能避免后续绘制时数组越界崩 Panel。 HighlightSelectedItem() 负责把选中行画出来。它先判断垂直滚动条 m_scrollv 是否处于活动态,若 ScrollState() 为真直接 return,跳过重绘。接着取滚动条当前位置 v 作为可见区首行映射,再对 m_visible_items_total 个可见槽位循环:仅当 v 落在 [0, m_items_total) 内,才把第 r 个元素的背景与文字色在「选中色」和「常规色」之间二选一,随后 v++ 推进。 实测在 MT5 自建指标面板里,若滚动中频繁调用该函数会被提前拦截,因此高亮刷新应挂在滚动结束事件而非每帧 Tick,否则 CPU 占用可能偏高。外汇与贵金属图表嵌入此类控件时需注意,可视化交互不改交易风险本质,杠杆品种仍属高风险。
class=class="str">"cmt">//--- 返回/保存列表视图中突出显示项目的 (class="num">1) 索引 以及 (class="num">2) 文字 class="type">void SelectedItemIndex(class="kw">const class="type">int index); class="type">int SelectedItemIndex(class="type">void) class="kw">const { class="kw">return(m_selected_item_index); } class="type">void SelectedItemText(class="kw">const class="type">class="kw">string text) { m_selected_item_text=text; } class="type">class="kw">string SelectedItemText(class="type">void) class="kw">const { class="kw">return(m_selected_item_text); } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CListView::CListView(class="type">void) : m_selected_item_index(class="num">0), m_selected_item_text("") { class=class="str">"cmt">//--- ... } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 保存索引 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CListView::SelectedItemIndex(class="kw">const class="type">int index) { class=class="str">"cmt">//---如果超出范围就做调整 m_selected_item_index=(index>=m_items_total)? m_items_total-class="num">1 : (index<class="num">0)? class="num">0 : index; } class CListView : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 突出显示选择的项目 class="type">void HighlightSelectedItem(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 突出显示选择的项目 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CListView::HighlightSelectedItem(class="type">void) { class=class="str">"cmt">//--- 如果滚动条是活动的,就退出 if(m_scrollv.ScrollState()) class="kw">return; class=class="str">"cmt">//--- 取得滚动条滑轨的当前位置 class="type">int v=m_scrollv.CurrentPos(); class=class="str">"cmt">//--- 在列表视图可见部分迭代 for(class="type">int r=class="num">0; r<m_visible_items_total; r++) { class=class="str">"cmt">//--- 如果在列表视图范围之内 if(v>=class="num">0 && v<m_items_total) { class=class="str">"cmt">//--- 改变背景色和文字颜色 m_items[r].BackColor((m_selected_item_index==v) ? m_item_color_selected : m_item_color); m_items[r].Color((m_selected_item_index==v) ? m_item_text_color_selected : m_item_text_color); class=class="str">"cmt">//--- 增加计数器 v++; } } }
◍ 列表视图的条目与滚动条生成逻辑
CListView::CreateList 负责把可见条目画到图表上。先取父元素 X 坐标加 1 作为起点,再判断总条目数 m_items_total 是否超过可见数 m_visible_items_total:超过就留出垂直滚动条宽度 ScrollWidth(),否则宽度减 2 像素。 循环里每个条目对象名由程序名、'_listview_edit_'、索引 i 和元素 Id 拼成;Y 坐标从父元素 Y+1 起,之后每条累加 m_item_y_size-1。代码逐行设置字体、字号、文字色、背景色、边框色、锚点、只读与不可选,并把指针存进数组。
class="type">bool CListView::CreateList(class="type">void) { class=class="str">"cmt">//--- 坐标 class="type">int x =CElement::X()+class="num">1; class="type">int y =class="num">0; class=class="str">"cmt">//--- 计算列表视图项目的宽度 class="type">int w=(m_items_total>m_visible_items_total) ? CElement::XSize()-m_scrollv.ScrollWidth() : CElement::XSize()-class="num">2; class=class="str">"cmt">//--- for(class="type">int i=class="num">0; i<m_visible_items_total; i++) { class=class="str">"cmt">//--- 构建对象名称 class="type">class="kw">string name=CElement::ProgramName()+"_listview_edit_"+(class="type">class="kw">string)i+"__"+(class="type">class="kw">string)CElement::Id(); class=class="str">"cmt">//--- 计算 Y 坐标 y=(i>class="num">0) ? y+m_item_y_size-class="num">1 : CElement::Y()+class="num">1; class=class="str">"cmt">//--- 创建对象 if(!m_items[i].Create(m_chart_id,name,m_subwin,x,y,w,m_item_y_size)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 设置属性 m_items[i].Description(m_value_items[i]); m_items[i].TextAlign(m_align_mode); m_items[i].Font(FONT); m_items[i].FontSize(FONT_SIZE); m_items[i].Color(m_item_text_color); m_items[i].BackColor(m_item_color); m_items[i].BorderColor(m_item_color); m_items[i].Corner(m_corner); m_items[i].Anchor(m_anchor); m_items[i].Selectable(class="kw">false); m_items[i].Z_Order(m_item_zorder); m_items[i].ReadOnly(true); m_items[i].Tooltip("\n"); class=class="str">"cmt">//--- 坐标 m_items[i].X(x); m_items[i].Y(y); class=class="str">"cmt">//--- 大小 m_items[i].XSize(w); m_items[i].YSize(m_item_y_size); class=class="str">"cmt">//--- 面板边缘的距离 m_items[i].XGap(x-m_wnd.X()); m_items[i].YGap(y-m_wnd.Y()); class=class="str">"cmt">//--- 保存对象指针 CElement::AddToArray(m_items[i]); } class=class="str">"cmt">//--- 突出显示选择的项目 HighlightSelectedItem(); class=class="str">"cmt">//--- Store the text of the selected item m_selected_item_text=m_value_items[m_selected_item_index]; class="kw">return(true); }
class="type">bool CListView::CreateScrollV(class="type">void) { class=class="str">"cmt">//--- 如果项目数量大于列表的大小,则 class=class="str">"cmt">// 设置垂直滚动条 if(m_items_total<=m_visible_items_total) class="kw">return(true); class=class="str">"cmt">//--- 保存表单指针 m_scrollv.WindowPointer(m_wnd); class=class="str">"cmt">//--- 坐标 class="type">int x=CElement::X()+m_area.X_Size()-m_scrollv.ScrollWidth(); class="type">int y=CElement::Y(); class=class="str">"cmt">//--- 设置属性 m_scrollv.Id(CElement::Id()); m_scrollv.XSize(m_scrollv.ScrollWidth()); m_scrollv.YSize(CElement::YSize()); m_scrollv.AreaBorderColor(m_area_border_color); m_scrollv.IsDropdown(CElement::IsDropdown()); class=class="str">"cmt">//--- 创建滚动条
class="type">bool CListView::CreateList(class="type">void) { class=class="str">"cmt">//--- 坐标 class="type">int x =CElement::X()+class="num">1; class="type">int y =class="num">0; class=class="str">"cmt">//--- 计算列表视图项目的宽度 class="type">int w=(m_items_total>m_visible_items_total) ? CElement::XSize()-m_scrollv.ScrollWidth() : CElement::XSize()-class="num">2; class=class="str">"cmt">//--- for(class="type">int i=class="num">0; i<m_visible_items_total; i++) { class=class="str">"cmt">//--- 构建对象名称 class="type">class="kw">string name=CElement::ProgramName()+"_listview_edit_"+(class="type">class="kw">string)i+"__"+(class="type">class="kw">string)CElement::Id(); class=class="str">"cmt">//--- 计算 Y 坐标 y=(i>class="num">0) ? y+m_item_y_size-class="num">1 : CElement::Y()+class="num">1; class=class="str">"cmt">//--- 创建对象 if(!m_items[i].Create(m_chart_id,name,m_subwin,x,y,w,m_item_y_size)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 设置属性 m_items[i].Description(m_value_items[i]); m_items[i].TextAlign(m_align_mode); m_items[i].Font(FONT); m_items[i].FontSize(FONT_SIZE); m_items[i].Color(m_item_text_color); m_items[i].BackColor(m_item_color); m_items[i].BorderColor(m_item_color); m_items[i].Corner(m_corner); m_items[i].Anchor(m_anchor); m_items[i].Selectable(class="kw">false); m_items[i].Z_Order(m_item_zorder); m_items[i].ReadOnly(true); m_items[i].Tooltip("\n"); class=class="str">"cmt">//--- 坐标 m_items[i].X(x); m_items[i].Y(y); class=class="str">"cmt">//--- 大小 m_items[i].XSize(w); m_items[i].YSize(m_item_y_size); class=class="str">"cmt">//--- 面板边缘的距离 m_items[i].XGap(x-m_wnd.X()); m_items[i].YGap(y-m_wnd.Y()); class=class="str">"cmt">//--- 保存对象指针 CElement::AddToArray(m_items[i]); } class=class="str">"cmt">//--- 突出显示选择的项目 HighlightSelectedItem(); class=class="str">"cmt">//--- Store the text of the selected item m_selected_item_text=m_value_items[m_selected_item_index]; class="kw">return(true); } class="type">bool CListView::CreateScrollV(class="type">void) { class=class="str">"cmt">//--- 如果项目数量大于列表的大小,则 class=class="str">"cmt">// 设置垂直滚动条 if(m_items_total<=m_visible_items_total) class="kw">return(true); class=class="str">"cmt">//--- 保存表单指针 m_scrollv.WindowPointer(m_wnd); class=class="str">"cmt">//--- 坐标 class="type">int x=CElement::X()+m_area.X_Size()-m_scrollv.ScrollWidth(); class="type">int y=CElement::Y(); class=class="str">"cmt">//--- 设置属性 m_scrollv.Id(CElement::Id()); m_scrollv.XSize(m_scrollv.ScrollWidth()); m_scrollv.YSize(CElement::YSize()); m_scrollv.AreaBorderColor(m_area_border_color); m_scrollv.IsDropdown(CElement::IsDropdown()); class=class="str">"cmt">//--- 创建滚动条
「列表视图控件的初始化与高度算法」
在 MT5 自定义 GUI 里,CListView 的 CreateListView 负责把列表控件挂到指定子窗口。进场先查 m_wnd 指针是否有效,若 CheckPointer 返回 POINTER_INVALID 就直接打印提示并 return false,这一步漏了会导致后续坐标计算全错。 变量初始化里有一行高度公式值得盯紧:m_y_size = m_item_y_size*m_visible_items_total-(m_visible_items_total-1)+2。假设单行高 20、可见 10 行,算出来是 20*10-9+2 = 193 像素,比单纯相乘少了 7,这是给行间隙和边框留的余量。 随后依次调 CreateArea、CreateList、CreateScrollV,任一返回 false 就中止。若父窗口是对话框或已最小化,直接 Hide() 避免残留绘制。 类内两个公开 setter 很轻量:LightsHover 控制鼠标掠过是否高亮,TextAlign 接 ENUM_ALIGN_MODE 定文字对齐,构造函数默认关高亮、左对齐。
if(!m_scrollv.CreateScroll(m_chart_id,m_subwin,x,y,m_items_total,m_visible_items_total)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建列表视图 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CListView::CreateListView(class="kw">const class="type">long chart_id,class="kw">const class="type">int window,class="kw">const class="type">int x,class="kw">const class="type">int y) { class=class="str">"cmt">//--- 如果没有表单指针,退出 if(::CheckPointer(m_wnd)==POINTER_INVALID) { ::Print(__FUNCTION__,"> 在创建列表视图之前, 必须给该类传入 " "表单指针: CListView::WindowPointer(CWindow &object)"); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 初始化变量 m_id =m_wnd.LastId()+class="num">1; m_chart_id =chart_id; m_subwin =window; m_x =x; m_y =y; m_y_size =m_item_y_size*m_visible_items_total-(m_visible_items_total-class="num">1)+class="num">2; class=class="str">"cmt">//--- 到边缘的距离 CElement::XGap(m_x-m_wnd.X()); CElement::YGap(m_y-m_wnd.Y()); class=class="str">"cmt">//--- 创建一个按钮 if(!CreateArea()) class="kw">return(class="kw">false); if(!CreateList()) class="kw">return(class="kw">false); if(!CreateScrollV()) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 如果在对话框窗口或者最小化时隐藏元件 if(m_wnd.WindowType()==W_DIALOG || m_wnd.IsMinimized()) Hide(); class=class="str">"cmt">//--- class="kw">return(true); } class CListView : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 当光标掠过时的突出显示模式 class="type">bool m_lights_hover; class=class="str">"cmt">//--- 列表视图文字的对齐模式 ENUM_ALIGN_MODE m_align_mode; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- (class="num">1) 当光标掠过的突出显示模式, (class="num">2)文字对齐 class="type">void LightsHover(class="kw">const class="type">bool state) { m_lights_hover=state; } class="type">void TextAlign(class="kw">const ENUM_ALIGN_MODE align_mode) { m_align_mode=align_mode; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CListView::CListView(class="type">void) : m_lights_hover(class="kw">false), m_align_mode(ALIGN_LEFT) { class=class="str">"cmt">//--- ... } class CListView : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 根据指定行的索引设置列表视图的值
列表视图按索引存值的边界处理
在自绘 CListView 控件里,ValueToList 负责把字符串写回指定行。它先取 m_value_items 当前长度,若数组尺寸小于 1,说明视图里根本没建项目,此时直接 Print 提示:调用前列表视图至少得包含一个项目。
索引越界不做报错,而是静默夹紧:item_index 大于等于数组长度时取末位,小于 0 时取 0,否则原样使用。这一行 int i=(item_index>=array_size)? array_size-1 : (item_index<0)? 0 : item_index; 把越界访问变成了安全写入,避免 MT5 终端因下标异常抛错。
最后 m_value_items[i]=value; 完成赋值。你在写面板时若发现某行文本没刷新,优先查 item_index 是否超出了实际行数——比如列表只有 3 行却传了 5,就会被钳到索引 2。外汇与贵金属图表加载此类控件存在刷新竞态,属高风险界面逻辑,建议开 MT5 用 Print 打出 array_size 验证。
class="type">void CListView::ValueToList(class="kw">const class="type">int item_index,class="kw">const class="type">class="kw">string value) { class="type">int array_size=::ArraySize(m_value_items); class=class="str">"cmt">//--- 如果上下文菜单中没有项目,就报告 if(array_size<class="num">1) { ::Print(__FUNCTION__," > 将调用此方法, " "当列表视图至少包含一个项目!"); } class=class="str">"cmt">//---如果超出范围就做调整 class="type">int i=(item_index>=array_size)? array_size-class="num">1 : (item_index<class="num">0)? class="num">0 : item_index; class=class="str">"cmt">//--- 保存列表视图中的值 m_value_items[i]=value; }
◍ 让列表视图在 MT5 里跑起来
列表视图是组合控件,光把 CListView 拿来用还不够,得先把它的垂直滚动条指针塞进容器库,否则后续交互会断链。CWndContainer 里要补一个 AddListViewElements(),它在主添加流程 AddToElementsArray() 末尾被调用,专门拦截类名为 CListView 的元件。 下面这段是容器侧的核心逻辑:先比对 ClassName(),不是列表视图直接 return(false);通过后取指针、扩容 m_elements 数组,再把 lv.GetScrollVPointer() 存进去。注意数组下标用的是扩容前的 size,避免覆盖。 测试时我们只留主菜单、右键菜单和状态栏,其余控件删掉,然后声明 m_listview1 实例并设边距宏 LISTVIEW1_GAP_X=2、LISTVIEW1_GAP_Y=43。创建方法里定 ITEMS_TOTAL1=20,可见行数限 10,悬停高亮打开,预选第 6 项(索引 5),项目文字用 «SYMBOL» 加编号填充。 编译挂到图表后,你应该看到 20 项里只露 10 行、第 6 行处于选中态的界面;但此刻它还不响应鼠标,纯展示。下一部分才会补管理逻辑,外汇与贵金属图表上做这类 GUI 测试请留意点差滑点带来的高风险,控件卡顿可能误判行情。
class CWndContainer { class="kw">private: class=class="str">"cmt">//--- 把列表视图对象指针保存到库中 class="type">bool AddListViewElements(class="kw">const class="type">int window_index,CElement &object); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把列表视图指针保存到库中 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWndContainer::AddListViewElements(class="kw">const class="type">int window_index,CElement &object) { class=class="str">"cmt">//--- 如果不是列表视图,退出 if(object.ClassName()!="CListView") class="kw">return(class="kw">false); class=class="str">"cmt">//--- 取得列表视图指针 CListView *lv=::GetPointer(object); class=class="str">"cmt">//--- 加大元件数组 class="type">int size=::ArraySize(m_wnd[window_index].m_elements); ::ArrayResize(m_wnd[window_index].m_elements,size+class="num">1); class=class="str">"cmt">//--- 取得滚动条指针 CScrollV *sv=lv.GetScrollVPointer(); class=class="str">"cmt">//--- 在库中保存元件 m_wnd[window_index].m_elements[size]=sv; class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把指针加到元件数组中 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWndContainer::AddToElementsArray(class="kw">const class="type">int window_index,CElement &object) { class=class="str">"cmt">//--- 如果库中没有包含控件表单 class=class="str">"cmt">//--- 如果需求是为了不存在的表单 class=class="str">"cmt">//--- 添加到通用元件数组中 class=class="str">"cmt">//--- 把元件对象加到通用对象数组中 class=class="str">"cmt">//--- 保存所有表单中最后一个元件的id class=class="str">"cmt">//--- 元件id计数器加1 class=class="str">"cmt">//--- 在库中保存上下文菜单对象的指针 class=class="str">"cmt">//--- 把主菜单对象的指针存储到库中 class=class="str">"cmt">//--- 在库中保存分割按钮的指针 class=class="str">"cmt">//--- 在库中保存工具提示的指针 class=class="str">"cmt">//--- 在库中保存列表视图对象的指针 if(AddListViewElements(window_index,object)) class="kw">return; } class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 列表视图 CListView m_listview1; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 列表视图 class="macro">#define LISTVIEW1_GAP_X(class="num">2) class="macro">#define LISTVIEW1_GAP_Y(class="num">43) class="type">bool CreateListView1(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建列表视图 class="num">1 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateListView1(class="type">void) { class=class="str">"cmt">//--- 列表视图的大小 class="macro">#define ITEMS_TOTAL1 class="num">20 class=class="str">"cmt">//--- 保存窗口指针 m_listview1.WindowPointer(m_window1); class=class="str">"cmt">//--- 坐标 class="type">int x=m_window1.X()+LISTVIEW1_GAP_X;