高速绘图控件

ChartImage1a.JPG

ChartImage2a.jpg

ChartImage3a.jpg

内容

  • 介绍
  • 免责声明
  • 特性
  • 文档结构
  • 开始 使用资源编辑器手动插入
  • 操作数据系列 在点上添加标签的酒吧系列
  • 操纵轴 轴在离散模式使用日期/时间轴
  • 自定义外观
  • 鼠标事件的通知 听图表上的一般事件听鼠标上的一系列事件
  • 使用游标
  • 使用了平移和缩放功能
  • 利用高速功能
  • 扩展功能
  • 从版本1升级。x至版本2.0
  • 从版本2升级。到版本3.x
  • 例子
  • 反馈
  • 历史
  • 谢谢
  • 链接

介绍

在我之前的一个项目中,我需要在一个图表控件上显示连续的数据流。我决定开发自己的控件,因为我找不到任何可以提供所需灵活性的自由控件。其中一个主要的限制是控件必须绘制大量的数据并能够快速地显示(在Pocket PC上)。通过只绘制新的数据点,而不是完整的序列,控件可以做到这一点。图表还能够显示静态数据。

这种控制是长时间工作的结果,有时为了提供足够灵活的东西供需要它的人使用而遇到挫折。我将非常感激反馈:一封邮件,留言板上的一篇文章或仅仅通过评价文章。当我不知道是否使用这个控件时,对我来说保持这个控件是没有意义的。

免责声明

这种控制是大量时间开发的结果,因此我在代码的使用上放置了一些次要的条件:

This code may be used for any non-commercial
and commercial purposes in a compiled form.
The code may be redistributed as long as it remains
unmodified and providing that the author name
and the disclaimer remain intact. The sources
can be modified with the author consent only.

This code is provided without any guarantees.
I cannot be held responsible for the damage or
the loss of time it causes. Use it at your own risks.

考虑到在此控件的开发上所花费的精力,这并不过分。如果此代码用于商业应用程序,那么请发送邮件让我知道。

特性

该控制的主要特点是:

  • 高速绘图(当轴是固定的),允许快速绘制数据
  • 无限级数(内存是限制)
  • 每个系列无限制的数据量
  • 线,点,面,酒吧,烛台和甘特系列提供
  • 最多四个轴(左、下、右、上轴)
  • 标准、对数或日期/时间轴
  • 自动和/或倒置轴(彼此独立)
  • 轴标签
  • 点标签
  • 光滑的曲线
  • 网格
  • 传说和标题
  • 交互性(控件中发生特定事件时的通知)
  • 支持手动缩放和鼠标平移
  • 支持游标
  • 支持轴上的滚动条
  • 高度可定制(颜色、标题、标签、边缘、字体等)
  • 对UNICODE的支持
  • 支持打印和保存到一个图像文件

文档结构

本文组织为一系列简短的教程,涵盖了控件的大多数方面。阅读完本文后,您将能够快速开始在自己的应用程序中使用该控件。

我决定从本文中删除所有类和函数的文档,因为它对用户不太友好,而且对我来说维护起来很困难。此外,随着代码的增长,要记录的类和函数的列表变得过于丰富,无法将所有内容都放在本文中。相反,我提供了一个可以从本文下载的doxygen文档:只需下载“doxygen文档”zip文件,解压缩所有文件并双击“索引”。html文件。

开始

此图表控件允许您在屏幕上绘制一系列数据。几个系列的不同类型可以添加到控制和多达四个轴可以使用。添加到图表中的系列与一个水平轴(底部或顶部)和一个垂直轴(右或左)相关联。这两个轴控制系列将如何在图表上显示。

为了能够在应用程序中使用图表控件,首先需要在项目中添加source压缩包中包含的文件。

重要提示:该控件在内部使用动态强制转换,因此必须启用RTTI(运行时类型信息),否则可能会发生崩溃。RTTI是不启用默认为VC6,所以要启用它,打开项目设置->“C / c++”选项卡→“c++语言”类,并确保选中了“启用运行时类型信息(RTTI)”复选框。

在应用程序中有两种使用图表控件的方法:手动插入,或者通过资源编辑器插入。

手动插入

    在对话框类中添加一个CChartCtrl类型的变量: 隐藏,复制代码/ / {{AFX_DATA (CChartDemoDlg) / /}} AFX_DATA CChartCtrl m_ChartCtrl; 在dialog类的OnInitDialog中,调用控件的Create方法。

使用资源编辑器

      在对话框资源中添加自定义控件,打开控件的属性,并为Class属性指定ChartCtrl。为了避免滚动条上的闪烁,您必须设置WS_CLIPCHILDREN样式(0x02000000L),如图所示。
Chart properties
    # include“ChartCtrl。h"在对话框头文件的开头。在对话框类中添加一个CChartCtrl类型的变量: 隐藏,复制代码/ / {{AFX_DATA (CChartDemoDlg) / /}} AFX_DATA CChartCtrl m_ChartCtrl; 在对话框类的CPP文件的DoDataExchange函数中添加一个DDX_Control行(不要忘记将ID和名称更改为适当的值): 隐藏,复制co全无CChartDemoDlg::DoDataExchange(CDataExchange* pDX) { CDialog: DoDataExchange (pDX); / / {{AFX_DATA_MAP (CChartDemoDlg) //用适当的ID和变量名添加这一行 DDX_Control (pDX IDC_CHARTCTRL m_ChartCtrl); / /}} AFX_DATA_MAP }

操作数据系列

几种类型的数据系列可以增加到控制:点系列,线系列,表面系列,条形系列,烛台系列或甘特系列。点的数据格式可能因系列而异(例如,candlestick系列和gantt系列使用不同的点格式)。

系列类型描述创建功能点类型点序列每个数据点由屏幕上的单个点表示。点的外观可以自定义线串数据点通过一条线连接。这条线的外观可以定制,也可以平滑表面序列数据点通过一条线连接,在这条线下面的区域用特定的画笔填充。该系列也可以垂直显示条形序列每个数据点被绘制成一定宽度的竖线。多个酒吧系列可以叠在一起,彼此不重叠。木条也可以水平绘制烛台序列每个数据点由五个属性组成:低值、高值、开放值、关闭值和X值(时间)。每一个点都画成一个烛台。本系列用于绘制金融数据每个数据点由三个属性组成:开始时间和结束时间以及一个Y值。每个点都被画为一个水平条,从开始时间开始到结束时间结束。该工具条以其Y值沿Y轴定位

选择了该系列之后,可以通过调用右侧列中列出的CChartCtrl类的helper函数之一将其添加到图表中。每个函数都接受两个可选参数:两个布尔值,用于描述级数是否附加到二级水平轴(上轴)和二级垂直轴(右轴)上。如果没有指定参数,则该系列附加到主水平轴(底部轴)和主垂直轴(左侧轴)上。

警告:在向图表中添加任何系列之前,您至少需要创建该系列所附的两个轴。如果不这样做,将导致控件断言。有关更多信息,请参阅“操纵轴”一节。

将这个系列添加到图表后,就可以用数据填充它了。有两种方法可以做到这一点:要么在一个块中设置数据,要么逐点添加数据。当有动态数据时使用后者:每次调用函数时都会更新图表。尽管这个调用非常快(在某些特定条件下),但在可能的情况下,最好在一个块中设置点。下面是一个小代码示例,它在图表中创建了两个系列并使用数据填充它们:一个系列在初始化时完全填充,另一个系列在调用函数OnDataReceived时填充(该函数仅在本示例中存在)。m_pLineSeries, m_pPointsSeries和m_ChartCtrl是CMyClass类的成员变量。

void CMyClass::Init()
{
  ....  // SNIP: Creation of the axes in the chart. This MUST be done before.
  m_pLineSeries = m_ChartCtrl.CreateLineSerie();
  m_pPointsSeries = m_ChartCtrl.CreatePointsSerie();

  double YValues[10];
  for (inti=0;i<10;i++)
    XValues[i] = YValues[i] = i;
  m_pLineSerie->SetPoints(XValues,YValues,10);
}

void CMyClass::OnDataReceived(double X, double Y)
{
  m_pPointsSeries->AddPoint(X, Y);
}

所有系列类都继承同一个抽象基类:CChartSerie。这个类处理所有系列通用的一般管理,但不了解点数据。点的概念是在子类CChartSerieBase中引入的,它是一个模板类,模板参数是要作为点操作的数据类型。这一点很重要,因为系列可能必须处理不同的数据类型:例如,point系列操作具有X和Y值的点,而candlestick系列操作具有5个值的点(开、关、高、低和时间值)。所有后续的系列都继承自CChartSerieBase,并提供它们所操作的数据类型。CChartSerieBase类已经处理了大部分数据管理,并通过纯虚函数将呈现委托给子类。每个系列在创建时也被分配一个Id。这个Id可以通过CChartSerie::GetSerieId()检索,并可用于从图表中删除该系列。

序列的一个重要特性是控制点的顺序:序列中的所有点将根据它们的值重新排序。默认情况下,这些点是根据它们的X值排序的,但是您可以通过根据它们的Y值排序来改变这种行为,或者根本不排序(在这种情况下,序列将保持添加到序列中的点的顺序)。点的顺序会对性能产生影响:如果点是有序的,控件就能够检索整个序列中第一个和最后一个可见点,并且只绘制中间的点。另一方面,你不能画出像椭圆这样的曲线。您可以通过调用CChartSerieBase:: setseriesordered来更改点的顺序。

控件中的不同系列通常是不言自明的。然而,酒吧系列需要一些解释。

酒吧系列

这个系列有点特殊,因为如果它们中的几个被绘制在同一个控件上,它们将相互影响。其目的是能够在不重叠的情况下绘制多个柱状序列:它们是相邻绘制的。为此,需要指定它们各自所属的组(一个简单的整数标识符)。同一组的系列画在一起(或在彼此之上的水平条):见两个图的例子。组Id的设置是通过SetGroupId函数完成的。

bars1.jpgbars2.jpg具有相同组Id的Bar系列 Bar系列与不同的组Id

您还可以通过调用SetInterSpace静态函数来控制所有条之间留下的空间的宽度。这将为所有序列设置以像素为单位的空间(因此,如果显示了两个以上的序列,则在所有地方使用相同的空间)。注意,您可以通过调用SetBarWidth来分别设置栏系列的宽度。

在点上添加标签

在用数据填充了系列之后,还可以在系列的特定点上添加标签:一个标签总是附加在特定点上。目前,只提供了一种类型的标签,气球标签:一个包含文本的圆角矩形,它用一条线连接到点上。当然,如果需要,您也可以提供自己的自定义标签(请参阅“扩展功能”一节)。

提供文本标签有两种方法:在创建标签时静态提供,或者通过注册一个对象在标签请求时提供文本来动态提供。第一种方法是最简单的,但也没有那么灵活。这里是一个代码片段,展示了如何这样做(假设m_pSeries已经创建和填充了足够的数据):

void CMyClass::Init()
{
  // SNIP...

  m_pSeries->CreateBalloonLabel(5,_T("This is a simple label"));
}

这个调用创建了一个带有文本“This is a simple label”的标签,并将其附加到索引5的点上。该函数返回一个指向新创建的标签的指针,以便您可以修改它的一些属性或将其存储起来供以后使用。

第二种方法稍微复杂一些,但提供了更多的灵活性:例如,您可以以更简单的方式显示标签中的点属性(例如X值、Y值……)。为此,您必须创建一个继承自cchartlabelprovider&pointtype>的类。并在创建标签时提供该类的实例。这个类是一个模板类,模板参数是附加标签的系列的点类型。这个类是一个简单的接口,您必须重写TChartString GetText(cchartseriebase&pserie, unsigned uPtIndex)方法。这个函数应该返回必须在标签中显示的文本。它接收一个指向该标签所附加的序列和点索引的指针。下面是一个标签提供程序类的例子:

class CCustomLabelProvider : public CChartLabelProvider<SChartXYPoint>
{
public:
  TChartString GetText(CChartSerieBase<SChartXYPoint>* pSeries, unsigned uPtIndex)
  {
    TChartStringStream ssText;
    SChartXYPoint Point = pSeries->GetPoint(uPtIndex);
    ssText << _T("X value=")  << Point.X;
    return ssText.str();
  }
};

这个代码片段展示了如何将其与标签一起使用。注意,m_pSeries应该是一个操作SChartXYPoint点的系列(点、线、面或条系列)。如果不是这样,您的代码将给出一个编译错误。

void CMyClass::Init()
{
  // SNIP...

  m_pLabelProvider = new CCustomLabelProvider();
  m_pSeries->CreateBalloonLabel(5, m_pLabelProvider);
}

控件不接受指针的所有权,所以当它不再需要时,由您负责删除它。在上面的例子中,它通常会在CMyClass析构器中被删除。在上面的例子中,你当然可以为所有你想添加的标签重用相同的标签提供程序,这还有另一个好处:如果你想在运行时改变标签的格式,你只需要在CustomLabelProvider中添加代码。不需要遍历所有现有的标签并更改其文本。当然,在这种情况下需要刷新控件,因为必须重绘标签。还要注意TChartStringStream类的使用,它是控件提供的类型定义(类似于TChartString)。定义UNICODE时解析为std::wstringstream,未定义UNICODE时解析为std::stringstream。

操纵轴

轴是图表的一个重要特性,因为它们控制控件中不同系列的显示方式。最多有四个轴可以在控制中使用:底部,顶部,左边和右边。每个系列的控制必须附在一个水平轴和一个垂直轴。这些轴是在图表中添加级数时指定的。底部和左侧轴是主要轴,顶部和右侧轴是次要轴(您将在控件的某些功能中遇到这种情况)。您可以选择三种类型的轴:标准轴,对数轴和日期/时间轴。当然,您可以在不同的轴位置选择不同类型的轴。

一旦您选择了在不同位置使用哪个轴,您需要在能够向控件添加任何数据之前创建它们。为此,只需通过指定轴连接的位置调用CreateStandardAxis、CreateLogarithmicAxis或CreateDateTimeAxis。如果在那个位置已经创建了一个轴,控件将销毁它,并用新的轴替换它。下面是一个简单的代码片段,展示了如何在底部创建日期/时间,在左侧创建一个标准轴:

void CMyClass::Init()
{
  CChartStandardAxis* pBottomAxis =
	m_ChartCtrl.CreateStandardAxis(CChartCtrl::BottomAxis);
  CChartLogarithmicAxis* pLeftAxis =
	m_ChartCtrl.CreateLogarithmicAxis(CChartCtrl::LeftAxis);
}

一旦您创建了这些轴,您就可以为它们设置一些属性。大多数属性在所有的轴类型之间共享(例如自动模式,最小值和最大值,轴标签,…)。一个轴可设置三种“自动”模式:全自动、屏幕自动和手动模式。

  • 全自动模式计算轴最小和最大的值基于附加到这个轴的所有系列(所有系列的所有点的最小值作为最低为axis和所有系列的所有点的最大值作为最大的轴)。
  • 屏幕自动模式计算轴的最小值和最大值基于所有系列的所有可见的点与这个轴。例如,如果所附图表只显示一个系列手动下轴和一个屏幕自动左轴,然后左轴将适应点目前可见的,而不是考虑的范围之外的点轴底部外(在全自动模式下,点轴底部会被考虑)。警告:如果一个系列的两个轴都在屏幕自动模式,结果是未定义的。
  • 在手动模式下,轴的最小值和最大值是由用户设置的,而不是由控制计算。

如果动态向控件添加数据,如果新的数据点超出了该轴的范围,则使用自动轴将刷新控件。下面是一个代码片段(继续上一个),显示了一个全自动轴(底部轴)和一个手动轴(左侧轴,对数轴):

void CMyClass::Init()
{
  // SNIP ...
  pBottomAxis->SetAutomaticMode(CChartAxis::FullAutomatic);
  // The call to SetAutomaticMode(CChartAxis::NotAutomatic) is not
  // really needed because this is the default.
  pLeftAxis->SetAutomaticMode(CChartAxis::NotAutomatic);
  pLeftAxis->SetMinMax(0.01,1000);
}

离散模式轴

所有轴的一个共同特征是离散模式,默认情况下是禁用的。激活时,此模式指定轴不显示连续范围的值,而只显示离散值,即由轴上的刻度指定的离散值。所有其他值都不由轴表示。不可能绘制与显示的刻度值不同的值。让我们举一个例子来让事情更清楚:假设您有一个底部标准轴,其刻度间隔为1.0(因此,显示的刻度为1、2、3等等)。尝试绘制X值为0.5的点将显示该点与它的值为1.0相同的位置。实际上,您可以认为两个节拍之间的区域是一个常数值。这就是为什么滴答标签显示在两个滴答的中间,而不是在滴答本身的原因。

下面是一个小代码片段,它显示了离散轴对系列的显示方式的影响。代码片段下的两个图像显示了启用离散模式(第一个图像)或禁用离散模式(第二个图像)的结果。

void CMyClass::Init()
{
  CChartStandardAxis* pBottomAxis =
    m_ChartCtrl.CreateStandardAxis(CChartCtrl::BottomAxis);
  pBottomAxis->SetMinMax(0, 10);
  CChartStandardAxis* pLeftAxis =
    m_ChartCtrl.CreateStandardAxis(CChartCtrl::LeftAxis);
  pLeftAxis->SetMinMax(0, 10);

  pBottomAxis->SetTickIncrement(false, 1.0);
  pBottomAxis->SetDiscrete(true);
  CChartLineSerie* pSeries = m_ChartCtrl.CreateLineSerie();
  double XVal[20];
  double YVal[20];
  for (int i=0; i<20; i++)
  {
    XVal[i] = YVal[i] = i/2.0;
  }
  pSeries->SetPoints(XVal,YVal,20);
}
DiscreteAxis.jpgNonDiscreteAxis.jpg离散模式启用 离散模式禁用

使用日期/时间轴

日期/时间轴使用起来有点特殊,所以下面解释一下如何利用这个特性。理解日期/时间轴的重要一点是,它们在内部与COleDateTime对象一起工作。原因很简单:COleDateTime是DATE类型的包装类,DATE类型只是一个double。由于图表中的点是用双值表示的,所以它非常适合:使用标准点(非日期/时间)和日期/时间点之间没有区别,这使得后者的使用不那么复杂。所有的积分仍然以双倍的形式存储,无论它们是否是日期/时间。

一旦创建了日期/时间轴,就可以在控件中填充数据。为此,没有任何改变:你必须从CChartSerie类中调用void AddPoint(双X,双Y)或void SetPoints(双X,双Y, int Count)。CChartCtrl类提供了两个静态函数,可以让你从一个COleDateTime转换为一个double,反之亦然:

double DateToValue(const COleDateTime& Date)
COleDateTime ValueToDate(double Value)

如果你有另一种格式的日期(例如time_t或SYSTEMTIME),这不是问题,因为COleDateTime对象可以用不同的时间格式构造(检查COleDateTime类的MSDN文档,看看你可以用哪种格式构造它)。

填充完数据后,可以配置axis以显示所需的内容。有几个与日期/时间轴相关的功能可用:

void SetDateTimeIncrement(TimeInterval Interval, int Multiplier)
void SetDateTimeFormat(bool bAutomatic, const TChartString& strFormat)
void SetReferenceTick(COleDateTime referenceTick)

第一个选项允许指定轴上显示的两个刻度之间的间隔。两个滴答之间的间隔将遵循正确的时间,这意味着如果您指定一个滴答增量为1个月(interval =CChartAxis::tiMonth和乘法器=1),那么两个滴答之间的间隔将是不规则的(28、30或31天)。第二个函数允许指定标记的格式。控件会根据打点间隔自动格式化打点标签,但是您可以通过调用此函数来覆盖它。查看MSDN上的COleDateTime::Format函数的文档以获得更多信息。最后,SetReferenceTick(COleDateTime referenceTick)函数允许您为轴指定一个引用刻度。参考刻度是用来作为绘制刻度的参考的日期:刻度将始终出现在这个日期。当您在SetDateTimeIncrement函数中指定的乘数不同于1时,这是有用的。例如,假设您指定了3个月的滴答增量,并且您想为February(以及May, August,…)设置一个滴答,那么您可以调用这个函数来设置February 1作为引用滴答。默认设置为2000年1月1日。

下面是一个简单的代码片段,它创建了一个日期/时间轴,并显示了不同函数的用法:

void CMyClass::Init()
{
  // Sets the axis min value to January 1st 2006 and the axis
  // max value to December 31st 2007.
  COleDateTime minValue(2006,1,1,0,0,0);
  COleDateTime maxValue(2007,12,31,0,0,0);
  pBottomAxis->SetMinMax(CChartCtrl::DateToValue(minValue),
			CChartCtrl::DateToValue(maxValue));
  // Sets the tick increment to 4 months (disable automatic tick increment)
  pBottomAxis->SetTickIncrement(false, CChartDateTimeAxis::tiMonth, 4);
  // Sets the tick label format for instance "Jan 2006"
  pBottomAxis->SetTickLabelFormat(false, _T("%b %Y"));
}

自定义外观

控件的视觉方面可以很容易地适应不同的需求。控件的不同部分(图例、标题、背景……)可以被修改以获得您想要的方面。与这些对象的所有交互都将通过CChartCtrl类进行:一些将按需创建(例如轴或系列),而另一些将在创建控件时创建(图例、标题等)。通常,您不会自己创建这些对象,而是将该任务委托给CChartCtrl类。惟一的例外是当您想使用自定义轴或自定义系列时(请参阅“扩展功能”一节)。例如,下面是一个设置渐变背景和停靠在控件底部图例的代码片段:

void CMyClass::Init()
{
  // SNIP

  // Disable the refresh of the control
  m_ChartCtrl.EnableRefresh(false);
  // Set the gradient for the background
  m_ChartCtrl.SetBackGradient(RGB(255,255,255),RGB(125,125,255),gtVertical);
  // Dock the legend at the bottom
  m_ChartCtrl.GetLegend()->DockLegend(CChartLegend::dsDockBottom);
  // Specifies that the legend entries are horizontally stacked
  m_ChartCtrl.GetLegend()->SetHorizontalMode(true);
  // Re-enable the refresh of the control
  m_ChartCtrl.EnableRefresh(true);
}
重要提示:自从该控件的版本1.4以来,每次调用修改控件上的属性都将导致该控件的完全刷新(甚至像更改某些文本的字体或对象的颜色这样的事情)。为了避免在不必要的时候刷新控件(例如,当你同时改变几个属性时),你应该首先禁用刷新,改变属性,然后重新启用刷新,如上面的代码片段所示。

从该控件的1.5版本开始,就引入了对UNICODE的支持。所有出现的std::string对象都被TChartString对象所取代,TChartString对象是一个简单的类型定义,如果UNICODE未被启用,它解析为std::string,如果UNICODE被启用,它解析为std::wstring。

鼠标事件的通知

有时,得到关于特定用户操作的通知并对它们作出适当的反应是有用的。例如,如果用户单击一个点,程序可以显示有关被单击点的信息。现在通过图表控件就可以做到这一点,本节将解释如何做到这一点。

不管您是想要监听图表本身上的一般鼠标事件(单击轴、图例……),还是对特定系列上的鼠标事件感兴趣,原理都略有不同。这两种情况都很容易实现。

听鼠标在图表上的一般事件

您必须实现CChartMouseListener接口,覆盖您感兴趣的方法,并通过调用CChartCtrl::RegisterMouseListener(CChartMouseListener *pMouseListener)将该类的实例注册到图表控件。根据鼠标事件发生在控件的哪个部分,调用该界面上的不同功能:标题、图例、轴或plot area。对于所有这些函数,始终传递两个参数:一个MouseEvent,它是一个列举鼠标事件类型的枚举(鼠标移动、左键单击……)和一个CPoint对象,它包含发生事件的点的屏幕坐标。对于某些函数,需要时传递一些附加参数。例如,当单击某个轴时,指向该轴的指针将被传递给函数。

下面是CChartMouseListener的一个实现,它在单击轴时做出反应,并显示一个消息框:

class CCustomMouseListener : public CChartMouseListener
{
public:
  void OnMouseEventAxis(MouseEvent mouseEvent, CPoint point,
          CChartAxis* pAxisClicked)
  {
    if (mouseEvent == CChartMouseListener::LButtonDoubleClick)
    {
      MessageBox(_T("Axis clicked"), _T("Info"), MB_OK);
    }
  }
};

然后你必须创建这个类的一个实例,并将它注册到控件中:

m_pMouseListener = new CCustomMouseListener();
m_ChartCtrl.RegisterMouseListener(m_pMouseListener);

这里您还需要自己删除指针。

监听一系列鼠标事件

侦听系列上的事件与侦听一般事件非常相似,不同之处在于侦听器是CChartSeriesMouseListener的实例,它是一个模板类,模板参数是系列的点的类型。当您希望检索某个点的特定值时,需要这样做以避免不必要的强制转换。另一个区别是必须在系列本身上注册侦听器,而不是在图表控件上注册。

下面是CChartSeriesMouseListener的实现,它会对序列上的单击作出反应,如果单击发生在某个点上,它会显示一个带有该点的Y值的消息框:

class CCustomMouseListener : public CChartSeriesMouseListener<SChartXYPoint>
{
public:
  void OnMouseEventSeries(MouseEvent mouseEvent, CPoint point,
          CChartSerieBase<SChartXYPoint>* pSerie, unsigned uPointIndex)
  {
    if (mouseEvent == CChartMouseListener::LButtonDoubleClick &&
        uPointIndex != INVALID_POINT)
    {
      TChartStringStream ssText;
      SChartXYPoint Point = pSeries->GetPoint(uPointIndex);
      ssText << _T("Y value=")  << Point.Y;
      TChartString strText = ssText.str();
      MessageBox(NULL,strText.c_str(), _T("Info"), MB_OK);
    }
  }
};

注意,当用户没有单击某个点时,也可以调用OnMouseEventSeries函数。例如,当用户在两点之间单击,但仍然在序列上时,就会出现这种情况。在这种情况下,将INVALID_POINT传递给uPointIndex参数。

然后你必须创建这个类的一个实例,并将它注册到这个系列中:

m_pMouseListener = new CCustomMouseListener();
m_pSeries.RegisterMouseListener(m_pMouseListener);

注意,这将只工作,如果系列操作的点类型SChartXYPoint(点,线,表面或棒系列)。如果不是这样,您的代码将生成一个编译错误。

出于性能原因,禁用了对一系列鼠标移动事件的检测。要启用它,请查看doxygen文档中的CChartSerie::EnableMouseNotifications函数。

使用游标

还可以向控件添加光标。支持两种类型的游标:“横线”游标和“拖线”游标。第一个是显示在绘图区域上的简单的十字,它随着鼠标移动;第二个是与特定轴关联的水平或垂直线,您可以通过单击它并随着鼠标移动来拖动它。对于每个游标,都可以注册一个侦听器,以便在光标被移动时得到通知。下面是一个代码片段,它创建了一个与底部和左侧轴相关联的“交叉”光标和一个与底部轴相关联的“拖动线”光标:

// Creates a cross-hair cursor associated with the two primary axes.
CChartCrossHairCursor* pCrossHair =
  m_ChartCtrl.CreateCrossHairCursor();
// Creates a dragline cursor associated with the bottom axis.
CChartDragLineCursor* pDragLine =
  m_ChartCtrl.CreateDragLineCursor(CChartCtrl::BottomAxis);
// Hides the mouse when it is over the plotting area.
m_ChartCtrl.ShowMouseCursor(false);

注意最后对CChartCtrl::ShowMouseCursor的调用。默认情况下,鼠标总是可见的,但当您使用十字光标时,有时最好在鼠标位于绘图区域上方时隐藏它。

如果你想在光标位置改变时得到通知,你必须实现CChartCursorListener接口,创建一个实例,并将其注册到光标上:

class CCustomCursorListener : public CChartCursorListener
{
public:
  void OnCursorMoved(CChartCursor *pCursor, double xValue, double yValue)
  {
    TChartStringStream ssText;
    ssText << _T("Cursor moved: xPos=") << xValue << _T(", yPos=") << yValue;

    // Do something with the string...
  }
};
CCustomCursorListener* pCursorListener = new CCustomCursorListener;
pDragLine->RegisterListener(pCursorListener);

OnCursorMoved函数接收X和Y值,但对于拖放游标,只使用其中一个值:如果游标与水平轴关联,则使用X值,否则使用Y值。

使用了平移和缩放功能

在1.1版本的控件中,缩放和平移功能已经添加到控件中。缩放用鼠标左键控制,平移用鼠标右键控制。要缩放图表的特定部分,只需左键单击图表(这将是缩放矩形的左上角)并拖动到右下角。将出现一个矩形。当您释放鼠标按钮时,四个轴将自动调整到您所选择的区域。缩放是默认启用的,但你可以禁用它通过调用CChartCtrl::SetZoomEnabled(bool bEnabled)。您还可以通过调用CChartAxis::SetZoomLimit(double dLimit)为每个轴指定缩放限制。这指定了在缩放时轴的最小范围。默认是0.001。

要平移控件,请右键单击控件上的某个地方并移动鼠标。鼠标下面的点将“跟随”鼠标的移动(实际上,轴min和轴max将会改变)。pan是默认启用的,但是你可以通过调用CChartCtrl::SetPanEnabled(bool bEnabled)来禁用它。

如果您左键单击图表(比如开始缩放),但是如果您移到左上角,那么使用缩放和缩放功能所做的所有修改都将被取消(控件将处于使用缩放和缩放操作之前的状态)。最后,还可以通过调用CChartAxis::SetPanZoomEnabled(bool bEnabled)来禁用特定轴的平移和缩放功能。

利用高速功能

直线和点级数允许您以较高的速率绘制数据。当您想要绘制来自外部设备(例如传感器)的数据时,通常会这样做。这是可能的,因为当您向这样的系列添加一个点时,控件不会完全刷新,只会绘制最后一个点(或最后一条线部分),这是非常有效的。但是,如果希望控件能够足够快地绘制数据,则必须考虑以下几点。

一个重要的问题是自动轴的使用可能会大大降低性能。这是因为,如果一个点绘制在轴的范围之外,轴的范围将自动调整,这意味着控件将完全刷新。因此,如果您使用的是自动底轴,并且有一个“滚动”跟踪,那么每个新点都将在轴的当前范围之外,并且每个点都将刷新控件。处理这个问题的更好方法是使用一个固定轴,并每秒手动增加轴的范围(或以合理的速度)。

另一个重要的一点是,在向系列中添加了一个新点之后,永远不要调用RefreshCtrl。当然,这将完全刷新应该避免的控件。最后,如果需要同时对控件进行一些修改或添加几个点,那么应该在EnableRefresh(false)和EnableRefresh(true)之间包装这些调用(参见“定制外观”一节)。

扩展功能

在某些特定情况下,您将需要使用新特性扩展控件,例如新的series类型。目前,有四个组件可以定制:系列、轴、点标签和游标。

要提供新轴、新标签或新游标,只需从基类(CChartAxis、CChartLabel或CChartCursor)继承并实现所需的虚函数。完成此操作后,您可以通过调用不同函数的自定义版本(CChartCtrl::AttachCustomAxis、CChartCtrl::AttachCustomLabel或CChartCtrl::AttachCustomCursor)来附加新对象。CChartLabel类是一个模板类。这个主题有点宽泛,需要深入很多细节,但是最简单的方法是查看现有的不同类。

如果您希望提供新的级数,这就有点不同了:首先必须考虑要在级数中操作的点的类型。如果您只需要操作带有X和Y值的点,那么您可以继承CChartXYSerie, CChartXYSerie已经提供了许多操作这些点的功能。然后必须实现所需的虚函数。查看以下系列:CChartLineSerie、CChartPointSerie、CChartSurfaceSerie和CChartBarSerie,以获得具体的示例。

如果您的级数操作其他类型的点,那么首先必须为该点创建一个结构,其中包含以下方法:双GetX()、双GetXMin()、双GetXMax()、双GetY()、双GetYMin)和双GetYMax()。完成此操作后,您必须继承CChartSerieBase并将此点作为模板参数提供。然后必须提供所需的虚函数。查看以下系列的具体示例:CChartCandlestickSerie和CChartGanttSerie。

从版本1升级。x至版本2.0

在2.0版本中,对控件进行了重构,导致API发生了变化。主要可见的变化是,每个轴类型现在都有其单独的类(CChartStandardAxis、CChartDateTimeAxis和cchartlogmicaxis)。这也意味着默认情况下不会创建轴,并且在向图表添加series之前必须自己创建它们(否则代码将断言)。这在“操纵坐标轴”部分中有介绍。

另一个变化是在图表中添加series的方式:CChartCtrl类中的AddSerie已经被删除,取而代之的是helper函数来创建特定的series类型(CreateLineSerie, CreatePointsSerie,…)。这些函数返回确切的系列类型,因此不再需要强制类型转换。“操作系列”一节将对此进行更详细的介绍。

从版本2升级。到版本3.x

release 3.0.0中的主要变化是series基类现在变成了一个模板类,模板参数是series所操作的点的类型。如果您没有通过提供新的series类型来扩展控件,这不会对您的代码产生影响。如果您提供了一个新的series类型,那么您的类必须继承CCharSerieBase并提供它正在操作的点的类型。如果您的级数只使用带有X和Y值的点,那么您可以简单地继承CChartXYSerie。查看现有的系列以获得更多示例。

另一个小修改是标签提供程序现在也是模板类(出于同样的原因)。并且监听一系列鼠标事件现在从图表本身的鼠标事件中分离出来。这两点在“在点上添加标签”部分和“关于鼠标事件的通知”部分中得到了很好的解释。

最后,CChartAxis::SetAutomatic方法已经被标记为deprecated,你应该使用CChartAxis::SetAutomaticMode代替(已经引入了一个附加的自动模式)。

例子

本节只是展示如何使用控件的两个代码片段。第一个片段重现了示波器例子的图像(见本文的顶部),第二个例子重现了“2008年的收入”图像。代码被记录在案,所以它应该不会太难理解。

示波器的例子:

// Disable the refresh of the control (avoid multiple refresh).
m_ChartCtrl.EnableRefresh(false);
// Create a bottom and left axes
CChartStandardAxis* pBottomAxis =
  m_ChartCtrl.CreateStandardAxis(CChartCtrl::BottomAxis);
CChartStandardAxis* pLeftAxis =
  m_ChartCtrl.CreateStandardAxis(CChartCtrl::LeftAxis);
// Sets the min and max values of the bottom and left axis to -15 -> 15
pBottomAxis->SetMinMax(-15,15);
pLeftAxis->SetMinMax(-15,15);

// Add a new series of type line to the control and add data to it
CChartLineSerie* pLineSeries = m_ChartCtrl.CreateLineSerie();
// Specifies that the points in the series are not ordered (needed to be able
// to draw an ellipse).
pLineSeries->SetSeriesOrdering(poNoOrdering);
for (int i=0;i<361;i++)
{
  double X = 10 * sin(i/360.0 * 2 * 3.141592);
  double Y = 10 * cos( (i-60)/360.0 * 2 * 3.141592);

  pLineSeries->AddPoint(X,Y);
}

// Defines the different colors (back color, axes color, ...)
COLORREF BackColor = RGB(0,50,0);
COLORREF GridColor = RGB(0,180,0);
COLORREF TextColor = RGB(0,180,0);
COLORREF SerieColor = RGB(0,255,0);

// Specifies a sunken border for the control
m_ChartCtrl.SetEdgeType(EDGE_SUNKEN);

// Sets the color of the border and the back color
m_ChartCtrl.SetBorderColor(TextColor);
m_ChartCtrl.SetBackColor(BackColor);

//Sets the color of the different elements of the bottom axis
m_ChartCtrl.GetBottomAxis()->SetAxisColor(TextColor);
m_ChartCtrl.GetBottomAxis()->SetTextColor(TextColor);
m_ChartCtrl.GetBottomAxis()->GetGrid()->SetColor(GridColor);

// Sets the color of the different elements of the left axis
m_ChartCtrl.GetLeftAxis()->SetAxisColor(TextColor);
m_ChartCtrl.GetLeftAxis()->SetTextColor(TextColor);
m_ChartCtrl.GetLeftAxis()->GetGrid()->SetColor(GridColor);

// Sets the color of the title, change the font to Times New Roman
// and add a string
m_ChartCtrl.GetTitle()->SetColor(TextColor);
m_ChartCtrl.GetTitle()->SetFont(140,_T("Times New Roman"));
m_ChartCtrl.GetTitle()->AddString(_T("An example of oscilloscope"));

// Change the color of the line series
pLineSeries->SetColor(SerieColor);

// Finally re-enable the refresh of the control. This will refresh the
// control if any refresh was still 'pending'.
m_ChartCtrl.EnableRefresh(true);

“超过2008年的收入”例子:

srand((unsigned int)time(NULL));

// Disable the refresh
m_ChartCtrl.EnableRefresh(false);
COleDateTime Min(2008,1,1,0,0,0);
COleDateTime Max(2008,10,1,0,0,0);
// Create the bottom axis and configure it properly
CChartDateTimeAxis* pBottomAxis =
  m_ChartCtrl.CreateDateTimeAxis(CChartCtrl::BottomAxis);
pBottomAxis->SetMinMax(Min,Max);
pBottomAxis->SetDiscrete(true);
pBottomAxis->SetTickIncrement(false,CChartDateTimeAxis::tiMonth,1);
pBottomAxis->SetTickLabelFormat(false,_T("%b"));
// Create the left axis and configure it properly
CChartStandardAxis* pLeftAxis =
  m_ChartCtrl.CreateStandardAxis(CChartCtrl::LeftAxis);
pLeftAxis->SetMinMax(0,100);
pLeftAxis->GetLabel()->SetText(_T("Units sold"));
// Create the right axis and configure it properly
CChartStandardAxis* pRightAxis =
  m_ChartCtrl.CreateStandardAxis(CChartCtrl::RightAxis);
pRightAxis->SetVisible(true);
pRightAxis->GetLabel()->SetText(_T("Income (kEuros)"));
pRightAxis->SetMinMax(0,200);

// Configure the legend
m_ChartCtrl.GetLegend()->SetVisible(true);
m_ChartCtrl.GetLegend()->SetHorizontalMode(true);
m_ChartCtrl.GetLegend()->UndockLegend(80,50);
// Add text to the title and set the font & color
m_ChartCtrl.GetTitle()->AddString(_T("Income over 2008"));
CChartFont titleFont;
titleFont.SetFont(_T("Arial Black"),120,true,false,true);
m_ChartCtrl.GetTitle()->SetFont(titleFont);
m_ChartCtrl.GetTitle()->SetColor(RGB(0,0,128));
// Sets a gradient background
m_ChartCtrl.SetBackGradient(RGB(255,255,255),RGB(150,150,255),gtVertical);

// Create two bar series and a line series and populate them with data
CChartBarSerie* pBarSeries1 = m_ChartCtrl.CreateBarSerie();
CChartBarSerie* pBarSeries2 = m_ChartCtrl.CreateBarSerie();
CChartLineSerie* pLineSeries = m_ChartCtrl.CreateLineSerie(false,true);
int lowIndex = -1;
int lowVal = 999;
for (int i=0;i<9;i++)
{
  COleDateTime TimeVal(2008,i+1,1,0,0,0);
  int DesktopVal = 20 + rand()%(100-30);
  pBarSeries1->AddPoint(TimeVal,DesktopVal);
  int LaptopVal = 10 + rand()%(80-20);
  pBarSeries2->AddPoint(TimeVal,LaptopVal);
  int Income = DesktopVal + LaptopVal*1.5;
  if (Income < lowVal)
  {
    lowVal = Income;
    lowIndex = i;
  }
  pLineSeries->AddPoint(TimeVal,Income);
}
// Configure the series properly
pBarSeries1->SetColor(RGB(255,0,0));
pBarSeries1->SetName(_T("Desktops"));
pBarSeries2->SetColor(RGB(68,68,255));
pBarSeries2->SetGradient(RGB(200,200,255),gtVerticalDouble);
pBarSeries2->SetName(_T("Laptops"));
pBarSeries2->SetBorderColor(RGB(0,0,255));
pBarSeries2->SetBorderWidth(3);
pLineSeries->SetColor(RGB(0,180,0));
pLineSeries->SetName(_T("Total income"));
pLineSeries->SetWidth(2);
pLineSeries->EnableShadow(true);

// Add a label on the line series.
TChartStringStream labelStream;
labelStream << _T("Min income: ") << lowVal;
CChartBalloonLabel<SChartXYPoint>* pLabel =
  pLineSeries->CreateBalloonLabel(lowIndex, labelStream.str() + _T(" kEuros"));
CChartFont labelFont;
labelFont.SetFont(_T("Microsoft Sans Serif"),100,false,true,false);
pLabel->SetFont(labelFont);

// Re enable the refresh
m_ChartCtrl.EnableRefresh(true);

反馈

这个控件的开发涉及到相当多的工作,就像任何其他软件项目一样,它可能仍然在文档中包含错误或错误。如果您遇到这样的问题,请让我知道(即使您自己解决了),以便我能尽快解决问题。控件的其他用户将为此感谢您。如果您在文档或文章中遇到错误,也可以使用相同的方法。

我也或多或少地在这个控件上工作,以添加新特性。如果你对一个可能对其他人有用的好特性有需求,请告诉我,我会把它添加到我的愿望列表中。然而,由于我在业余时间进行这种控制,我的时间相当有限。

最后,如果你喜欢这个控件,请不要犹豫在论坛留言或者给文章评分,这是非常感谢的。谢谢你!

历史

  • 08/05/2006: 1.0版本发布
  • 19/08/2006: 1.1版本发布 修复了ScreenToValue函数(CChartAxis)的Bug修复了RemoveAllSeries函数(CChartCtrl)的Bug增加了对手动缩放的支持增加了对鼠标平移的支持,以指定轴上的刻度增量增加了对控件大小调整的支持
  • 09/04/2007: 1.2版本发布 GDI泄漏修正隐形系列不考虑轴和图例(感谢jerminator-jp)的能力,改变轴的文字颜色的能力,改变边界的颜色的绘图面积表面系列增加
  • 16/02/2008: 1.3版本发布 增加的日期/时间轴错误修复对数标签如何显示(尾随0)改变缩放矩形颜色的能力删除了编译器警告VC2005错误修复缩放
  • 14/04/2008: 1.4版本发布 添加了对滚动条的支持条形系列添加的图例可以停靠在任何一边或浮动支持图例水平模式支持图例的透明背景对多个对象的阴影支持removepoint from begin,RemovePointsFromEnd和添加点在CChartSeries类支持梯度背景EnableRefresh和UndoPanZoom函数添加在CChartCtrl的可能性,以启用/禁用一个特定轴的缩放和设置它的限制速度改进的系列(最小和最大缓存,系列)系列的命令可以删除使用指针错误修正为对数无形的传奇系列的Bug修复轴(1数字不显示)从控制故障修复Bug修复,当删除系列如果线的宽度大于1笔系列自动轴的Bug修复
  • 20/08/2008: 1.5版本发布 增加了对UNICODE的支持添加支持印刷自动隐藏滚动条基线选择酒吧系列性能补丁滚动条闪烁的删除(见这里)故障修复:滚动条现在已更新当轴是翻版的错误修正:调用AddPoint没有图纸错误修复的新问题:蜱虫标签日志轴并不总是正确(舍入误差)故障修复:ChartPointSerie最后一点没有显示故障修复:将鼠标移动到控制范围之外并不会停止缩放或平移操作(该按钮可以在控制范围之外释放)
  • 13/04/2009: 2.0版本发布 现在不同的轴类型分为不同类别修改添加系列控制的方式来提高灵活性添加光标显示离散轴能力通知鼠标事件发生在控制标签分能力显示一个光滑曲线添加ChartFont:允许斜体,大胆或下划线字体添加日期/时间轴的SetReferenceTick函数能够为每个点存储用户数据系列现在有一个Id删除CChartObject类分现在存储在一个标准的数组,而不是std::矢量效率实现二叉搜索寻找第一个和最后一个可见的点(效率)线系列现在使用折线代替MoveTo / LineTo(效率)错误修复时使用日期/时间轴的蜱虫间隔年Bug修复:酒吧系列是从错误的轴
  • 11/06/2009: 2.0.1版本发布 优化:对平移特征进行了平滑化优化:对直线序列不再绘制具有相同X、Y值的点。Bug修复:在某些情况下,代码崩溃当访问点有效范围之外的Bug修复:当系列被移除时,传说是访问了系列(崩溃)故障修复:一系列清除时,新分不了正确错误修复:插入点的X值已经存在系列的没有添加点正确错误修复CChartFont类
  • 07/08/2009: 2.0.2版本发布 错误修正:当添加没有点和顺序的系列时,控件崩溃错误修正:没有正确绘制线条的阴影错误修正:在没有任何数据的情况下使用自动日期/时间轴时,代码崩溃
  • 28/12/2009:版本3.0.0发布 Series现在是模板类,模板参数是point类型。这允许控制操作任何类型的点添加烛台和甘特系列添加支持保存图表图像文件栏系列可以堆叠添加了一个新的轴自动模式:屏幕自动模式监听鼠标事件系列已经搬到了一个CChartSeriesMouseListener类故障修复:当X或Y值被修改,重新排序错误修复系列:设置标准轴上的蜱虫增量没有显示正确的数字
  • 17/01/2010:版本3.0.1发布 错误修正:当使用标签与点系列,点的边界改变颜色。通过提供指定边框颜色的方法修正了此问题。Bug修正:在没有在系列中注册鼠标监听器的情况下点击系列时代码崩溃。错误修复:检测鼠标事件在某些系列崩溃错误修复:CChartTitle::SetVisible没有实现
  • 13/07/2010:版本3.0.2发布 Bug修复:在ClearSerie函数中,Clear()被Clear()取代。修复:清除系列时的内存泄漏(标签没有被删除)

谢谢

我要感谢这个社区的所有人,当我开始编程时,他们给了我很大的帮助。也要感谢所有为控制做出贡献的人们的各种帮助或反馈:toxcct, Chris Maunder, Kevin Hoffman, jerminator-jp, Laurie Gellatly, Eugene Pustovoyt, Andrej Ritter, Nick Holgate, Nick Schultz, Johann Obermayr, Pierre Schramm和Kevin Winter。特别感谢Bruno Lavier花在控制上的时间。我希望我没有忘记任何人。

链接

  • Chris Maunder的颜色选择器控件(用于演示应用程序)
  • 用贝塞尔原语绘制光滑曲线

本文转载于:http://www.diyabc.com/frontweb/news13601.html

原文地址:https://www.cnblogs.com/Dincat/p/13478631.html