3

I've searched in vain for the last two weeks for an answer to this one, but I'm stumped.

I'm working with some code that creates a sample image from a Graphics object constructed from a metafile, all residing in a memory stream in order to avoid the need for Windows.Forms (it's a console app), and I'm using the function CopyEnhMetaFile (imported from gdi32.dll), to save the metafile out to disk as a real EMF. You can look here, here, here and here, for some basic notes on how I put this together.

It works fine when I write it top-down as simple main() script (as seen in the codeproject example). But when I try to bundle the metafile/graphics object into a class with methods, I am unable to obtain the MetafileHandle, because GetHenhmetafile() spits back a parameter is not valid exception.

According to this source, that exception is a clear indicator that the method has been invoked at least once before. But have a look at my code. I sure can't see where I've invoked it twice. Maybe you can?

In any case, I'm strongly suspecting that I am either not fully understanding something fundamental about the way these objects can be used (MemoryStreams, Metafiles, or P/Invoked functions), or I'm missing something basic about the way C# classes work, and I was hoping someone could give me a push in the right direction.

[Edited to add back in the successful code, and leave only the contextual bits for the code that is broken, per suggestion]

Here is the code that worked:

class EmfGenerator
{
    static void Main()
    {
        const int width = 450;
        const int height = 325;

        Metafile m;
        using (var stream = new MemoryStream())
        {
            Graphics offScreenBufferGraphics; //this is a throw-away object needed for the deviceContext
            using (offScreenBufferGraphics = Graphics.FromHwndInternal(IntPtr.Zero))
            {
                IntPtr deviceContextHandle = offScreenBufferGraphics.GetHdc();

                m = new Metafile(
                stream,
                deviceContextHandle,
                new RectangleF(0, 0, width, height),
                MetafileFrameUnit.Pixel, //scaling only works properly with integers due to decimal truncation, so use milimeters or pixels here
                EmfType.EmfPlusOnly); //force GDI+ mode
                offScreenBufferGraphics.ReleaseHdc(); //once we have our metafile, we no longer need the context handle
            }
        }

        using (Graphics g = Graphics.FromImage(m))
        {
            //draw a picture
            g.Clear(Color.White);
            //etc...
        } 

        // Save it as a metafile
        IntPtr iptrMetafileHandle = m.GetHenhmetafile();
        CopyEnhMetaFile(iptrMetafileHandle, @"emf_binary_sample.emf"); //this gives us just the metafile
        DeleteEnhMetaFile(iptrMetafileHandle);

    }
}

And here is the code that doesn't work. One note: I originally wrote it with the "using" constructs above, and had the same error. So, I rebuilt it without, on the chance that the using wrappers were destroying something too early. I got the same error either way.

class MetafileExperiment
{
    protected Graphics G; //the working graphics object
    protected Metafile M; //the working metafile
    protected IntPtr MetafileHandle;

    public MetafileExperiment(int startingWidth, int startingHeight)
    {
        var stream = new MemoryStream();
        var bfr = Graphics.FromHwndInternal(IntPtr.Zero);
        IntPtr dev = bfr.GetHdc();

        M = new Metafile(
            stream,
            dev,
            new RectangleF(0, 0, startingWidth, startingHeight),
            MetafileFrameUnit.Pixel, //scaling only works properly with integers due to decimal truncation, so use milimeters or pixels here
            EmfType.EmfPlusOnly); //force GDI+ mode

        //the handle is needed in order to use the P/Invoke to save out and delete the metafile in memory.
        MetafileHandle = M.GetHenhmetafile(); // Parameter is not valid 

        bfr.ReleaseHdc(); 

        G = Graphics.FromImage(M);

    }

}    

As you can see, I put the GetHenhmetafile() in the constructor, directly after creating the metafile itself. I did this on some notes I found that said you could only invoke this method once per instance (See here, for example). For the adventurous, the entire repo can be found here.

On the off chance it's helpful, here's the exception details in the broken code (the inner exception is null):

System.ArgumentException was unhandled
  _HResult=-2147024809
  _message=Parameter is not valid.
  HResult=-2147024809
  IsTransient=false
  Message=Parameter is not valid.
  Source=System.Drawing
  StackTrace:
       at System.Drawing.Imaging.Metafile.GetHenhmetafile()
       at SimpleEmfGenerator.MetafileExperiment..ctor(Int32 startingWidth, Int32 startingHeight) in c:\Users\ggauthier\Repositories\Articulate\SimpleEmfGenerator\SimpleEmfGenerator\MetafileExperiment.cs:line 40
       at SimpleEmfGenerator.EmfGenerator.Main() in c:\Users\ggauthier\Repositories\Articulate\SimpleEmfGenerator\SimpleEmfGenerator\EmfGenerator.cs:line 108
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: 
Greg Gauthier
  • 1,336
  • 1
  • 12
  • 25
  • What is the size in the context of this process, maybe this is similar to http://stackoverflow.com/questions/5801652/bitmap-while-assigning-height-width-crashes/5802113#5802113 – V4Vendetta Dec 05 '13 at 03:16
  • @V4Vendetta - Width=450, Height=325 (in pixels) - pretty much the same as the working code. The dpi range I've been working with is 88 - 120; typical run is 96dpi. The size of the files, as bitmaps, it roughly 572K. I'd be a little more than shocked if there wasn't 572k available for an in-memory image. – Greg Gauthier Dec 05 '13 at 03:50
  • Doesn't appear to be anything to do with your p/invoke. You can probably remove all that from the question. Would help us if you stripped out all the unnecessary code. Hard to see the forest for the trees. Problem is in the call to `GetHenhmetafile`, so that means you are creating the `Metafile` object differently. I'd try to get variants of the code that focus entirely on those differences. – David Heffernan Dec 05 '13 at 10:02
  • @DavidHeffernan I removed everything except the failing bit, and its surrounding context. If it would help, I can put just that, and a short piece of code that attempts to instantiate this class, in a project on github for perusal. – Greg Gauthier Dec 05 '13 at 14:43
  • I think you should have kept the code that worked as well as that which does not. And the we could have concentrated on the differences. As I recall the two versions of code were quite different in the way you instantated the Metafile object. You should show us those two instantiations as a compare and contrast exercise. – David Heffernan Dec 05 '13 at 14:52
  • @DavidHeffernan good idea. Added back in. – Greg Gauthier Dec 05 '13 at 15:13
  • Why are you calling `Graphics.FromHwndInternal()`. The documentation tells you not to do that. What do you want to use as reference DC. For the screen DC you would normally call `GetDC()` in Win32. Not sure what the equivalent is in .net/GDI+. I'm not the GDI+ guy. – David Heffernan Dec 05 '13 at 15:15
  • OK, I can now see that the two blocks of code should indeed be equivalent. Odd. I'll have a wee dig about. – David Heffernan Dec 05 '13 at 15:19
  • @DavidHeffernan Well, I'm just sort of learning here, myself. From what I understood, I needed a device context handle for the Metafile constructor. In order to do that, I needed a graphics object from a window handle. I know the doc says its only intended to be used "internally", but I don't know any other way to do this. If there's an alternative, I'm all ears! What I wrote is what I could find out here in the googlesphere... – Greg Gauthier Dec 05 '13 at 15:26
  • Right, my working premise now is that GDI+ needs to be initialised. That doesn't happen in a console app, but it does in a GUI app. Need to work out how to initialise it........ – David Heffernan Dec 05 '13 at 15:28

2 Answers2

4

This is an initialization problem. The core issue is that you create the Metafile from an empty stream and GDI+ appears to delay the actual creation of the native metafile until it has a good reason to. With the quirk (aka bug) that GetHenhmetafile() isn't a good enough reason.

The workaround is to force it to do so, put this line of code before the call:

    using (Graphics.FromImage(M)) {}

Do beware that creating a Graphics object from the metafile after you've obtained the native handle is not possible, you'll see that code fail with the same exception. It isn't clear why you'd want to do that.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • +1 Well, whilst I could diagnose it, I had no ruddy clue how to fix it! Well done. – David Heffernan Dec 05 '13 at 15:31
  • @Hans Passant - I moved the call to just after the creation of the metafile, because the doc I was reading on the error said it could only be called once successfully, and I wanted to try to eliminate the chance that something else was calling it invisibly after the metafile was created. That's really the only reason. I'll give your suggestion a shot! – Greg Gauthier Dec 05 '13 at 15:33
  • So, it solved the problem of the error on GetHenhMetafile(), but it created a whole new problem. I think the 'using' is destroying the graphics object in the constructor before I can actually use it in any methods. (I also tried changing it to `using (G = Graphics.FromImage(M)) {}`). Anyway, I get a "parameter is not valid" error on the Graphics object now, instead of on the call to GetHenhMetafile(). – Greg Gauthier Dec 05 '13 at 16:12
  • I specifically warned about that. The basic rule is that you get to play with metafile through the native handle OR by using the Graphics class. You cannot do both. I have no idea why you are doing this so can't guess at good advice. – Hans Passant Dec 05 '13 at 16:24
  • This is your answer. It's just a wag to get GDI+ initialised. Do it earlier, and then do the real code. – David Heffernan Dec 05 '13 at 20:59
  • Thanks guys. I sorted it out. Hans was right. I just needed to understand what he was telling me to do. Which took a few hours. I'm a little slow. – Greg Gauthier Dec 06 '13 at 00:28
0

I'm not sure if this is just because it's fixed in .NET 4.0, but I can save a Metafile without resorting to the ugly DllImport.

Sorry for this being VB.NET instead of C#, but that's what I use...

Public Shared Sub Test()
    Dim ms As New IO.MemoryStream()
    Dim img As New Bitmap(1000, 1000)
    Dim imgGraphics = Graphics.FromImage(img)
    Dim hdc = imgGraphics.GetHdc()
    Dim mf = New Metafile(ms, hdc, EmfType.EmfPlusOnly)
    imgGraphics.ReleaseHdc()
    ' Important - The above is just a test.  In production code, you should eventually dispose of stuff

    Using g = Graphics.FromImage(mf)
        g.DrawRectangle(Pens.Black, 20, 30, 15, 16)
        g.DrawRectangle(Pens.Black, 140, 130, 15, 16)
    End Using
    ' Note: it's important that the Graphics used to draw into the MetaFile is disposed
    ' or GDI+ won't flush.

    ' Easy Way
    Dim buffer1 = ms.GetBuffer() ' This produces a 444 byte buffer given the example above

    ' The Hard way
    Dim enhMetafileHandle = mf.GetHenhmetafile
    Dim bufferSize = GetEnhMetaFileBits(enhMetafileHandle, 0, Nothing)
    Dim buffer(bufferSize - 1) As Byte
    Dim ret = GetEnhMetaFileBits(enhMetafileHandle, bufferSize, buffer)

End Sub
dwilliss
  • 862
  • 7
  • 19