图形界面 X: 高级列表和表格管理。代码优化 (集成构建 7)·综合运用
🧩

图形界面 X: 高级列表和表格管理。代码优化 (集成构建 7)·综合运用

(3/3)·从滚动条到 CTable 的可编程管理,把界面函数库优化成易读易扩的实战体系

进阶 第 3/3 篇
很多交易者写了 MQL5 面板后,列表和表格的属性只能靠手动调,运行时改不了行高列宽就卡壳。其实给控件加几个编程接口,EA 就能在跑的时候自己重排界面,省掉反复编译的麻烦。

CTable 可编程控件的几个核心方法

在这次函数库更新里,只有 CTable 这一种表格类型拿到了可编程控制能力。对外暴露的公共方法覆盖了重构表格、增列、增行、清表,以及水平和垂直滚动,相当于把表格的骨架和视口都交给了代码去摆弄。 重建表格必须走 CTable::Rebuilding(),四个参数分别是列总数、可见列数、行总数、可见行数。方法一进来先 Clear() 清掉旧结构,再按参数重设数组大小和滚动条缩略比例;当 rows_total 大于 visible_rows_total 时垂直滚动条才会出现,columns_total 大于 visible_columns_total 时水平滚动条才会出现,这个阈值判断直接决定了视口里要不要留出滚动条宽度。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 重构表格                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTable::Rebuilding(const class="type">int columns_total, const class="type">int visible_columns_total, const class="type">int rows_total, const class="type">int visible_rows_total)
  {
class=class="str">"cmt">//--- 清除表格
   Clear();
class=class="str">"cmt">//--- 设置表格和其可见部分大小
   TableSize(columns_total,rows_total);
   VisibleTableSize(visible_columns_total,visible_rows_total);
class=class="str">"cmt">//--- 调整滚动条大小
   m_scrollv.ChangeThumbSize(rows_total,visible_rows_total);
   m_scrollh.ChangeThumbSize(columns_total,visible_columns_total);
class=class="str">"cmt">//---检查垂直滚动条是否存在
   class="type">bool is_scrollv=m_rows_total>m_visible_rows_total;
class=class="str">"cmt">//--- 检查水平滚动条是否存在
   class="type">bool is_scrollh=m_columns_total>m_visible_columns_total;
class=class="str">"cmt">//--- 计算沿 Y 轴的表格大小
   class="type">int y_size=CalculationYSize();
class=class="str">"cmt">//--- 改变垂直滚动条大小
   m_scrollv.ChangeYSize(y_size);
class=class="str">"cmt">//--- 改变表格大小
   m_y_size=(is_scrollh)?y_size+m_scrollh.ScrollWidth()-class="num">1 : y_size;
   m_area.YSize(m_y_size);
   m_area.Y_Size(m_y_size);
class=class="str">"cmt">//--- 调整水平滚动条沿 Y 轴的位置
   m_scrollh.YDistance(CElementBase::Y2()-m_scrollh.ScrollWidth());
class=class="str">"cmt">//--- 如果需要水平滚动条
   if(is_scrollh)
     {
      class=class="str">"cmt">//--- 根据垂直滚动条的存在设置大小
      if(!is_scrollv)
        m_scrollh.ChangeXSize(m_x_size);
      else
        {
         class=class="str">"cmt">//--- 计算和更改水平滚动条的宽度
         class="type">int x_size=m_area.XSize()-m_scrollh.ScrollWidth()+class="num">1;
         m_scrollh.ChangeXSize(x_size);
        }
     }
class=class="str">"cmt">//--- 创建表格单元
   CreateCells();
class=class="str">"cmt">//--- 若有必要, 显示滚动条
   if(rows_total>visible_rows_total)
     {
      if(CElementBase::IsVisible())
        m_scrollv.Show();
     }
   if(columns_total>visible_columns_total)
     {
      if(CElementBase::IsVisible())
        m_scrollh.Show();
     }
  }
逐行看这段:Clear() 先拆干净;TableSize 和 VisibleTableSize 分开管总维度和视口维度;两个 ChangeThumbSize 把滚动条滑块长度按“总数/可见数”比例算好;is_scrollv 与 is_scrollh 是两个布尔开关,后面表格高度让不让出滚动条宽度全看它们。CreateCells() 在末尾才生成具体单元,所以重建过程是先定框架再填内容。 AddColumn() 的逻辑和 AddRow() 高度对称,所以只拆一列的加法就够了。数组先 ArrayResize 加一位,CellInitialize 用默认值铺单元;若新列总数超过了可见额度,就走 YResize() 让出水平滚动条空间并直接显示滚动条,否则按 CalculationColumnWidth 算宽、逐个 CreateCell 画对象,最后 ZebraFormatRows() 做斑马纹并 UpdateTable() 刷新。
MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在表格里添加一列                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTable::AddColumn(class="type">void)
  {
class=class="str">"cmt">//--- 数组大小递增一个元素
   class="type">uint array_size=ColumnsTotal();
   m_columns_total=array_size+class="num">1;
   ::ArrayResize(m_vcolumns,m_columns_total);
class=class="str">"cmt">//--- 设置行数组大小
   RowResize(array_size,m_rows_total);
class=class="str">"cmt">//--- 以省缺值初始化数组
   CellInitialize(array_size);
class=class="str">"cmt">//--- 如果列总数超过可见额度
   if(m_columns_total>m_visible_columns_total)
     {
      class=class="str">"cmt">//--- 调整表格沿 Y 轴的大小
      YResize();
      class=class="str">"cmt">//--- 如果没有垂直滚动条, 令水平滚动条占据表格的全宽
      if(m_rows_total<=m_visible_rows_total)
        m_scrollh.ChangeXSize(m_x_size);
      class=class="str">"cmt">//--- 调整滑块大小并显示滚动条
      m_scrollh.ChangeThumbSize(m_columns_total,m_visible_columns_total);
      class=class="str">"cmt">//--- 显示滚动条
      if(CElementBase::IsVisible())
        m_scrollh.Show();
      class=class="str">"cmt">//--- 将行格式化为斑马样式
      ZebraFormatRows();
      class=class="str">"cmt">//--- 刷新表格
      UpdateTable();
      class="kw">return;
     }
class=class="str">"cmt">//--- 计算列宽
   class="type">int width=CalculationColumnWidth();
class=class="str">"cmt">//--- 调整最后一列宽度
   if(m_columns_total>=m_visible_columns_total)
      width=CalculationColumnWidth(true);
class=class="str">"cmt">//--- 计算 X 坐标
   class="type">int x=CalculationCellX(array_size);
class=class="str">"cmt">//--- 
   for(class="type">uint r=class="num">0; r<m_rows_total && r<m_visible_rows_total; r++)
     {
      class=class="str">"cmt">//--- 计算 Y 坐标
      class="type">int y=CalculationCellY(r);
      class=class="str">"cmt">//--- 创建对象
      CreateCell(array_size,r,x,y,width);
      class=class="str">"cmt">//--- 为表头设置相应的颜色
      if(m_fix_first_row && r==class="num">0)
        m_columns[array_size].m_rows[r].BackColor(m_headers_color);
     }
class=class="str">"cmt">//--- 将行格式化为斑马样式
   ZebraFormatRows();
class=class="str">"cmt">//--- 刷新表格
   UpdateTable();
  }
注意 for 循环里 r<m_visible_rows_total 这个上限:不可见行根本不创建图形对象,UI 开销被压在视口内。表头着色靠 m_fix_first_row 和 r==0 判定,说明固定首行是单独开关而非默认行为。 垂直和水平滚动方法算法跟列表控件的实现一致,原文没贴代码,附库文件里能翻到。开 MT5 把这几个方法挂到自定义面板,改 Rebuilding 的 visible 参数就能直观看到滚动条阈值切换。

MQL5 / C++
class CTable : class="kw">public CElement
  {
 class="kw">public:
   class=class="str">"cmt">//--- 重构表格
   class="type">void               Rebuilding(const class="type">int columns_total, const class="type">int visible_columns_total, const class="type">int rows_total, const class="type">int visible_rows_total);
   class=class="str">"cmt">//--- 在表格里添加一列
   class="type">void               AddColumn(class="type">void);
   class=class="str">"cmt">//--- 在表格里添加一行
   class="type">void               AddRow(class="type">void);
   class=class="str">"cmt">//--- 清除表格 (删除所有行和列)
   class="type">void               Clear(class="type">void);
   class=class="str">"cmt">//--- 表格滚动: (class="num">1) 垂直以及 (class="num">2) 水平
   class="type">void               VerticalScrolling(const class="type">int pos=WRONG_VALUE);
   class="type">void               HorizontalScrolling(const class="type">int pos=WRONG_VALUE);
  };

「用定时器驱动表格与列表的渐进填充」

做一个验证用的 MQL 应用,图形界面开两个选项卡:第一个放 CTable 表格加六个控件(清除/重构按钮,行总数、列总数、可视行数、可视列数编辑框),第二个放列表视图和复选框列表,同样用清除/重构按钮加项目总数、可见项目数编辑框控制,附带日历和时间下拉控件。这样能在 MT5 里直接看到控件类方法怎么跑。 顺带提一个对界面开发很有用的 CTimeCounter 类,它只用三个字段(步长 m_step、间隔 m_pause、计数器 m_time_counter)和两个方法管重绘频率。构造时默认 m_step=16 毫秒、m_pause=1000 毫秒,SetParameters() 可改步长和间隔,CheckTimeCounter() 每被调用一次若未到间隔就累加步长返回 false,到了就清零返回 true。 库文件路径在这版变了:控件类在 Include\EasyAndFastGUI\Controls,其余移到 Include\EasyAndFastGUI 根目录。自定义文件里要写 #include <EasyAndFastGUI\WndEvents.mqh> 和 #include <EasyAndFastGUI\TimeCounter.mqh> 才能用。示例里建了两个计数器,m_counter1 设 16/500 管状态条,m_counter2 设 16/150 管列表表格刷新。 定时器事件里用 m_counter2.CheckTimeCounter() 卡节奏:每过 150 毫秒,若表格行数少于编辑框值就 AddRow(),列少就 AddColumn();列表视图和复选框列表项数不够就 AddItem("SYMBOL "+数量) 并 Scrolling() 把滑块推到末尾,最后 m_chart.Redraw()。外汇与贵金属 GUI 自动化测试涉及实盘逻辑,高风险,参数仅作界面验证用。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                 TimeCounter.mqh |
class=class="str">"cmt">//|                        版权所有 class="num">2016, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                              [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 定时计数                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTimeCounter
  {
class="kw">private:
   class=class="str">"cmt">//--- 计数器步长
   class="type">uint              m_step;
   class=class="str">"cmt">//--- 定时间隔
   class="type">uint              m_pause;
   class=class="str">"cmt">//--- 定时计数器
   class="type">uint              m_time_counter;
   class=class="str">"cmt">//---
class="kw">public:
                     CTimeCounter(class="type">void);
                    ~CTimeCounter(class="type">void);
   class=class="str">"cmt">//--- 设置步长和定时间隔
   class="type">void              SetParameters(const class="type">uint step, const class="type">uint pause);
   class=class="str">"cmt">//--- 检查指定的时间间隔是否已过
   class="type">bool              CheckTimeCounter(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造器                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTimeCounter::CTimeCounter(class="type">void) : m_step(class="num">16),
                                   m_pause(class="num">1000),
                                   m_time_counter(class="num">0)
                                   
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构器                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTimeCounter::~CTimeCounter(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置步长和定时间隔                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTimeCounter::SetParameters(const class="type">uint step, const class="type">uint pause)
  {
   m_step  =step;
   m_pause =pause;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查是否指定的时间间隔已过                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTimeCounter::CheckTimeCounter(class="type">void)
  {
class=class="str">"cmt">//--- 如果指定时间间隔未过, 则增加计数器
   if(m_time_counter<m_pause)
     {
      m_time_counter+=m_step;
       class="kw">return(false);
     }
class=class="str">"cmt">//--- 计数器清零
   m_time_counter=class="num">0;
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                      Program.mqh |
class=class="str">"cmt">//|                        版权所有 class="num">2016, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                              [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <EasyAndFastGUI\WndEvents.mqh>
class="macro">#include <EasyAndFastGUI\TimeCounter.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建应用程序类                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CProgram : class="kw">public CWndEvents
  {
class="kw">protected:
   class=class="str">"cmt">//--- 定时计数器
   CTimeCounter      m_counter1; class=class="str">"cmt">// 用于更新状态条
   CTimeCounter      m_counter2; class=class="str">"cmt">// 用于更新列表和表格
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造器                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CProgram::CProgram(class="type">void)
  {
class=class="str">"cmt">//--- 设置定时计数器参数
   m_counter1.SetParameters(class="num">16,class="num">500);
   m_counter2.SetParameters(class="num">16,class="num">150);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 定时器                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnTimerEvent(class="type">void)
  {
   CWndEvents::OnTimerEvent();
   ...
class=class="str">"cmt">//--- 在控件刷新之间的暂停
   if(m_counter2.CheckTimeCounter())
     {
      class=class="str">"cmt">//--- 如果总数小于指定, 在表格里添加一行
      if(m_table.RowsTotal()<m_spin_edit1.GetValue())
         m_table.AddRow();
      class=class="str">"cmt">//--- 如果总数小于指定, 在表格里添加一列
      if(m_table.ColumnsTotal()<m_spin_edit2.GetValue())
         m_table.AddColumn();
      class=class="str">"cmt">//--- 如果总数小于指定, 在列表视图里添加一项
      if(m_listview.ItemsTotal()<m_spin_edit5.GetValue())
        {
         m_listview.AddItem("SYMBOL " + class="type">class="kw">string(m_listview.ItemsTotal()));
         class=class="str">"cmt">//--- 将滚动条滑块移到列表末尾
         m_listview.Scrolling();
        }
      class=class="str">"cmt">//--- 如果总数小于指定, 在复选框列表里添加一项
      if(m_checkbox_list.ItemsTotal()<m_spin_edit5.GetValue())
        {
         m_checkbox_list.AddItem("Checkbox " + class="type">class="kw">string(m_checkbox_list.ItemsTotal()));
         class=class="str">"cmt">//--- 将滚动条滑块移到列表末尾
         m_checkbox_list.Scrolling();
        }
      class=class="str">"cmt">//--- 图表重绘
      m_chart.Redraw();
     }
  }

◍ 用图表事件驱动表格与列表重建

在 MT5 自定义指标或 EA 里,想让界面控件随用户点击实时变化,核心就是重写 OnEvent 并拦截 CHARTEVENT_CUSTOM 偏移量。上面这段处理器把四个按钮的 lparam 与控件 Id() 比对,分别走清空或重建逻辑,读者可直接拷进自己的 CProgram 类里改条件验证。 第二个按钮触发时,表格会按四个 SpinEdit 的当前值重构:m_table.Rebuilding 接收行、列及两个偏移参数,随后 InitializeTable 与 UpdateTable 必须顺序调用,否则界面不会刷新。漏掉 UpdateTable 是新手最常卡住的点,图表上看起来像没反应。 第四个按钮则同时重建 CheckboxList 和 ListView,并硬性选中第 8 项(索引 7)。循环里用 i%2!=0 决定复选框勾选状态,也就是奇数行勾上、偶数行空着——这种规律填充在批量生成品种监视列表时很实用,贵金属与外汇符号都可套用。 外汇与贵金属界面工具开发属高风险环境,参数误写可能导致图表卡顿或客户端无响应,建议在策略测试器外的独立图表先跑通再上实盘。

MQL5 / C++
class="type">void CProgram::OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- 按钮按下事件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      Print(__FUNCTION__," > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
      class=class="str">"cmt">//--- 第一个按钮事件
      if(lparam==m_simple_button1.Id())
        {
         class=class="str">"cmt">//--- 清除表格
         m_table.Clear();
         class="kw">return;
        }
      class=class="str">"cmt">//--- 第二个按钮事件
      if(lparam==m_simple_button2.Id())
        {
         class=class="str">"cmt">//--- 重构表格
         m_table.Rebuilding((class="type">int)m_spin_edit3.GetValue(),(class="type">int)m_spin_edit4.GetValue(),
                            (class="type">int)m_spin_edit1.GetValue(),(class="type">int)m_spin_edit2.GetValue());
         class=class="str">"cmt">//--- 表格初始化
         InitializeTable();
         class=class="str">"cmt">//--- 刷新表格以便显示变化
         m_table.UpdateTable();
         class="kw">return;
        }
      class=class="str">"cmt">//--- 第三个按钮事件
      if(lparam==m_simple_button3.Id())
        {
         class=class="str">"cmt">//--- 清除列表
         m_listview.Clear();
         m_checkbox_list.Clear();
         class="kw">return;
        }
      class=class="str">"cmt">//--- 第二个按钮事件
      if(lparam==m_simple_button4.Id())
        {
         class=class="str">"cmt">//--- 重构列表
         m_checkbox_list.Rebuilding((class="type">int)m_spin_edit5.GetValue(),(class="type">int)m_spin_edit6.GetValue());
         m_listview.Rebuilding((class="type">int)m_spin_edit5.GetValue(),(class="type">int)m_spin_edit6.GetValue());
         class=class="str">"cmt">//--- 在列表视图中选择第八个项目
         m_listview.SelectItem(class="num">7);
         class=class="str">"cmt">//--- 用数据填充列表视图
         class="type">int items_total=m_listview.ItemsTotal();
         for(class="type">int i=class="num">0;i<items_total;i++)
            m_listview.SetItemValue(i,"SYMBOL "+class="type">class="kw">string(i));
         class=class="str">"cmt">//--- 用数据填充复选框列表, 每隔一秒勾选复选框
         items_total=m_checkbox_list.ItemsTotal();
         for(class="type">int i=class="num">0;i<items_total;i++)
           {
            m_checkbox_list.SetItemValue(i,"Checkbox "+class="type">class="kw">string(i));
            m_checkbox_list.SetItemState(i,(i%class="num">2!=class="num">0)?true:false);
           }
         class=class="str">"cmt">//---
         class="kw">return;
        }
      class=class="str">"cmt">//---
      class="kw">return;
     }
  }

把工具请下神坛

CTable 类在最近一次更新里补齐了列与行的动态管理:DeleteColumn() 和 DeleteRow() 重新回到接口,配合 IsSortedColumnIndex() 可以确认当前排序列,避免手动维护顺序时踩坑。下面这段代码截取自更新后的类声明,逐行看就是控件该有的样子。 //--- (1) 当前排序方向,(2) 已排序数组索引 int IsSortDirection(void) const { return(m_last_sort_direction); } // 返回上一次排序方向(升/降),外部据此判断表状态 int IsSortedColumnIndex(void) const { return(m_is_sorted_column_index); } // 返回正在排序的列索引,未排序时返回-1一类初始值 //--- 表重建 void Rebuilding(const int columns_total,const int rows_total,const bool redraw=false); // 按新行列数重建表,调用后需重设标题与列宽 //--- 在指定索引处向表添加一列 void AddColumn(const int column_index,const bool redraw=false); // 在 column_index 位置插一列,redraw 控制是否立即重绘 //--- 删除表格中指定索引处的一列 void DeleteColumn(const int column_index,const bool redraw=false); // 删列,之前讨论里有人误以为此方法被移除,实际是找错工程目录 //--- 在指定索引处向表添加一行 void AddRow(const int row_index,const bool redraw=false); // 插一行,用于动态日志或报价面板 //--- 按指定索引删除表中的一行 void DeleteRow(const int row_index,const bool redraw=false); // 删行,和 DeleteColumn 对称 //--- 删除所有行 void DeleteAllRows(const bool redraw=false); // 清数据留骨架 //--- 清除表格。只保留一列和一行。 void Clear(const bool redraw=false); // 重置到最小表结构 实盘里把 EasyAndFast 库拖进 EA 做面板,本质只是省掉自己写 CCanvas 的时间。外汇和贵金属波动受杠杆与消息牵引,高风险始终在,界面再顺手也不能替你过滤爆仓概率;下载 zip(约 540 KB)后在 MetaEditor 里编译跑一遍,比看任何说明都来得直接。

MQL5 / C++
  class=class="str">"cmt">//--- (class="num">1) 当前排序方向,(class="num">2) 已排序数组索引
  class="type">int                IsSortDirection(class="type">void)                const { class="kw">return(m_last_sort_direction);    }
  class="type">int                IsSortedColumnIndex(class="type">void)            const { class="kw">return(m_is_sorted_column_index); }
  class=class="str">"cmt">//--- 表重建
  class="type">void               Rebuilding(const class="type">int columns_total,const class="type">int rows_total,const class="type">bool redraw=false);
  class=class="str">"cmt">//--- 在指定索引处向表添加一列
  class="type">void               AddColumn(const class="type">int column_index,const class="type">bool redraw=false);
  class=class="str">"cmt">//--- 删除表格中指定索引处的一列
  class="type">void               DeleteColumn(const class="type">int column_index,const class="type">bool redraw=false);
  class=class="str">"cmt">//--- 在指定索引处向表添加一行
  class="type">void               AddRow(const class="type">int row_index,const class="type">bool redraw=false);
  class=class="str">"cmt">//--- 按指定索引删除表中的一行
  class="type">void               DeleteRow(const class="type">int row_index,const class="type">bool redraw=false);
  class=class="str">"cmt">//--- 删除所有行
  class="type">void               DeleteAllRows(const class="type">bool redraw=false);
  class=class="str">"cmt">//--- 清除表格。只保留一列和一行。
  class="type">void               Clear(const class="type">bool redraw=false);
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到控件状态与异常提示,你专注决策就好。把界面里机械的滚动与重绘交给工具,人力留给策略判断。

常见问题

本篇在 CTable 可编程控制小节补充了对应方法,通过基类暴露的接口在 MQL 应用里直接赋值重绘,无需重启指标。
可以,小布盯盘的 AIGC 模块会扫描品种页上的界面状态,对滚动条失联或表格越界给出提示,减少你手动排错时间。
只需继承新的 CElement 并包含 Element.mqh,原有频繁复用的方法已上移,你写的控件文件不必再贴重复坐标计算代码。
可编程控制滚动条在本文第一节之前的函数库变化里铺垫,完整控件测试见本篇测试控件的应用程序小节及前篇基础。