Determining exact glyph height in specified font - c#

I have searched a lot and tried much but I can not find the proper solution.
I wonder is there any approach for determining exact glyph height in specified font?
I mean here when I want to determine the height of DOT glyph I should receive small height but not height with paddings or the font size.
I have found the solution for determining exact glyph width here (I have used the second approach) but it does not work for height.
UPDATE: I need solution for .NET 1.1

It's not that hard to get the character metrics. GDI contains a function GetGlyphOutline that you can call with the GGO_METRICS constant to get the height and width of the smallest enclosing rectangle required to contain the glyph when rendered. I.e, a 10 point glyph for a dot in font Arial will give a rectangle of 1x1 pixels, and for the letter I 95x.14 if the font is 100 points in size.
These are the declaration for the P/Invoke calls:
// the declarations
public struct FIXED
{
public short fract;
public short value;
}
public struct MAT2
{
[MarshalAs(UnmanagedType.Struct)] public FIXED eM11;
[MarshalAs(UnmanagedType.Struct)] public FIXED eM12;
[MarshalAs(UnmanagedType.Struct)] public FIXED eM21;
[MarshalAs(UnmanagedType.Struct)] public FIXED eM22;
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int x;
public int y;
}
[StructLayout(LayoutKind.Sequential)]
public struct POINTFX
{
[MarshalAs(UnmanagedType.Struct)] public FIXED x;
[MarshalAs(UnmanagedType.Struct)] public FIXED y;
}
[StructLayout(LayoutKind.Sequential)]
public struct GLYPHMETRICS
{
public int gmBlackBoxX;
public int gmBlackBoxY;
[MarshalAs(UnmanagedType.Struct)] public POINT gmptGlyphOrigin;
[MarshalAs(UnmanagedType.Struct)] public POINTFX gmptfxGlyphOrigin;
public short gmCellIncX;
public short gmCellIncY;
}
private const int GGO_METRICS = 0;
private const uint GDI_ERROR = 0xFFFFFFFF;
[DllImport("gdi32.dll")]
static extern uint GetGlyphOutline(IntPtr hdc, uint uChar, uint uFormat,
out GLYPHMETRICS lpgm, uint cbBuffer, IntPtr lpvBuffer, ref MAT2 lpmat2);
[DllImport("gdi32.dll", ExactSpelling = true, PreserveSig = true, SetLastError = true)]
static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
The actual code, rather trivial, if you don't consider the P/Invoke redundancies. I tested the code, it works (you can adjust for getting the width as well from GLYPHMETRICS).
Note: this is ad-hoc code, in the real world, you should clean up the HDC's and objects with ReleaseHandle and DeleteObject. Thanks to a comment by user2173353 to point this out.
// if you want exact metrics, use a high font size and divide the result
// otherwise, the resulting rectangle is rounded to nearest int
private int GetGlyphHeight(char letter, string fontName, float fontPointSize)
{
// init the font. Probably better to do this outside this function for performance
Font font = new Font(new FontFamily(fontName), fontPointSize);
GLYPHMETRICS metrics;
// identity matrix, required
MAT2 matrix = new MAT2
{
eM11 = {value = 1},
eM12 = {value = 0},
eM21 = {value = 0},
eM22 = {value = 1}
};
// HDC needed, we use a bitmap
using(Bitmap b = new Bitmap(1,1))
using (Graphics g = Graphics.FromImage(b))
{
IntPtr hdc = g.GetHdc();
IntPtr prev = SelectObject(hdc, font.ToHfont());
uint retVal = GetGlyphOutline(
/* handle to DC */ hdc,
/* the char/glyph */ letter,
/* format param */ GGO_METRICS,
/* glyph-metrics */ out metrics,
/* buffer, ignore */ 0,
/* buffer, ignore */ IntPtr.Zero,
/* trans-matrix */ ref matrix);
if(retVal == GDI_ERROR)
{
// something went wrong. Raise your own error here,
// or just silently ignore
return 0;
}
// return the height of the smallest rectangle containing the glyph
return metrics.gmBlackBoxY;
}
}

Can you update the question to include what you have tried ?
By dot glyph I assume you mean the punctuation mark detailed here ?
Is this Glyph height displayed on screen or a printed page ?
I managed to modify the first method in the link you posted in order to count the matching vertical pixels, however identifying the largest height of the glyph is fiddly to do unless you are willing to draw character by character, so this wasn't really a general working solution like the article.
In order to have a general working solution would need identify the largest single pixel vertical region of the character / glyph, then count the number of pixels in that region.
I also managed to verify that Graphics.MeasureString, TextRenderer.MeasureText and Graphics.MeasureCharacterRanges all returned the bounding box which gave a number similar to the font height.
The alternative to this is to Glyph.ActualHeight property which gets the rendered height of the framework element. This part of WPF and the related GlyphTypeface and GlyphRun classes. I wasn't able to test them at this time having only Mono.
The steps for getting Glyph.ActualHeight are as follows
Initialise the arguments for GlyphRun
Initialise GlyphRun object
Access relevant Glyph using glyphTypeface.CharacterToGlyphMap[text[n]] or more correctly glyphTypeface.GlyphIndices[n], where glyphTypeface is your GlyphTypeface, which is created from the Typeface object you make in Step 1.
Relevant resources on using them include
The Thing about Glyphs
GlyphRun and So Forth
Measuring Text
Glyphs Particularly the picture the bottom.
Futher references on GDI (What these classes use under the hood is GDI or GDI+) and Fonts in Windows include
GDI
Windows Font Mapping

Here's a solution involving WPF. We create an intermediate Geometry object in order to retrieve the accurate bounding box of our text. The advantage of this solution is that it does not actually render anything. Even if you don't use WPF for your interface, you may use this piece of code to do your measurements only, assuming the font rendering size would be the same in GDI, or close enough.
var fontFamily = new FontFamily("Arial");
var typeface = new Typeface(fontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
var fontSize = 15;
var formattedText = new FormattedText(
"Hello World",
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
fontSize,
Brushes.Black);
var textGeometry = formattedText.BuildGeometry(new Point(0, 0));
double x = textGeometry.Bounds.Left;
double y = textGeometry.Bounds.Right;
double width = textGeometry.Bounds.Width;
double height = textGeometry.Bounds.Height;
Here, "Hello world" measurements are about 77 x 11 units. A single dot gives 1.5 x 1.5.
As an alternative solution, still in WPF, you could use GlyphRun and ComputeInkBoundingBox(). It's a bit more complex and won't support automatic font substitution, though. It would look like this:
var glyphRun = new GlyphRun(glyphTypeFace, 0, false, fontSize,
glyphIndexList,
new Point(0, 0),
advanceWidths,
null, null, null, null,
null, null);
Rect glyphInkBox = glyphRun.ComputeInkBoundingBox();

Related

How to get WPF Image control to linearly interpret color channels from WriteableBitmap?

I have a C++ library which returns a pointer to a float array representing an image, with each pixel being made up of 4 floats, or one for each RGBA color channel. I also have a WPF Image control which needs to display this image.
To this end I am using WriteableBitmap as follows:
[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
private static readonly int renderWidth = 1280;
private static readonly int renderHeight = 720;
private static readonly Int32Rect dirtyRect = new Int32Rect(0, 0, renderWidth, renderHeight);
private static readonly uint byteLength = (uint)(renderWidth * renderHeight * 16);
private WriteableBitmap writeableBitmap;
private IntPtr pixelPtr; // Assigned elsewhere
public MainWindow() {
InitializeComponent();
writeableBitmap = new WriteableBitmap(renderWidth, renderHeight, 96, 96, PixelFormats.Rgba128Float, null);
renderImage.Source = writeableBitmap; // renderImage is a WPF Image control
}
public void NewStartImage() {
writeableBitmap.Lock();
CopyMemory(writeableBitmap.BackBuffer, pixelPtr, byteLength);
writeableBitmap.AddDirtyRect(dirtyRect);
writeableBitmap.Unlock();
}
NewStartImage() is called whenever the native library is done processing the data represented by pixelPtr. Now all this works fine, except that for some reason the colors are all brighter when displayed in the Image control than they are in the original data.
The change in brightness seems to be following some sort of square law. Here are some comparisons of the original Red channel value vs the displayed value:
Original Red Channel
Displayed Red Channel
0
0
0.1
0.34
0.5
0.73
0.8
0.9
1
1
I also ruled out any problems with the copying of memory by checking the values stored in the BackBuffer, and they matched up with the original values.
How can I make the WPF Image control display the original colors without reiterating the entire array or something similarly expensive?

What governs DC scaling?

This code gets different scaling depending on which computer I run it on.
Metafile image;
IntPtr dib;
var memoryHdc = Win32Utils.CreateMemoryHdc(IntPtr.Zero, 1, 1, out dib);
try
{
image = new Metafile(memoryHdc, EmfType.EmfOnly);
using (var g = Graphics.FromImage(image))
{
Render(g, html, left, top, maxWidth, cssData, stylesheetLoad, imageLoad);
}
}
finally
{
Win32Utils.ReleaseMemoryHdc(memoryHdc, dib);
}
Going into the Render method, the Metafile object has a PixelFormat of DontCare and consequently does not have valid vertical or horizontal resolutions.
Coming out of the Render method, it has a value of Format32bppRgb and PhysicalDimension.Width and PhysicalDimension.Height have increased to accommodate the rendered image.
How can I make scaling independent of local settings?
Here's the implementation of CreateMemoryHdc (I didn't write it, it's from an OSS library).
public static IntPtr CreateMemoryHdc(IntPtr hdc, int width, int height, out IntPtr dib)
{
// Create a memory DC so we can work off-screen
IntPtr memoryHdc = CreateCompatibleDC(hdc);
SetBkMode(memoryHdc, 1);
// Create a device-independent bitmap and select it into our DC
var info = new BitMapInfo();
info.biSize = Marshal.SizeOf(info);
info.biWidth = width;
info.biHeight = -height;
info.biPlanes = 1;
info.biBitCount = 32;
info.biCompression = 0; // BI_RGB
IntPtr ppvBits;
dib = CreateDIBSection(hdc, ref info, 0, out ppvBits, IntPtr.Zero, 0);
SelectObject(memoryHdc, dib);
return memoryHdc;
}
As you can see, the width, height and bit depth passed to the DC constructor are constant. Creating the metafile produces different physical dimensions. Right after executing this
image = new Metafile(memoryHdc, EmfType.EmfOnly);
the metafile has PhysicalDimension.Height (and width) of 26.43 on my workstation and 31.25 on the server to which I am deploying, so the difference in scaling is already evident and therefore probably not a consequence of anything in the rendering.
This may be relevant. BitMapInfo is defined in the OSS library and looks like this:
internal struct BitMapInfo
{
public int biSize;
public int biWidth;
public int biHeight;
public short biPlanes;
public short biBitCount;
public int biCompression;
public int biSizeImage;
public int biXPelsPerMeter;
public int biYPelsPerMeter;
public int biClrUsed;
public int biClrImportant;
public byte bmiColors_rgbBlue;
public byte bmiColors_rgbGreen;
public byte bmiColors_rgbRed;
public byte bmiColors_rgbReserved;
}
so possibly setting biXPelsPerMeter and biYPelsPerMeter will help. The above code doesn't set them and may be allowing platform values.
Unfortunately, setting these values doesn't seem to make any difference. msdn says
biXPelsPerMeter
The horizontal resolution, in pixels-per-meter, of the
target device for the bitmap. An application can use this value to
select a bitmap from a resource group that best matches the
characteristics of the current device.
So these settings are used when loading a bitmap from a resource. No help here.
This all looks pertinent https://www.codeproject.com/articles/177394/%2fArticles%2f177394%2fWorking-with-Metafile-Images-in-NET
It may help to know that this code does not run in an application. It renders HTML as a metafile for printing, and it lives inside a Web API webservice.
There is no user interface so I'm not sure how to interpret the question of whether it is DPI Aware. The evidence suggests it's DPI affected so the question may be pertinent.
GDI doesn't scale. Use GDI+ for device independence. You will lose antialiasing but most print devices are high DPI anyway.
Does the library in use have an option to use GDI+ instead?
(In my own case, yes. Problem solved.)

Find the height of text in a panel

I have panel that I have customized. I use it to display text. But sometimes that text is too long and wraps to the next line. Is there some way I can auto resize the panel to show all the text?
I am using C# and Visual Studio 2008 and the compact framework.
Here is the code I am wanting to adjust the size for:
(Note: HintBox is my own class that inherits from panel. So I can modify it as needed.)
public void DataItemClicked(ShipmentData shipmentData)
{
// Setup the HintBox
if (_dataItemHintBox == null)
_dataItemHintBox = HintBox.GetHintBox(ShipmentForm.AsAnObjectThatCanOwn(),
_dataShipSelectedPoint,
new Size(135, 50), shipmentData.LongDesc,
Color.LightSteelBlue);
_dataItemHintBox.Location = new Point(_dataShipSelectedPoint.X - 100,
_dataShipSelectedPoint.Y - 50);
_dataItemHintBox.MessageText = shipmentData.LongDesc;
// It would be nice to set the size right here
_dataItemHintBox.Size = _dataItemHintBox.MethodToResizeTheHeightToShowTheWholeString()
_dataItemHintBox.Show();
}
I am going to give the answer to Will Marcouiller because his code example was the closest to what I needed (and looks like it will work). However, this is what I think I will use:
public static class CFMeasureString
{
private struct Rect
{
public readonly int Left, Top, Right, Bottom;
public Rect(Rectangle r)
{
this.Left = r.Left;
this.Top = r.Top;
this.Bottom = r.Bottom;
this.Right = r.Right;
}
}
[DllImport("coredll.dll")]
static extern int DrawText(IntPtr hdc, string lpStr, int nCount,
ref Rect lpRect, int wFormat);
private const int DT_CALCRECT = 0x00000400;
private const int DT_WORDBREAK = 0x00000010;
private const int DT_EDITCONTROL = 0x00002000;
static public Size MeasureString(this Graphics gr, string text, Rectangle rect,
bool textboxControl)
{
Rect bounds = new Rect(rect);
IntPtr hdc = gr.GetHdc();
int flags = DT_CALCRECT | DT_WORDBREAK;
if (textboxControl) flags |= DT_EDITCONTROL;
DrawText(hdc, text, text.Length, ref bounds, flags);
gr.ReleaseHdc(hdc);
return new Size(bounds.Right - bounds.Left, bounds.Bottom - bounds.Top +
(textboxControl ? 6 : 0));
}
}
This uses the os level call to draw text. By P-Invoking it I can get the functionality I need (multi line wrapping). Note that this method does not include any margins. Just the actual space taken up by the text.
I did not write this code. I got it from http://www.mobilepractices.com/2007/12/multi-line-graphicsmeasurestring.html. That blog post had my exact problem and this fix. (Though I did make a minor tweak to make it a extension method.)
You could use the Graphics.MeasureString() method.
With a code sample of your text assignment onto your panel, I could perhaps provide a code sample using the MeasureString() method, if you need it.
I have no way to know whether the Graphics.MeasureString() method is part of the Compact Framework, as it is not said on the page I linked.
EDIT #1
Here's a link where I answered to another text-size related question, while I look for writing a sample for you. =)
EDIT #2
Here's another link related to your question. (Next edit is the sample code. =P)
EDIT #3
public void DataItemClicked(ShipmentData shipmentData) {
// Setup the HintBox
if (_dataItemHintBox == null)
_dataItemHintBox = HintBox.GetHintBox(ShipmentForm.AsAnObjectThatCanOwn(),
_dataShipSelectedPoint,
new Size(135, 50), shipmentData.LongDesc,
Color.LightSteelBlue);
// Beginning to measure the size of the string shipmentData.LongDesc here.
// Assuming that the initial font size should be 30pt.
Single fontSize = 30.0F;
Font f = new Font("fontFamily", fontSize, FontStyle.Regular);
// The Panel.CreateGraphics method provides the instance of Graphics object
// that shall be used to compare the string size against.
using (Graphics g = _dataItemHintBox.CreateGraphics()) {
while (g.MeasureString(shipmentData.LongDesc, f).Width > _dataItemHintBox.Size.Width - 5) {
--fontSize;
f = new Font("fontFamily", fontSize, FontStyle.Regular);
}
}
// Font property inherited from Panel control.
_dataItemHintBox.Font = f;
// End of font resizing to fit the HintBox panel.
_dataItemHintBox.Location = new Point(_dataShipSelectedPoint.X - 100,
_dataShipSelectedPoint.Y - 50);
_dataItemHintBox.MessageText = shipmentData.LongDesc;
// It would be nice to set the size right here
_dataItemHintBox.Size = _dataItemHintBox.MethodToResizeTheHeightToShowTheWholeString()
_dataItemHintBox.Show();
}
Disclaimer: This code has not been tested and is off the top of my head. Some changes might be obligatory in order for you to test it. This provides a guideline to achieve what you seem to want to accomplish. There might be a better way to do this, but I know this one works. Well, the algorithm works, as you can see in my other answers.
Instead of the line:
SizeF fontSize = 30.0F;
You could as well do the following:
var fontSize = _dataItemHintBox.Font.Size;
Why is this?
Because Font.Size property is readonly. So, you need to create a new instance of the System.Drawing.Font class each time the Font.Size shall change.
In your comparison, instead of having the line:
while (g.MeasureString(shipmentData.LongDesc, f)...)
you could also have:
while (g.MeasureString(shipmentData.LongDesc, _dataItemHintBox.Font)...)
This would nullify the need for a second Font class instance, that is f.
Please feel free to post feedbacks as I could change my sample to fit your reality upon the feedbacks received, so that it better helps you. =)
I hope this helps! =)
You can use whichever of the TextRenderer.MeasureText overloads is appropriate for you. Using this function, you can determine the actual rendered size of a string and adjust your panel's size accordingly.
If you're trying to measure inside the Paint event, then you could use the MeasureString function on your e.Graphics object, but resizing inside Paint is not wise. Using TextRenderer avoids your having to create a Graphics object with CreateGraphics() and disposing of it when you're finished.
EDIT
Since TextRenderer is not supported on the compact framework (I missed the tag the first time I saw the question), you'll have to use MeasureString() function on the Graphics object. Something like this:
public Size GetStringSize(string text)
{
using(Graphics g = yourPanel.CreateGraphics())
{
return g.MeasureString(text, yourPanel.Font);
}
}

Measure String inside RichTextBox Control

Can somebody please explain how I would go about measuring the string inside a richtextbox control so that the I can automatically resize the richtextbox control according to its content?
Thank you
Edit:
I've thought about it, and since the below answer won't work if there are different fonts in the RichTextBox Control, what if, I could get the upper-left coords of the richtextbox control and then get the bottom-right coords of the very last line of text inside the rtb. That would essentially give me the Width and Height of the string inside the RichTextBox Control. Is this possible? Or is this a bad idea to do it this way?
Put the following code in the ContentsResized event:
Private Sub rtb_ContentsResized(ByVal sender As Object, ByVal e As System.Windows.Forms.ContentsResizedEventArgs) Handles txtQuestion.ContentsResized
Dim h = e.NewRectangle.Height, w = e.NewRectangle.Width
h = Math.Max(h, sender.Font.Height)
h = Math.Min(h, Me.ClientSize.Height - 10 - sender.Top)
h += sender.Height - sender.ClientSize.Height + 1
sender.Height = h
End Sub
Assuming that someone is typing into the control, you could use an event to fire every time a character is entered (increment counter) and decrement when it is deleted. This would give you a true count.
Edit:
Have you tried this to adjust the height?
richTextBox1.Height = (int)(1.5 * richTextBox1.Font.Height) + richTextBox1.GetLineFromCharIndex(richTextBox1.Text.Length + 1) * richTextBox1.Font.Height + 1 + richTextBox1.Margin.Vertical;
richTextBox1.SelectionStart = 0;
richTextBox1.SelectionStart = richTextBox1.Text.Length;
Or you can do this using Width:
Graphics g = Graphics.FromHwnd(richTextBox1.Handle);
SizeF f = g.MeasureString(richTextBox1.Text, richTextBox1.Font);
richTextBox1.Width = (int)(f.Width)+5;
Try calling GetPreferredSize(Size.Empty). It is defined in the Control class, and if overriden property by the RichTextBoxControl, ought to give you what you are looking for.
If you pass something other than Size.Empty into the method, then it will use that value as a maximum constraint. Using Size.Empty means that the potential size is unbounded.
You can measure a string by calling TextRenderer.MeasureText.
However, if the text contains multiple fonts, this will not work.
EDIT: You're looking for the GetPositionFromCharIndex method.
Note that if there are multiple lines, you should take the max of the X coordinates of the last character on each line.
I found a solution for the Rich text box height issues.. i have modified it a for general use..
Create following structs in your application....
[StructLayout(LayoutKind.Sequential)]
public struct RECT {
public Int32 left;
public Int32 top;
public Int32 right;
public Int32 bottom;
}
[StructLayout(LayoutKind.Sequential)]
public struct SCROLLBARINFO {
public Int32 cbSize;
public RECT rcScrollBar;
public Int32 dxyLineButton;
public Int32 xyThumbTop;
public Int32 xyThumbBottom;
public Int32 reserved;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public Int32[] rgstate;
}
Create following private variables in your class for form (where ever you need to calculate rich text height)
private UInt32 SB_VERT = 1;
private UInt32 OBJID_VSCROLL = 0xFFFFFFFB;
[DllImport("user32.dll")]
private static extern
Int32 GetScrollRange(IntPtr hWnd, UInt32 nBar, out Int32 lpMinPos, out Int32 lpMaxPos);
[DllImport("user32.dll")]
private static extern
Int32 GetScrollBarInfo(IntPtr hWnd, UInt32 idObject, ref SCROLLBARINFO psbi);
Add following method to your Class for form
private int CalculateRichTextHeight(string richText) {
int height = 0;
RichTextBox richTextBox = new RichTextBox();
richTextBox.Rtf = richText;
richTextBox.Height = this.Bounds.Height;
richTextBox.Width = this.Bounds.Width;
richTextBox.WordWrap = false;
int nHeight = 0;
int nMin = 0, nMax = 0;
SCROLLBARINFO psbi = new SCROLLBARINFO();
psbi.cbSize = Marshal.SizeOf(psbi);
richTextBox.Height = 10;
richTextBox.ScrollBars = RichTextBoxScrollBars.Vertical;
int nResult = GetScrollBarInfo(richTextBox.Handle, OBJID_VSCROLL, ref psbi);
if (psbi.rgstate[0] == 0) {
GetScrollRange(richTextBox.Handle, SB_VERT, out nMin, out nMax);
height = (nMax - nMin);
}
return height;
}
You may need to modify above method to make it work as per your requirement...
Make sure to send Rtf string as parameter to method not normal text and also make sure to assign available width and height to the Richtextbox variable in the method...
You can play with WordWrap depending on your requirement...
Add on to bathineni's great answer:
Background: I needed to measure RTF output height for rendering onto paper and because I have custom dynamic page headers/footers I needed to control paging).
(RichTextBox.GetLineFromCharIndex let me down because of complex RTF; including lines & multi column Tables with wordwrap).
Anyhow all was working fine, until someone else used my app with the dreaded windows "Make text and other items larger or smaller" (DPI settings.) - in short now measuring bigger sized fonts it screwed up the page length calculations. (the printer still rendered the text and columns correctly - only the page lengths were now all wrong.)
Only factoring DPI difference failed as in short bigger text didn't fit properly into source RTF tx and cellx values.
Anyhow, in case others are doing similar crazy things bit of trial and error came up with the following (eventually very few) mods to the bathineni CalculateRichTextHeight method:
RichTextBox richTextBox = new RichTextBox(); // same as original
int dpix = richTextBox.CreateGraphics().DpiX; // get dpi
richTextBox.WordWrap = true; // I needed this, you many not
// ... set size etc - same as original answer
richTextBox.Scale(new System.Drawing.SizeF(dpix / 96, dpix / 96)); // scale RTB
// ...
// 96? my original calculations based on windows default 96dpi settings.
Seems the otherwise obscure Control.Scale(sizef) is actually useful for something after all.
Note: if converting results to actual printed lines, (in my case all my \pard's were "\sl-240\slmult0" which comes out to 16 (pix?) per line) also remember to re-factor the divisor.
i.e. in my case:
lines = height / (int)(16 * (dpix / 96))

How do I find the position / location of a window given a hWnd without NativeMethods?

I'm currently working with WatiN, and finding it to be a great web browsing automation tool. However, as of the last release, it's screen capturing functionality seems to be lacking. I've come up with a workable solution for capturing screenshots from the screen (independently generating code similar to this StackOverflow question) in addition to some code by Charles Petzold. Unfortunately, there is a missing component: Where is the actual window?
WatiN conveniently provides the browser's hWnd to you, so we can (with this simplified example) get set to copy an image from the screen, like so:
// browser is either an WatiN.Core.IE or a WatiN.Core.FireFox...
IntPtr hWnd = browser.hWnd;
string filename = "my_file.bmp";
using (Graphics browser = Graphics.FromHwnd(browser.hWnd) )
using (Bitmap screenshot = new Bitmap((int)browser.VisibleClipBounds.Width,
(int)browser.VisibleClipBounds.Height,
browser))
using (Graphics screenGraphics = Graphics.FromImage(screenshot))
{
int hWndX = 0; // Upper left of graphics? Nope,
int hWndY = 0; // this is upper left of the entire desktop!
screenGraphics.CopyFromScreen(hWndX, hWndY, 0, 0,
new Size((int)browser.VisibileClipBounds.Width,
(int)browser.VisibileClipBounds.Height));
screenshot.Save(filename, ImageFormat.Bmp);
}
Success! We get screenshots, but there's that problem: hWndX and hWndY always point to the upper left most corner of the screen, not the location of the window we want to copy from.
I then looked into Control.FromHandle, however this seems to only work with forms you created; this method returns a null pointer if you pass the hWnd into it.
Then, further reading lead me to switch my search criteria...I had been searching for 'location of window' when most people really want the 'position' of the window. This lead to another SO question that talked about this, but their answer was to use native methods.
So, Is there a native C# way of finding the position of a window, only given the hWnd (preferably with only .NET 2.0 era libraries)?
I just went through this on a project and was unable to find any managed C# way.
To add to Reed's answer the P/Invoke code is:
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect);
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
Call it as:
RECT rct = new RECT();
GetWindowRect(hWnd, ref rct);
No - if you didn't create the form, you have to P/Invoke GetWindowRect. I don't believe there is a managed equivalent.
The answer is as others have stated, probably "No, you cannot take a screenshot of a random window from an hwnd without native methods.". Couple of caveats before I show it:
Forewarning:
For anyone who wants to use this code, note that the size given from the VisibleClipBounds is only inside the window, and does not include the border or title bar. It's the drawable area. If you had that, you might be able to do this without p/invoke.
(If you could calculate the border of the browser window, you could use the VisibleClipBounds. If you wanted, you could use the SystemInformation object to get important info like Border3DSize, or you could try to calculate it by creating a dummy form and deriving the border and title bar height from that, but that all sounds like the black magic that bugs are made of.)
This is equivalent to Ctrl+Printscreen of the window. This also does not do the niceties that the WatiN screenshot capability does, such as scroll the browser and take an image of the whole page. This is suitable for my project, but may not be for yours.
Enhancements:
This could be changed to be an extension method if you're in .NET 3 and up-land, and an option for the image type could be added pretty easily (I default to ImageFormat.Bmp for this example).
Code:
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
public class Screenshot
{
class NativeMethods
{
// http://msdn.microsoft.com/en-us/library/ms633519(VS.85).aspx
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect);
// http://msdn.microsoft.com/en-us/library/a5ch4fda(VS.80).aspx
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
}
/// <summary>
/// Takes a screenshot of the browser.
/// </summary>
/// <param name="b">The browser object.</param>
/// <param name="filename">The path to store the file.</param>
/// <returns></returns>
public static bool SaveScreenshot(Browser b, string filename)
{
bool success = false;
IntPtr hWnd = b.hWnd;
NativeMethods.RECT rect = new NativeMethods.RECT();
if (NativeMethods.GetWindowRect(hWnd, ref rect))
{
Size size = new Size(rect.Right - rect.Left,
rect.Bottom - rect.Top);
// Get information about the screen
using (Graphics browserGraphics = Graphics.FromHwnd(hWnd))
// apply that info to a bitmap...
using (Bitmap screenshot = new Bitmap(size.Width, size.Height,
browserGraphics))
// and create an Graphics to manipulate that bitmap.
using (Graphics imageGraphics = Graphics.FromImage(screenshot))
{
int hWndX = rect.Left;
int hWndY = rect.Top;
imageGraphics.CopyFromScreen(hWndX, hWndY, 0, 0, size);
screenshot.Save(filename, ImageFormat.Bmp);
success = true;
}
}
// otherwise, fails.
return success;
}
}

Categories

Resources