图形界面 X: 高级列表和表格管理。代码优化 (集成构建 7)·进阶篇
(2/3)· 控件还靠手动调属性?这篇把列表、滚动条和表格的可编程管理一次讲透
◍ 用代码把滚动条滑块推到想要的位置
在自建 MT5 面板里,列表项动态增长时,若想让视图始终跟着最新数据走,就得在运行时操纵滚动条滑块。CScrollV 和 CScrollH 都提供了 MovingThumb() 方法,垂直与水平逻辑几乎一致,下面只拆垂直部分。 MovingThumb() 只有一个参数 pos,默认值是 WRONG_VALUE。不传位置直接调用,滑块会落到列表最末端——这对实时追加日志或报价行的自动滚屏很实用。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 管理垂直滚动条的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CScrollV : class="kw">public CScroll { class="kw">public: class=class="str">"cmt">//--- 移动滑块到指定位置 class="type">void MovingThumb( const class="type">int pos= WRONG_VALUE ); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 移动滑块到指定位置 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CScrollV::MovingThumb( const class="type">int pos= WRONG_VALUE ) { class=class="str">"cmt">//--- 如果不需要滚动条, 离开 if (m_items_total<=m_visible_items_total) class="kw">return; class=class="str">"cmt">// --- 检查滑块位置 class="type">uint check_pos= class="num">0; class=class="str">"cmt">//--- 若超出范围则调整位置 if (pos< class="num">0 || pos>m_items_total-m_visible_items_total) check_pos=m_items_total-m_visible_items_total; else check_pos=pos; class=class="str">"cmt">//--- 保存滑块位置 CScroll::CurrentPos(check_pos); class=class="str">"cmt">//--- 计算并设置滚动条滑块 Y 坐标 CalculateThumbY(); }
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 管理垂直滚动条的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CScrollV : class="kw">public CScroll { class="kw">public: class=class="str">"cmt">//--- 移动滑块到指定位置 class="type">void MovingThumb( const class="type">int pos= WRONG_VALUE ); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 移动滑块到指定位置 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CScrollV::MovingThumb( const class="type">int pos= WRONG_VALUE ) { class=class="str">"cmt">//--- 如果不需要滚动条, 离开 if (m_items_total<=m_visible_items_total) class="kw">return; class=class="str">"cmt">// --- 检查滑块位置 class="type">uint check_pos= class="num">0; class=class="str">"cmt">//--- 若超出范围则调整位置 if (pos< class="num">0 || pos>m_items_total-m_visible_items_total) check_pos=m_items_total-m_visible_items_total; else check_pos=pos; class=class="str">"cmt">//--- 保存滑块位置 CScroll::CurrentPos(check_pos); class=class="str">"cmt">//--- 计算并设置滚动条滑块 Y 坐标 CalculateThumbY(); }
「列表类的私有辅助与重构清除逻辑」
CListView 把重复出现两次以上的坐标与尺寸计算收进三个私有方法,每个只占用一行调用:CalculationItemY 按前一项底边减 1 算出当前项 Y 坐标,CalculationItemsWidth 在有纵向滚动条时扣掉滚动条宽度,CalculationYSize 用「单项高度×可见数-(可见数-1)+2」得到列表总高。
class="type">int CListView::CalculationItemY( const class="type">int item_index= class="num">0 ) { class="kw">return ((item_index> class="num">0 )?m_items[item_index- class="num">1 ].Y2()- class="num">1 : CElementBase::Y()+ class="num">1 ); } class="type">int CListView::CalculationItemsWidth( class="type">void ) { class="kw">return ((m_items_total>m_visible_items_total) ?CElementBase::XSize()-m_scrollv.ScrollWidth()- class="num">1 : CElementBase::XSize()- class="num">2 ); } class="type">int CListView::CalculationYSize( class="type">void ) { class="kw">return (m_item_y_size*m_visible_items_total-(m_visible_items_total- class="num">1 )+ class="num">2 ); }
class="type">void CListView::Clear( class="type">void ) { for ( class="type">int r= class="num">0 ; r<m_visible_items_total; r++) m_items[r].Delete(); CElementBase::FreeObjectsArray(); m_selected_item_text = ""; m_selected_item_index = class="num">0 ; ListSize( class="num">0 ); m_scrollv.Hide(); m_scrollv.MovingThumb( class="num">0 ); m_scrollv.ChangeThumbSize(m_items_total,m_visible_items_total); CElementBase::AddToArray(m_area); }
class="type">void CListView::Rebuilding( const class="type">int items_total, const class="type">int visible_items_total) { Clear(); ListSize(items_total); VisibleListSize(visible_items_total); class="type">int y_size=CalculationYSize(); if (y_size!=CElementBase::YSize()) { m_area.YSize(y_size); m_area.Y_Size(y_size); CElementBase::YSize(y_size); } m_scrollv.ChangeThumbSize(m_items_total,m_visible_items_total); m_scrollv.ChangeYSize(y_size); CreateList(); if (m_items_total>m_visible_items_total) { if (CElementBase::IsVisible()) m_scrollv.Show(); } }
列表视图的增项与滚动实现
在 MT5 自定义控件里,CListView 承担纯文本列表的呈现。往列表塞数据靠 AddItem(),它先按 ItemsTotal() 取当前数量,把内部数组 m_item_total 扩 1,再用 ArrayResize 把 m_item_value 拉大,把新字符串写进末尾下标。 若总项数已超过可视上限 m_visible_items_total,就走滚动逻辑:调 m_scrollv.ChangeThumbSize 重算滑块比例,父可见时 Show() 出垂直滚动条;随后把可见项的宽度统一成列表宽减滚动条宽再减 1,避免文字压到滚动条上。 不足一屏时走另一分支:算坐标、CreateItem 建对象、HighlightSelectedItem 高亮,且当 array_size==1 时把首项错误文本存进 m_selected_item_text,这一细节容易在复用代码时漏掉。 Scrolling() 负责程序化滚动,唯一参数 pos 缺省 WRONG_VALUE,意为滚到末位。函数先判 m_items_total<=m_visible_items_total 就直接 return,无滚动条不干活;last_pos_index 固定为总数减可见数,pos<0 或越界都夹到末位,再 MovingThumb+UpdateList 完成位移。 外汇与贵金属 GUI ea 开发属高风险领域,这类控件逻辑错误只会造成显示异常,不会直接下单,但回测时若列表越界仍可能拖慢 tick 处理。CBheckBoxList 已内置类似滚动,可对照移植。
class CListView : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 添加项目至列表 class="type">void AddItem(const class="type">class="kw">string value=""); }; class="type">void CListView::AddItem(const class="type">class="kw">string value="") { class="type">int array_size=ItemsTotal(); m_items_total=array_size+class="num">1; ::ArrayResize(m_item_value,m_items_total); m_item_value[array_size]=value; if(m_items_total>m_visible_items_total) { m_scrollv.ChangeThumbSize(m_items_total,m_visible_items_total); if(CElementBase::IsVisible()) m_scrollv.Show(); if(m_visible_items_total<class="num">1) class="kw">return; class="type">int width=CElementBase::XSize()-m_scrollv.ScrollWidth()-class="num">1; if(width==m_items[class="num">0].XSize()) class="kw">return; for(class="type">int i=class="num">0;i<m_items_total && i<m_visible_items_total;i++) { m_items[i].XSize(width); m_items[i].X_Size(width); } class="kw">return; } class="type">int x=CElementBase::X()+class="num">1; class="type">int y=CalculationItemY(array_size); class="type">int width=CalculationItemsWidth(); CreateItem(array_size,x,y,width); HighlightSelectedItem(); if(array_size==class="num">1) m_selected_item_text=m_item_value[class="num">0]; } class CListView : class="kw">public CElement { class="kw">public: class="type">void Scrolling(const class="type">int pos=WRONG_VALUE); }; class="type">void CListView::Scrolling(const class="type">int pos=WRONG_VALUE) { if(m_items_total<=m_visible_items_total) class="kw">return; class="type">int index=class="num">0; class="type">int last_pos_index=m_items_total-m_visible_items_total; if(pos<class="num">0) index=last_pos_index; else index=(pos>last_pos_index)?last_pos_index:pos; m_scrollv.MovingThumb(index); UpdateList(index); }
◍ 表格类私有方法精简与斑马纹一键化
CTable 这次重构把九类高频重复逻辑收进了私有方法:行数组缩放、单元格省缺初始化、X/Y 轴尺寸与坐标计算、列宽测算、Y 轴尺寸变更。调用关系清晰后,主流程可读性明显提升,且未改动对外行为。
列宽测算 CalculationColumnWidth() 只带一个布尔参,默认 false 算全部列总宽;传 true 时走递归算最后一列宽。之所以分开算,是因为末列右缘常因滚动条存在而不与表格右缘对齐——垂直滚动条出现条件为 m_rows_total > m_visible_rows_total,此时要扣掉 ScrollWidth()。
创建或改宽时触发 ColumnsXResize():先取列宽,再双层循环把每格的 X、X_Distance、XSize、X_Size、XGap 全部重设;若末列则改用 CalculationColumnWidth(true)。方法尾段处理排序箭头——固定表头时索引 +1,且箭头只在 m_is_sorted_column_index 落于可视列范围内才 ShiftSortArrow() 平移。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算列宽 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CTable::CalculationColumnWidth(const class="type">bool is_last_column=false) { class="type">int width=class="num">0; class=class="str">"cmt">//--- 检查垂直滚动条的存在 class="type">bool is_scrollv=m_rows_total>m_visible_rows_total; class=class="str">"cmt">//--- if(!is_last_column) { if(m_visible_columns_total==class="num">1) width=(is_scrollv)?m_x_size-m_scrollv.ScrollWidth() : width=m_x_size-class="num">2; else { if(is_scrollv) width=(m_x_size-m_scrollv.ScrollWidth())/class="type">int(m_visible_columns_total); else width=m_x_size/(class="type">int)m_visible_columns_total+class="num">1; } } else { width=CalculationColumnWidth(); class="type">int last_column=(class="type">int)m_visible_columns_total-class="num">1; class="type">int w=m_x_size-(width*last_column-last_column); width=(is_scrollv)?w-m_scrollv.ScrollWidth()-class="num">1 : w-class="num">2; } class=class="str">"cmt">//--- class="kw">return(width); }
width=0 初始化返回宽度;第6行判断总行是否超可视行决定有无垂直滚动条;第8–15行非末列分支,单列时扣滚动条宽或减2,多列时均分并+1防舍入丢失;第17–22行末列分支先递归取普通列宽,再用总宽减前n-1列占用得末列宽,滚动条存在再减1。
斑马纹以前要逐格 CellColor() 涂色,现在控件创建前调一次 IsZebraFormatRows(第二种颜色) 即可。ZebraFormatRows() 里若 m_is_zebra_format_rows==clrNONE 直接退出;固定首行时 r==0 跳过,偶数行用斑马色奇数行用省缺色,否则反转。外汇与贵金属图表上挂此类自定义表格控件属高风险操作,参数误设可能导致面板绘制异常。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 表格格式化为斑马样式 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTable::ZebraFormatRows(class="type">void) { class=class="str">"cmt">//--- 如果此禁用模式, 离开 if(m_is_zebra_format_rows==clrNONE) class="kw">return; class=class="str">"cmt">//--- 省缺颜色 class="type">class="kw">color clr=m_cell_color; class=class="str">"cmt">//--- for(class="type">uint c=class="num">0;c<m_columns_total;c++) { for(class="type">uint r=class="num">0;r<m_rows_total;r++) { if(m_fix_first_row) { if(r==class="num">0) class="kw">continue; class=class="str">"cmt">//--- clr=(r%class="num">2==class="num">0)?m_is_zebra_format_rows : m_cell_color; } else clr=(r%class="num">2==class="num">0)?m_cell_color : m_is_zebra_format_rows; class=class="str">"cmt">//--- 在公共数组里保存单元背景色 m_vcolumns[c].m_cell_color[r]=clr; } } }
m_vcolumns[c].m_cell_color[r] 供绘制读取。剩余几个私有方法逻辑平铺,无递归或跨类依赖,开 MT5 按头列表对照源码即可验证。
class CTable : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 调整行数组大小 class="type">void RowResize(const class="type">uint column_index,const class="type">uint new_size); class=class="str">"cmt">//--- 使用省缺值初始化单元格 class="type">void CellInitialize(const class="type">uint column_index,const class="type">int row_index=WRONG_VALUE); class=class="str">"cmt">//--- 计算沿 X 轴的表格大小 class="type">int CalculationXSize(class="type">void); class=class="str">"cmt">//--- 计算沿 Y 轴的表格大小 class="type">int CalculationYSize(class="type">void); class=class="str">"cmt">//--- 计算单元格的 X 坐标 class="type">int CalculationCellX(const class="type">int column_index=class="num">0); class=class="str">"cmt">//--- 计算单元格的 Y 坐标 class="type">int CalculationCellY(const class="type">int row_index=class="num">0); class=class="str">"cmt">//--- 计算列宽 class="type">int CalculationColumnWidth(const class="type">bool is_last_column=false); class=class="str">"cmt">//--- 计算列宽 class="type">void ColumnsXResize(class="type">void); class=class="str">"cmt">//--- 更改沿 Y 轴的表格大小 class="type">void YResize(class="type">void); }; class="type">int CTable::CalculationColumnWidth(const class="type">bool is_last_column=false) { class="type">int width=class="num">0; class="type">bool is_scrollv=m_rows_total>m_visible_rows_total; if(!is_last_column) { if(m_visible_columns_total==class="num">1) width=(is_scrollv)?m_x_size-m_scrollv.ScrollWidth() : width=m_x_size-class="num">2; else { if(is_scrollv) width=(m_x_size-m_scrollv.ScrollWidth())/class="type">int(m_visible_columns_total); else width=m_x_size/(class="type">int)m_visible_columns_total+class="num">1; } } else { width=CalculationColumnWidth(); class="type">int last_column=(class="type">int)m_visible_columns_total-class="num">1; class="type">int w=m_x_size-(width*last_column-last_column); width=(is_scrollv)?w-m_scrollv.ScrollWidth()-class="num">1 : w-class="num">2; } class="kw">return(width); } class="type">void CTable::ColumnsXResize(class="type">void) { class="type">int width=CalculationColumnWidth(); for(class="type">uint c=class="num">0;c<m_columns_total && c<m_visible_columns_total;c++) { class="type">int x=CalculationCellX(c); if(c+class="num">1>=m_visible_columns_total) width=CalculationColumnWidth(true); for(class="type">uint r=class="num">0;r<m_rows_total && r<m_visible_rows_total;r++) { m_columns[c].m_rows[r].X(x); m_columns[c].m_rows[r].X_Distance(x); m_columns[c].m_rows[r].XSize(width); m_columns[c].m_rows[r].X_Size(width); m_columns[c].m_rows[r].XGap(CalculateXGap(x)); } } if(m_is_sorted_column_index==WRONG_VALUE) class="kw">return; class="type">int l=(m_fix_first_column)?class="num">1:class="num">0; class="type">int h=m_scrollh.CurrentPos()+l; if(m_is_sorted_column_index>=h && m_is_sorted_column_index<(class="type">int)m_visible_columns_total) { ShiftSortArrow(m_is_sorted_column_index); } } class CTable : class="kw">public CElement { class="kw">private: class="type">class="kw">color m_is_zebra_format_rows; class="kw">public: class="type">void IsZebraFormatRows(const class="type">class="kw">color clr) { m_is_zebra_format_rows=clr; } }; class CTable : class="kw">public CElement { class="kw">private: class="type">void ZebraFormatRows(class="type">void); }; class="type">void CTable::ZebraFormatRows(class="type">void) { if(m_is_zebra_format_rows==clrNONE) class="kw">return; class="type">class="kw">color clr=m_cell_color; for(class="type">uint c=class="num">0;c<m_columns_total;c++) { for(class="type">uint r=class="num">0;r<m_rows_total;r++) { if(m_fix_first_row) { if(r==class="num">0) class="kw">continue; clr=(r%class="num">2==class="num">0)?m_is_zebra_format_rows : m_cell_color; } else clr=(r%class="num">2==class="num">0)?m_cell_color : m_is_zebra_format_rows; m_vcolumns[c].m_cell_color[r]=clr; } } }