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

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

问题描述

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

我怎么...

  1. .. 获取特定的系列点(系列名称 ="My Series")
  2. 当释放时,系列点应该改变它的位置/值

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

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

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

解决方案

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

通过鼠标与图表交互的问题在于,Chart 中有不是一个而是三个坐标系在起作用:

  • 图表元素,例如 LegendAnnotation 以相应容器的百分比来衡量.这些数据组成了一个 ElementPosition,通常从 0-100% 开始.

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

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

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

务必查看下面的更新!

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

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

//保存可移动部件的变量:ChartArea ca_ = null;系列 s_ = null;数据点 dp_ = 空;布尔同步 = 假;

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

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

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

//两个辅助函数:void SyncAllPoints(ChartArea ca, Series s){foreach (s.Points 中的数据点 dp) SyncAPoint(ca, s, dp);同步 = 真;}void SyncAPoint(ChartArea ca, Series s, DataPoint dp){浮动 mh = dp.MarkerSize/2f;浮动像素=(浮动)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 来保存一个 RectangleF,它具有 DataPoint<的 clientRectangle 的标记.

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

时,您当然需要对它们进行初始设置

这是 Resize 事件:

private void chart1_Resize(object sender, EventArgs e){同步=假;}

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

private void chart1_PrePaint(object sender, ChartPaintEventArgs e){如果(!同步)SyncAllPoints(ca_,s_);}

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

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

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

private void chart1_MouseDown(object sender, MouseEventArgs e){foreach(s_.Points 中的数据点 dp)if (((RectangleF)dp.Tag).Contains(e.Location)){dp.Color = 颜色.橙色;dp_ = dp;休息;}}

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

private void chart1_MouseMove(object sender, MouseEventArgs e){if (e.Button.HasFlag(MouseButtons.Left) && dp_ != null){浮动 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();}别的{光标 = Cursors.Default;foreach(s_.Points 中的数据点 dp)if (((RectangleF)dp.Tag).Contains(e.Location)){光标 = Cursors.Hand;休息;}}}

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

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

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

系列 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- 和 YValues 的任何 DataType.只需要调整 Tooltip 格式..

最后一点:为了防止用户将 DataPoint 移出 ChartArea,您可以将此检查添加到 if-clauseMouseMove 事件:

 RectangleF ippRect = InnerPlotPositionClientRectangle(chart1, ca_);如果 (!ippRect.Contains(e.Location)) 返回;

对于 InnerPlotPositionClientRectangle 函数 看这里!

更新:

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

DataPoint curPoint = null;private void chart1_MouseUp(对象发送者,MouseEventArgs e){curPoint = 空;}private void chart1_MouseMove(对象发送者,MouseEventArgs e){if (e.Button.HasFlag(MouseButtons.Left)){ChartArea ca = chart1.ChartAreas[0];轴 ax = ca.AxisX;轴 ay = ca.AxisY;HitTestResult hit = chart1.HitTest(e.X, e.Y);if (hit.PointIndex >= 0) curPoint = hit.Series.Points[hit.PointIndex];如果(curPoint != null){系列 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天全站免登陆