本文适用于 GUI 开发人员,这些开发人员要编写可移植、可重用和速度更快的控件,用于看到量大且复杂的数据。当前存在一些常见的问题,如性能差,还存在一些可用性问题,如不能清楚地显示大型数据集,所以用户可以很容易地通过浏览本文进行分析。另外,程序数据结构和可视数据表示彼此之间的依赖性通常也变得非常强。因此,控件的专用性变得非常强,如果不进行重要修改,就不能在其他应用程序中使用。本文提供了一种方法,可用来设计复杂的控件,解决以上讨论的问题。本文中,将使用图表查看器控件的一些示例来说明基本概念。这些概念还可以适用于多种多样的其他控件。
定义
图表代表一组对象及对象之间的关系。对象叫做节点。节点之间的关系叫做边。因此,一个可视图表就是一组节点(有或没有标签的正方形、长方形、圆等)和连接节点的边(直线或曲线)。定义节点与边的位置的算法叫做布局。
请注意,节点中可以包含其他节点和边(子图表)。如果某些节点的边从这些节点起连接到任一个给定节点,那么这些节点叫做这个给定节点的父节点。如果某些节点的边从任一个给定节点起连接到这些节点,那么这些节点叫做这个给定节点的子节点。
| |
|
| |
|
图表控件中的常见问题
在很多应用程序中都使用图表控件来显示数据。如果要在不同的应用程序中使用相同的图表控件,就必须提供一种方法来自定义该图表控件。这种自定义一定不能影响应用程序的性能。以下是在不同应用程序中应用控件时可能遇到的各种问题列表:
- 节点和边的外观。图表元素可以有不同的颜色与形状,可以有文本标签(也可以没有)等等。不可能提前预知图表元素的外观,也不能提前实现图表元素。此外,可能存在不同情况下使用的很多图表布局。您可能需要一种方法来根据应用程序对布局进行更改。
- 用户交互。您有时候可能显示一个静态图表,这种图表不能更改,而有时您又允许用户用某种方式更改图表(添加或删除节点和边,移动节点与边、更改标题等等)。
- 处理外部数据。当处理大型数据集时,经常会收到来自外部源的数据,如本地文件或远程数据库。
图表控件上必须提供滚动和缩放工具才能浏览大型图表。下一部分说明如何灵活地解决这个问题而不降低任何性能。
自定义的外观和用户交互
通常您可以区别控件的两个部分,即区别数据元素与核心部分,数据元素表示数据的各个部分,核心部分负责将数据组织为一个整体。图表数据元素由节点和边组成。当需要时,核心部分会使用数据元素的自定义版本来提供一些功能,如滚动、缩放、绘图和事件处理。例如,当您单击鼠标按钮时,核心部分会定义这个事件发生的位置。如果事件发生在某个数据元素上,那么事件信息会传递到这个数据元素的处理程序,此外核心部分会处理事件本身。
图表中有两样东西项目会随着不同应用程序而改变,您应当进行自定义。一个是数据元素的外观与行为;另一个是组织元素的方式。如果要很容易地自定义控件,就需要为这些东西定义界面,然后仅通过界面将这些东西用于控件。所以,如果要进行某种更改,只需要采用新的方式实现界面,无需更改控件中的代码。这就是所谓的“策略”模式。
图表控件采用了以下策略:
- INodeHandler
- IEdgeHandler
- ILayout
class INodeHandler
{
public:
// 绘制给定的节点
virtual void Draw(Node) = 0;
// 返回描述正方形的尺寸。此函数在布局中用来确定
// 节点位置,没有交叉点。
virtual idvc::dsize GetSize(Node)= 0;
// 设置所有后续 Draw 调用函数中要使用的缩放系数。此函数
// 在实现缩放时由控件的核心部分使用。
virtual void SetZoomFactor(double f) {};
// 处理鼠标单击事件
virtual ChangesType HandleClick(Node n, double inX, double inY,
int kstate, idvc::MouseButton Button);
// 处理工具提示事件
virtual ChangesType HandleOnTooltip(Node n, CGraphTooltipEvent* pEvent); }; // 结束 INodeHandler
}; // 结束 INodeHandler
class ILayout
{
public:
/// 这个函数应为给定节点内的所有节点
/// 生成新布局,并计算新布局的尺寸。内部节点的位置
/// 必须根据给定节点的左上角进行定义,
/// 假使给定节点的坐标是 (0,0)。
virtual void Make( Node ) = 0;
/// 与 Make 一样,但是应使用以前布局
/// 的信息,然后尝试保持已摆放节点
/// 的相对位置。
virtual void Update( Node ) = 0;
/// 这个函数在更改了已拥有节点的尺寸
/// 或跳过 ILayout 的参数时
/// 用于重新计算节点与边的坐标。
/// 它假定以前调用过 Make 或 Update,而且
/// 所有已拥有节点的尺寸。与 Make 和 Update 不同,它
/// 不可递归。
virtual void Resize( Node ) = 0;
};
以上类定义了三种不同情况下的布局策略函数,即:
- 图表需要完全重新排列时
- 图表结构已部分更改,只需要重新排列更改部分时
- 只有节点尺寸更改了,需要重新计算坐标(不需要定义节点和边的相对位置)时
定义这种区别的主要目的是为了减少布局计算时间。如果向某个大型图表中添加一个节点,就不需要重新计算整个图表的布局。
快速绘制和事件处理
应解决的最后但并非不重要的问题是,如何快速对事件做出反应(至少是重新绘制事件)。当处理大型数据集时,控件应允许您快速地滚动和缩放内容。此处的主要问题是,事件处理和绘图函数是由用户定义的(通过上面描述的界面),而控件中的每个元素在绘图和事件处理中可以采用自己的实现方式。因此,不能保证快速进行处理。不过,可以减少元素函数调用的数量。
void CContent::DrawContent(idvc::IPainter* p)
{
// 确定应重新绘制的无效长方形
idvcfrw::CInvalidRegion InvalidRegions(draw_rect, valid_rect);
for(int i = 0; i < InvalidRegions.size(); ++i)
{
// 得到对应于下一个无效区域的长方形
idvc::drect rect = InvalidRegions[i];
// 查找并重新绘制与无效长方形相交的节点
NodeSet nodes = graph->HitNodeTest(rect.left, rect.top, rect.right, rect.bottom);
for_each(nodes.begin(), nodes.end(), DrawNode(p,scale));
// 查找并重新绘制与无效长方形相交的边
EdgeSet es = graph->HitEdgeTest(rect.left, rect.top, rect.right, rect.bottom);
for_each(es->Begin(), es->End(), DrawEdge(p,scale));
};
};
利用窗口事件中也拥有发生事件的点或长方形这一事实,可采用与绘图类似的方式来组织事件处理(至少对于窗口事件而言)。这样,控件可以确定节点与边,这些节点与边受任何给定事件的影响,而且只针对这些元素调用事件处理函数,因此大大地减少了处理时间。
数据加载
当处理大型数据集时,这些数据集通常存储在某个外部数据源中。外部数据源可能随着应用程序的不同而有所不同(文件、数据库等)。因此,您需要使用一种机制来独立地从外部数据源快速地加载数据。快速常常意味着加载部分数据,因为如果要真正地获得大型数据集,无论如何都不能快速地执行加载。但是,控件一般只需要数据中的一小部分来进行处理,您应当只加载这一部分数据。
有两种方法可用来实现部分加载。第一种类似于上面说明的快速绘图和事件处理。您需要定义一个界面,这个界面与数据源无关,可用来加载数据。您应当尝试实现以下方法,即可以用于定义需要加载的数据,而不是执行全部加载。然后可以定义应加载的数据元素,通过界面只针对这些元素调用加载函数。
不总是可以将元素定义为自动加载。另一种实现部分加载的方法是让用户输入。这种情况下,用户负责定义应何时加载数据,以及应加载哪些数据。
ChangesType PortNodeHandler::HandleClick(Node n, double inX, double inY,
int kstate, idvc::MouseButton Button)
{
ChangesType processed = ctNone;
idvc::dpoint pos = n->GetPosition();
idvc::dsize size = n->GetSize();
// 如果节点没有嵌套的节点且使用鼠标左键对其单击
if ( (n->GetOwned()->GetCount() == 0) && (Button == idvc::mbLeft) )
{
if (node_drawer.IsLeftPortClicked(n, inX, inY))
{
bool hide = ( CountAllParents(n) == CountVisibleParents(n) );
// 如果所有父节点可见
if( hide ) Fold(n, fdParents);
else Unfold(n, fdParents);
}
else if (node_drawer.IsRightPortClicked(n, inX, inY))
{
bool hide = ( CountAllChildren(n) == CountVisibleChildren(n) );
// 如果所有子节点可见
if( hide ) Fold(n, fdChildren);
else Unfold(n, fdChildren);
}
else
{
// 如果用户单击节点本身,则会选中它
SetFlag(n, Node::fSelected, !IsFlagSet(n, Node::fSelected));
};
processed = ctAll;
};
OnClick.fire(n, inX, inY, kstate, Button);
return processed;
};
结论
以下是创建可移植、快速控件中的主要概念:
- 将控件分为两个部分。第一个部分是可自定义的类,表示组织的数据元素和方法(策略)。第二部分是核心(永久)部分,提供诸如滚动、缩放、绘图和事件处理等常用功能。核心部分只在需要时才通过严格定义的界面使用策略。
- 核心部分应最大程度地减少自定义部分的调用。在处理大型数据集时,每次只绘制或处理一小部分子集。所以,如果可以实现所需子集的快速选择,并仅针对这个子集调用自定义部分,那么性能将会有所提高。
- 当使用外部数据源时,控件应最大程度地减少数据加载。通过两种方法可以达到此目的。第一种与最大程度地减少自定义部分的调用相同。请注意,应当最大程度地减少外部数据源的调用。当可以进行选择性加载且可以提前确定应加载的数据时,这种方法可以使用。第二种方法涉及用户交互。当用户每次处理小型数据子集时,可以采用以下方法实现自定义部分,也就是可视元素允许用户手工选择要加载的元素来进一步处理。
下图描述了如何将这些原则应用到图表控件:

图 1. 图表设计
使用这些原则可获得高度自定义、可移植且快速的控件,这些控件可处理大型数据集,也可进行调整以便用于很多应用程序。



方便我们称呼您,我公司将承诺对于您的个人信息将完全保密

