如何将图像设置为图表轴或沿图表轴?

时间:2018-04-24 00:07:29

标签: c# .net winforms mschart

我正在尝试使用彩色光谱条作为图表的轴。这个想法是将图像上的颜色与沿着底部x轴的相关波长相匹配。条带需要更改大小以匹配图表区域的更改,并扩展和收缩部分以匹配图表区域中的滚动缩放。

我尝试过使用图片注释,但随着图表区域的变化,注释尺寸仍然是固定的。此外,聚焦在鼠标位置的滚动缩放显然对注释没有影响。

最接近的方法是使用图像作为图表区域的背景。这会在图表区域更改时自动缩放图像,但滚动缩放对背景图像没有影响。此外,理想的是使背景清晰,以避免模糊数据绘图点。我可以编辑图像以获得一个大的透明部分,底部只有一条彩色条带,但即使这样,该条带也可能会遮挡较低强度的数据点。

Spectrum作为注释和背景: enter image description here

注释不缩放,背景很好: enter image description here

注释和背景都没有缩放比例缩放: enter image description here

2 个答案:

答案 0 :(得分:5)

这是个好主意。

最简单的方法是在图表的Paint事件中绘制图像,可能是PrePaint

enter image description here

让我们开始工作..我们将使用允许我们缩放和裁剪的DrawImage重载。为此,我们需要两个矩形。

第一个挑战是始终获得正确的目标矩形。

为此,我们需要将InnerPlotPosition从相对位置转换为绝对像素。

这两个功能将有所帮助:

RectangleF ChartAreaClientRectangle(Chart chart, ChartArea CA)
{
    RectangleF CAR = CA.Position.ToRectangleF();
    float pw = chart.ClientSize.Width / 100f;
    float ph = chart.ClientSize.Height / 100f;
    return new RectangleF(pw * CAR.X, ph * CAR.Y, pw * CAR.Width, ph * CAR.Height);
}

RectangleF InnerPlotPositionClientRectangle(Chart chart, ChartArea CA)
{
    RectangleF IPP = CA.InnerPlotPosition.ToRectangleF();
    RectangleF CArp = ChartAreaClientRectangle(chart, CA);

    float pw = CArp.Width / 100f;
    float ph = CArp.Height / 100f;

    return new RectangleF(CArp.X + pw * IPP.X, CArp.Y + ph * IPP.Y, 
                            pw * IPP.Width, ph * IPP.Height);
}

使用这些数字设置目标矩形非常简单:

Rectangle tgtR = Rectangle.Round(new RectangleF(ipr.Left, ipr.Bottom - 15, ipr.Width, 15));

您可以根据自己的喜好选择身高。

下一个挑战是源矩形。

没有缩放就可以了:

Rectangle srcR = new Rectangle( 0, 0, bmp.Width, bmp.Height);

但是对于缩放和平移,我们需要扩展它;为此,我们可以使用x轴和ScaleView的{​​{1}}和Minimum值。

我们计算轴上第一个和最后一个点的因子:

Maximum

现在我们得到源矩形可能是这样的:

double f1 = ax.ScaleView.ViewMinimum / (ax.Maximum - ax.Minimum);
double f2 = ax.ScaleView.ViewMaximum / (ax.Maximum - ax.Minimum);

让我们把它放在一起:

int x  = (int)(bmp.Width * f1);
int xx = (int)(bmp.Width * f2);
Rectangle srcR = new Rectangle( x, 0, xx - x, bmp.Height);

一些注意事项:

  • 当然我建议使用Image资源而不是总是从磁盘加载!

  • 绘图将始终覆盖数据点和网格。你可以..

    • 选择不同的最小值以腾出空间
    • 使图像变小
    • 将其移至x轴标签下方
    • 使图像半透明
    • 使x轴如此胖,以至于它可以容纳图像条:private void chart_PrePaint(object sender, ChartPaintEventArgs e) { // a few short names Graphics g = e.ChartGraphics.Graphics; ChartArea ca = chart.ChartAreas[0]; Axis ax = ca.AxisX; // pixels of plot area RectangleF ipr = InnerPlotPositionClientRectangle(chart, ca); // scaled first and last position double f1 = ax.ScaleView.ViewMinimum / (ax.Maximum - ax.Minimum); double f2 = ax.ScaleView.ViewMaximum / (ax.Maximum - ax.Minimum); // actual drawing with the zooming overload using (Bitmap bmp = (Bitmap)Bitmap.FromFile(imagePath)) { int x = (int)(bmp.Width * f1); int xx = (int)(bmp.Width * f2); Rectangle srcR = new Rectangle( x, 0, xx - x, bmp.Height); Rectangle tgtR = Rectangle.Round( new RectangleF(ipr.Left , ipr.Bottom - 15, ipr.Width, 15)); g.DrawImage(bmp, tgtR, srcR, GraphicsUnit.Pixel); } }

对于后一种解决方案,您可能希望根据缩放状态偏移y位置。又快又脏:ax.LineWidth = 10。为避免黑色条纹也使轴透明或chart.BackColor ..

<强>更新

您还可以恢复使用int yoff = (ax.ScaleView.IsZoomed ? 12 : 5);。它可以缩放其StripLine,并且您必须在更改缩放视图时创建合适的图像,即缩放或平移时。对于这些,上面的代码将用于创建新图像。有关向图表添加和替换不同BackgroundImage的示例,请参阅this post! (相关部分接近标记图像的末尾!)

事实上,我发现这种方式是最佳解决方案,并添加了第二个答案。

答案 1 :(得分:4)

替代方案和推荐解决方案:

我涉足我在其他答案中提到的最后一个选项,发现它相当不错;它是类似的广泛,所以我决定发布第二个答案。

我们的想法是使用恰当StripLine的{​​{1}}。

优点是在所有图表元素下显示,并且永远不会在轴,网格,数据点上绘制或与缩放工具冲突。

enter image description here

由于BackgroundImage必须重复更新,我把它放在一个函数中:

这是功能;它使用相同的两个辅助函数来计算像素位置,因为另一个答案是..:

StripLine

许多评论和链接都适用,特别是我们用于void updateStripLine(Chart chart, ChartArea ca, string name) { // find our stripline; one could pass in a class level variable as well StripLine sl = ca.AxisY.StripLines.Cast<StripLine>() .Where(s => s.Tag.ToString() == name).FirstOrDefault(); if (sl != null) // either clean-up the resources.. { var oldni = chart.Images.FindByName(name); if (oldni != null) { oldni.Image.Dispose(); chart.Images.Remove(oldni); oldni.Dispose(); } } else // or, create the line { sl = new StripLine(); sl.Tag = name; ca.AxisY.StripLines.Add(sl); } ca.RecalculateAxesScale(); RectangleF ipr = InnerPlotPositionClientRectangle(chart, ca); Axis ax = ca.AxisX; Axis ay = ca.AxisY; double f1 = ax.ScaleView.ViewMinimum / (ax.Maximum - ax.Minimum); double f2 = ax.ScaleView.ViewMaximum / (ax.Maximum - ax.Minimum); Bitmap b0 = (Bitmap)chart.Images["spectrum"].Image; int x = (int)(b0.Width * f1); int xx = (int)(b0.Width * f2); Rectangle srcR = new Rectangle( x, 0, xx - x, b0.Height); Rectangle tgtR = Rectangle.Round(new RectangleF(0,0, ipr.Width , 10)); // create bitmap and namedImage: Bitmap bmp = new Bitmap( tgtR.Width, tgtR.Height); using (Graphics g = Graphics.FromImage(bmp)) { g.DrawImage(b0, tgtR, srcR, GraphicsUnit.Pixel); } NamedImage ni = new NamedImage(name, bmp); chart.Images.Add(ni); sl.BackImageWrapMode = ChartImageWrapMode.Scaled; sl.StripWidth = ay.PixelPositionToValue(0) - ay.PixelPositionToValue(12); sl.Interval = 100; // make large enough to avoid another sLine showing up sl.IntervalOffset = 0; sl.BackImage = name; } 的{​​{1}}。

还有一些说明:

  • 我使用(四)轴转换函数之一,PixelPositionToValue来计算像素高度为12px; NamedImage,因此我使用两个像素值来获得正确的差异

  • 要识别StripLine我使用StripLine属性。当然StripLine属性会更自然,但它是只读的。不明白为什么?!

  • TagName事件以及AxisViewChanged事件调用该函数;这确保在需要时始终会调用它。为了避免来自Resize的无效呼叫,我这样做:PrePaint当然,如果您在此轴上使用其他PrePaint,则应该进行调整。

  • 代码使用与以前相同的图像;但是我把它放在了if (ay.StripLines.Count == 0) updateStripLine(chart, ca, "sl");的第一个StripLines中。这也是第一个答案中的一个选项。

    NamedImage

  • 我也希望能妥善处理旧图像。