"Parameter Not Valid" on call to GetHenhmetafile(), trying to save an in-memory Graphics object as EMF

StackOverflow https://stackoverflow.com/questions/20390268

質問

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: 
役に立ちましたか?

解決

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.

他のヒント

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
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top