Click here to monitor SSC
Av rating:
Total votes: 10
Total comments: 3


Jonathan Evans
Capturing Screenshots for Automated Error-Reporting
01 March 2011

So often, a screendump will explain a great deal about a bug, and prevent much frustration in the process of error-reporting. For EAP testing or user-acceptance testing, it can speed up the whole process dramatically. Including a screendump with an automated error-report isn't hard either, as Jonathan Evans explains ...

Why Automated Error-Reporting?

Automated error reporting is essential, especially if you're close to your customers.  Bugs in applications will always happen – but the way that you deal with them can make a big difference to you and your users.  It is not enough to just display an error message and hope it will get reported.  What incentive is there for your users to report it?  You've already demonstrated that you don't care by letting the error though.  You'll only find out that it's really critical when then they call up and scream down the phone at you.

Automatically reporting it is much better – at the very least you can monitor trends and prioritise effort.  If you're really proactive, you can get errors going straight into your error tracking system.  But to really impress your users you can call them; preferably before they call you.

So, having chosen to automate your error reports, what should you capture?  There is no end to the useful information that can be attached to an error report: The essential items are application version and OS version; then there are log files, event logs, loaded assemblies and lists of the other running processes.  The one that I have always found very useful, though, is a screenshot. 

How to capture screenshots

As the cliché goes, a picture is worth a thousand words.  This is especially true when those words are being written by a non-technical user who has plenty of their own problems to sort out.  I've found all sorts of answers in a screenshot: un-escaped characters in a text box (later inserted, insecurely, into a SQL query); strange visual artefacts caused by low GDI memory; and of course dozens of other open applications that happen to coincide with an “out of memory” error.

Grabbing the bitmap of the screen

A quick search in Google quickly reveals a dozen ways to grab an image of the screen the screen.  The basics are as follows.

1.      To start you'll need to get a handle to the desktop window.  The easiest way (in fact the only way I've found) is to call the Windows API.

System.IntPtr hWnd = GetDesktopWindow();

2.      Now, having got a handle we need to find the rectangle (so we can dimension the bitmap onto which we'll draw the desktop).  Again, we must resort to the API.

var rectStruct = new RECT();
GetWindowRect(_windowHandle, ref rectStruct);

       var rectangle = System.Drawing.Rectangle(rectStruct .Left, rectStruct.Top, rectStruct.Right - rectStruct.Left – 1, rectStruct.Bottom - rectStruct.Top – 1);

3.      Now, we can create the bitmap for the image:

       var screenshot = new System.Drawing.Bitmap(rectangle .Width, rectangle.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

4.      Finally, we can grab the image:

using(var screenshotGraphics = System.Drawing.Graphics.FromImage(screenshot))

{

    screenshotGraphics.CopyFromScreen(0, 0, 0, 0, new System.Drawing.Size(desktopRectangle.Width, desktopRectangle.Height));

}

And there you go, screenshot now contains your image.

Including the mouse pointer in an image

Getting the mouse cursor on there is a little harder, but well worth it since it's very handy to know where they were clicking.  The following lines must follow immediately from the line containing “CopyFromScreen in step 4 above.

1.      To get the system cursor we'll need to go the Windows again:

System.IntPtr handle = GetCursor();

var cursor = new System.Windows.Forms.Cursor(handle);

2.      The we need to work out where to put the image:

var mousePosition = System.Windows.Forms.Cursor.Position;

var cursorRectangle = new System.Drawing.Rectangle(new System.Drawing.Point(mousePosition.X - cursor.HotSpot.X,mousePosition.Y – cursor.HotSpot.Y), cursor.Size);

3.      Finally we need to render the cursor onto our screenshot.

cursor.Draw(screenshotGraphics, cursorRectangle);

That's fairly easy really. 

Adding the image to an error-report

How about attaching the image to your error report?  That isn't always easy.  The simplest way is to convert it to a string – base-64 encoding will ensure it can be passed through HTTP or stuck in an XML document without causing problems.

public static string BitmapToString(System.Drawing.Bitmap bitmap)

{

    using(var memoryStream = new System.IO.MemoryStream())

    {

        bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Bmp);

        return System.Convert.ToBase64String(memoryStream.GetBuffer());

    }

}

At the other end the following will convert it back:

publicstatic System.Drawing.Bitmap StringToBitmap(string buffer)

{

    var memoryStream = new System.IO.MemoryStream(System.Convert.FromBase64String(buffer));

    return (System.Drawing.Bitmap) System.Drawing.Image.FromStream(memoryStream);

}

And the result :...

image

Including just the relevant parts of the screen

But hang on!  As a customer I'm not sure I want my software supplier to see everything I was working on when their flaky application crashed.  I want them only to see the windows belong to their application.  How do we do that?  Regions.  These are “shapes” that can be manipulated and combined using a variety of operators to create a mask.  In this case I'm going to create a big blank and then knock out holes for the windows by “excluding” their rectangles.

1.      Let's start by creating the bitmap onto which to paint the results.

var maskedScreenshot = (System.Drawing.Bitmap) screenshot.Clone();

2.      Now, the region that will mask the paint operation:

var screen = new System.Drawing.Rectangle(0, 0, maskedScreenshot.Width, maskedScreenshot.Height);      

var mask = new System.Drawing.Region(screen);

mask.Intersect(screen);

3.      Let's mask all the windows owned by this application:

foreach(System.Windows.Forms.Form form in System.Windows.Forms.Application.OpenForms)

    mask.Exclude(form.DesktopBounds);

4.      Finally, paint  using the mask to leave our windows unaffected:

using(var graphics = System.Drawing.Graphics.FromImage(maskedScreenshot))

{

    using(var brush = new System.Drawing.Drawing2D.HatchBrush(System.Drawing.Drawing2D.HatchStyle.Percent90, maskColor, maskColor))

    {

        graphics.FillRegion(brush, mask);

    }

}

That leaves maskedScreenshot with the masked image.  Now your results will look more like this:

image

No secrets revealed. 

Wrapping it up

The attached code shows how this can all be packaged and used to report with screenshots using SmartAssembly.  I've placed most of the code above in a WindowInfo class that can be used a bit more naturally in code and allows screenshots of individual windows to be collected.

A couple of points you might want to consider...

1.      The solution provided does not take into account other windows obscuring yours (in which case you might capture portions of sensitive information);

2.      Capturing the cursor after the mask has been applied (which will allow the position of the cursor to be seen even if it is outside of your windows at the time);

3.      Some windows (for example file dialogues) will not appear in the System.Windows.Forms.Application.OpenForms collection, and so will be missed.

Points 1 and 3 above can be solved by enumerating windows using Windows API methods.



This article has been viewed 3652 times.
Jonathan Evans

Author profile: Jonathan Evans

Jonathan Evans is a developer working in the financial services sector in London. He specializes in the design and architecture of reusable systems, and has worked with desktop, server and web applications in multiuser environments. He is still amazed that people will pay him to muck around with computers. You can find him on LinkedIn.

Search for other articles by Jonathan Evans

Rate this article:   Avg rating: from a total of 10 votes.


Poor

OK

Good

Great

Must read
 
Have Your Say
Do you have an opinion on this article? Then add your comment below:
You must be logged in to post to this forum

Click here to log in.


Subject: Attaching files to SmartAssembly error reports
Posted by: Dom Smith (view profile)
Posted on: Friday, March 04, 2011 at 9:02 AM
Message: There's an easier way to attach screenshots to error reports using SmartAssembly. You save the screenshot as a temporary file, and then use the AttachFile() method to attach the file to the error report[1]. This method allows you to send the image as a file, rather than as a base-64 encoded string.

[1] http://www.red-gate.com/supportcenter/Content?p=SmartAssembly&c=SmartAssembly/help/6.0/SA_Attach_Log_File.htm&toc=SmartAssembly/help/6.0/toc1362539.htm

Subject: CodeSmith Insight offers this ability out of the box.
Posted by: Blake Niemyjski (not signed in)
Posted on: Friday, March 04, 2011 at 12:08 PM
Message: Hello,

I work for CodeSmith Tools and we have been offering this functionality out of the box in all of our CodeSmith Insight non-web clients (Winforms, WPF and more...). CodeSmith Insight is much more than just an error reporting service!

For more information on CodeSmith Insight please visit the website (http://www.codesmithtools.com/product/insight#comparison).

Subject: Citrix
Posted by: Anonymous (not signed in)
Posted on: Sunday, March 06, 2011 at 11:50 PM
Message: Does this work for citrix clients? We have a very simple screen capture system (this, but pared down) that works everywhere except on citrix clients...

 






recommended site pinvoke

PInvoke.net is a user-driven wiki which provides .NET developers with native method signatures, so they don't have to spend time writing them from scratch.




Top rated articles
C# Async: What is it, and how does it work?
 The biggest new feature in C#5 is Async, and its associated Await (contextual) keyword. Anybody who is... Read more...

Towards the Perfect Build
 An automated build and deployment system is no longer a dream. Now that PowerShell has matured as a... Read more...

Practical PowerShell: Pruning File Trees and Extending Cmdlets
 One of the most radical features of PowerShell is amongst the least known. It is possible to extend... Read more...

TortoiseSVN and Subversion Cookbook Part 4: Sharing Common Code
 Michael Sorens continues his series on source control with Subversion and TortoiseSVN by describing... Read more...

Feature Usage Reporting in Early Access Programs
 After doing Web development, you can get very used to the luxury of having basic information about your... Read more...

Most viewed articles
A Complete URL Rewriting Solution for ASP.NET 2.0
 Ever wondered whether it's possible to create neater URLS, free of bulky Query String parameters?... Read more...

Visual Studio Setup - projects and custom actions
 This article describes the kinds of custom actions that can be used in your Visual Studio setup project. Read more...

.NET Application Architecture: the Data Access Layer
 Find out how to design a robust data access layer for your .NET applications. Read more...

Web Parts in ASP.NET 2.0
 Most Web Parts implementations allow users to create a single portal page where they can personalize... Read more...

Calling Cross Domain Web Services in AJAX
 The latest craze for mashups involves making cross-domain calls to Web Services from APIs made publicly... Read more...

Over 400,000 Microsoft professionals subscribe to the Simple-Talk technical journal. Join today, it's fast, simple, free and secure.

Join Simple Talk