内容
概论
格式化斑马样式
选择和弃选表格行
列标题
相对于列宽调整字符串长度
事件处理
结论
概论
首篇文章 图形界面 I: 函数库结构准备 (第 1 章) 详细研究了函数的目的。您将在每章结尾处找到含有链接的文章列表。从那里, 您还可以下载当前开发阶段的函数库完整版。文件必须位于与存档相同的目录中。
时至今日, CTable 是函数库中所含的最先进类型表格。表格由 OBJ_EDIT 类型的编辑框汇集而成, 但其进一步开发成为问题。例如, 它难以实现通过手动拖动标题边框调整列的大小, 且不能管理表格的各个图形对象的可见区域。这已达到极限。
因此, 在函数库开发的当前阶段, 进而开发 CCanvasTable 类型的表格更为合理。有关渲染表格以前版本和更新的信息, 请参见此处:
图形界面 VII: 表格控件 (第 1 章)
图形界面 X: 函数库的轻松快捷更新 (集成编译 3)
屏幕截图展示了最新版渲染表格的外观。如您所见, 此刻它毫无生气。它只是一个带有数据的表格单元格。可以为单元格指定对齐方法。除了滚动条和窗体大小的自动调整之外, 该表格还没有其它交互性。
图例. 1. 渲染表格的以前版本。
为了纠正这种状况, 我们来用新功能补充渲染表格。在当前更新中将包含以下功能:
格式化斑马样式
选择表格行, 再次单击时弃选
添加了列标题, 可在鼠标悬浮和点击时改变颜色
当单元格空间不足时, 则依照文本自动调整列宽
能够通过拖动其边框来改变每列的标题宽度
格式化斑马样式
在最近的一篇文章里已将格式化斑马样式添加到 CTable 表格里。如果表格包含很多单元格, 它有助于更好地表格导航。让我们在渲染表格中实现这种模式。
使用 CCanvasTable::IsZebraFormatRows() 方法启用此模式。它将传递第二个颜色用于样式, 而通常的单元格颜色将使用第一个颜色。
//++//| 创建渲染表格的类 |//++class CCanvasTable : public CElement {private: //- 格式化斑马样式模式 color m_is_zebra_format_rows; //-public: //- 格式化斑马样式 void IsZebraFormatRows(const color clr) { m_is_zebra_format_rows=clr; } };
在不同类型的表格中这种风格的可视化方法有所不同。在 CCanvasTable 的情况下, 正常模式中 表格背景 (绘图画板) 完全用普通单元格颜色填充。当斑马样式被激活时, 开始一个循环。每次迭代中计算每行的坐标, 且用两种颜色交替为区域着色。这是利用绘制填充矩形的 FillRectangle() 方法完成的。
class CCanvasTable : public CElement {public: //- 绘制表格行的背景 void DrawRows(void); };//++//| 绘制表格行的背景 |//++void CCanvasTable::DrawRows(void) {//- 如果禁用格式化斑马样式模式 if(m_is_zebra_format_rows==clrNONE) { //- 用一种颜色填充画板 m_table.Erase(::ColorToARGB(m_cell_color)); return; }//- 标题坐标 int x1=0,x2=m_table_x_size; int y1=0,y2=0;//- 格式化斑马样式 for(int r=0; r<m_rows_total; r++) { //- 计算坐标 y1=(r*m_cell_y_size)-r; y2=y1+m_cell_y_size; //- 行颜色 uint clr=::ColorToARGB((r%2!=0)? m_is_zebra_format_rows : m_cell_color); //- 绘制行背景 m_table.FillRectangle(x1,y1,x2,y2,clr); } }
行颜色可根据您的喜好设置。结果就是, 斑马模式下的渲染表格将如下所示:
图例. 2. 格式化斑马样式模式下的渲染表格。
选择和弃选表格行
行选择将需要其它字段和方法进行存储和设置:
所选行的背景和文本的颜色
索引和文本
class CCanvasTable : public CElement {private: //- 颜色用于 (1) 背景 (2) 所选行文本 color m_selected_row_color; color m_selected_row_text_color; //- (1) 索引 (2) 所选行文本 int m_selected_item; string m_selected_item_text; //-public: //- 返回 (1) 索引 (2) 表格中所选行文本 int SelectedItem(void) const { return(m_selected_item); } string SelectedItemText(void) const { return(m_selected_item_text); } //-private: //- 绘制表格行的背景 void DrawRows(void); };
可以通过 CCanvasTable::SelectableRow() 方法启用/禁用可选行模式:
class CCanvasTable : public CElement {private: //- 可选行模式 bool m_selectable_row; //-public: //- 行选择 void SelectableRow(const bool flag) { m_selectable_row=flag; } };
为了选择一行, 需要一个用于绘制用户定义区域的单独方法。下面给出了 CCanvasTable::DrawSelectedRow() 方法的代码。它计算画板上所选区域的坐标, 然后用于 绘制填充矩形。
class CCanvasTable : public CElement {private: //- 绘制所选行 void DrawSelectedRow(void); };//++//| 绘制所选行 |//++void CCanvasTable::DrawSelectedRow(void) {//- 设置初始坐标以便检查条件 int y_offset=(m_selected_item*m_cell_y_size)-m_selected_item;//- 坐标 int x1=0,x2=0,y1=0,y2=0;//- x1=0; y1=y_offset; x2=m_table_x_size; y2=y_offset+m_cell_y_size-1;//- 绘制填充矩形 m_table.FillRectangle(x1,y1,x2,y2,::ColorToARGB(m_selected_row_color)); }
一个辅助的 CCanvasTable::TextColor() 方法用来重绘文本, 它会判断单元格中的文本颜色:
class CCanvasTable : public CElement {private: //- 返回单元格文本的颜色 uint TextColor(const int row_index); };//++//| 返回单元格文本的颜色 |//++uint CCanvasTable::TextColor(const int row_index) { uint clr=::ColorToARGB((row_index==m_selected_item)? m_selected_row_text_color : m_cell_text_color);//- 返回标题颜色 return(clr); }
若要选择表格行, 需要双击它。这需要 CCanvasTable::OnClickTable() 方法, 它通过 CHARTEVENT_OBJECT_CLICK 标识符在控件的事件处理器中调用。
在方法开始时应通过几个检查。程序会离开方法, 如果:
行选择模式被禁用;
滚动条已激活;
未在表格上点击。
如果检查通过, 则为了计算点击坐标, 需要获取 距画板边缘的当前偏移量 和 鼠标光标的 Y 坐标。在这之后, 在循环里 判断所点击的行。一旦找到该行, 需要检查当前是否已被选中, 如果是 — 取消选择。如果已选择该行, 则需要存储其索引和第一列中的文本。在循环中完成行搜索后重新绘制表格。发送一条消息, 其中包含:
ON_CLICK_LIST_ITEM 事件的标识符
控件的标识符
所选行的索引
所选行的文本。
class CCanvasTable : public CElement {private: //- 在元素上点按处理 bool OnClickTable(const string clicked_object); };//++//| 点击控件处理 |//++bool CCanvasTable::OnClickTable(const string clicked_object) {//- 如果禁用选择行模式, 离开 if(!m_selectable_row) return(false);//- 如果滚动条激活, 离开 if(m_scrollv.ScrollState() || m_scrollh.ScrollState()) return(false);//- 如果对象名不同, 离开 if(m_table.Name()!=clicked_object) return(false);//- 获取 X 和 Y 轴的偏移量 int xoffset=(int)m_table.GetInteger(OBJPROP_XOFFSET); int yoffset=(int)m_table.GetInteger(OBJPROP_YOFFSET);//- 确定鼠标光标下方的文本编辑框坐标 int y=m_mouse.Y()-m_table.Y()+yoffset;//- 确定点击的行 for(int r=0; r<m_rows_total; r++) { //- 设置初始坐标以便检查条件 int y_offset=(r*m_cell_y_size)-r; //- 沿 Y 轴检查状态 bool y_pos_check=(y>=y_offset && y<y_offset+m_cell_y_size); //- 如果点击并非此行, 则转至下一个 if(!y_pos_check) continue; //- 如果点击已选行, 取消选择 if(r==m_selected_item) { m_selected_item =WRONG_VALUE; m_selected_item_text =""; break; } //- 保存行索引 m_selected_item =r; m_selected_item_text =m_vcolumns[0].m_vrows[r]; break; }//- 表格重绘 DrawTable();//- 发送有关消息 ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,m_selected_item_text); return(true); }
带有选定行的渲染表格如下方式显示:
图例. 3. 选择和弃选渲染表格一行的演示。
列标题
任何没有标题的表格都是空白的。标题也将在这种类型的表中绘制, 但会在单独的画板上。为了做到这一点, 在 CCanvasTable 类中包含其它 CRectCanvas 类的实例, 并 创建一个单独的画板创建方法。此方法的代码不会在这里提供: 它几乎与创建表格相同。仅有的区别是预设对象的大小和位置。
class CCanvasTable : public CElement {private: //- 创建表格的对象 CRectCanvas m_headers; //-private: bool CreateHeaders(void); };
现在研究与列标题相关的属性。它们可在创建表之前进行配置。
表头的显示模式。
标题的大小 (高度)。
不同状态下的标题背景颜色。
标题文本颜色。
与这些属性相关的字段和方法:
class CCanvasTable : public CElement {private: //- 表头的显示模式 bool m_show_headers; //- 标题的大小 (高度) int m_header_y_size; //- 不同状态下的标题 (背景) 颜色 color m_headers_color; color m_headers_color_hover; color m_headers_color_pressed; //- 标题文本颜色 color m_headers_text_color; //-public: //- (1) 标题显示模式, (2) 标题高度 void ShowHeaders(const bool flag) { m_show_headers=flag; } void HeaderYSize(const int y_size) { m_header_y_size=y_size; } //- (1) 背景 (2) 标题文本颜色 void HeadersColor(const color clr) { m_headers_color=clr; } void HeadersColorHover(const color clr) { m_headers_color_hover=clr; } void HeadersColorPressed(const color clr) { m_headers_color_pressed=clr; } void HeadersTextColor(const color clr) { m_headers_text_color=clr; } };
方法需要 设置标题名称。此外, 还需要一个 数组来保存这些数值。数组的大小等于列数, 并在设置表格大小时使用相同的 CCanvasTable::TableSize() 方法中设置。
class CCanvasTable : public CElement {private: //- 标题文本 string m_header_text[]; //-public: //- 设置指定标题的文本 void SetHeaderText(const int column_index,const string value); };//++//| 在指定索引处填充标题数组 |//++void CCanvasTable::SetHeaderText(const uint column_index,const string value) {//- 检查列超界 uint csize=::ArraySize(m_vcolumns); if(csize<1 || column_index>=csize) return;//- 将数值保存到数组中 m_header_text[column_index]=value; }
在单元格和标题中的文本对齐将使用通用的 CCanvasTable::TextAlign() 方法进行。在表格单元中沿 X 轴的对齐与标题相匹配, 而 沿 Y 轴的对齐通过传递的数值定义。在此版本里, 标题文本在 Y 轴上将位于中间 — TA_VCENTER, 且单元格的便宜自单元格的顶部边缘进行调整 — TA_TOP。
class CCanvasTable : public CElement {private: //- 返回指定列中的文本对齐模式 uint TextAlign(const int column_index,const uint anchor); };//++//| 返回指定列中的文本对齐模式 |//++uint CCanvasTable::TextAlign(const int column_index,const uint anchor) { uint text_align=0;//- 当前列的文本对齐 switch(m_vcolumns[column_index].m_text_align) { case ALIGN_CENTER : text_align=TA_CENTER|anchor; break; case ALIGN_RIGHT : text_align=TA_RIGHT|anchor; break; case ALIGN_LEFT : text_align=TA_LEFT|anchor; break; }//- 返回对齐类型 return(text_align); }
在许多表格以及操作系统环境中, 当光标悬浮在两个标题之间的边界时, 指针会改变。以下截图通过 MetaTrader 5 交易终端工具箱窗口中表格的示例描述了这种情形。如果点击这个新出现的指针, 它将切换更改列宽度的模式。此列的背景颜色也会改变。
图例. 4. 鼠标指针悬浮在标题边界关联处。
我们来为所开发的函数库准备相同的图像。文章末尾的附件包含一个文件夹, 内有函数库控件的所有图像。在 Enums.mqh 文件里添加 新的指针标识符至 ENUM_MOUSE_POINTER 枚举, 用于沿 X 和 Y 轴的大小改变:
//++//| 指针类型的枚举 |//++enum ENUM_MOUSE_POINTER { MP_CUSTOM =0, MP_X_RESIZE =1, MP_Y_RESIZE =2, MP_XY1_RESIZE =3, MP_XY2_RESIZE =4, MP_X_RESIZE_RELATIVE =5, MP_Y_RESIZE_RELATIVE =6, MP_X_SCROLL =7, MP_Y_SCROLL =8, MP_TEXT_SELECT =9 };
需要在 CPointer 类中相应进行添加, 以使该指针类型可在控件的类中使用。
//++//| Pointer.mqh |//| 版权所有 2015, MetaQuotes 软件公司 |//| https://www.mql5.com |//++//- 资源#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp"#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp"//++//| 创建鼠标光标的类 |//++class CPointer : public CElement {private: //- 设置鼠标光标的图像 void SetPointerBmp(void); };//++//| 基于光标类型设置光标图标 |//++void CPointer::SetPointerBmp(void) { switch(m_type) { ... case MP_X_RESIZE_RELATIVE : m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp"; m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp"; break; case MP_Y_RESIZE_RELATIVE : m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp"; m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp"; break; ... }//- 如果指定为自定义类型 (MP_CUSTOM) if(m_file_on=="" || m_file_off=="") ::Print(__FUNCTION__," > 必须为光标设置两个图像"); }
此处需要附加的字段:
确定拖动标题边框的时刻
为了确定鼠标光标从一个标题区域移动到另一个标题区域的时刻。这需要节省资源, 因此只有当相邻区域的边界交错时才会重新绘制标题。
class CCanvasTable : public CElement {private: //- 确定鼠标光标从一个标题转换到另一个标题的时刻 int m_prev_header_index_focus; //- 拖动标题边框以改变列宽的状态 int m_column_resize_control; };
CCanvasTable::HeaderColorCurrent() 方法可以根据当前模式、鼠标光标位置和鼠标左键状态获取标题的当前颜色。焦点覆盖标题 将在 CCanvasTable::DrawHeaders() 方法里判断, 该方法设计用于绘制标题背景, 并将被传递到此作为检查结果。
class CCanvasTable : public CElement {private: //- 返回当前的标题背景色 uint HeaderColorCurrent(const bool is_header_focus); };//++//| 返回当前的标题背景色 |//++uint CCanvasTable::HeaderColorCurrent(const bool is_header_focus) { uint clr=clrNONE;//- 如果无焦点 if(!is_header_focus || !m_headers.MouseFocus()) clr=m_headers_color; else { //- 如果鼠标左键按下且未处于列宽改变进程 bool condition=(m_mouse.LeftButtonState() && m_column_resize_control==WRONG_VALUE); clr=(condition)? m_headers_color_pressed : m_headers_color_hover; }//- 返回标题颜色 return(::ColorToARGB(clr)); }
CCanvasTable::DrawHeaders() 方法的代码表述如下。此处, 如果鼠标光标不在标题区域, 则 整个画板将以指定的颜色填充。如果焦点在标题上, 那么有必要确定它们当中的哪一个拥有焦点。为此, 需要确定鼠标光标的相对坐标, 并在计算标题坐标的循环中检查每个标题是否处于焦点。另外, 在此还要考虑改变列宽模式。在计算此模式中使用 附加偏移量。如果找到焦点, 则必须保存列索引。
class CCanvasTable : public CElement {private: //- 自分隔线边界的偏移, 按照更改列宽模式显示鼠标指针 int m_sep_x_offset; //-private: //- 绘制标题 void DrawHeaders(void); };//++//| 绘制标题背景 |//++void CCanvasTable::DrawHeaders(void) {//- 如果非焦点, 重置标题颜色 if(!m_headers.MouseFocus()) { m_headers.Erase(::ColorToARGB(m_headers_color)); return; }//- 检查焦点是否在标题上 bool is_header_focus=false;//- 鼠标光标坐标 int x=0;//- 坐标 int x1=0,x2=0,y1=0,y2=m_header_y_size;//- 得到鼠标光标的相对坐标 if(::CheckPointer(m_mouse)!=POINTER_INVALID) { //- 获取 X 轴偏移量 int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET); //- 确定鼠标光标坐标 x=m_mouse.X()-m_headers.X()+xoffset; }//- 清除标题背景 m_headers.Erase(::ColorToARGB(clrNONE,0));//- 考虑到更改列宽模式偏移 int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : 0;//- 绘制标题背景 for(int i=0; i<m_columns_total; i++) { //- 计算坐标 x2+=m_vcolumns[i].m_width; //- 检查焦点 if(is_header_focus=x>x1+((i!=0)? sep_x_offset : 0) && x<=x2+sep_x_offset) m_prev_header_index_focus=i; //- 绘制标题背景 m_headers.FillRectangle(x1,y1,x2,y2,HeaderColorCurrent(is_header_focus)); //- 计算下一标题偏移 x1+=m_vcolumns[i].m_width; } }
一旦绘制了标题背景, 就需要绘制网格 (标题框)。CCanvasTable::DrawHeadersGrid() 方法用于此目的。首先, 绘制通用框, 之后在循环里画 分割线。
class CCanvasTable : public CElement {private: //- 绘制表格标题网格 void DrawHeadersGrid(void); };//++//| 绘制表格标题网格 |//++void CCanvasTable::DrawHeadersGrid(void) {//- 网格颜色 uint clr=::ColorToARGB(m_grid_color);//- 坐标 int x1=0,x2=0,y1=0,y2=0; x2=m_table_x_size-1; y2=m_header_y_size-1;//- 绘制边框 m_headers.Rectangle(x1,y1,x2,y2,clr);//- 分割线 x2=x1=m_vcolumns[0].m_width; for(int i=1; i<m_columns_total; i++) { m_headers.Line(x1,y1,x2,y2,clr); x2=x1+=m_vcolumns[i].m_width; } }
最后, 绘制标题文本。此任务用 CCanvasTable::DrawHeadersText() 方法执行。在此, 需要循环查看每个标题, 在每次迭代时确定 文本坐标 和 对齐方式。标题名称作为循环的最后一个操作。相对于列宽调整文本也在此进行。CCanvasTable::CorrectingText() 方法 即用于此目的。在本文的下一节将对其进行更详细的描述。
class CCanvasTable : public CElement {private: //- 绘制表格标题文本 void DrawHeadersText(void); };//++//| 绘制表格标题文本 |//++void CCanvasTable::DrawHeadersText(void) {//- 计算坐标和偏移 int x=0,y=m_header_y_size/2; int column_offset =0; uint text_align =0;//- 文本颜色 uint clr=::ColorToARGB(m_headers_text_color);//- 字体属性 m_headers.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);//-绘制文本 for(int c=0; c<m_columns_total; c++) { //- 获取文本的 X 坐标 x=TextX(c,column_offset); //- 获取文本的对齐模式 text_align=TextAlign(c,TA_VCENTER); //- 绘制列名称 m_headers.TextOut(x,y,CorrectingText(c,0,true),clr,text_align); } }
所有列出的绘制标题的方法均在通用的 CCanvasTable::DrawTableHeaders() 方法里调用。如果标题显示模式被禁用, 则禁止进入该方法。
class CCanvasTable : public CElement {private: //- 绘制表格标题 void DrawTableHeaders(void); };//++//| 绘制表格标题 |//++void CCanvasTable::DrawTableHeaders(void) {//- 如果禁用标题, 离开 if(!m_show_headers) return;//- 绘制标题 DrawHeaders();//- 绘制网格 DrawHeadersGrid();//- 绘制标题文本 DrawHeadersText(); }
利用 CCanvasTable::CheckHeaderFocus() 方法检查焦点是否在标题上。程序在两种情况下会离开方法:
如果标题显示模式被禁用
或者如果更改列宽的过程已经开始。
此后, 获取光标在画板上的相对坐标。循环 搜索任何标题上的焦点 以及 检查自最后一次调用该方法后是否有变化。如果注册了新焦点 (标题边界交错的时刻), 则 需要重置之前保存的标题索引 并停止循环。
class CCanvasTable : public CElement {private: //- 检查标题上的焦点 void CheckHeaderFocus(void); };//++//| 检查标题上的焦点 |//++void CCanvasTable::CheckHeaderFocus(void) {//- 如果 (1) 标题被禁用 (2) 列宽变更已开始, 离开 if(!m_show_headers || m_column_resize_control!=WRONG_VALUE) return;//- 标题坐标 int x1=0,x2=0;//- 获取 X 轴偏移量 int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);//- 得到鼠标光标的相对坐标 int x=m_mouse.X()-m_headers.X()+xoffset;//- 考虑到更改列宽模式偏移 int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : 0;//- 搜索焦点 for(int i=0; i<m_columns_total; i++) { //- 计算右坐标 x2+=m_vcolumns[i].m_width; //- 如果标题焦点改变 if((x>x1+sep_x_offset && x<=x2+sep_x_offset) && m_prev_header_index_focus!=i) { m_prev_header_index_focus=WRONG_VALUE; break; } //- 计算左坐标 x1+=m_vcolumns[i].m_width; } }
与其相对, 只有当边界交错时, 标题才会重绘。这样可以节省 CPU 资源。CCanvasTable::ChangeHeadersColor() 方法设计用于此任务。在此, 如果标题显示模式被禁用, 或正在改变其宽度的过程, 程序将离开该方法。如果方法开头的检查通过, 则检查标题的焦点, 并重绘它们。
class CCanvasTable : public CElement {private: //- 改变标题颜色 void ChangeHeadersColor(void); };//++//| 改变标题颜色 |//++void CCanvasTable::ChangeHeadersColor(void) {//- 如果禁用标题, 离开 if(!m_show_headers) return;//- 如果光标已激活 if(m_column_resize.IsVisible() && m_mouse.LeftButtonState()) { //- 保存拖拽列的索引 if(m_column_resize_control==WRONG_VALUE) m_column_resize_control=m_prev_header_index_focus; //- return; }//- 非焦点 if(!m_headers.MouseFocus()) { //- 如果尚未指出不在焦点 if(m_prev_header_index_focus!=WRONG_VALUE) { //- 重置焦点 m_prev_header_index_focus=WRONG_VALUE; //- 改变颜色 DrawTableHeaders(); m_headers.Update(); } }//- 若在焦点 else { //- 检查标题的焦点 CheckHeaderFocus(); //- 如果无焦点 if(m_prev_header_index_focus==WRONG_VALUE) { //- 改变颜色 DrawTableHeaders(); m_headers.Update(); } } }
以下是 CCanvasTable::CheckColumnResizeFocus() 方法的代码。需要确定标题间边界的焦点, 并负责在更改列宽时显示/隐藏光标。在方法开始时有两次检查。如果列宽改变模式被禁用, 程序将离开该方法。如果启用并正在更改列宽, 则需要更新鼠标光标坐标并离开该方法。
如果更改列宽度的过程尚未开始, 那么如果光标位于标题区域中, 则 尝试在循环中确定其中一个 的边框上的焦点。如果发现焦点, 更新鼠标光标坐标, 令其可见 并离开方法。如果焦点未发现, 则 指针将被隐藏。
class CCanvasTable : public CElement {private: //- 检查标题边框上的焦点以更改其宽度 void CheckColumnResizeFocus(void); };//++//| 检查标题边框上的焦点以更改其宽度 |//++void CCanvasTable::CheckColumnResizeFocus(void) {//- 如果禁用更改列宽模式, 离开 if(!m_column_resize_mode) return;//- 如果开始更改列宽, 离开 if(m_column_resize_control!=WRONG_VALUE) { //- 更新光标坐标并令其可见 m_column_resize.Moving(m_mouse.X(),m_mouse.Y()); return; }//- 检查标题边框上的焦点 bool is_focus=false;//- 如果鼠标光标位于标题区域 if(m_headers.MouseFocus()) { //- 标题坐标 int x1=0,x2=0; //- 获取 X 轴偏移量 int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET); //- 得到鼠标光标的相对坐标 int x=m_mouse.X()-m_headers.X()+xoffset; //- 搜索焦点 for(int i=0; i<m_columns_total; i++) { //- 计算坐标 x1=x2+=m_vcolumns[i].m_width; //- 验证焦点 if(is_focus=x>x1-m_sep_x_offset && x<=x2+m_sep_x_offset) break; } //- 如果这是焦点 if(is_focus) { //- 更新光标坐标并令其可见 m_column_resize.Moving(m_mouse.X(),m_mouse.Y()); //- 显示光标 m_column_resize.Show(); return; } }//- 如果非焦点, 隐藏指针 if(!m_headers.MouseFocus() || !is_focus) m_column_resize.Hide(); }
最终结果如下:
图例. 5. 列标题。
相对于列宽调整字符串长度
此前, 若要使文本不与相邻单元格重叠, 必须手动选择列宽并重新编译文件来查看结果。很自然, 这极不方便。
我们让字符串长度自动调整, 如果它不适合表格单元格。重新绘制表格时, 以前调整过的字符串将不会再次调整。将另一个数组添加到表格属性的结构中 以便保存这些字符串。
class CCanvasTable : public CElement {private: //- 表格数值和属性的数组 struct CTOptions { string m_vrows[]; string m_text[]; int m_width; ENUM_ALIGN_MODE m_text_align; }; CTOptions m_vcolumns[]; };
作为结果, m_vrows[] 将保存全部文本, 而 m_text[] 数组将保存调整版本的文本。
CCanvasTable::CorrectingText() 方法将负责调整 标题和表格单元两者内的 字符串长度。在识别操作的文本之后, 获取其宽度。接下来, 检查字符串的全文是否适合单元格, 同时考虑单元格边缘的所有偏移量。如果它适合, 将之保存在 m_text[] 数组里并离开方法。在当前版本中, 仅保存调整过的单元格文本, 但不针对标题。
如果文本不合适, 则 应修剪多余的字符, 并添加省略号 ('…')。省略号将表明所显示的文本已被裁剪。这个过程很容易实现:
1)获取字符串长度。
2)之后在循环里从最后一个字符开始遍历所有字符, 删除最后一个字符并将修剪后的文本保存在临时变量中。
如果没有字符剩下, 返回一个空字符串。
4)只要有字符剩下, 获取包含省略号在内的结果字符串宽度。
5)检查依次形成的字符串是否适合表格单元, 同时考虑自单元格边缘指定的偏移。
6)如果字符串合适, 则将其保存到方法的局部变量中并停止循环。
7)此后, 将调整后的字符串保存到 m_text[] 数组中, 并从方法返回。
class CCanvasTable : public CElement {private: //- 返回依据列宽调整的文本 string CorrectingText(const int column_index,const int row_index,const bool headers=false); };//++//| 返回依据列宽调整的文本 |//++string CCanvasTable::CorrectingText(const int column_index,const int row_index,const bool headers=false) {//- 获取当前文本 string corrected_text=(headers)? m_header_text[column_index]: m_vcolumns[column_index].m_vrows[row_index];//- 自单元格边缘在 X 轴上的偏移 int x_offset=m_text_x_offset*2;//- 获取画板对象的指针 CRectCanvas *obj=(headers)? ::GetPointer(m_headers) : ::GetPointer(m_table);//- 获取文本宽度 int full_text_width=obj.TextWidth(corrected_text);//- 如果它适合单元格, 将调整后的文本保存在单独的数组中并返回 if(full_text_width<=m_vcolumns[column_index].m_width-x_offset) { //- 如果这些不是标题, 保存调整后的文本 if(!headers) m_vcolumns[column_index].m_text[row_index]=corrected_text; //- return(corrected_text); }//- 如果文本不适合单元格, 则需要调整文本 (修剪过多字符并添加省略号) else { //- 用来操纵字符串 string temp_text=""; //- 获取字符串长度 int total=::StringLen(corrected_text); //- 从字符串中逐个删除字符, 直到所需文本宽度 for(int i=total-1; i>=0; i) { //- 删除一个字符 temp_text=::StringSubstr(corrected_text,0,i); //- 如果什么都没剩下, 保留空字符串 if(temp_text=="") { corrected_text=""; break; } //- 在检查之前添加省略号 int text_width=obj.TextWidth(temp_text+"..."); //- 如果适合单元格 if(text_width<m_vcolumns[column_index].m_width-x_offset) { //- 保存文本并停止循环 corrected_text=temp_text+"..."; break; } } }//- 如果这些不是标题, 保存调整后的文本 if(!headers) m_vcolumns[column_index].m_text[row_index]=corrected_text;//- 返回调整后的文字 return(corrected_text); }
当重绘表格时使用调整后的字符串, 在更改列宽过程中尤为重要。不必在所有表格单元格中反复替换调整的文本, 只需在列单元格中进行此操作, 宽度将会被改变。这样可以节省 CPU 资源。
CCanvasTable::Text() 方法将确定是否需要调整指定列的文本, 或者发送以前调整的版本即可足矣。其代码如下所见:
class CCanvasTable : public CElement {private: //- 返回文本 string Text(const int column_index,const int row_index); };//++//| 返回文本 |//++string CCanvasTable::Text(const int column_index,const int row_index) { string text="";//- 如果不是更改列宽模式, 调整文本 if(m_column_resize_control==WRONG_VALUE) text=CorrectingText(column_index,row_index);//- 如果处于更改列宽度模式下, 则... else { //- ...仅宽度改变的列需要调整文本 if(column_index==m_column_resize_control) text=CorrectingText(column_index,row_index); //- 对于所有其它, 使用以前调整的文字 else text=m_vcolumns[column_index].m_text[row_index]; }//- 返回文本 return(text); }
以下是 CCanvasTable::ChangeColumnWidth() 方法的代码, 设计用于改变列宽。
最小列宽设为 30 像素。如果标题显示被禁用, 程序将离开方法。如果检查通过, 则在标题的边界处检查焦点。如果检查确定进程尚未开始/完成, 辅助变量为零, 程序离开方法。如果进程正在运行, 则获取光标的相对 X 坐标。如果 进程恰好刚刚开始, 则必须保存当前光标的 X 坐标 (x_fixed 变量) 和所拖拽列的宽度 (prev_width 变量)。涉及此目的的局部变量是静态的。因此, 每次进入此方法时, 它们的值将会保存, 直到进程完成为止。
现在, 计算列的新宽度。如果结果达到 最小列宽度, 程序将离开方法。否则, 指定列的新宽度保存在表格属性的结构中。此后, 重新计算并重新应用表格维度, 并在方法结束时重新绘制表格。
class CCanvasTable : public CElement {private: //- 列的最小宽度 int m_min_column_width; //-private: //- 更改拖拽列的宽度 void ChangeColumnWidth(void); };//++//| 构造器 |//++CCanvasTable::CCanvasTable(void) : m_min_column_width(30) { ... }//++//| 更改拖拽列的宽度 |//++void CCanvasTable::ChangeColumnWidth(void) {//- 如果禁用标题, 离开 if(!m_show_headers) return;//- 检查标题边界的焦点 CheckColumnResizeFocus();//- 辅助变量 static int x_fixed =0; static int prev_width =0;//- 如果完成, 重置该值 if(m_column_resize_control==WRONG_VALUE) { x_fixed =0; prev_width =0; return; }//- 获取 X 轴偏移量 int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);//- 得到鼠标光标的相对坐标 int x=m_mouse.X()-m_headers.X()+xoffset;//- 如果更改列宽的过程刚刚开始 if(x_fixed<1) { //- 保存当前列的 X 坐标和宽度 x_fixed =x; prev_width =m_vcolumns[m_column_resize_control].m_width; }//- 计算列的新宽度 int new_width=prev_width+(x-x_fixed);//- 如果小于指定的极限, 保持不变 if(new_width<m_min_column_width) return;//- 保存列的新宽度 m_vcolumns[m_column_resize_control].m_width=new_width;//- 计算表格大小 CalculateTableSize();//- 调整表的大小 ChangeTableSize();//- 表格重绘 DrawTable(); }
结果如下:
图例. 5. 相对于列的可变宽度调整字符串长度。
事件处理
表格对象的颜色管理 和 更变列宽度 是通过控件的鼠标移动事件 (CHARTEVENT_MOUSE_MOVE) 进行处理。
//++//| 事件处理器 |//++void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) {//- 处理光标移动事件 if(id==CHARTEVENT_MOUSE_MOVE) { //- 如果控件被隐藏, 离开 if(!CElementBase::IsVisible()) return; //- 如果子窗口的数字不匹配, 离开 if(!CElementBase::CheckSubwindowNumber()) return; //- 检查焦点覆盖在元素上 CElementBase::CheckMouseFocus(); m_headers.MouseFocus(m_mouse.X()>m_headers.X() && m_mouse.X()<m_headers.X2() && m_mouse.Y()>m_headers.Y() && m_mouse.Y()<m_headers.Y2()); //- 如果滚动条处于激活状态 if(m_scrollv.ScrollBarControl() || m_scrollh.ScrollBarControl()) { ShiftTable(); return; } //- 更改对象颜色 ChangeObjectsColor(); //- 更改拖拽列的宽度 ChangeColumnWidth(); return; } ... }
需要另一个新的事件标识符, 以便确定鼠标左键状态的变更时刻。需要在事件处理器程序代码的多个模块中摆脱重复检查和并发处理。将 ON_CHANGE_MOUSE_LEFT_BUTTON 标识符添加到 Define.mqh 文件:
//++//| Defines.mqh |//| 版权所有 2015, MetaQuotes 软件公司|//| https://www.mql5.com |//++ #define ON_CHANGE_MOUSE_LEFT_BUTTON (33) // 改变鼠标左键的状态
此外, CMouse::CheckChangeLeftButtonState() 方法已经添加到类中以获取鼠标的当前参数 (CMouse)。它可以确定鼠标左键状态的变化时刻。该方法在类的处理器中调用。如果鼠标左键的状态发生变化, 方法发送消息 ON_CHANGE_MOUSE_LEFT_BUTTON 标识符。消息随后可在任何控件中被接收并处理。
//++//| 获取鼠标参数的类 |//++class CMouse {private: //- 检查鼠标左键的状态变化 bool CheckChangeLeftButtonState(const string mouse_state); };//++//| 处理移动鼠标光标的事件 |//++void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) {//- 处理光标移动事件 if(id==CHARTEVENT_MOUSE_MOVE) { //- 坐标和鼠标左键的状态 m_x =(int)lparam; m_y =(int)dparam; m_left_button_state =CheckChangeLeftButtonState(sparam); ... } }//++//| 检查鼠标左键的状态变化 |//++bool CMouse::CheckChangeLeftButtonState(const string mouse_state) { bool left_button_state=(bool)int(mouse_state);//- 发送关于鼠标左键状态更改的消息 if(m_left_button_state!=left_button_state) ::EventChartCustom(m_chart.ChartId(),ON_CHANGE_MOUSE_LEFT_BUTTON,0,0.0,"");//- return(left_button_state); }
所需的 ON_CHANGE_MOUSE_LEFT_BUTTON 标识符的事件处理在 CCanvasTable 类之中:
将类中的某些字段归零;
调整滚动条;
重绘表格:
//++//| 事件处理器 |//++void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) {//- 更改鼠标左键的状态 if(id==CHARTEVENT_CUSTOM+ON_CHANGE_MOUSE_LEFT_BUTTON) { //- 如果禁用标题, 离开 if(!m_show_headers) return; //- 如果鼠标左键释放 if(!m_mouse.LeftButtonState()) { //- 重置宽度更改模式 m_column_resize_control=WRONG_VALUE; //- 隐藏光标 m_column_resize.Hide(); //- 考虑最近变化来调整滚动条 HorizontalScrolling(m_scrollh.CurrentPos()); } //- 重置标题上最后一个焦点的索引 m_prev_header_index_focus=WRONG_VALUE; //- 更改对象颜色 ChangeObjectsColor(); } }
本文的动画截图展示了 MQL 应用程序的运行结果, 可从下面的链接下载, 以便进一步学习。
结论
函数库的当前更新改进了 CCanvasTable 类型的渲染表格。这不是表格的最终版本。它将进一步开发, 并将添加新的功能。
创建图形界面函数库的当前规划图如下所示。
图例. 6. 函数库结构的当前开发状态。
您可从下面下载最新版本的函数库和文件进行测试。
如果您在使用这些文件中提供的材料时有任何疑问, 可以参考函数库开发系列文章之一的详细描述, 或在本文的评论中提出您的问题。