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:
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.