如何拖动DataPoint并将其在Chart控件中移动 [英] How to drag a DataPoint and move it in a Chart control

查看:153
本文介绍了如何拖动DataPoint并将其在Chart控件中移动的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

我怎么..

  1. ..抓住特定的序列点(序列名称="My Series")
  2. 释放时,序列点应更改其位置/值

这就像通过拖动事件使系列点可移动一样.

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

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

解决方案

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

通过鼠标与图表交互的问题是,在Chart中,工作的坐标系不是一个,而是三个.

  • 图表元素(如LegendAnnotation)以相应容器的百分比进行度量.这些数据组成一个ElementPosition,通常来自0-100%.

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

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

对于我们的任务,我们需要在鼠标像素数据值之间进行转换.

请查看下面的更新!

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

首先,我们创建一些类级别的变量,这些变量包含对目标的引用:

// 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并不总是有效!如果在错误的时间调用它,它将返回null.我们从PrePaint事件中调用它,这很好.该标志将有助于保持效率.

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

MouseDown中,我们遍历Points集合,直到找到带有包含鼠标位置的Tag的集合.然后我们将其存储并更改其颜色.

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];

请注意,我同时为Axes设置了MinimaMaxima以及Intervals.这样会自动显示LabelsGridLinesTickMarks等,从而阻止Chart疯狂运行.

还请注意,这将适用于X-和YValue的任何DataType.只需调整Tooltip格式..

最后的提示:为防止用户将DataPointChartArea移开,可以将此检查添加到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;
        }
}

I want to be able to grab a datapoint drawn in a chart and to move it and change its position by dragging it over the chart control.

How can I ..

  1. ..grab the specific series point (series name ="My Series")
  2. When released the series point should change its position/ values

It's like making series points movable with drag event.

Here the color dots (points) should be able to move:

There are some charts like devExpress chart which perform this task but I want to do it in normal MS chart.

解决方案

Moving a DataPoint is not a built-in feature of the Chart control. We need to code it..

The problem with interacting with a Chart by mouse is that there are not one but three coordinate systems at work in a Chart:

  • The chart elements, like a Legend or an Annotation are measured in percentages of the respective containers. Those data make up an ElementPosition and usually go from 0-100%.

  • The Mouse coordinates and all graphics drawn in one of the three Paint events, all work in pixels; they go from 0-Chart.ClientSize.Width/Height.

  • The DataPoints have an x-value and one (or more) y-values(s). Those are doubles and they can go from and to anywhere you set them to.

For our task we need to convert between mouse pixels and data values.

DO see the Update below!

There are several ways to do this, but I think this is the cleanest:

First we create a few class level variables that hold references to the targets:

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

When we set up the chart we fill some of them:

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

Next we need two helper functions. They do the 1st conversion between pixels and data values:

    // 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));
    }

Note that I chose to use the Tag of each DataPoints to hold a RectangleF that has the clientRectangle of the DataPoint's Marker.

These rectangles will change whenever the chart is resized or other changes in the Layout, like sizing of a Legend etc.. have happend, so we need to re-synch them each time! And, of course you need to initially set them whenever you add a DataPoint!

Here is the Resize event:

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

The actual refreshing of the rectangles is being triggered from the PrePaint event:

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

Note that calling the ValueToPixelPosition is not always valid! If you call it at the wrong time it will return null.. We are calling it from the PrePaint event, which is fine. The flag will help keeping things efficient.

Now for the actual moving of a point: As usual we need to code the three mouse events:

In the MouseDown we loop over the Points collection until we find one with a Tag that contains the mouse position. Then we store it and change its Color..:

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;
        }
}

In the MouseMove we do the reverse calculation and set the values of our point; note that we also synch its new position and trigger the Chart to refresh the display:

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;
          }
   }
}

Finally we clean up in the MouseUp event:

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

Here is how I have set up my chart:

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];

Note that I have set both the Minima and Maxima as well as the Intervals for both Axes. This stops the Chart from running wild with its automatic display of Labels, GridLines, TickMarks etc..

Also note that this will work with any DataType for X- and YValues. Only the Tooltip formatting will have to be adapted..

Final note: To prevent the users from moving a DataPoint off the ChartArea you can add this check into the if-clause of the MouseMove event:

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

For the InnerPlotPositionClientRectangle function see here!

Update:

On revisiting the code I wonder why I didn't choose a simpler way:

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;
        }
}

这篇关于如何拖动DataPoint并将其在Chart控件中移动的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆