Processing Global Mouse and Keyboard Hooks in C#
http://www.codeproject.com/KB/cs/globalhook.aspx
Drawing on an in-memory metafile in C#
http://nicholas.piasecki.name/blog/2009/06/drawing-o-an-in-memory-metafile-in-c-sharp/
by Nicholas Piasecki on June 13th, 2009
So metafiles in the GDI world of Windows are a vector format that is essentially a sequence of GDI drawing commands (we know it as System.Drawing.Graphics in the .NET world) serialized to disk. Because it's a vector format, we can play back the metafile to different device contexts, all with different DPIs, and get more or less the same high-fidelity result on each device. But working with an in-memory metafile in C# isn't as intuitive as one might think.
A problem that you will encounter is that while the metafile calculates its DPI according to the resolution of the reference device, the .NET Graphics instance that you get to draw on seems to always be set to 96 DPI. That simply will not work: the result, in fact, is a lot of cussing when your carefully measured measurements simply don't line up when you play back the metafile later. Here's a link to a guy who had the same exact problem. It gets really confusing because the amount of error will vary depending on the machine you run it on, since the reference device is the physical resolution (i.e., the desktop you're looking at).
The secret is to apply a scale transform to your Graphics instance to fix the DPI mismatch.
Here's some sample code:
/// <summary>
/// Adds a new metafile page, selects it as the current metafile, and
/// returns its graphics object. The caller is responsible for disposing
/// the returned Graphics instance. The unit of measurement in the
/// returned Graphics instance is GraphicsUnit.Inch.
/// </summary>
/// <param name="pageSize">The size of the page, in inches.</param>
/// <returns>a Graphics instance to render the page onto; the caller is
/// responsible for disposing of this Graphics instance</returns>
protected Graphics GetNextPage(SizeF pageSize)
{
IntPtr deviceContextHandle;
Graphics offScreenBufferGraphics;
Graphics metafileGraphics;
MetafileHeader metafileHeader;
KeyValuePair<Metafile, MemoryStream> pair;
this.currentStream = new MemoryStream();
using (offScreenBufferGraphics = Graphics.FromHwndInternal(IntPtr.Zero))
{
deviceContextHandle = offScreenBufferGraphics.GetHdc();
this.currentMetafile = new Metafile(
this.currentStream,
deviceContextHandle,
new RectangleF(0, 0, pageSize.Width, pageSize.Height),
MetafileFrameUnit.Inch,
EmfType.EmfOnly);
pair = new KeyValuePair<Metafile, MemoryStream>(
this.currentMetafile,
this.currentStream);
this.metafiles.Add(pair);
offScreenBufferGraphics.ReleaseHdc();
metafileGraphics = Graphics.FromImage(this.currentMetafile);
metafileHeader = this.currentMetafile.GetMetafileHeader();
metafileGraphics.ScaleTransform(
metafileHeader.DpiX / metafileGraphics.DpiX,
metafileHeader.DpiY / metafileGraphics.DpiY);
metafileGraphics.PageUnit = GraphicsUnit.Inch;
metafileGraphics.SetClip(new RectangleF(0, 0, pageSize.Width, pageSize.Height));
}
Most of this is mumbo-jumbo related to my particular PrintDocumentBase implementation, but the real gem is the ScaleTransform() call: that's where I account for the mismatch between the resolution of the metafile itself and Graphics instance that I'll be doodling on. (My implementation also expects the page size to set so additional clipping information can be set, but that's not important to this discussion.)
I can now doodle on the returned graphics object with abandon, resting assured that when I say "draw a line that is one inch long" I will get a line in my metafile that is one inch long. (My instance is storing the Metafile and MemoryStream pair in a dictionary. When I want to save the metafile to disk, I just have to call ToArray() on the MemoryStream.)
Bonus: Printing a serialized Metafile
Now let's say that I have a byte[] array that corresponds to a serialized Metafile. How do I actually print that to my printer? We'll have to do some P/Invokes. I have my own class called PrinterDocumentBase that basically returns a byte[] array on each call to GetPage(int pageNo). I assume that what I get back from that call in the function below is a serialized Metafile. (I have other PrinterBase inheritors that assume that byte[] array represents something different, like EPL2 data or ZPL data, and sends it to the printer appropriately.)
/// <summary>
/// Prints the given document.
/// </summary>
/// <param name="document">the document, which is guaranteed not to be
/// null in and in a language that the printer supports</param>
/// <param name="copies">the number of copies, which is guaranteed to be
/// a positive number</param>
protected override void DoPrint(PrinterDocumentBase document, int copies)
{
IntPtr deviceContext;
NativeMethods.DOCINFO documentInfo;
int hardMarginLeft;
int hardMarginTop;
int pagesCount;
documentInfo = new NativeMethods.DOCINFO();
documentInfo.fwType = 0;
documentInfo.lpszDatatype = null;
documentInfo.lpszDocName = document.Name;
documentInfo.lpszOutput = null;
documentInfo.cbSize = Marshal.SizeOf(documentInfo);
pagesCount = document.Prepare();
deviceContext = NativeMethods.CreateDC(
"WINSPOOL",
this.Name.Normalize(),
null,
IntPtr.Zero);
if (deviceContext != IntPtr.Zero)
{
hardMarginLeft = NativeMethods.GetDeviceCaps(
deviceContext,
NativeMethods.DeviceCap.PHYSICALOFFSETX);
hardMarginTop = NativeMethods.GetDeviceCaps(
deviceContext,
NativeMethods.DeviceCap.PHYSICALOFFSETY);
hardMarginLeft = (int)(hardMarginLeft * 100F / NativeMethods.GetDeviceCaps(deviceContext, NativeMethods.DeviceCap.LOGPIXELSX));
hardMarginTop = (int)(hardMarginTop * 100F / NativeMethods.GetDeviceCaps(deviceContext, NativeMethods.DeviceCap.LOGPIXELSY));
for (int copyIdx = 0; copyIdx < copies; ++copyIdx)
{
if (NativeMethods.StartDoc(deviceContext, documentInfo) > 0)
{
for (int i = 0; i < pagesCount; ++i)
{
byte[] data;
data = document.GetPage(i);
using (var ms = new MemoryStream(data))
using (var metafile = Metafile.FromStream(ms))
{
using (var g = Graphics.FromHdcInternal(deviceContext))
{
g.TranslateTransform(-hardMarginLeft, -hardMarginTop);
if (NativeMethods.StartPage(deviceContext) > 0)
{
g.DrawImage(metafile, 0, 0);
g.Flush();
if (NativeMethods.EndPage(deviceContext) <= 0)
{
throw new Win32Exception();
}
}
else
{
throw new Win32Exception();
}
}
}
}
if (NativeMethods.EndDoc(deviceContext) <= 0)
{
throw new Win32Exception();
}
}
}
}
else
{
throw new Win32Exception();
}
if (deviceContext != IntPtr.Zero)
{
NativeMethods.DeleteDC(deviceContext);
}
}
return metafileGraphics;
}
heStartDocand such functions are documented in MSDN and are pretty boring for this discussion.
Because when I specify I want text to print at a certain measurement in my metafile, I do have to do some dancing to shift back the hard margins of the printer–I'll assume that I was smart enough to account for this when I was doodling on my metafile.
Keep in mind that when you do set GraphicsUnit.Inch as I have, your pen widths will be in whole inches, which can result in some pretty wide strokes! Took me a while to figure that one out, too, though it was blindingly obvious.
Hope that helps someone get on the right path!
Комментариев нет:
Отправить комментарий