1

I'm drawing some data using Winforms (with a C# solution).

I plot error bars using chart series (with the chart.Series[item].Points.AddXY() method). However, I link the error bars' middle points with a drawing in the Paint event handler, since series doesn't seem to be drawable with custom pens. The Paint event looks like the following (it has some custom shenanigans for offsetting each curve along the x-axis):

private void Chart_Paint(object sender, PaintEventArgs e)
{
    var g = e.Graphics;
    g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
    for (int line = 0; line < _meanLines.Count; line++)
    {
        Pen pen = _pens[line];
        _xOffset = LinesOffset(_meanLines.Count, line, _xOffsetRatio);

        List<(float x, float y)> points = _meanLines[line].Select(mean => (float.Parse(mean.x) * (1 + _xOffset), mean.y)).ToList();
        
        List<Point> pointsList = points.Select(p => new Point((int)chart.ChartAreas[0].AxisX.ValueToPixelPosition(p.x), (int)chart.ChartAreas[0].AxisY.ValueToPixelPosition(p.y))).ToList();

        for (int p = 1; p < pointsList.Count; p++)
        {
            g.DrawLine(pen, pointsList[p - 1], pointsList[p]);
        }
    }
}

Everything works fine, and my chart looks like I want it to look: chart as displayed when the software is running.

I then want to export my chart as an EMF file (to be later be included in LaTex), using: chart.SaveImage($@"C:\Users\User\Documents\DataPlotter\{figureName}.emf", ChartImageFormat.Emf);

But then my lines drawn in the Paint event handler are missing: chart as exported in emf.

Is there a way to keep these lines in an emf export?

I tried exporting the chat as a bitmap to check if the drawn lines were exported, and it worked (the following code saved the aforementioned image with the lines displayed):

using (Bitmap bmp = new Bitmap(chart.Size.Width, chart.Size.Height)) 
            {
                chart.DrawToBitmap(bmp, new Rectangle(0, 0, chart.Size.Width, chart.Size.Height));
                using (Graphics g = Graphics.FromImage(bmp)) 
                {
                    g.DrawLine(new Pen(Color.Black, 2), new Point(0, 0), new Point(chart.Size.Width, chart.Size.Height));
                    chart.SaveImage($@"C:\Users\User\Documents\DataPlotter\{figureName}_trueEmf.emf", ChartImageFormat.Emf); // The drawn lines are missing
                    bmp.Save($@"C:\Users\User\Documents\DataPlotter\{figureName}_png.png", System.Drawing.Imaging.ImageFormat.Png); // Every drawn line appear
                    bmp.Save($@"C:\Users\User\Documents\DataPlotter\{figureName}_bmpEmf.emf", System.Drawing.Imaging.ImageFormat.Emf); // Every drawn line appear but saved as a bitmap
                } 
            }

But I still can't find a way to export these lines in a vectorial image. I can export an emf file using the last line with the bmp.Save method, but it's not drawn using vectors (I can't import it to Inkscape and save an eps file for instance).

I feel like there's no way to make DrawLine() draw vectors, but I don't know if .NET provides something else for drawing vectors (appart from using a chart.Series object, but I can't find a way to do so while using a custom pen...

Does anyone have an idea?

Thanks!

Gautzilla
  • 33
  • 1
  • 3
  • Have you tried to derive a Graphics object from the Bitmap (`using (var g = Graphics.FromImage(bmp))`), after `chart.DrawToBitmap()`, then draw the same thing you were drawing in the Paint handler, using this other Graphics object this time? – Jimi Dec 15 '22 at 18:43
  • Hi @Jimi, thanks for the answer. I just tried that, and end up with the same result: bmp exports are fine and draw all the chart's content, but emf exports lack the line drawn with the custom pens. I'm not really at ease with all of Winforms concepts (it's kinda new to me) so I tried to juggle a bit with the drawings and export but all of my tests end up the same way! – Gautzilla Dec 15 '22 at 22:50
  • I'm wondering if I'm following your instructions properly tho, as it seems the drawings I make on the derived Graphics object do not appear, even in the running software's chart window. My code looks like the following: `using(Bitmap bmp...) { chart.DrawToBitmap... using(Graphics g = Graphic.FromImage(bmp)) { g.DrawLine... chart.SaveImage... } }` The line drawn with g.DrawLine does not appear in the EMF file saved thanks to chart.SaveImage – Gautzilla Dec 15 '22 at 23:01
  • Would you mind to edit your answer and add the code there which draw the extra lines? Your comment above looks like a good start. – Doc Brown Dec 16 '22 at 07:16
  • Thanks @DocBrown, I edited the comment with the aforementioned code and added some comments on the results I managed to get! – Gautzilla Dec 16 '22 at 11:17
  • @Gautzilla: I would be more interested in the code of your paint event. – Doc Brown Dec 16 '22 at 11:19
  • @DocBrown Got it, I just edited it in! – Gautzilla Dec 16 '22 at 11:30
  • I had an idea how to approach this, unfortunately, it turned out to be a harder problem than I expected. The idea was to use `chart.SaveImage` to save the chart to a temporary file, then reload the file (`mf=new Metafile("filename.emf")`) and create a gfx context for adding the extra elements from your paint event (`gfx = Graphics.FromImage(mf)`). Unfortunately, this last command always throws an OutOfMemory error, which is a sign GDI+ struggles with the specific EMF format created by the chart. – Doc Brown Dec 16 '22 at 13:07
  • ... another idea I had was to let the `OnPaint` method of the chart draw itself on some gfx context of a newly created Metafile object. That works to some degree, but when closely looking at the result, I saw the chart itself was just a raster image which does not scale when zooming. Only the extra lines were vector elements. – Doc Brown Dec 16 '22 at 13:13

1 Answers1

0

Ok, finally I worked it out, this is the most simple solution I could come up with.

First, refactor your Chart_Paint method so you can reuse the drawing of those additional lines outside of the Paint event. It should look like this:

private void Chart_Paint(object sender, PaintEventArgs e)
{
    DrawExtraGraphics(e.Graphics);
}

Now, we can save the chart to a temporary memory stream, read that stream again and overlay the drawing with the extra lines:

MemoryStream ms = new MemoryStream();
chart.SaveImage(ms, ImageFormat.Emf);
ms.Seek(0, SeekOrigin.Begin);
using (var mf0 = new Metafile(ms))
{
    // "this" can be the form, that will be sufficient
    using (var gfx = this.CreateGraphics())
    {
        // this creates an empty EMF file
        using (var mf = new Metafile(MyFilename, gfx.GetHdc()))
        {
            // gfx context for drawing in the file
            using (var igfx = Graphics.FromImage(mf))
            {
                // draw the chart
                igfx.DrawImage(mf0, 0, 0);
                // draw the extra lines
                DrawExtraGraphics(igfx);
            }
        }
    }
}

(I found the code for creating and saving a new EMF here.)

The result is an EMF which contains the chart as well as the extra lines as vector elements. Hope this helps!

Doc Brown
  • 19,739
  • 7
  • 52
  • 88
  • Thank you so much for taking the time to help me. I'll try our solution as soon as I have enough time and I'll provide some feedback on how it went! – Gautzilla Dec 16 '22 at 15:51
  • You are welcome. I learned a lot of things from this question, for example I did not know about the chart control in .Net, always thought one requires additional third party components for this. – Doc Brown Dec 16 '22 at 20:56
  • I just spent some time implementing your solution, which works like a charm! Thank you so much, even if I had thought of the general workflow idea, I wouldn't have been able to code it myself. – Gautzilla Dec 17 '22 at 00:58