GDI +曲线“溢出”

我目前正在使用GDI +绘制折线图,​​并使用Graphics.DrawCurve来平滑线条。 问题是曲线并不总是与我提供的点匹配,这使曲线在某些点上长出图形框架,如下所示(红色是Graphics.DrawLines ,绿色是Graphics.DrawCurve )。

图形

我该如何解决这个问题?

最简单的解决方案是设置张力:

在此处输入图像描述

使用默认张力绘制绿色曲线,蓝色曲线设置为0.1f的张力:

 private void panel1_Paint(object sender, PaintEventArgs e) { e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; e.Graphics.DrawLines(Pens.Red, points.ToArray()); e.Graphics.DrawCurve(Pens.Green, points.ToArray()); e.Graphics.DrawCurve(Pens.Blue, points.ToArray(), 0.1f); } 

你需要测试什么是最好的折衷方案, 0.2f仍然可以, 0.3f已经透支了很多..

要获得一个非常好的解决方案,您需要使用DrawBeziers 。 这将允许您绘制可以通过点的曲线,而无需任何透支,并完全控制曲线的半径; 但要这样你就需要’找’,即计算出好的 control points ,这不过是微不足道的……:

在此处输入图像描述

这个结果绝不是完美的,但已经足够复杂了。我已经用相同的颜色显示了curve points和它们各自的control points 。 对于每个点,都有一个传入传出控制点。 对于平滑曲线,它们需要在曲线点上具有相同的切线/渐变。

我使用一些辅助函数来计算关于段的一些事情:

  • 渐变列表
  • 渐变符号列表
  • 段长度列表
  • 点之间的水平和垂直间隙列表

主函数计算bezier points的数组,即curve points ,每对之间的左前角下一个右 control points

Paint事件中,它的使用方式如下:

 List bezz = getBezz(points); using (Pen pen = new Pen(Color.Black, 2f)) e.Graphics.DrawBeziers(pen, bezz.ToArray()); 

以下是我使用的function:

 List getGradients(List p) { List grads = new List(); for (int i = 0; i < p.Count - 1; i++) { float dx = p[i + 1].X - p[i].X; float dy = p[i + 1].Y - p[i].Y; if (dx == 0) grads.Add(dy == 0 ? 0 : dy > 0 ? float.PositiveInfinity : float.NegativeInfinity); else grads.Add(dy / dx); } return grads; } List getLengths(List p) { List lengs = new List(); for (int i = 0; i < p.Count - 1; i++) { float dx = p[i + 1].X - p[i].X; float dy = p[i + 1].Y - p[i].Y; lengs.Add((float)Math.Sqrt(dy * dy + dx * dx)); } return lengs; } List getGaps(List p, bool horizontal) { List gaps = new List(); for (int i = 0; i < p.Count - 1; i++) { float dx = p[i + 1].X - p[i].X; float dy = p[i + 1].Y - p[i].Y; gaps.Add(horizontal ? dx : dy); } return gaps; } List getSigns(List g) { return g.Select(x => x > 0 ? 1 : x == 0 ? 0 : -1).ToList(); } 

最后是主要function; 在这里我做了一个区别:极值点(最小值和最大值)的控制点应该与点本身在同一高度。 这样可以防止垂直溢出。 它们很容易找到:它们的渐变迹象总是会有所改变。

对于输入和输出控制点,其他点需要具有相同的梯度。 我使用段的渐变之间的平均值。 (也许一个称重的平均值会更好……)然后我根据段长来衡量它们的距离。

 List getBezz(List points) { List bezz = new List(); int pMax = points.Count; List hGaps = getGaps(points, true); List vGaps = getGaps(points, false); List grads = getGradients(points); List lengs = getLengths(points); List signs = getSigns(grads); PointF[] bezzA = new PointF[pMax * 3 - 2]; // curve points for (int i = 0; i < pMax; i++) bezzA[i * 3] = points[i]; // left control points for (int i = 1; i < pMax; i++) { float x = points[i].X - hGaps[i - 1] / 2f; float y = points[i].Y; if (i < pMax - 1 && signs[i - 1] == signs[i]) { float m = (grads[i-1] + grads[i]) / 2f; y = points[i].Y - hGaps[i-1] / 2f * m * vGaps[i-1] / lengs[i-1]; } bezzA[i * 3 - 1] = new PointF(x, y); } // right control points for (int i = 0; i < pMax - 1; i++) { float x = points[i].X + hGaps[i] / 2f; float y = points[i].Y; if (i > 0 && signs[i-1] == signs[i]) { float m = (grads[i-1] + grads[i]) / 2f; y = points[i].Y + hGaps[i] / 2f * m * vGaps[i] / lengs[i]; } bezzA[i * 3 + 1] = new PointF(x, y); } return bezzA.ToList(); } 

请注意,我没有编码具有相同x坐标的点的情况。 所以这对于“function图”来说是可以的,但不是,比如数字,例如星星……

也许你只是想看看“超越界限”问题,因为这不是超调问题,而是有界限。 在这种情况下,您可以使用System.Drawing.Drawing2D.GraphicsPath对象确定曲线的实际边界:

 GraphicsPath gp = new GraphicsPath(); gp.AddCurve(listOfPoints); RectangleF bounds = gp.GetBounds(); 

您可以直接绘制GraphicsPath:

 graphics.DrawPath(Pens.Black, gp); 

就解决边界问题而言,线必然会超过某些轴上的顶点。 当线条与边界对齐时,更容易看到这一事实。

鉴于以下几点:

为了使它们弯曲,它们必须以某种方式超越它们的界限:

如果你永远不想超过它们的垂直边界,你可以简单地确保贝塞尔曲线手柄具有与顶点相同的Y值,但它们会在X上超调:

或相反亦然:

你可以故意下冲到足以避免曲线过冲的方式。 这可以通过交换贝塞尔手柄来实现,贝塞尔手柄可能位于线中心,具有顶点: