内容
- 概论
- 鼠标光标在指定画板上的相对坐标
- 表格结构变化
- 确定可视部分的行范围
- 表格单元中的图标
- 悬浮时高亮显示表格行
- 快速重绘表格单元的方法
- 用于测试控件的应用程序
- 结论
概论
首篇文章 图形界面 I: 函数库结构的准备 (第 1 章) 详细研究了这个函数库的作用。您将在每章结尾处找到文章链接的列表。从那里, 您还可以下载当前开发阶段的函数库完整版本。文件必须位于与存档中相同的目录中。
我们继续补充渲染表格 (CCanvasTable) 与新功能。此次将添加以下功能。
- 悬浮时高亮显示表格行。
- 为每个单元格添加一个图标数组的能力以及一种切换它们的方法。
- 在运行时设置并修改单元格中文本的能力。
此外, 代码和某些算法已经优化, 以便更快地重绘表格。
光标在指定画板上的相对坐标
为了消除很多类中的重复代码, 以及计算画板上相对坐标的方法, 已将 CMouse::RelativeX() 和 CMouse::RelativeY() 方法添加到 CMouse 类中以便检索坐标。必须将 CRectCanvas 类型对象的引用传递给这些方法, 以便 参考画板可见部分的当前偏移量 来计算相对坐标。
//+------------------------------------------------------------------+ //| 获取鼠标参数的类 | //+------------------------------------------------------------------+ class CMouse { public: //--- 返回所传递画板对象的鼠标光标相对坐标 int RelativeX(CRectCanvas &object); int RelativeY(CRectCanvas &object); }; //+------------------------------------------------------------------+ //| 返回鼠标光标相对 X 坐标 | //| 来自所传递画板对象 | //+------------------------------------------------------------------+ int CMouse::RelativeX(CRectCanvas &object) { return(m_x-object.X()+(int)object.GetInteger(OBJPROP_XOFFSET)); } //+------------------------------------------------------------------+ //| 返回鼠标光标相对 Y 坐标 | //| 来自所传递画板对象 | //+------------------------------------------------------------------+ int CMouse::RelativeY(CRectCanvas &object) { return(m_y-object.Y()+(int)object.GetInteger(OBJPROP_YOFFSET)); }
在含数库的进一步开发中, 将使用这些方法来获取所有绘制控件的相对坐标。
表格结构变化
为了尽可能优化渲染表格代码的执行效率, 需要对 CTOptions 类型的表格结构稍微进行修改和补充, 并添加允许构建多维数组的新结构。此处的任务是基于以前计算的数值来重新绘制表格的某些片段。例如, 这有可能是表格列和行边界的坐标。
例如, 计算并保存列边框的 X 坐标 仅在 CCanvasTable::DrawGrid() 方法中才合理, 该方法仅在绘制整个表格时用来绘制网格。而当用户选择表格行时, 可以使用预定值。这同样适用于悬浮时表格行时的高亮显示 (这将会在本文中进一步讨论)。
创建一个单独的结构 (CTRowOptions) 并声明其实例的数组 来保存表格行的 Y 坐标, 以及其它未来可能的行属性。行的 Y 坐标是在 CCanvasTable::DrawRows() 方法里进行计算, 设计用于绘制行的背景。由于此方法是在绘制网格之前调用, CCanvasTable::DrawGrid() 方法使用来自 CTRowOptions 结构的预算数值。
创建一个单独的 CTCell 类型结构 用来保存表格单元的属性。CTRowOptions 结构中的实例数组会声明为此类型, 如同一个 表格行数组。此结构将存储:
- 图标数组
- 图标大小的数组
- 单元格中所选 (所显示) 图标的索引
- 完整文本
- 缩写文本
- 文本颜色
由于每个图标都是一组像素的数组, 所以需要一个单独的 结构 (CTImage) 和一个用于存储它们的动态数组。这些结构的代码可以在下面的列表中找到:
class CCanvasTable : public CElement { private: //--- 图标像素的数组 struct CTImage { uint m_image_data[]; }; //--- 表格单元的属性 struct CTCell { CTImage m_images[]; // 图标数组 uint m_image_width[]; // 图标宽度数组 uint m_image_height[]; // 图标高度数组 int m_selected_image; // 所选 (所显示) 图标的索引 string m_full_text; // 完整文本 string m_short_text; // 缩写文本 color m_text_color; // 文本颜色 }; //--- 表格行和列的属性数组 struct CTOptions { int m_x; // 列的左边缘 X 坐标 int m_x2; // 列的右边缘 X 坐标 int m_width; // 列宽 ENUM_ALIGN_MODE m_text_align; // 列单元内的文本对齐模式 int m_text_x_offset; // 文本偏移量 string m_header_text; // 列标题文本 CTCell m_rows[]; // 表格行数组 }; CTOptions m_columns[]; //--- 表格行属性数组 struct CTRowOptions { int m_y; // 行顶边缘 Y 坐标 int m_y2; // 行底边缘 Y 坐标 }; CTRowOptions m_rows[]; };
所用数据类型的全部方法均进行了适当地修改。
确定可视部分的行范围
由于一个表格内也许含有很多行, 所以在某一行上搜索焦点之后重新绘制表可能会显著拖慢进程。这同样适用于选择一行并手动更改列宽时调整文本长度。为了避免延迟, 有必要确定表格的可视部分中第一个和最后一个的索引, 并分配一个循环, 在该范围内进行迭代。CCanvasTable::VisibleTableIndexes() 方法即是为此目的而实现的。它首先确定可视部分的边界。上边界是可视部分沿 Y 轴的偏移, 而 下边界定义为上边界 + 可视部分沿 Y 轴的尺寸。
现在, 将所获得的边界值除以表格设置中定义的行高, 即足以确定可视部分的顶行和底行的索引。在最后一列超出范围的情况下, 在方法结束时进行调整。
class CCanvasTable : public CElement { private: //--- 用于确定表格可视部分的索引 int m_visible_table_from_index; int m_visible_table_to_index; //--- private: //--- 确定表格可视部分的索引 void VisibleTableIndexes(void); }; //+------------------------------------------------------------------+ //| 构造器 | //+------------------------------------------------------------------+ CCanvasTable::CCanvasTable(void) : m_visible_table_from_index(WRONG_VALUE), m_visible_table_to_index(WRONG_VALUE) { ... } //+------------------------------------------------------------------+ //| 确定表格可视部分的索引 | //+------------------------------------------------------------------+ void CCanvasTable::VisibleTableIndexes(void) { //--- 确定边界并考虑到表格可视部分的偏移 int yoffset1 =(int)m_table.GetInteger(OBJPROP_YOFFSET); int yoffset2 =yoffset1+m_table_visible_y_size; //--- 确定表格可视部分的第一个和最后一个索引 m_visible_table_from_index =int(double(yoffset1/m_cell_y_size)); m_visible_table_to_index =int(double(yoffset2/m_cell_y_size)); //--- 如果未超出范围, 低端索引加一 m_visible_table_to_index=(m_visible_table_to_index+1>m_rows_total)? m_rows_total : m_visible_table_to_index+1; }
索引将在 CCanvasTable::DrawTable() 方法里确定。可为方法传递一个 参数 来指定必须 只重新绘制表格的可视部分。参数的省缺值是 false, 即 表示重绘整个表格。下面的代码列表展示了此方法的缩写版本。
//+------------------------------------------------------------------+ //| 绘制表格 | //+------------------------------------------------------------------+ void CCanvasTable::DrawTable(const bool only_visible=false) { //--- If not indicated to redraw only the visible part of the table if(!only_visible) { //--- 为整个表格设置从开始到结束的行索引 m_visible_table_from_index =0; m_visible_table_to_index =m_rows_total; } //--- 获取表格可视部分的行索引 else VisibleTableIndexes(); //--- 绘制表格行背景 //--- 绘制所选行 //--- 绘制网格 //--- 绘制图标 //--- 绘制文本 //--- 显示最后绘制的变化 //--- 如果启用, 更新标题 //--- 相对于滚动条调整表格 }
在确定表格行上的焦点方法中也需要 调用 CCanvasTable::VisibleTableIndexes():
//+------------------------------------------------------------------+ //| 检查表格行上的焦点 | //+------------------------------------------------------------------+ int CCanvasTable::CheckRowFocus(void) { int item_index_focus=WRONG_VALUE; //--- 获取鼠标光标之下的相对 Y 坐标 int y=m_mouse.RelativeY(m_table); ///--- 获取表格局部区域的索引 VisibleTableIndexes(); //--- 搜索焦点 for(int i=m_visible_table_from_index; i<m_visible_table_to_index; i++) { //--- 如果行焦点已改变 if(y>m_rows[i].m_y && y<=m_rows[i].m_y2) { item_index_focus=i; break; } } //--- 返回焦点行的索引 return(item_index_focus); }
表格单元中的图标
可以为每个单元分配多个图标, 并可在程序运行期间进行切换。添加了用于设置图标自单元顶部和左边缘偏移量的字段和方法:
class CCanvasTable : public CElement { private: //--- 图标自单元边缘的偏移量 int m_image_x_offset; int m_image_y_offset; //--- public: //--- 图标自单元边缘的偏移量 void ImageXOffset(const int x_offset) { m_image_x_offset=x_offset; } void ImageYOffset(const int y_offset) { m_image_y_offset=y_offset; } };
若要将图标分配给指定的单元, 必须传递它们的终端本地目录路径。在此之前, 它们必须作为资源 (#resource) 包含在 MQL 应用程序中。CCanvasTable::SetImages() 方法即是为此设计的。在此, 如果传递了一个空数组, 或者检测到数组超限, 则程序离开方法。
如果检查通过, 则调整单元数组的大小。之后, 循环使用 ::ResourceReadImage() 方法将图标内容读取到一维数组, 将每个像素的颜色存储到数组中。图标大小存储到相应的数组。需要分配循环将图标绘制到画板上。省缺情况下, 将在单元格中选择数组的第一个图标。
class CCanvasTable : public CElement { public: //--- 为指定单元格设置图标 void SetImages(const uint column_index,const uint row_index,const string &bmp_file_path[]); }; //+------------------------------------------------------------------+ //| 为指定单元格设置图标 | //+------------------------------------------------------------------+ void CCanvasTable::SetImages(const uint column_index,const uint row_index,const string &bmp_file_path[]) { int total=0; //--- 如果所传递的数组大小为零, 离开 if((total=CheckArraySize(bmp_file_path))==WRONG_VALUE) return; //--- 检查是否超出数组范围 if(!CheckOutOfRange(column_index,row_index)) return; //--- 调整数组大小 ::ArrayResize(m_columns[column_index].m_rows[row_index].m_images,total); ::ArrayResize(m_columns[column_index].m_rows[row_index].m_image_width,total); ::ArrayResize(m_columns[column_index].m_rows[row_index].m_image_height,total); //--- for(int i=0; i<total; i++) { //--- 省缺选择数组的第一个图标 m_columns[column_index].m_rows[row_index].m_selected_image=0; //--- 将传递的图标写入数组并存储其大小 if(!ResourceReadImage(bmp_file_path[i],m_columns[column_index].m_rows[row_index].m_images[i].m_image_data, m_columns[column_index].m_rows[row_index].m_image_width[i], m_columns[column_index].m_rows[row_index].m_image_height[i])) { Print(__FUNCTION__," > 错误: ",GetLastError()); return; } } }
若要查找特定单元格有多少图标, 请使用 CCanvasTable::ImagesTotal() 方法:
class CCanvasTable : public CElement { public: //--- 返回指定单元格中图标的总数 int ImagesTotal(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| 返回指定单元格中图标的总数 | //+------------------------------------------------------------------+ int CCanvasTable::ImagesTotal(const uint column_index,const uint row_index) { //--- 检查是否超出数组范围 if(!CheckOutOfRange(column_index,row_index)) return(WRONG_VALUE); //--- 返回图标数组的大小 return(::ArraySize(m_columns[column_index].m_rows[row_index].m_images)); }
现在研究用来绘制图标的方法。首先, 新的 CColors::BlendColors() 方法 已被添加到 CColors 类中, 考虑到重叠图标透明度的情况下这样可以正确混合上、下端颜色。以及用于获取所传递颜色透明度值的辅助 CColors::GetA() 方法。
在 Colors::BlendColors() 方法中, 所传递的颜色首先被分离出 RGB 分量, 并从顶部颜色中提取阿尔法通道。阿尔法通道将被转换为零到一之间的值。如果所传递的颜色不包含透明度, 则不进行混合。在有透明度的情况下, 则 两个所传递颜色的每个分量均会参考顶部颜色的透明度进行混色。之后, 如果它们超出范围 (255), 则调整所获分量的值。
//+------------------------------------------------------------------+ //| 操纵颜色的类 | //+------------------------------------------------------------------+ class CColors { public: double GetA(const color aColor); color BlendColors(const uint lower_color,const uint upper_color); }; //+------------------------------------------------------------------+ //| 获取 A 分量值 | //+------------------------------------------------------------------+ double CColors::GetA(const color aColor) { return(double(uchar((aColor)>>24))); } //+------------------------------------------------------------------+ //| 参考顶部颜色的透明度, 混合两种颜色 | //+------------------------------------------------------------------+ color CColors::BlendColors(const uint lower_color,const uint upper_color) { double r1=0,g1=0,b1=0; double r2=0,g2=0,b2=0,alpha=0; double r3=0,g3=0,b3=0; //--- 以 ARGB 格式转换颜色 uint pixel_color=::ColorToARGB(upper_color); //--- 获取上、下端颜色的分量 ColorToRGB(lower_color,r1,g1,b1); ColorToRGB(pixel_color,r2,g2,b2); //--- 获取的透明度百分比从 0.00 到1.00 alpha=GetA(upper_color)/255.0; //--- 如果有透明度 if(alpha<1.0) { //--- 参考阿尔法通道混合分量 r3=(r1*(1-alpha))+(r2*alpha); g3=(g1*(1-alpha))+(g2*alpha); b3=(b1*(1-alpha))+(b2*alpha); //--- 调整所获值 r3=(r3>255)? 255 : r3; g3=(g3>255)? 255 : g3; b3=(b3>255)? 255 : b3; } else { r3=r2; g3=g2; b3=b2; } //--- 组合所获分量并返回颜色 return(RGBToColor(r3,g3,b3)); }
现在, 编写一个绘制图标的方法很容易。CCanvasTable::DrawImage() 方法的代码展示如下。它必须传递表格单元的索引, 即图标将被绘制之处。在方法伊始, 参考偏移量, 以及所选择单元的索引, 及其大小, 获取图标的坐标。然后, 通过双循环逐个输出图标。如果指定的像素为空 (没有颜色), 则循环将转到下一个像素。若有颜色, 则确定单元格背景颜色和当前像素颜色, 参考重叠颜色的透明度将这两种颜色混合在一起, 并将所得颜色绘制在画板上 。
class CCanvasTable : public CElement { private: //--- 在指定的单元格中绘制一个图标 void DrawImage(const int column_index,const int row_index); }; //+------------------------------------------------------------------+ //| 在指定的单元格中绘制一个图标 | //+------------------------------------------------------------------+ void CCanvasTable::DrawImage(const int column_index,const int row_index) { //--- 计算坐标 int x =m_columns[column_index].m_x+m_image_x_offset; int y =m_rows[row_index].m_y+m_image_y_offset; //--- 在单元格中选择的图标及其大小 int selected_image =m_columns[column_index].m_rows[row_index].m_selected_image; uint image_height =m_columns[column_index].m_rows[row_index].m_image_height[selected_image]; uint image_width =m_columns[column_index].m_rows[row_index].m_image_width[selected_image]; //--- 绘图 for(uint ly=0,i=0; ly<image_height; ly++) { for(uint lx=0; lx<image_width; lx++,i++) { //--- 如果没有颜色, 转到下一个像素 if(m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i]<1) continue; //--- 获取下层 (单元格背景) 的颜色, 以及图标指定像素的颜色 uint background =(row_index==m_selected_item)? m_selected_row_color : m_table.PixelGet(x+lx,y+ly); uint pixel_color =m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i]; //--- 混合颜色 uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color)); //--- 绘制重叠图标的像素 m_table.PixelSet(x+lx,y+ly,foreground); } } }
CCanvasTable::DrawImages() 方法设计用于一次性绘制表格的所有图标, 并考虑到何时有必要仅绘制表格的可视部分 。在当前版本的表格中, 只有当列中的文本与左侧对齐时, 才能绘制图标。此外, 每次迭代检查图标是否分配给单元格, 以及其像素的数组是否为空。如果所有检查都通过, 则调用 CCanvasTable::DrawImage() 方法绘制图标。
class CCanvasTable : public CElement { private: //--- 绘制表格的所有图标 void DrawImages(void); }; //+------------------------------------------------------------------+ //| 绘制表格的所有图标 | //+------------------------------------------------------------------+ void CCanvasTable::DrawImages(void) { //--- 计算坐标 int x=0,y=0; //--- 列 for(int c=0; c<m_columns_total; c++) { //--- 如果文本未与左侧对齐, 转到下一列 if(m_columns[c].m_text_align!=ALIGN_LEFT) continue; //--- 行 for(int r=m_visible_table_from_index; r<m_visible_table_to_index; r++) { //--- 如果这个单元格不包含图标, 转到下一个 if(ImagesTotal(c,r)<1) continue; //--- 单元格中选定的图标 (省缺情况下选择第一个 [0]) int selected_image=m_columns[c].m_rows[r].m_selected_image; //--- 如果像素数组为空, 转到下一个 if(::ArraySize(m_columns[c].m_rows[r].m_images[selected_image].m_image_data)<1) continue; //--- 绘制图标 DrawImage(c,r); } } }
下面的屏幕截图显示一个在单元格中含有图标的表格示例:
图例. 1. 在单元格中含有图标的表格。
悬浮时高亮显示表格行
对于悬浮时要高亮显示的渲染表格行, 将需要额外的字段和方法。使用 CCanvasTable::LightsHover() 方法 来启用高亮模式。行的颜色可在 CCanvasTable::CellColorHover() 方法的帮助下设置。
class CCanvasTable : public CElement { private: //--- 单元格在不同状态时的颜色 color m_cell_color; color m_cell_color_hover; //--- 悬浮时行高亮显示模式 bool m_lights_hover; //--- public: //--- 单元格在不同状态时的颜色 void CellColor(const color clr) { m_cell_color=clr; } void CellColorHover(const color clr) { m_cell_color_hover=clr; } //--- 悬浮时行高亮显示模式 void LightsHover(const bool flag) { m_lights_hover=flag; } };
高亮显示一行不需要在光标移动时重新绘制整个表格。再有, 强烈建议不要这样做, 因为它大大拖慢了应用程序, 占用了太多的 CPU 资源。在鼠标光标第一次/新进入到表格区域时, 只需查找焦点一次就足够了 (遍历整个行数组)。CCanvasTable::CheckRowFocus() 方法即用于此目的。一旦找到焦点并保存行索引后, 只需在移动光标时进行简单检查, 焦点是否在已保存索引的行上发生变化。所描述的算法已在 CCanvasTable::ChangeRowsColor() 方法里实现, 如下面的列表所示。CCanvasTable::RedrawRow() 方法用来修改行的颜色, 其代码稍后介绍。CCanvasTable::ChangeRowsColor() 方法是在 CCanvasTable::ChangeObjectsColor() 方法里调用, 用来修改表格对象的颜色。
class CCanvasTable : public CElement { private: //--- 确定行焦点 int m_item_index_focus; //--- 确定鼠标光标从一行转换到另一行的时刻 int m_prev_item_index_focus; //--- private: //--- 悬停时更改行颜色 void ChangeRowsColor(void); }; //+------------------------------------------------------------------+ //| 悬停时更改行颜色 | //+------------------------------------------------------------------+ void CCanvasTable::ChangeRowsColor(void) { //--- 如果禁用悬停时的行高亮显示, 离开 if(!m_lights_hover) return; //--- 如果不在焦点 if(!m_table.MouseFocus()) { //--- 如果还未指出不在焦点 if(m_prev_item_index_focus!=WRONG_VALUE) { m_item_index_focus=WRONG_VALUE; //--- 改变颜色 RedrawRow(); m_table.Update(); //--- 重置焦点 m_prev_item_index_focus=WRONG_VALUE; } } //--- 如果在焦点 else { //--- 检查行上的焦点 if(m_item_index_focus==WRONG_VALUE) { //--- 获取焦点所在行的索引 m_item_index_focus=CheckRowFocus(); //--- 改变行颜色 RedrawRow(); m_table.Update(); //--- 保存为以前的焦点索引 m_prev_item_index_focus=m_item_index_focus; return; } //--- 获取鼠标光标之下的相对 Y 坐标 int y=m_mouse.RelativeY(m_table); //--- 验证焦点 bool condition=(y>m_rows[m_item_index_focus].m_y && y<=m_rows[m_item_index_focus].m_y2); //--- 如果焦点改变了 if(!condition) { //--- 获取焦点所在行的索引 m_item_index_focus=CheckRowFocus(); //--- 改变行颜色 RedrawRow(); m_table.Update(); //--- 保存为以前的焦点索引 m_prev_item_index_focus=m_item_index_focus; } } }
CCanvasTable::RedrawRow() 方法有两种快速重绘表格行模式:
- 当选择一行时
- 处于悬浮时高亮显示一行模式。
该方法需要传递相应的参数来指定所需模式。省缺情况下, 参数设置为 false, 表示在高亮显示表格行模式下使用该方法。该类还包含两种模式的特殊字段来确定当前和先前 所选择的/高亮 表格行。因此, 标记其它行, 仅需要重绘前一行和当前行, 而不是整个表。
如果没有定义索引 (WRONG_VALUE), 程序将离开该方法。接下来, 需要确定已定义了多少个索引。如果这是第一此进入表格, 并且只定义了一个索引 (当前), 则相应地, 颜色将只在当前行中更改。如果是再次进入, 颜色将会更改两行 (当前和以前)。
现在需要确定更改行颜色的顺序。如果当前行的索引大于前一行的索引, 则表示光标向下移动。然后, 首先更改前一个索引的颜色, 然后在当前索引中更改颜色。在相反的情况下, 也反过来做。方法还要考虑离开表格区域时刻, 此时没有定义当前行索引, 而前一行的索引仍然存在。
一旦所有参与操作的局部变量被初始化, 行背景, 网格, 图标和文本将被严格地绘制。
class CCanvasTable : public CElement { private: //--- 根据指定的模式重绘指定的表格行 void RedrawRow(const bool is_selected_row=false); }; //+------------------------------------------------------------------+ //| 根据指定的模式重绘指定的表行 | //+------------------------------------------------------------------+ void CCanvasTable::RedrawRow(const bool is_selected_row=false) { //--- 当前以及前一行的索引 int item_index =WRONG_VALUE; int prev_item_index =WRONG_VALUE; //--- 相对于指定模式初始化行索引 if(is_selected_row) { item_index =m_selected_item; prev_item_index =m_prev_selected_item; } else { item_index =m_item_index_focus; prev_item_index =m_prev_item_index_focus; } //--- 如果没有定义索引, 离开 if(prev_item_index==WRONG_VALUE && item_index==WRONG_VALUE) return; //--- 将要绘制的行数和列数 int rows_total =(item_index!=WRONG_VALUE && prev_item_index!=WRONG_VALUE)? 2 : 1; int columns_total =m_columns_total-1; //--- 坐标 int x1=1,x2=m_table_x_size; int y1[2]={0},y2[2]={0}; //--- 确定序列中的数值数组 int indexes[2]; //--- 如果 (1) 鼠标光标向下移动, 或如果 (2) 首次进入 if(item_index>m_prev_item_index_focus || item_index==WRONG_VALUE) { indexes[0]=(item_index==WRONG_VALUE || prev_item_index!=WRONG_VALUE)? prev_item_index : item_index; indexes[1]=item_index; } //--- 如果鼠标光标向上移动 else { indexes[0]=item_index; indexes[1]=prev_item_index; } //--- 绘制行的背景 for(int r=0; r<rows_total; r++) { //--- 计算行的上、下边界坐标 y1[r]=m_rows[indexes[r]].m_y+1; y2[r]=m_rows[indexes[r]].m_y2-1; //--- 确定处于高亮显示模式的焦点所在行 bool is_item_focus=false; if(!m_lights_hover) is_item_focus=(indexes[r]==item_index && item_index!=WRONG_VALUE); else is_item_focus=(item_index==WRONG_VALUE)?(indexes[r]==prev_item_index) :(indexes[r]==item_index); //--- 绘制行背景 m_table.FillRectangle(x1,y1[r],x2,y2[r],RowColorCurrent(indexes[r],is_item_focus)); } //--- 网格颜色 uint clr=::ColorToARGB(m_grid_color); //--- 绘制边界 for(int r=0; r<rows_total; r++) { for(int c=0; c<columns_total; c++) m_table.Line(m_columns[c].m_x2,y1[r],m_columns[c].m_x2,y2[r],clr); } //--- 绘制图标 for(int r=0; r<rows_total; r++) { for(int c=0; c<m_columns_total; c++) { //--- 绘制图标, 如果 (1) 它存在于单元格中, 且 (2) 此列的文本靠左侧对齐 if(ImagesTotal(c,r)>0 && m_columns[c].m_text_align==ALIGN_LEFT) DrawImage(c,indexes[r]); } } //--- 计算坐标 int x=0,y=0; //--- 文本对齐模式 uint text_align=0; //--- 绘制文字 for(int c=0; c<m_columns_total; c++) { //--- 获取 (1) 文本的 X 坐标, 和 (2) 文本对齐模式 x =TextX(c); text_align =TextAlign(c,TA_TOP); //--- for(int r=0; r<rows_total; r++) { //--- (1) 计算坐标, 并 (2) 绘制文本 y=m_rows[indexes[r]].m_y+m_text_y_offset; m_table.TextOut(x,y,m_columns[c].m_rows[indexes[r]].m_short_text,TextColor(c,indexes[r]),text_align); } } }
结果如下:
图例. 2. 悬浮时高亮显示表格行的演示。
快速重绘表格单元的方法
快速重绘表格行的方法已经研究完毕。现在将展示快速重绘单元格的方法。例如, 如果需要更改表格任何单元格中的文本, 颜色或图标, 只需重绘单元格, 而非整个表格。私有 CCanvasTable::RedrawCell() 方法即用于此目的。只有单元格内容将被重绘, 而其框架将不会被更新。如果启用了高亮模式, 则要确定背景颜色。在确定值并初始化局部变量之后, 将在单元格中绘制背景, 图标 (如果分配了, 且如果文本左侧对齐) 和文本。
class CCanvasTable : public CElement { private: //--- 重绘指定的表格单元 void RedrawCell(const int column_index,const int row_index); }; //+------------------------------------------------------------------+ //| 重绘指定的表格单元 | //+------------------------------------------------------------------+ void CCanvasTable::RedrawCell(const int column_index,const int row_index) { //--- 坐标 int x1=m_columns[column_index].m_x+1; int x2=m_columns[column_index].m_x2-1; int y1=m_rows[row_index].m_y+1; int y2=m_rows[row_index].m_y2-1; //--- 计算坐标 int x=0,y=0; //--- 检查焦点 bool is_row_focus=false; //--- 如果启用行高亮显示模式 if(m_lights_hover) { //--- (1) 获取鼠标光标的相对 Y 坐标, 和 (2) 指定表格行上的焦点 y=m_mouse.RelativeY(m_table); is_row_focus=(y>m_rows[row_index].m_y && y<=m_rows[row_index].m_y2); } //--- 绘制单元背景 m_table.FillRectangle(x1,y1,x2,y2,RowColorCurrent(row_index,is_row_focus)); //--- 绘制图标, 如果 (1) 它存在于单元格中, 且 (2) 此列的文本靠左侧对齐 if(ImagesTotal(column_index,row_index)>0 && m_columns[column_index].m_text_align==ALIGN_LEFT) DrawImage(column_index,row_index); //--- 获取文本对齐模式 uint text_align=TextAlign(column_index,TA_TOP); //--- 绘制文字 for(int c=0; c<m_columns_total; c++) { //--- 获取文本的 X 坐标 x=TextX(c); //--- 停止循环 if(c==column_index) break; } //--- (1) 计算 Y 坐标, 并 (2) 绘制文本 y=y1+m_text_y_offset-1; m_table.TextOut(x,y,m_columns[column_index].m_rows[row_index].m_short_text,TextColor(column_index,row_index),text_align); }
现在我们来研究方法, 它们允许在单元格中更改文本, 文本颜色和图标 (从已分配的之中选择)。公有 CCanvasTable::SetValue() 和 CCanvasTable::TextColor() 方法必须用来设置文本和其颜色。这些方法需要传递单元格 (列和行) 的索引, 以及要设置的数值。对于 CCanvasTable::SetValue() 方法, 它是要在单元格中显示的字符串值。此处, 完整传递的字符串及其缩写版本 (如果全部字符串不适合单元格宽度) 存储在表格的结构 (CTCell) 的相应字段中。文本颜色必须传递给 CCanvasTable::TextColor() 方法。作为这两种方法中的第四个参数, 您可以指定 是否需要立即重新绘制单元格, 或者稍后 通过调用 CCanvasTable::UpdateTable () 方法。
class CCanvasTable : public CElement { private: //--- 为指定的表格单元设置数值 void SetValue(const uint column_index,const uint row_index,const string value,const bool redraw=false); //--- 为指定的表格单元设置文本颜色 void TextColor(const uint column_index,const uint row_index,const color clr,const bool redraw=false); }; //+------------------------------------------------------------------+ //| 在指定的索引处填充数组 | //+------------------------------------------------------------------+ void CCanvasTable::SetValue(const uint column_index,const uint row_index,const string value,const bool redraw=false) { //--- 检查是否超出数组范围 if(!CheckOutOfRange(column_index,row_index)) return; //--- 将数值存储到数组中 m_columns[column_index].m_rows[row_index].m_full_text=value; //--- 调整并存储文本, 如果它不适合单元格 m_columns[column_index].m_rows[row_index].m_short_text=CorrectingText(column_index,row_index); //--- 如果已指定, 重绘单元格 if(redraw) RedrawCell(column_index,row_index); } //+------------------------------------------------------------------+ //| 填充文本颜色数组 | //+------------------------------------------------------------------+ void CCanvasTable::TextColor(const uint column_index,const uint row_index,const color clr,const bool redraw=false) { //--- 检查是否超出数组范围 if(!CheckOutOfRange(column_index,row_index)) return; //--- 将文本颜色存储在公共数组中 m_columns[column_index].m_rows[row_index].m_text_color=clr; //--- 如果已指定, 重绘单元格 if(redraw) RedrawCell(column_index,row_index); }
单元中的图标可以通过 CCanvasTable::ChangeImage() 方法更改。即将切换的图表索引 必须在此指定为第三个参数。如前面所描述的用于改变单元格属性的方法, 能够指定是否要立即或稍后重新绘制单元格。
class CCanvasTable : public CElement { private: //--- 更改指定单元格中的图标 void ChangeImage(const uint column_index,const uint row_index,const uint image_index,const bool redraw=false); }; //+------------------------------------------------------------------+ //| 更改指定单元格中的图标 | //+------------------------------------------------------------------+ void CCanvasTable::ChangeImage(const uint column_index,const uint row_index,const uint image_index,const bool redraw=false) { //--- 检查是否超出数组范围 if(!CheckOutOfRange(column_index,row_index)) return; //--- 获取单元格图标的数量 int images_total=ImagesTotal(column_index,row_index); //--- 如果 (1) 没有图标, 或是 (2) 超出范围, 离开 if(images_total==WRONG_VALUE || image_index>=(uint)images_total) return; //--- 如果指定的图标与已选定的图标匹配, 离开 if(image_index==m_columns[column_index].m_rows[row_index].m_selected_image) return; //--- 保存单元格所选图标的索引 m_columns[column_index].m_rows[row_index].m_selected_image=(int)image_index; //--- 如果已指定, 重绘单元格 if(redraw) RedrawCell(column_index,row_index); }
重绘整个表格需要另一个公共方法 — CCanvasTable::UpdateTable()。它可以在两种模式下调用:
- 当有必要简单地更新表格以便显示由上述方法所做的最新变化时。
- 如果进行了更改, 有必要完全重绘表格时。
省缺情况下, 该方法的唯一参数设置为 false, 表示刷新无需重绘。
class CCanvasTable : public CElement { private: //--- 更新表格 void UpdateTable(const bool redraw=false); }; //+------------------------------------------------------------------+ //| 更新表格 | //+------------------------------------------------------------------+ void CCanvasTable::UpdateTable(const bool redraw=false) { //--- 如果指定,重绘表格 if(redraw) DrawTable(); //--- 更新表格 m_table.Update(); }
以下是完工后的结果:
图例. 3. 渲染表格新功能演示。
展示此结果的智能交易系统可在本文附带的文件中下载。在程序执行期间, 所有表格单元格 (5 列和 30 行) 中的图标将以 100 毫秒的频率刷新。下面的屏幕截图显示了 CPU 负载, 没有用户通过图形界面与 MQL 应用程序交互。刷新频率为 100 毫秒时的 CPU 负载不超过 3%。
图例. 4. 执行 MQL 应用程序测试期间的 CPU 负载。
用于测试控件的应用程序
例如, 当前版本渲染表格的 "智能" 已经足以创建与"市场观察" 窗口中相同的表格。让我们试着展示这一点。例如, 创建一个 5 列和 25 行的表格。在 MetaQuotes-Demo 服务器上有 25 个可用的品种。表中的数据如下:
- Symbol – 金融工具 (当前货币对)。
- Bid – 供给价。
- Ask – 采购价。
- Spread (!) – 供给价和采购价之间的点差。
- Time – 最后报价的时间。
让我们准备相同的图标来表示最新的价格变化, 如同 "市场观察" 窗口的表格。在创建控件的方法中, 表格单元的首次初始化将立即进行, 并且通过调用自定义类的辅助 CProgram::InitializingTable() 方法来执行。
//+------------------------------------------------------------------+ //| 创建应用程序的类 | //+------------------------------------------------------------------+ class CProgram : public CWndEvents { private: //--- 初始化表格 void InitializingTable(void); }; //+------------------------------------------------------------------+ //| 初始化表格 | //+------------------------------------------------------------------+ void CProgram::InitializingTable(void) { //--- 标题数组 string text_headers[COLUMNS1_TOTAL]={"Symbol","Bid","Ask","!","Time"}; //--- 品种数组 string text_array[25]= { "AUDUSD","GBPUSD","EURUSD","USDCAD","USDCHF","USDJPY","NZDUSD","USDSEK","USDHKD","USDMXN", "USDZAR","USDTRY","GBPAUD","AUDCAD","CADCHF","EURAUD","GBPCHF","GBPJPY","NZDJPY","AUDJPY", "EURJPY","EURCHF","EURGBP","AUDCHF","CHFJPY" }; //--- 图标数组 string image_array[3]= { "::Images\\EasyAndFastGUI\\Icons\\bmp16\\circle_gray.bmp", "::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_up.bmp", "::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_down.bmp" }; //--- for(int c=0; c<COLUMNS1_TOTAL; c++) { //--- 设置标题 m_canvas_table.SetHeaderText(c,text_headers[c]); //--- for(int r=0; r<ROWS1_TOTAL; r++) { //--- 设置图标 m_canvas_table.SetImages(c,r,image_array); //--- 设置品名 if(c<1) m_canvas_table.SetValue(c,r,text_array[r]); //--- 所有单元的省缺值 else m_canvas_table.SetValue(c,r,"-"); } } }
省缺的表格单元刷新率在运行期间为 16 毫秒。另一个辅助 CProgram::UpdateTable() 方法已为此目的而创建。此处, 若是周末则程序离开方法 (周六或周日)。然后, 双重循环遍历表格的所有列和行。在双重循环中 获取每个品种的最后两笔即时报价, 且在 分析价格变化 之后, 设置相应的数值。
class CProgram : public CWndEvents { private: //--- 初始化表格 void InitializingTable(void); }; //+------------------------------------------------------------------+ //| 更新表格数值 | //+------------------------------------------------------------------+ void CProgram::UpdateTable(void) { MqlDateTime check_time; ::TimeToStruct(::TimeTradeServer(),check_time); //--- 如果是星期六或星期天, 离开 if(check_time.day_of_week==0 || check_time.day_of_week==6) return; //--- for(int c=0; c<m_canvas_table.ColumnsTotal(); c++) { for(int r=0; r<m_canvas_table.RowsTotal(); r++) { //--- 获取数据的品种 string symbol=m_canvas_table.GetValue(0,r); //--- 获取最后两笔即时报价的数据 MqlTick ticks[]; if(::CopyTicks(symbol,ticks,COPY_TICKS_ALL,0,2)<2) continue; //--- 将数组设置为时间序列 ::ArraySetAsSeries(ticks,true); //--- 品种列 - 品种。Determine the price direction. if(c==0) { int index=0; //--- 如果价格未变化 if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid) index=0; //--- 如果供给价格向上变化 else if(ticks[0].bid>ticks[1].bid) index=1; //--- 如果供给价格向下变化 else if(ticks[0].bid<ticks[1].bid) index=2; //--- 设置相应的图标 m_canvas_table.ChangeImage(c,r,index,true); } else { //--- 价格差异列 - 点差 (!) if(c==3) { //--- 获取并设置点差的点数大小 int spread=(int)::SymbolInfoInteger(symbol,SYMBOL_SPREAD); m_canvas_table.SetValue(c,r,string(spread),true); continue; } //--- 获取小数位数 int digit=(int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS); //--- 供给价列 if(c==1) { m_canvas_table.SetValue(c,r,::DoubleToString(ticks[0].bid,digit)); //--- 如果价格有变化, 设定与方向对应的颜色 if(ticks[0].bid!=ticks[1].bid) m_canvas_table.TextColor(c,r,(ticks[0].bid<ticks[1].bid)? clrRed : clrBlue,true); //--- continue; } //--- 采购价列 if(c==2) { m_canvas_table.SetValue(c,r,::DoubleToString(ticks[0].ask,digit)); //--- 如果价格有变化, 设定与方向对应的颜色 if(ticks[0].ask!=ticks[1].ask) m_canvas_table.TextColor(c,r,(ticks[0].ask<ticks[1].ask)? clrRed : clrBlue,true); //--- continue; } //--- 最后品种价格的到达时刻列 if(c==4) { long time =::SymbolInfoInteger(symbol,SYMBOL_TIME); string time_msc =::IntegerToString(ticks[0].time_msc); int length =::StringLen(time_msc); string msc =::StringSubstr(time_msc,length-3,3); string str =::TimeToString(time,TIME_MINUTES|TIME_SECONDS)+"."+msc; //--- color clr=clrBlack; //--- 如果价格未变化 if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid) clr=clrBlack; //--- 如果供给价格向上变化 else if(ticks[0].bid>ticks[1].bid) clr=clrBlue; //--- 如果供给价格向下变化 else if(ticks[0].bid<ticks[1].bid) clr=clrRed; //--- 设置数值和文本颜色 m_canvas_table.SetValue(c,r,str); m_canvas_table.TextColor(c,r,clr,true); continue; } } } } //--- 更新表格 m_canvas_table.UpdateTable(); }
得到以下结果:
图例. 5. 市场观察窗口中的数据与自定义模拟的比较。
本文中的测试应用程序可以使用以下链接进行下载, 以便进一步学习。
结论
创建图形界面的函数库的当前开发阶段如下图所示。
图例. 6. 当前开发阶段的函数库结构。
您可以从下面下载最新版本的函数库和文件进行测试。
若您在使用这些文件提供的材料时有任何疑问, 可以参考函数库开发系列文章之一的详细描述, 或在本文的评论中提出您的问题。