可移植和可重用 GUI 控件的设计

转帖|其它|编辑:郝浩|2010-08-13 12:15:49.000|阅读 841 次

概述:本文适用于 GUI 开发人员,这些开发人员要编写可移植、可重用和速度更快的控件,用于看到量大且复杂的数据。当前存在一些常见的问题,如性能差,还存在一些可用性问题,如不能清楚地显示大型数据集,所以用户可以很容易地通过浏览本文进行分析。另外,程序数据结构和可视数据表示彼此之间的依赖性通常也变得非常强。因此,控件的专用性变得非常强,如果不进行重要修改,就不能在其他应用程序中使用。本文提供了一种方法,可用来设计复杂的控件,解决以上讨论的问题。

# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>

  本文适用于 GUI 开发人员,这些开发人员要编写可移植、可重用和速度更快的控件,用于看到量大且复杂的数据。当前存在一些常见的问题,如性能差,还存在一些可用性问题,如不能清楚地显示大型数据集,所以用户可以很容易地通过浏览本文进行分析。另外,程序数据结构和可视数据表示彼此之间的依赖性通常也变得非常强。因此,控件的专用性变得非常强,如果不进行重要修改,就不能在其他应用程序中使用。本文提供了一种方法,可用来设计复杂的控件,解决以上讨论的问题。本文中,将使用图表查看器控件的一些示例来说明基本概念。这些概念还可以适用于多种多样的其他控件。

  定义

  图表代表一组对象及对象之间的关系。对象叫做节点。节点之间的关系叫做边。因此,一个可视图表就是一组节点(有或没有标签的正方形、长方形、圆等)和连接节点的边(直线或曲线)。定义节点与边的位置的算法叫做布局。

  请注意,节点中可以包含其他节点和边(子图表)。如果某些节点的边从这些节点起连接到任一个给定节点,那么这些节点叫做这个给定节点的父节点。如果某些节点的边从任一个给定节点起连接到这些节点,那么这些节点叫做这个给定节点的子节点。

  图表控件中的常见问题

  在很多应用程序中都使用图表控件来显示数据。如果要在不同的应用程序中使用相同的图表控件,就必须提供一种方法来自定义该图表控件。这种自定义一定不能影响应用程序的性能。以下是在不同应用程序中应用控件时可能遇到的各种问题列表:

  • 节点和边的外观。图表元素可以有不同的颜色与形状,可以有文本标签(也可以没有)等等。不可能提前预知图表元素的外观,也不能提前实现图表元素。此外,可能存在不同情况下使用的很多图表布局。您可能需要一种方法来根据应用程序对布局进行更改。
  • 用户交互。您有时候可能显示一个静态图表,这种图表不能更改,而有时您又允许用户用某种方式更改图表(添加或删除节点和边,移动节点与边、更改标题等等)。
  • 处理外部数据。当处理大型数据集时,经常会收到来自外部源的数据,如本地文件或远程数据库。

  图表控件上必须提供滚动和缩放工具才能浏览大型图表。下一部分说明如何灵活地解决这个问题而不降低任何性能。

  自定义的外观和用户交互

  通常您可以区别控件的两个部分,即区别数据元素与核心部分,数据元素表示数据的各个部分,核心部分负责将数据组织为一个整体。图表数据元素由节点和边组成。当需要时,核心部分会使用数据元素的自定义版本来提供一些功能,如滚动、缩放、绘图和事件处理。例如,当您单击鼠标按钮时,核心部分会定义这个事件发生的位置。如果事件发生在某个数据元素上,那么事件信息会传递到这个数据元素的处理程序,此外核心部分会处理事件本身。

  图表中有两样东西项目会随着不同应用程序而改变,您应当进行自定义。一个是数据元素的外观与行为;另一个是组织元素的方式。如果要很容易地自定义控件,就需要为这些东西定义界面,然后仅通过界面将这些东西用于控件。所以,如果要进行某种更改,只需要采用新的方式实现界面,无需更改控件中的代码。这就是所谓的“策略”模式。

图表控件采用了以下策略:

  1. INodeHandler
  2. IEdgeHandler
  3. 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. 将控件分为两个部分。第一个部分是可自定义的类,表示组织的数据元素和方法(策略)。第二部分是核心(永久)部分,提供诸如滚动、缩放、绘图和事件处理等常用功能。核心部分只在需要时才通过严格定义的界面使用策略。
     
  2. 核心部分应最大程度地减少自定义部分的调用。在处理大型数据集时,每次只绘制或处理一小部分子集。所以,如果可以实现所需子集的快速选择,并仅针对这个子集调用自定义部分,那么性能将会有所提高。
     
  3. 当使用外部数据源时,控件应最大程度地减少数据加载。通过两种方法可以达到此目的。第一种与最大程度地减少自定义部分的调用相同。请注意,应当最大程度地减少外部数据源的调用。当可以进行选择性加载且可以提前确定应加载的数据时,这种方法可以使用。第二种方法涉及用户交互。当用户每次处理小型数据子集时,可以采用以下方法实现自定义部分,也就是可视元素允许用户手工选择要加载的元素来进一步处理。

  下图描述了如何将这些原则应用到图表控件:
 

  图 1. 图表设计

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

 


标签:

本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com

文章转载自:网络转载

为你推荐

  • 推荐视频
  • 推荐活动
  • 推荐产品
  • 推荐文章
  • 慧都慧问
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP