如何将图像设置为图表轴或沿图表轴设置?
How does one set an image as or along a chart axis?
我正在尝试使用彩色光谱带作为图表的轴。这个想法是将图像上的颜色与底部 x 轴上的相关波长相匹配。条带需要更改大小以匹配图表区域的变化,并扩展和收缩部分以匹配图表区域中的滚动缩放。
我曾尝试使用图像注释,但随着图表区域的变化,注释尺寸保持不变。还有,关注鼠标位置的滚动缩放显然对标注没有影响。
最接近的方法是使用图像作为图表区域的背景。这会随着图表区域的变化自动缩放图像,但滚动缩放对背景图像没有影响。此外,理想的情况是背景清晰,以避免模糊数据绘图点。我可以编辑图像,使其具有较大的透明部分,底部只有一条彩色条带,但即便如此,该条带可能会遮盖较低强度的数据点。
频谱作为注释和背景:
注释未缩放,背景缩放良好:
注释和背景都不会随着缩放而缩放:
这个主意不错。
最简单的方法是在图表的 Paint
事件中绘制图像,也许 PrePaint
。
让我们开始工作.. 我们将使用 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
的 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);
让我们把它放在一起:
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);
}
}
一些注意事项:
当然我会推荐使用图像资源而不是总是从磁盘加载!
绘图将始终覆盖数据点和网格。你也可以..
选择不同的最小值来腾出空间
缩小图片
将它移到 x 轴标签下方
使图像半透明
将 x 轴设置得足够粗,可以容纳图像条:ax.LineWidth = 10
对于后一种解决方案,您可能希望根据缩放状态偏移 y 位置。又快又脏:int yoff = (ax.ScaleView.IsZoomed ? 12 : 5);
。为避免黑色条纹也使轴透明或 chart.BackColor..
更新:
您也可以恢复使用 StripLine
。它可以缩放其 BackgroundImage
并且您必须在更改比例视图时创建合适的图像,即缩放或平移时。为此,上面的大部分代码将用于创建新图像。请参阅 以了解向图表添加和替换变量 NamedImage
的示例! (有关标记图片的相关部分已接近尾声!)
事实上,我发现这种方式是最好的解决方案,并添加了第二个答案。
替代和推荐的解决方案:
我尝试了我在另一个答案中提到的最后一个选项,发现它相当不错;它同样广泛,所以我决定 post 第二个答案。
想法是使用 StripLine
和恰到好处的 BackgroundImage
。
优点是它会很好地显示在所有图表元素下并且永远不会绘制在轴、网格、数据点上或与缩放工具冲突。
由于StripLine
必须反复更新我把它放在一个函数中:
这是函数;它使用与其他答案相同的两个辅助函数来计算像素位置..:[=32=]
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;
}
许多评论和链接都适用,尤其是我们用于 StripLine
.
的 NamedImage
更多注意事项:
我使用其中一个(四个)轴转换函数,PixelPositionToValue
计算出一个像素高度为12px; StripLine
采用 values,所以我使用两个像素值来获得正确的差异 value.
为了识别StripLine
我用的是Tag
属性。当然 Name
属性 会自然得多,但它是只读的。不知道为什么?!
函数从 AxisViewChanged
、Resize
事件以及 PrePaint
事件调用;这确保它总是在需要时被调用。为了避免来自 PrePaint
的无效调用,我是这样做的: if (ay.StripLines.Count == 0) updateStripLine(chart, ca, "sl");
当然,如果你在这个轴上使用其他 StripLines
,你应该适应..
代码使用了与之前相同的图片;但我已经把它放到第一个 NamedImage
中,叫做 spectrum
。这也是第一个答案中的一个选项。
NamedImage spectrum = new NamedImage("spectrum", Bitmap.FromFile(imagePath);
chart.Images.Add(spectrum);
它也确保妥善处理旧图像,我希望..
我正在尝试使用彩色光谱带作为图表的轴。这个想法是将图像上的颜色与底部 x 轴上的相关波长相匹配。条带需要更改大小以匹配图表区域的变化,并扩展和收缩部分以匹配图表区域中的滚动缩放。
我曾尝试使用图像注释,但随着图表区域的变化,注释尺寸保持不变。还有,关注鼠标位置的滚动缩放显然对标注没有影响。
最接近的方法是使用图像作为图表区域的背景。这会随着图表区域的变化自动缩放图像,但滚动缩放对背景图像没有影响。此外,理想的情况是背景清晰,以避免模糊数据绘图点。我可以编辑图像,使其具有较大的透明部分,底部只有一条彩色条带,但即便如此,该条带可能会遮盖较低强度的数据点。
频谱作为注释和背景:
注释未缩放,背景缩放良好:
注释和背景都不会随着缩放而缩放:
这个主意不错。
最简单的方法是在图表的 Paint
事件中绘制图像,也许 PrePaint
。
让我们开始工作.. 我们将使用 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
的 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);
让我们把它放在一起:
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);
}
}
一些注意事项:
当然我会推荐使用图像资源而不是总是从磁盘加载!
绘图将始终覆盖数据点和网格。你也可以..
选择不同的最小值来腾出空间
缩小图片
将它移到 x 轴标签下方
使图像半透明
将 x 轴设置得足够粗,可以容纳图像条:
ax.LineWidth = 10
对于后一种解决方案,您可能希望根据缩放状态偏移 y 位置。又快又脏:int yoff = (ax.ScaleView.IsZoomed ? 12 : 5);
。为避免黑色条纹也使轴透明或 chart.BackColor..
更新:
您也可以恢复使用 StripLine
。它可以缩放其 BackgroundImage
并且您必须在更改比例视图时创建合适的图像,即缩放或平移时。为此,上面的大部分代码将用于创建新图像。请参阅 NamedImage
的示例! (有关标记图片的相关部分已接近尾声!)
事实上,我发现这种方式是最好的解决方案,并添加了第二个答案。
替代和推荐的解决方案:
我尝试了我在另一个答案中提到的最后一个选项,发现它相当不错;它同样广泛,所以我决定 post 第二个答案。
想法是使用 StripLine
和恰到好处的 BackgroundImage
。
优点是它会很好地显示在所有图表元素下并且永远不会绘制在轴、网格、数据点上或与缩放工具冲突。
由于StripLine
必须反复更新我把它放在一个函数中:
这是函数;它使用与其他答案相同的两个辅助函数来计算像素位置..:[=32=]
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;
}
许多评论和链接都适用,尤其是我们用于 StripLine
.
NamedImage
更多注意事项:
我使用其中一个(四个)轴转换函数,
PixelPositionToValue
计算出一个像素高度为12px;StripLine
采用 values,所以我使用两个像素值来获得正确的差异 value.为了识别
StripLine
我用的是Tag
属性。当然Name
属性 会自然得多,但它是只读的。不知道为什么?!函数从
AxisViewChanged
、Resize
事件以及PrePaint
事件调用;这确保它总是在需要时被调用。为了避免来自PrePaint
的无效调用,我是这样做的:if (ay.StripLines.Count == 0) updateStripLine(chart, ca, "sl");
当然,如果你在这个轴上使用其他StripLines
,你应该适应..代码使用了与之前相同的图片;但我已经把它放到第一个
NamedImage
中,叫做spectrum
。这也是第一个答案中的一个选项。NamedImage spectrum = new NamedImage("spectrum", Bitmap.FromFile(imagePath); chart.Images.Add(spectrum);
它也确保妥善处理旧图像,我希望..