How can I draw a Polyline onto an Image (WPF)

919 views Asked by At

I've tried a few different approaches to this, but can't seem to get a combination that works.

Creating WPF app in C#, Visual Studio.

System.Windows.Shapes.Polyline works really nicely to draw into a Canvas in real-time, but I want to be able to draw in higher resolution onto a non-visual component that I can then render onto an Image.

If I create a Polyline on a Canvas that's visible in the UI, this works fine:

// Make rendertarget size of full page
RenderTargetBitmap rtb = new RenderTargetBitmap((int)wPage, (int)hPage, 96, 96, PixelFormats.Default);
// Render the polyline
rtb.Render(lineVirt); 
// Apply to background image
imgBG.Source = rtb;

But if I create a Polyline on a Canvas that's not visible in the UI, then nothing renders to the image. This is probably fair enough. My guess is that the component recognises that it's not visible and therefore doesn't bother to render.

I've considered putting the Canvas somewhere in the UI buried under other controls, but that seems like a horrible kind of hack.

Essentially, all I need is a clean and fast way to draw a multi-point line of a specified width and color onto an Image. I thought that Polyline would work well, but only seems to work in a visible container.

What are my options?

2

There are 2 answers

1
Clemens On BEST ANSWER

You do not need a rendered Canvas or any other visible Panel at all.

Just use basic drawing primitives available at the Visual layer.

The DrawGeometry method below draws a Geometry onto a BitmapSource, using the bitmap's rendered size, i.e. the size that takes its DPI into account, and returns the resulting BitmapSource.

public static BitmapSource DrawGeometry(
    BitmapSource source, Pen pen, Geometry geometry)
{
    var visual = new DrawingVisual();
    var rect = new Rect(0, 0, source.Width, source.Height);

    using (var dc = visual.RenderOpen())
    {
        dc.DrawImage(source, rect);
        dc.DrawGeometry(null, pen, geometry);
    }

    var target = new RenderTargetBitmap(
        (int)rect.Width, (int)rect.Height, 96, 96, PixelFormats.Default);

    target.Render(visual);
    return target;
}

In order to draw in the bitmap's pixel units and hence ignore its DPI, modify the method like this:

var rect = new Rect(0, 0, source.PixelWidth, source.PixelHeight);

using (var dc = visual.RenderOpen())
{
    dc.DrawRectangle(new ImageBrush(source), null, rect);
    dc.DrawGeometry(null, pen, geometry);
}

The following method uses the above to draw a polyline as an IEnumerable<Point>.

public static BitmapSource DrawPolyline(
    BitmapSource source, Pen pen, IEnumerable<Point> points)
{
    var geometry = new PathGeometry();

    if (points.Count() >= 2)
    {
        var figure = new PathFigure { StartPoint = points.First() };
        figure.Segments.Add(new PolyLineSegment(points.Skip(1), true));
        geometry.Figures.Add(figure);
    }

    return DrawGeometry(source, pen, geometry);
}

It would be used like

var source = new BitmapImage(new Uri(...));

var pen = new Pen
{
    Brush = Brushes.Blue,
    Thickness = 2,
};

var points = new List<Point>
{
    new Point(100, 100),
    new Point(1000, 100),
    new Point(1000, 1000),
    new Point(100, 1000),
    new Point(100, 100),
};

image.Source = DrawPolyline(source, pen, points);
0
Joey On

Your canvas needs a size, so someone or something has to Arrange it. That might already be enough to get it to render, but the only reliable way of rendering arbitrary visuals to a bitmap is to actually place them in the visual tree of a window that's displayed and thus laid out by WPF. You can then render to the bitmap in a deferred task at ContextIdle priority to ensure that layout is complete.