Unmanaged Code, Screenshots and IronPython

IronPython & Windows Forms, Part X

Windows Forms

Note

This is part of a series of tutorials on using IronPython with Windows Forms.

 

 

Screenshots from IronPython

This is an example from work that I am allowed to show you. Smile

It comes from our functional test suite, where the only way we could test a feature was by taking a screenshot of part of the main form.

This is pretty easy, but can only be done from unmanaged code, it is GDI functionality and there is no .NET API that we could find.

This means that we have to drop into C#. Using unmanaged code from C# is very easy, but you need to add a class attribute specifying the dll you are using. C# attributes cannot be applied from IronPython.

So the following is a great example of :

  • Taking a screenshot with IronPython
  • Creating a class in C# and using it from IronPython
  • Using unmanaged code from IronPython and C#

What a bargain for a single example. Surprised

To ensure that it this is not a frivolous and trivial example, we will use the screenshot code to do something useful, like create some ASCII art...

Note

The code in this example is adapted from this page.

If you are running Windows and don't want to install Visual Studio, you can download a copy of UnmanagedCode.dll.

Alternatively, if you want more of a walk-through in creating a new class library with Visual Studio, visit C# and the Clipboard.

The functions we need to use are in user32.dll and GDI32.dll.

The attribute we need to mark our methods with is DllImport.

To make the managed (C#) wrapper round the unmanaged code, create a new class library called 'UnmanagedCode' in Visual Studio [1] with the code shown below.

It creates two classes in the UnmanagedCode namespace : GDI32 and User32.

You can see what the class members look like, they are references to the functions in the Dlls, marked with the DllImport attribute, and declared as public static extern. We need to declare the types of the arguments passed to the functions and their return values. We use IntPtr instead of int as type declarations for the handle values. This allows the code to work on 64 bit platforms.

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace UnmanagedCode
{
    public class GDI32
    {
        [DllImport("GDI32.dll")]
        public static extern IntPtr CreateCompatibleDC(IntPtr hdc);

        [DllImport("GDI32.dll")]
        public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth,
            int nHeight);

        [DllImport("GDI32.dll")]
        public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

        [DllImport("GDI32.dll")]
        public static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest,
                                         int nWidth, int nHeight, IntPtr hdcSrc,
                                         int nXSrc, int nYSrc, int dwRop);

        [DllImport("GDI32.dll")]
        public static extern bool DeleteDC(IntPtr hdc);

        [DllImport("GDI32.dll")]
        public static extern bool DeleteObject(IntPtr hObject);
    }

    public class User32
    {
        [DllImport("user32.dll")]
        public static extern IntPtr GetDesktopWindow();

        [DllImport("user32.dll")]
        public static extern IntPtr GetTopWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern IntPtr GetWindow(IntPtr hWnd, uint wCmd);

        [DllImport("User32.dll")]
        public static extern IntPtr GetWindowDC(IntPtr hWnd);

        [DllImport("User32.dll")]
        public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);

    }
}

When you build this class library you should create a file called UnmanagedCode.dll. This contains our newly created classes. Place this dll in the same directory as the Python script you use it from.

Originally we had another class with a static method that used all this to take the screenshot. Translating it into IronPython turned out to be as easy as removing the declarations and the semicolons. Smile

So here is the IronPython code to do the business :

import clr
clr.AddReference('UnmanagedCode')
clr.AddReference('System.Drawing')

from System.Drawing import Bitmap, Image
from UnmanagedCode import User32, GDI32

def ScreenCapture(x, y, width, height):
    hdcSrc = User32.GetWindowDC(User32.GetDesktopWindow())
    hdcDest = GDI32.CreateCompatibleDC(hdcSrc)
    hBitmap = GDI32.CreateCompatibleBitmap(hdcSrc, width, height)
    GDI32.SelectObject(hdcDest, hBitmap)

    # 0x00CC0020 is the magic number for a copy raster operation
    GDI32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, 0x00CC0020)
    result = Bitmap(Image.FromHbitmap(hBitmap))
    User32.ReleaseDC(User32.GetDesktopWindow(), hdcSrc)
    GDI32.DeleteDC(hdcDest)
    GDI32.DeleteObject(hBitmap)
    return result

I'm not going to step through this in detail, but you can follow the basic flow. We add a reference to our class library and import the two classes from it.

Then we get a handle and context for the main screen and create a new handle for the destination image we are creating.

We then do some GDI magic involving the BitBlt function and a copy raster operation. We then create a new Bitmap image object from the filled in bitmap.

Our new ScreenCapture function takes an x and y co-ordinate for the top left corner, plus a width and a height for the rectangle we wish to take a screenshot of.

Lets put this into action by taking a screenshot of this smiley - Smile - and turning it into ASCII art.

Now down to business. We'll assume a pixel is light (represented by a space) if its R+G+B value is greater than 384 (half of 256 * 3). Otherwise the pixel is dark and we will represent it with an 'X'.

So we need to iterate over all the pixels in each row. luckily the Bitmap class has a useful method, GetPixel. This returns a Color which we can interrogate for its value.

# You'll have to trust me about the location on my screen
image = ScreenCapture(84, 179, 16, 16)
for y in range(image.Height):
    row = []
    for x in range(image.Width):
        color = image.GetPixel(x, y)
        value = color.R + color.G + color.B
        if value > 384:
            row.append(' ')
        else:
            row.append('X')
    print ''.join(row)

And the result ?

     XXXXX
   XX     XX
  X         X
 X  XX   XX  X
 X           X
X    X   X    X
X    X   X    X
X             X
X X         X X
X  XXXXXXXXX  X
 X  X     X  X
 X   XXXXX   X
  X         X
   XX     XX
     XXXXX

Of course, you might want to do something else with the image, like saving it.

To do this you might use the Save method of the image, and specify an ImageFormat. Like this :

clr.AddReference('System.Drawing')
from System.Drawing.Imaging import ImageFormat

def SaveBitmap(image, filename):
    image.Save(filename, ImageFormat.Bmp)

Very Happy

[1]You can get Visual Studio Express for free if you haven't got it already.

For buying techie books, science fiction, computer hardware or the latest gadgets: visit The Voidspace Amazon Store.

Hosted by Webfaction

Return to Top

Page rendered with rest2web the Site Builder

Last edited Fri Nov 27 18:32:35 2009.

Counter...