如何拖动数据点并在图表控件中移动它

How to drag a DataPoint and move it in a Chart control

我希望能够抓取绘制在图表中的数据点并移动它并通过将其拖到图表控件上来更改其位置。

我怎么能..

  1. ..抓取具体系列点(系列名="My Series")
  2. 释放系列点时应更改其位置/值

这就像通过拖动事件使系列点可移动。

这里的颜色点(点)应该可以移动:

有一些图表(例如 devExpress 图表)可以执行此任务,但我想在普通 MS 图表中执行此任务。

移动 DataPoint 不是 Chart 控件的内置功能。我们需要对其进行编码..

通过鼠标与图表交互的问题是 不是一个而是三个 坐标系在 Chart:

中工作
  • 图表元素,如 LegendAnnotation 以各自容器的百分比来衡量。这些数据构成一个 ElementPosition,通常来自 0-100%.

  • 在三个 Paint 事件之一中绘制的鼠标坐标和所有图形均以像素为单位;他们来自 0-Chart.ClientSize.Width/Height.

  • DataPoints 有一个 x 值和一个(或多个)y 值。这些是双打,它们可以往返于您设置的任何位置。

对于我们的任务,我们需要在 鼠标像素 数据值 .

之间进行转换

请查看下面的更新!

有几种方法可以做到这一点,但我认为这是最干净的方法:

首先我们创建一些 class 级别变量来保存对目标的引用:

// variables holding moveable parts:
ChartArea ca_ = null;
Series s_ = null;
DataPoint dp_ = null;
bool synched = false;

当我们设置图表时,我们会填充其中的一些:

ca_ = chart1.ChartAreas[0];
s_ = chart1.Series[0];

接下来我们需要两个辅助函数。他们在像素和数据值之间进行第一次转换:

    // two helper functions:
    void SyncAllPoints(ChartArea ca, Series s)
    {
        foreach (DataPoint dp in s.Points) SyncAPoint(ca, s, dp);
        synched = true;
    }

    void SyncAPoint(ChartArea ca, Series s, DataPoint dp)
    {
        float mh = dp.MarkerSize / 2f;
        float px = (float)ca.AxisX.ValueToPixelPosition(dp.XValue);
        float py = (float)ca.AxisY.ValueToPixelPosition(dp.YValues[0]);
        dp.Tag = (new RectangleF(px - mh, py - mh, dp.MarkerSize, dp.MarkerSize));
    }

请注意,我选择使用每个 DataPointsTag 来保存具有 DataPoint 标记的 clientRectangle 的 RectangleF

每当图表 调整大小 或布局中的其他更改(例如图例的大小等)时,这些矩形将 更改 发生了,所以我们每次都需要重新同步它们!而且,当然,您需要在添加 DataPoint!

时初始设置它们

这是 Resize 事件:

private void chart1_Resize(object sender, EventArgs e)
{
    synched = false;
}

矩形的实际刷新是由 PrePaint 事件触发的:

private void chart1_PrePaint(object sender, ChartPaintEventArgs e)
{
    if ( !synched) SyncAllPoints(ca_, s_);
}

请注意,调用 ValueToPixelPosition 并不总是有效!如果您在错误的时间调用它,它将 return null.. 我们从 PrePaint 事件中调用它,这很好。该标志将有助于保持效率。

现在点的实际移动:像往常一样,我们需要编写三个鼠标事件:

MouseDown 中,我们遍历 Points 集合,直到找到一个包含鼠标位置的 Tag 集合。然后我们存储它并改变它的颜色..:[=​​63=]

private void chart1_MouseDown(object sender, MouseEventArgs e)
{
    foreach (DataPoint dp in s_.Points)
        if (((RectangleF)dp.Tag).Contains(e.Location))
        {
            dp.Color = Color.Orange;
            dp_ = dp;
            break;
        }
}

MouseMove中我们进行反向计算并设置我们的点值;请注意,我们还同步了它的新位置并触发 Chart 刷新显示:

private void chart1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button.HasFlag(MouseButtons.Left) && dp_ != null)
    {
        float mh = dp_.MarkerSize / 2f;
        double vx = ca_.AxisX.PixelPositionToValue(e.Location.X);
        double vy = ca_.AxisY.PixelPositionToValue(e.Location.Y);

        dp_.SetValueXY(vx, vy);
        SyncAPoint(ca_, s_, dp_);
        chart1.Invalidate();
    }
   else
   {
       Cursor = Cursors.Default;
       foreach (DataPoint dp in s_.Points)
          if (((RectangleF)dp.Tag).Contains(e.Location))
          {
             Cursor = Cursors.Hand; break;
          }
   }
}

最后我们在MouseUp事件中清理:

    private void chart1_MouseUp(object sender, MouseEventArgs e)
    {
        if (dp_ != null)
        {
            dp_.Color = s_.Color;
            dp_ = null;
        }
    }

以下是我设置图表的方式:

Series S1 = chart1.Series[0];
ChartArea CA = chart1.ChartAreas[0];
S1.ChartType = SeriesChartType.Point;
S1.MarkerSize = 8;
S1.Points.AddXY(1, 1);
S1.Points.AddXY(2, 7);
S1.Points.AddXY(3, 2);
S1.Points.AddXY(4, 9);
S1.Points.AddXY(5, 19);
S1.Points.AddXY(6, 9);

S1.ToolTip = "(#VALX{0.##} / #VALY{0.##})";

S1.Color = Color.SeaGreen;

CA.AxisX.Minimum = S1.Points.Select(x => x.XValue).Min();
CA.AxisX.Maximum = S1.Points.Select(x => x.XValue).Max() + 1;
CA.AxisY.Minimum = S1.Points.Select(x => x.YValues[0]).Min();
CA.AxisY.Maximum = S1.Points.Select(x => x.YValues[0]).Max() + 1;
CA.AxisX.Interval = 1;
CA.AxisY.Interval = 1;

ca_ = chart1.ChartAreas[0];
s_ = chart1.Series[0];

请注意,我已经设置了 MinimaMaxima 以及 AxesIntervals。这会阻止 Chart 从 运行 自动显示 LabelsGridLinesTickMarks 等。

另请注意,这适用于任何 DataType 的 X 和 Y 值。只有 Tooltip 格式需要调整..

最后说明:为了防止用户将 DataPoint 移出 ChartArea,您可以将此检查添加到 MouseMove 事件的 if-clause 中:

  RectangleF ippRect = InnerPlotPositionClientRectangle(chart1, ca_);
  if (!ippRect.Contains(e.Location) ) return;

对于InnerPlotPositionClientRectangle函数

更新:

在重新访问代码时,我想知道为什么我没有选择更简单的方法:

DataPoint curPoint = null;

private void chart1_MouseUp(object sender, MouseEventArgs e)
{
    curPoint = null;
}

private void chart1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button.HasFlag(MouseButtons.Left))
    {
        ChartArea ca = chart1.ChartAreas[0];
        Axis ax = ca.AxisX;
        Axis ay = ca.AxisY;

        HitTestResult hit = chart1.HitTest(e.X, e.Y);
        if (hit.PointIndex >= 0) curPoint = hit.Series.Points[hit.PointIndex];

        if (curPoint != null)
        {
            Series s = hit.Series;
            double dx = ax.PixelPositionToValue(e.X);
            double dy = ay.PixelPositionToValue(e.Y);

            curPoint.XValue = dx;
            curPoint.YValues[0] = dy;
        }
}

下载 Microsoft 图表控件示例环境

https://code.msdn.microsoft.com/Samples-Environments-for-b01e9c61

检查这个:

图表功能 -> 交互式图表 -> 选择 -> 通过拖动更改值