如何拖动DataPoint并将其在Chart控件中移动 [英] How to drag a DataPoint and move it in a Chart control
问题描述
我希望能够抓住图表中绘制的数据点,并通过将其拖动到图表控件上来移动它并更改其位置.
我怎么..
- ..抓住特定的序列点(序列名称="My Series")
- 释放时,序列点应更改其位置/值
这就像通过拖动事件使系列点可移动一样.
这里的色点(点)应该可以移动:
有些图表(例如devExpress图表)可以执行此任务,但我想在普通MS图表中完成.
移动DataPoint
不是Chart
控件的内置功能.我们需要对其进行编码.
通过鼠标与图表交互的问题是,在Chart
中,工作的坐标系不是一个,而是三个.
-
图表元素(如
Legend
或Annotation
)以相应容器的百分比进行度量.这些数据组成一个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));
}
请注意,我选择使用每个DataPoints
的Tag
来容纳具有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
设置了Minima
和Maxima
以及Intervals
.这样会自动显示Labels
,GridLines
,TickMarks
等,从而阻止Chart
疯狂运行.
还请注意,这将适用于X-和YValue的任何DataType
.只需调整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;
}
}
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 ..
- ..grab the specific series point (series name ="My Series")
- 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 anAnnotation
are measured in percentages of the respective containers. Those data make up anElementPosition
and usually go from0-100%
.The Mouse coordinates and all graphics drawn in one of the three
Paint
events, all work in pixels; they go from0-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屋!