OutOfMemoryException on drawing wide lines with Wi

2019-07-24 03:58发布

问题:

This one is crazy. I just draw a couple of thousands of lines in OnPaint handler. There is no problem, when pen.Width <= 1, or when there aren't many lines on the screen.

OK, I draw a scaled map. Line width scales with the map. When I zoom SOME maps, I get OutOfMemoryException. WHY?!

When I set pen.Width to 1 - no problem. When I set it to correspond tracks widths - some maps draw OK, some throw the exception AT CERTAIN ZOOM LEVELS.

What's going on? It's has NOTHING to do with actual memory usage. I've double checked this.

BTW, the pen.Width I set is around 2 when it happens.

The code looks like foreach (...) g.DrawLine(...) - and it crashes after drawing a couple of hundreds of lines.

If I won't find solution to this, I'll have to drop line width scaling which would greatly degrade the quality of presentation. Or I can do an ugly hack trying to catch this exception (if it can be caught)...

NOTE: I don't use any bitmaps. I don't operate on huge arrays. I don't open any files during drawing. There is an array of vectors (about 10k elements), I just draw all of them as separate lines, using some different pens for various map objects. When I don't touch pen.Width - no exception occurs. When I set pen.Width - some maps are displayed correctly with all zoom levels, but some throw the exception. The 5 pens are created in OnPaint event before entering the drawing loop and are properly disposed after exiting the loop. Before drawing each line its width is set.

I tried to limit line coordinates to only ones actually visible in viewport. It's redundant, since Graphics object takes care of it by itself. Of course it didn't help. I tried it on some smaller window sizes - didn't help. I tried to switch double buffering on and off. No joy. I'm out of ideas.

EDIT:

private void DrawMap(PaintEventArgs e) {
    var pens = new[] {
        new Pen(TrackColor),
        new Pen(SwitchColor),
        new Pen(RoadColor),
        new Pen(RiverColor),
        new Pen(CrossColor)
    };
    var b = Splines.Bounds;
    var g = e.Graphics;
    var f = true; // OutFull;
    var tr = GetTransformation();
    float ts = tr[0], tx = tr[1], ty = tr[2];
    TrackSpline[] visible = !f ? Splines.GetSubset(ts, _Viewport) : null;
    var ct = f ? Splines.Count : visible.Length;
    for (int i = 0; i < ct; i++) {
        TrackSpline s = f ? Splines[i] : visible[i];
        var pen = pens[s.T];
        pen.Width = ts * s.W;
        if (ts < 0.01 || s.L) {
            var p1 = new PointF(s.A.X * ts + tx, s.A.Y * ts + ty);
            var p2 = new PointF(s.D.X * ts + tx, s.D.Y * ts + ty);
            g.DrawLine(pen, p1, p2);
        } else {
            var p1 = new PointF(s.A.X * ts + tx, s.A.Y * ts + ty);
            var p2 = new PointF(s.B.X * ts + tx, s.B.Y * ts + ty);
            var p3 = new PointF(s.C.X * ts + tx, s.C.Y * ts + ty);
            var p4 = new PointF(s.D.X * ts + tx, s.D.Y * ts + ty);
            try {
                g.DrawBezier(pen, p1, p2, p3, p4);
            } catch (OutOfMemoryException) {
                g.DrawLine(pen, p1, p4);
            }
        }
    }
    foreach (var p in pens) p.Dispose();
}

See the ugly hack here? It works flawlessly and I don't even see which curves are replaced with lines. Obviously g.DrawBezier throws the exception. I don't like ugly hacks...

回答1:

Here's the solution, thanks to hint from @LarsTech:

private void DrawMap(PaintEventArgs e) {
    var pens = new[] { // TODO: draw layers instead
        new Pen(TrackColor),
        new Pen(SwitchColor),
        new Pen(RoadColor),
        new Pen(RiverColor),
        new Pen(CrossColor)
    };
    var b = Splines.Bounds;
    var g = e.Graphics;
    var f = true; // OutFull; // (TODO: limiting vectors to visible ones)
    var tr = GetTransformation(); // gets scale and translation for points
    float ts = tr[0], tx = tr[1], ty = tr[2];
    TrackSpline[] visible = !f ? Splines.GetSubset(ts, _Viewport) : null;
    var ct = f ? Splines.Count : visible.Length;
    for (int i = 0; i < ct; i++) {
        TrackSpline s = f ? Splines[i] : visible[i];
        var pen = pens[s.T];
        pen.Width = ts * s.W;
        if (ts < 0.01 || s.L) {
            var p1 = new PointF(s.A.X * ts + tx, s.A.Y * ts + ty);
            var p2 = new PointF(s.D.X * ts + tx, s.D.Y * ts + ty);
            g.DrawLine(pen, p1, p2);
        } else {
            var p1 = new PointF(s.A.X * ts + tx, s.A.Y * ts + ty);
            var p2 = new PointF(s.B.X * ts + tx, s.B.Y * ts + ty);
            var p3 = new PointF(s.C.X * ts + tx, s.C.Y * ts + ty);
            var p4 = new PointF(s.D.X * ts + tx, s.D.Y * ts + ty);
            var b1c = Math.Abs(p1.X - p2.X) >= 0.1f || Math.Abs(p1.Y - p2.Y) > 0.1f;
            var b2c = Math.Abs(p3.X - p4.X) >= 0.1f || Math.Abs(p3.Y - p4.Y) > 0.1f;
            if (b1c && b2c) g.DrawBezier(pen, p1, p2, p3, p4); else g.DrawLine(pen, p1, p4);
        }
    }
    foreach (var p in pens) p.Dispose();
}

In his linked answer we read:

That's a bug with the pen and the widen method. Make sure your startpoint of the path and the endpoint of the path are not the same.

Yes, a bug in .NET, reported to Microsoft and apparently not fixed yet. And here it shows with Bezier curves, which look too much like straight lines ;)

I guess zero-lenght lines could throw similar exception.

Note that I check the distance between points coordinates is greater than 0.1f, not 0! It's important. The exception is thrown if the points are close enough to each other, not only when they equal. I could calculate the distance between points, but for performance reasons it's better not to.

It's not good for performance to have such checks for every curve and line - but it seems somehow better than catching the false exception. The check could be probably optimized a little, or moved into "scale changed" handler.

BTW: GetTransformation() method in my code just brings scale, X and Y offsets for all points. If you wonder why I don't use built in transformation and do it manually - it's because built in transformation doesn't work with double buffering. Another bug in .NET or just feature? Without double buffering drawing is painfully slow, so it has to be used here.