Transparent graphics with pure GDI (Part 1 1/2) 7


Last post, I said that the next part of my transparent graphics series would be Part 2, and would introduce a class I’ve written to simplify drawing to glass, drawing partially transparent shapes and text, etc.I haven’t posted this yet, simply because life has got in the way – since my last post, I spent a couple of weeks back home in Australia visiting friends and colleagues, and traveling a bit on the way back here.  (I went camel-riding.  They’re surprisingly weird animals.)  I’m afraid I simply haven’t had time to write the article and to polish the final bits of the library code.

Part 2 really is coming, and I’m sorry it’s taking so long.  In the meantime, here is a quick post, Part 1 1/2, addressing a couple of questions asked in the comments for Part 1.

GDI+

GDI+ is great, and is a good and easy way to render good-looking graphics.  I don’t want to disparage it.  However, it does have some flaws, and I’m starting to think that including it in the software I work on was a mistake.  The main problem is speed: it’s mostly CPU-bound on Vista and Windows 7, and often on XP too.  From memory, and I don’t believe this is documented on MSDN, GDI+ only makes use of hardware acceleration through GDI hardware acceleration, and it can only do so when its coordinates are integer wholes.  Using floating-point coordinates for drawing such as RectF, PointF etc will prevent this.

If I was to implement the same feature back then (2006? 2007?) with the knowledge I’ve accumulated since, I would use GDI techniques like the ones in this series.

If I was to implement the same feature now, assuming I had XE2, I’d probably use FireMonkey embedded in the VCL application.  FireMonkey supports hardware acceleration, zooming or scaling etc, all built in.  This particular feature was a zoomable user interface for a specific part of the software I work on, and speedy, fast zooming is an essential feature.

Non-rectangular shapes, such as selections

Commenter Ulrich asked about drawing a non-rectangular selection.  There are two ways to do this:

  1. Draw a polygon on a 32-bit bitmap.  It will require per-pixel alpha so that the areas outside the polygon are fully transparent, so the transparency loop (iterating through the pixels and premultiplying the alpha) will have to be modified to handle this.
  2. Draw and keep one solid rectangular non-per-pixel bitmap, and instead use clipping regions to restrict where GDI can blend the image.  The basic idea is that you can make GDI clip its drawing to an arbitrary shape, so create a region in the shape of the selection (probably using CreatePolygonRgn) and use IntersectClipRgn to set the device context’s clipping region to the combination of its existing region (normally, the whole visible area) and your polygonal region.  When you blend in your rectangular image, it will only appear within the polygonal shape set as the clipping area.  Don’t forget to set the DC’s clipping region back afterwards.  CodeProject has a good article about using clipping regions.

A selection is normally ‘live’, that is, the user sees it update as the mouse moves.  This means that the drawing and alpha premultiplication has to be done many times a second.  Method 1, using per-pixel alpha, is what I use in my own software, and it seems to be fast enough with a well-optimised premultiplication method.  Method 2, using clipping regions where the region is updated on the fly instead of the bitmap, is almost certainly going to be faster.  I haven’t tested this and it’s usually not wise to say ‘technique X will be faster’ without measuring, but I’m fairly sure. If I was implementing the feature today instead of years ago this is what I’d investigate.  Measure and decide.

So there you go – my apologies for the short post and absence of code samples.  Part 2 of the series really is coming “soon”.

Discussions about this page on Google+


7 thoughts on “Transparent graphics with pure GDI (Part 1 1/2)

  • Ulrich

    Dave,

    thanks for the update. Originally our software used approach 1 and it was reaaaaaly sluggish. Your previous article inspired me to (a) use a 1×1 pixel bitmap in the rectangular case and (b) combine rectangular alphablending + clipping for the non-rectangular case, so by now we’re using approach 2 and it’s **considerably** faster – no need to measure that. :-)

    Best regards,
    Uli

  • Dave

    Hi Uli,

    Cool! I remember you saying you moved to a 1×1 bitmap, and it’s interesting to hear you tried clipping and that it’s fast. Are you using a 1×1 bitmap for (b) too? That would probably be the fastest of all.

    Approach 1 is actually not very sluggish in our software, although you do see the CPU meter spike, and I think it’s because of the premultiplication method. I’ll try to remember to dig into a few alternative implementations in Part 2. The code in Part 1 was quite simple – clear (hopefully), but with plenty of optimisation potential.

    Cheers,

    David

  • Ulrich

    Yes, 1×1 for everything. I’ll try to attach my code for dissection ;-) :

    procedure NormalizeRect(var r: TRect);
    var
    t: Integer;
    begin
    if r.Left > r.Right then
    begin
    t := r.Right;
    r.Right := r.Left;
    r.Left := t;
    end;
    if r.Top > r.Bottom then
    begin
    t := r.Bottom;
    r.Bottom := r.Top;
    r.Top := t;
    end;
    end;

    // AlphaBlendRect: draws an alphablended rectangle:
    procedure AlphaBlendRect(DC: HDC; const ARect: TRect; AColor: TColor; AIntensity: Byte);
    var
    Bitmap: TBitmap;
    BlendParams: TBlendFunction;
    rClip, rBlend: TRect;

    function GetBlendColor: TRGBQuad;

    function PreMult(b: Byte): Byte;
    begin
    Result := (b * AIntensity) div $FF;
    end;

    var
    cr: TColorRef;
    begin
    cr := ColorToRGB(AColor);
    Result.rgbBlue := PreMult(GetBValue(cr));
    Result.rgbGreen := PreMult(GetGValue(cr));
    Result.rgbRed := PreMult(GetRValue(cr));
    Result.rgbReserved := AIntensity;
    end;

    begin
    GetClipBox(DC, rClip);
    NormalizeRect(rClip);
    rBlend := ARect;
    NormalizeRect(rBlend);

    if not IntersectRect(rBlend, rClip, rBlend) then
    Exit;

    Bitmap := TBitmap.Create;
    try
    Bitmap.PixelFormat := pf32bit;
    Bitmap.SetSize(1, 1);
    PRGBQuad(Bitmap.ScanLine[0])^ := GetBlendColor;

    BlendParams.BlendOp := AC_SRC_OVER;
    BlendParams.BlendFlags := 0;
    BlendParams.SourceConstantAlpha := $FF;
    BlendParams.AlphaFormat := AC_SRC_ALPHA;

    Windows.AlphaBlend(
    DC, rBlend.Left, rBlend.Top, rBlend.Right – rBlend.Left, rBlend.Bottom – rBlend.Top,
    Bitmap.Canvas.Handle, 0, 0, 1, 1,
    BlendParams);
    finally
    Bitmap.Free;
    end;
    end;

    // AlphaBlendPolygon: draws an alphablended polygon:
    procedure AlphaBlendPolygon(DC: HDC; const APoints: array of TPoint; AColor: TColor; AIntensity: Byte);

    procedure SetClip(APoints: array of TPoint); // pass APoints by value
    var
    rgn: HRGN;
    begin
    LPtoDP(DC, APoints[0], Length(APoints));
    rgn := CreatePolygonRgn(APoints[0], Length(APoints), ALTERNATE);
    try
    ExtSelectClipRgn(DC, rgn, RGN_AND);
    finally
    DeleteObject(rgn);
    end;
    end;

    var
    SaveIndex: Integer;
    rClip: TRect;
    begin
    SaveIndex := SaveDC(DC);
    try
    SetClip(APoints);
    GetClipBox(DC, rClip);
    AlphaBlendRect(DC, rClip, AColor, AIntensity);
    finally
    RestoreDC(DC, SaveIndex);
    end;
    end;