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:
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".
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:
- 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.
- 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".
Dave,
ReplyDeletethanks for the update. Originally our software used approach 1 and it was reaaaaaly sluggish. Your previous article inspired me to (a) use a 1x1 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
Hi Uli,
ReplyDeleteCool! I remember you saying you moved to a 1x1 bitmap, and it's interesting to hear you tried clipping and that it's fast. Are you using a 1x1 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
Yes, 1x1 for everything. I'll try to attach my code for dissection ;-) :
ReplyDeleteprocedure 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;
Once again, but hopefully readable: http://pastebin.com/TUKCPEQf
ReplyDeleteDave, feel free to merge my last to comments.
great! can't wait for part 2.
ReplyDeleteThanks a lot for the good work.
Ulrich: Hmm, I'll do some experiments. Might take a few days, but...
ReplyDeleteDave, I don't want to oblige you into anything. I posted my code just in case anybody (including you :-)) might be interested.
ReplyDelete