Critical Development

Language design, framework development, UI design, robotics and more.

Using Extension Methods to Manipulate Control Bitmaps in Compact Framework

Posted by Dan Vanderboom on April 11, 2008

I’m loving extension methods.  All of the methods that I wish BCL classes had, I can now add.  While I consider it highly unfortunate that we can’t yet add extension properties, events, or static members of any kind, still it’s a great amount of power in terms of making functionality discoverable in ways not possible before.

During the implementation of my Compact Framework application’s MVC framework, I wanted to support displaying views modally.  However, using a screen stack of UserControls that are all hosted in a single master Form object, I lose out on this built-in functionality and so found myself in need of creating this behavior myself.  One of the difficulties in doing this is displaying a view that may not cover every portion of other views beneath it; if the user clicks on one of the views “underneath”, that control gets activated, and if pressed on a control, that control will handle the event (such as Button.Click).

My solution to the problem is simple.  Take a snapshot of the master form and everything on it, create a PictureBox control that covers the whole form and bring it to front, and set its image to the snapshot bitmap.  Doing this provides the illusion that the user is still looking at the same form full of controls, and yet if they touch any part of the screen, they’ll be touching a PictureBox that just ignores them.  The application is then free to open a new view UserControl on top of that.  When the window is finally closed, the MVC infrastructure code tears down the PictureBox, and the real interface once again becomes available for interaction.

Screenshots before and after screen capture and darkening

In addition, I wanted the ability to emphasize the modal view, so you can see from the picture above that I decided to accomplish this by de-emphasizing the background bitmap.  By darkening the snapshot, the pop-up modal view really does seem to pop out.  The only problem with bitmap manipulation using the Compact Framework library is that it’s extremely slow, but I get around this by using some unsafe code to manipulate the memory region where the bitmap image gets mapped.  (If you’re unfamiliar with the unsafe keyword, don’t worry: this code actually is safe to use.)

Here is the full source code for taking a snapshot of a form (or any control), as well as adjusting the brightness.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.Runtime.InteropServices;

public static class ControlBitmapExtensions
{
    [DllImport("coredll.dll")]
    private static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight,
        IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);

    public struct PixelData
    {
        public byte Blue;
        public byte Green;
        public byte Red;
    }

    public static Bitmap GetSnapshot(this Control Control)
    {
        Rectangle rect = new Rectangle(0, 0, Control.Width, Control.Height - 1);
        Graphics g = Control.CreateGraphics();
        Bitmap Snapshot = new Bitmap(rect.Width, rect.Height);
        Graphics gShapshot = Graphics.FromImage(Snapshot);
        BitBlt(gShapshot.GetHdc(), 0, 0, rect.Width, rect.Height, g.GetHdc(), rect.Left, rect.Top, 0xCC0020);
        gShapshot.Dispose();

        return Snapshot;
    }

    public static unsafe Bitmap AdjustBrightness(this Bitmap Bitmap, decimal Percent)
    {
        Percent /= 100;
        Bitmap Snapshot = (Bitmap)Bitmap.Clone();
        Rectangle rect = new Rectangle(0, 0, Bitmap.Width, Bitmap.Height);

        BitmapData BitmapBase = Snapshot.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
        byte* BitmapBaseByte = (byte*)BitmapBase.Scan0.ToPointer();

        // the number of bytes in each row of a bitmap is allocated (internally) to be equally divisible by 4
        int RowByteWidth = rect.Width * 3;
        if (RowByteWidth % 4 != 0)
        {
            RowByteWidth += (4 - (RowByteWidth % 4));
        }

        for (int i = 0; i < RowByteWidth * rect.Height; i += 3)
        {
            PixelData* p = (PixelData*)(BitmapBaseByte + i);

            p->Red = (byte)Math.Round(Math.Min(p->Red * Percent, (decimal)255));
            p->Green = (byte)Math.Round(Math.Min(p->Green * Percent, (decimal)255));
            p->Blue = (byte)Math.Round(Math.Min(p->Blue * Percent, (decimal)255));
        }

        Snapshot.UnlockBits(BitmapBase);
        return Snapshot;
    }

    public static Bitmap Brighten(this Bitmap Bitmap, decimal PercentChange)
    {
        return AdjustBrightness(Bitmap, 100 + PercentChange);
    }

    public static Bitmap Darken(this Bitmap Bitmap, decimal PercentChange)
    {
        return AdjustBrightness(Bitmap, 100 - PercentChange);
    }
}

 

Because Control is extended by GetSnapshot, and Bitmap is extended by AdjustBrightness, Brighten, and Darken, I can write very clear and simple code like this on the consuming side:

Bitmap bitmap = MyForm.GetSnapshot().Darken(40);

…and voila!  I have a snapshot.  Note that because Darken extends Bitmap, it can now be used with any Bitmap.  As we read from this code from left to right, we’re observing a pipeline of transformations.  MyForm is the source data, GetSnapshot is the first step, Darken is the next change, and with more extension methods on Bitmap we could continue to process this in a way that is very natural to think about and construct.

I do have to admit that I cheated a little, though.  Even with the direct memory manipulation with pointers, it still didn’t perform very well on the Symbol and DAP devices I tested on.  So instead of adjusting the brightness on every pixel, I only darken every third pixel.  They’re close enough together that you can’t really tell the difference; however, the closer to 100 percent you darken or brighten an image, the more apparent the illusion will be, since two thirds of the pixels won’t be participating.  So it’s good for subtle effects, but I wouldn’t count on it for all scenarios.

This every-third-pixel dirty trick happens in the for loop, where you see i += 3, so go ahead and experiment with it.  Just be careful not to set it to large even numbers or you’ll end up with stripes!

5 Responses to “Using Extension Methods to Manipulate Control Bitmaps in Compact Framework”

  1. Tarek Haoula said

    Hi Dan,

    Once again, another excellent post. Your solution works nicely.

    Just a question though. Do you need to call ReleaseDC in GetSnapshot?

    Kind Regards,
    Tarek

  2. Dan Vanderboom said

    Good question. I’m not aware of any need to explicitly release the device context. I do call Dispose on the gSnapshot Graphics object (though the call to Dispose should be in a finally block), and that should release any device context for that object (a call is made to a method called Destroy in AGL). As far as the g Graphics object goes, in the full .NET Framework makes an internal call is made to GdipCreateFromHWND in gdiplus.dll. This call “creates a Graphics object that is associated with a specified window”, according to this web page.

    If the Compact Framework version of it operates along the same lines, this sounds like the existing control’s window already had a device context and the Grpahics object being instantiated merely associates with this. In the CF version of Control.CreateGraphics, a call to GetDraw is made into AGL, which is an internal black box as far as I know. So I don’t want to destroy the device context of a control that’s still in use.

  3. Stoyko Neykov said

    Hello Dan,

    The code is great, but I’ve found that the part with set pixel data is too slow for my requirements


    for (int i = 0; i Red = (byte)Math.Round(Math.Min(p->Red * Percent, (decimal)255));
    p->Green = (byte)Math.Round(Math.Min(p->Green * Percent, (decimal)255));
    p->Blue = (byte)Math.Round(Math.Min(p->Blue * Percent, (decimal)255));
    }

    The way that we set each byte is little slow by those math functions, instead that we must notice that floating point operations are not so fast, so based on this point we will keep the percent in the integer value multiplyed by 65536 and when multiply it with color byte and after that shift right by 16 indexes

    Take a look in my optimized example


    public static Bitmap AdjustBrightness(this Bitmap image, decimal Percent)
    {
    // No changes needed then just return the image ;-)
    if (Percent == 100)
    {
    return image;
    }
    Percent /= 100;
    int p = (int)(Percent * 65536); // because my device supports only 16bit

    // I don't need backup of my image so I've removed this
    //Bitmap Snapshot = (Bitmap)image.Clone();

    Rectangle rect = new Rectangle(0, 0, image.Width, image.Height);
    BitmapData BitmapBase = image.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    unsafe
    {
    byte* BitmapBaseByte = (byte*)BitmapBase.Scan0.ToPointer();

    // the number of bytes in each row of a bitmap is allocated (internally) to be equally divisible by 4
    int RowByteWidth = rect.Width * 3;
    if (RowByteWidth % 4 != 0)
    {
    RowByteWidth += (4 - (RowByteWidth % 4));
    }

    // We do not need each time to define this pointer;
    PixelData* pd = null;

    // Calculate outside the row bytes lenght;
    int lenght = RowByteWidth * rect.Height;

    // Now we must notice that if the percent is above 1 then we are going to do brightness
    // If not - darkness
    // And notice that brightness is much more harder to implement because we can't leave zero based colors not changed
    if (Percent > 1)
    {
    int pixelInfo = 0;
    for (int i = 0; i Blue * p) >> 16;
    // If color byte value is 0 or something too low we are almost unable to bright it, so I decided to set it to 10, works great for my needs
    pixelInfo = (pixelInfo == 0) ? 10 : pixelInfo;
    // Prevent overflow because if the value is more than 255 it will be casted as 0, so I need to keep it to 255
    pd->Blue = (byte)((pixelInfo & 0xFFFFFF00) > 0 ? 0xFF : pixelInfo);
    pixelInfo = (pd->Red * p) >> 16;
    pixelInfo = (pixelInfo == 0) ? 10 : pixelInfo;
    pd->Red = (byte)((pixelInfo & 0xFFFFFF00) > 0 ? 0xFF : pixelInfo);
    pixelInfo = (pd->Green * p) >> 16;
    pixelInfo = (pixelInfo == 0) ? 10 : pixelInfo;
    pd->Green = (byte)((pixelInfo & 0xFFFFFF00) > 0 ? 0xFF : pixelInfo);
    }
    }
    else
    {
    // multiply the pixel data by percent and shift the useless (16)bits
    for (int i = 0; i Blue = (byte)((pd->Blue * p) >> 16);
    pd->Red = (byte)((pd->Red * p) >> 16);
    pd->Green = (byte)((pd->Green * p) >> 16);
    }
    }
    }
    image.UnlockBits(BitmapBase);
    return image;
    }

    Best Regards,
    Stoyko Neykov

  4. Dan Vanderboom said

    Great work, Stoyko! Thanks for the very helpful tips. I’m going to repost the article with your changes (and give you credit, of course). I’m sure it will be appreciated by those who stumble upon this.

  5. Teemu Avellan said

    Thanks for the code! I made my own optimization for it. The basic idea is to calculate the adjusted values for all original values and then just assing values to bitmap, i found it almost twice faster than Stoyko’s example on QVGA pictures. Here’s code:

    public static unsafe Bitmap AdjustBrightness(this Bitmap original, decimal Percent)
    {
    Percent /= 100;
    byte[] newvalues = new byte[256];
    for (int i = 0; i < 256; ++i)
    {
    newvalues[i] = (byte)Math.Round(Math.Min(i * Percent, (decimal)255));
    if (newvalues[i] == 0)
    {
    newvalues[i] = 10;
    }
    }

    Bitmap b = new Bitmap(original);
    BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height),
    ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    int stride = bmData.Stride;
    System.IntPtr Scan0 = bmData.Scan0;
    unsafe
    {
    byte* p = (byte*)(void*)Scan0;
    int nOffset = stride – b.Width * 3;
    int nWidth = b.Width *3;
    for (int y = 0; y < b.Height; ++y)
    {
    for (int x = 0; x < nWidth; ++x)
    {
    p[0] = newvalues[p[0]];
    ++p;
    }
    p += nOffset;
    }
    }

    b.UnlockBits(bmData);

    return b;
    }

    -Teemu Avellan

Leave a comment