In this the second, and concluding, part of our experiments with Alpha Blended forms in Delphi, I will modify the demo application to use a compressed alpha transparent PNG image instead of a BMP. I will also move the bitmap to a resource file, demonstrate run-time premultiplication and enhance the splash screen with a few visual gimmicks.
If you haven’t read part 1 yet I strongly suggest you do so first: Alpha Blended Splash Screen in Delphi - Part 1. This part will be right here waiting for you when you get back.
For this part I have replaced the sample bitmap with another one. Mostly becausethe original was too big but also because I didn’t quite like the look of it. The new one is just a grid of 15 colored glass orbs. Not very meaningful, but I love glass orbs
Each of the techniques we try out in this part builds on the source code of the previous steps. If you don’t want to enter the code manually or copy/paste from the article, you can “cheat” and download the complete source.
Loading the bitmap from a resource
The first step in improving our demo application is to get rid of the external bitmap file. Instead of reading the splash bitmap from an external file, we move the bitmap to a resource file, link it into the application and read it from there instead.
Resource files
A resource file, most commonly seen with the file extension .res
, is a container for resources such as strings, bitmaps, icons, cursors, forms and dialogs. Resource files are rarely used by themselves. Instead they are linked into the applications or DLLs that uses them.
Resource files can be created with specialized resource editors such as Colin Wilson’s opensource XN Resource Editor or a commercial tool such as Resource Builder. Or if you are really desperate, the crummy, buggy imagedit.exe
that came with Delphi up to Delphi 2006 or the equally buggy, 20 year old Resource Workshop. The upcoming Delphi 2008 is supposed to (finally!) include some sort of integrated resource management.
You can also skip the resource editor and let Delphi create the resource file for you. Delphi comes with a resource compiler brcc32.exe
that can be used to compile resource script files into binary resource files. All you have to do is include the resource script file in your project and Delphi will take care of the rest.
The resource script is just a plain text file with the file extension .rc
. The complete resource script syntax is beyond the scope of this article (also, I can’t find the documentation for it anymore), but for our purpose it is very simple; Each line of the file lists the name of the resource, the resource type and the source filename of the resource.
Normally the resource type for a bitmap would be BITMAP
, but because our bitmap is a bit special (remember it’s a 32 bit bitmap with premultiplied alpha) and theBITMAP
resource type is reserved for “plain vanilla” bitmaps, we have to use theRCDATA
resource type. RCDATA
is the resource equivalent of a BLOB in a database; It’s a data type that can be used to store anything you like. For example Delphi’s forms (the DFM files) are stored as RCDATA
resources.
Loading the bitmap
With me so far? OK, let’s try it out:
- Create a text file with the following content and save it as splash.rc.
// Name Type Filename SPLASH RCDATA "splash.bmp"
- Next, add the resource script file to your Delphi project.
Delphi should now add the following code to your project file:{$R 'splash.res' 'splash.rc'}
- Modify the application to load the bitmap from a resource instead of from an external file:
procedure TFormSplash.Execute; var Stream: TStream; ··· begin ··· Bitmap := TBitmap.Create; try // Bitmap.LoadFromFile('splash.bmp'); Stream := TResourceStream.Create(HInstance, 'SPLASH', RT_RCDATA); try Bitmap.LoadFromStream(Stream); finally Stream.Free; end; ···
-
You might wonder why we don’t just use
TBitmap.LoadFromResourceName()
. The reason is simply thatLoadFromResourceName
is hardwired to only load theBITMAP
resource type and thus can’t be used to load ourRCDATA
resource. - Save, compile and run.
Once you compile the project, Delphi should automatically compile thesplash.rc
resource script into thesplash.res
resource file and link it into the application.
Run-time premultiplication
As you might remember from the first part, I promised to show you how to premultiply the bitmap at run-time. Premultiplication requires us to perform the following simple transformation on each pixel in the bitmap: Color = Color * Alpha / 255. The following function does just that:
procedure PremultiplyBitmap(Bitmap: TBitmap);
var
Row, Col: integer;
p: PRGBQuad;
begin
for Row := 0 to Bitmap.Height-1 do
begin
Col := Bitmap.Width;
p := Bitmap.ScanLine[Row];
while (Col > 0) do
begin
p.rgbBlue := p.rgbBlue * p.rgbReserved div 255;
p.rgbGreen := p.rgbGreen * p.rgbReserved div 255;
p.rgbRed := p.rgbRed * p.rgbReserved div 255;
inc(p);
dec(Col);
end;
end;
end;
Since there’s only 256 possible color values per channel and only 256 possible alpha values, we can optimize this process by sacrificing a little memory (256*256 = 64Kb) and replace the calculations with a table lookup. Here’s the optimized version:
procedure PremultiplyBitmap(Bitmap: TBitmap);
var
Row, Col: integer;
p: PRGBQuad;
PreMult: array[byte, byte] of byte;
begin
// precalculate all possible values of a*b
for Row := 0 to 255 do
for Col := Row to 255 do
begin
PreMult[Row, Col] := Row*Col div 255;
if (Row <> Col) then
PreMult[Col, Row] := PreMult[Row, Col]; // a*b = b*a
end;
for Row := 0 to Bitmap.Height-1 do
begin
Col := Bitmap.Width;
p := Bitmap.ScanLine[Row];
while (Col > 0) do
begin
p.rgbBlue := PreMult[p.rgbReserved, p.rgbBlue];
p.rgbGreen := PreMult[p.rgbReserved, p.rgbGreen];
p.rgbRed := PreMult[p.rgbReserved, p.rgbRed];
inc(p);
dec(Col);
end;
end;
end;
My tests show the optimized version to be 2-3 times faster. The function still has plenty of optimization opportunities but since it’s already fast enough, further performance gains doesn’t seem worth the resulting obfuscation of the code. I clocked the above function to 5 mS with the sample bitmap.
To try it out, go back to your original PhotoShop image and save it as a normal 32-bit bitmap with an alpha channel:
- Open or create a transparent image in PhotoShop.
- Merge Visible Layers (
Shift+Ctrl+E
).
This flattens the image while keeping the transparency. - Auto-select the Image Layer (
Ctrl+Click
on the layer or Right-click on the layer+Select Pixels). - Switch to the Channels tab and Save selection as channel ().
- Save the image as a BMP (make sure Alpha Channels is enabled and checked).
Under Advanced Modes, select the 32-bit, A8R8G8B8 format.
Now modify your application to perform premultiplication on the bitmap after is has been loaded:
···
Bitmap.LoadFromStream(Stream);
finally
Stream.Free;
end;
ASSERT(Bitmap.PixelFormat = pf32bit, 'Wrong bitmap format - must be 32 bits/pixel');
// Perform run-time premultiplication
PremultiplyBitmap(Bitmap);
// Resize form to fit bitmap
ClientWidth := Bitmap.Width;
ClientHeight := Bitmap.Height;
···
Before you compile you should delete the old splash.res
to force the resource compiler to recompile with your new bitmap.
Give it a go before we move on.
Loading the bitmap from a PNG
Finally the part you’ve probably been waiting for: How do we replace the uncompressed bitmap with a nicely compressed PNG bitmap?
One alternative to GraphicEx is Gustavo Daud’s PNGDelphi library. Unfortunately this library was removed from SourceForge while I was writing this article, but according to rumors it will likely appear again soon.
Since Delphi doesn’t include native support for the PNG format (yet) we have to use a third-party library to load the PNG image. Luckily Mike Lischke’s open source GraphicsEx library does the job nicely so going back to PhotoShop, we can now save our bitmap as a PNG (make sure you save it with an alpha channel) and modify the application to load the PNG instead:
- First we have to include the
GraphicEx
unit:··· implementation uses GraphicEx; ···
-
I have included a copy of GraphicEx in the
.\GraphicEx
sub folder of the sample source, but you can also download the original yourself. Remember to add the GraphicEx folder to the project search path.
If you don’t plan to use any of the other graphic formats supported by GraphicEx, I suggest you modify theGraphicConfiguration.inc
file to disable everything but PNG support to save space. Basically only thePortableNetworkGraphic
conditional should be defined. - Next we modify the
splash.rc
resource script to include the PNG bitmap:// Name Type Filename SPLASH RCDATA "splash.png"
- Finally we modify the code to use a
TPNGGraphic
object instead of aTBitmap
:··· // Bitmap := TBitmap.Create; Bitmap := TPNGGraphic.Create; try ···
-
Because
TPNGGraphic
descends fromTBitmap
we can leave the rest of the code alone.
Using a compressed bitmap gives us a considerable smaller footprint and while some of this saving is offset by the added PNG support code, we still save enough space to make it worthwhile.
Using alpha transparent PNG bitmaps also has the benefit that they are much easier to create.
Loading the PNG with GDI+
Another way to support PNG bitmaps is with the aid of GDI+. GDI+ is an object oriented library that enhances and encapsulates GDI with support for 2D vector graphics, imaging and typography. GDI+ is shipped with Windows XP and later but is also available as a separate redistributable for older systems. If you read through Microsoft’s description of GDI+ you might get the impression that GDI+ will replace GDI completely and that GDI will soon become obsolete:
As its name suggests, GDI+ is the successor to Windows Graphics Device Interface (GDI), the graphics device interface included with earlier versions of Windows. Windows XP or Windows Server 2003 supports GDI for compatibility with existing applications, but programmers of new applications should use GDI+ for all their graphics needs because GDI+ optimizes many of the capabilities of GDI and also provides additional features.
This nonsense resembles the FUD Microsoft spread to scare developers into abandoning native Win32 development and move to .NET. The truth is that GDI+ is just a framework layer on top of GDI and not a very nicely designed framework at that.
Anyway, back in the real world; GDI+ has support for the regular Windows graphic formats (BMP, ICO, WMF, EMF, EMF+) as well as the most widely used raster formats: GIF, JPEG, PNG, TIFF and Exif. For our purpose we only need GDI+ to read PNG files.
GDI+ provides its own set of image classes and while it would be perfectly feasible to use these directly, it is beyond the scope of this tutorial. Instead I use a GDI+ wrapper library. There are a few to chose from but only Prodigy’s GDI+ wrapperimplement the GdipCreateHBITMAPFromBitmap
API function which we need.
In order to use GDI+ instead of GraphicEx we need to make a few changes:
- Remove the
GraphicEx
unit and include theGdipApi
,GdipObj
andActiveX
units instead:··· implementation uses GdipApi, GdipObj, ActiveX; ···
The GDI+ wrapper available from Prodigy’s site doesn’t support newer versions of Delphi, so I suggest you just use the version I have bundled with the sample source. The GDI+ library is in the
.\GDI+
sub folder so go ahead and add that to the project search path. - Next, modify the
TFormSplash.Execute
method to use GDI+:procedure TFormSplash.Execute; var Stream: TStream; PNGBitmap: TGPBitmap; BitmapHandle: HBITMAP; StreamAdapter: IStream; Bitmap: TBitmap; ··· begin ··· Bitmap := TBitmap.Create; try // Load the PNG from a resource Stream := TResourceStream.Create(HInstance, 'SPLASH', RT_RCDATA); try // Wrap the VCL stream in a COM IStream StreamAdapter := TStreamAdapter.Create(Stream); try // Create and load a GDI+ bitmap from the stream PNGBitmap := TGPBitmap.Create(StreamAdapter); try // Convert the PNG to a 32 bit GDI bitmap PNGBitmap.GetHBITMAP(MakeColor(0,0,0,0), BitmapHandle); // Wrap the bitmap in a VCL TBitmap Bitmap.Handle := BitmapHandle; finally PNGBitmap.Free; end; finally StreamAdapter := nil; end; finally Stream.Free; end; ASSERT(Bitmap.PixelFormat = pf32bit, 'Wrong bitmap format - must be 32 bits/pixel'); // Perform run-time premultiplication PremultiplyBitmap(Bitmap); ···
That should be it, but if you run the above code you will probably end up in the debugger’s CPU view on an INT3
break point within ntdll.dll
. The reason is that there’s a small bug in Delphi’s TStreamAdapter
class.
It seems that the bug only manifests itself when run in the debugger, and only as a break point, but I recommend you fix it anyway.
Fixing TStreamAdapter.stat
The problem with TStreamAdapter
is in its implementation of the IStream.stat
method. The stat
method takes two parameters: A STATSTG
out parameter and a STATFLAG
value. The STATFLAG
value specifies if the stat
method should return a value in the STATSTG.pwcsName
member. If it does return a value, it is the responsibility of the called object (i.e. TStreamAdapter
) to allocate memory for the string value, and the responsibility of the caller (i.e. GDI+) to deallocate the string. Now TStreamAdapter.stat
completely ignores the STATFLAG
parameter, which is understandable because it doesn’t know anything about filenames, but unfortunately it also fails to zero the STATSTG.pwcsName
member. The result is that the caller (GDI+ in this case) receives an invalid string pointer. When GDI+ later dutifully calls coTaskMemFree
to deallocate the string, Windows objects and stops our application with a break point.
Luckily the bug is very easy to work around:
- Add the following code just above
TFormSplash.Execute
:type TFixedStreamAdapter = class(TStreamAdapter) public function Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult; override; stdcall; end; function TFixedStreamAdapter.Stat(out statstg: TStatStg; grfStatFlag: Integer): HResult; begin Result := inherited Stat(statstg, grfStatFlag); statstg.pwcsName := nil; end;
- Modify
TFormSplash.Execute
to use the new class and Bob’s your uncle:··· // Wrap the VCL stream in a COM IStream StreamAdapter := TFixedStreamAdapter.Create(Stream); ···
[Update 2008-05-28] It seems the problem is known:
QC 45528: Potential issue in TStreamAdapter.Stat implementation
GraphicEx or GDI+?
Remember that if you use GDI+ and plan to support Windows 2000 or earlier, you should distribute
gdiplus.dll
together with your application. Also note that the correct location for yourcopy ofgdiplus.dll
is in the same folder as your application,not thesystem32
folder.
The choice between GraphicEx and GDI+ is up to you; One is not better than the other. GraphicEx has the advantage that you can compile it into your application so you don’t have to rely on a DLL which may or may not be present on the target system. GDI+ on the other hand has the advantage that it relieves your application of the PNG support code.
I suggest that you just choose which everfeels best to you or if you are already using one or the other, stick with that.
Please take note that both GraphicEx and Prodigy’s GDI+ wrapper are licensed under the Mozilla Public License (MPL).
More Bling Bling
For the final touch I will add a fade effect to our splash form. TheUpdateLayeredWindow
API we use already provides the means to specify an alpha value to be applied on the entire bitmap; Namely the SourceConstantAlpha
member of the BLENDFUNCTION
parameter. This overriding alpha value is applied in addition to the per-pixel alpha values specified by the bitmap itself.
If we set SourceConstantAlpha
to 0 (zero), the form becomes completely transparent. If we set the value to 255 it becomes completely opaque, still with respect to the per-pixel alpha values. So in order to fade the form in or out we simply specify an increasing or decreasing sequence of SourceConstantAlpha
values:
procedure TFormSplash.Execute;
var
Ticks: DWORD;
···
begin
···
// Setup alpha blending parameters
BlendFunction.BlendOp := AC_SRC_OVER;
BlendFunction.BlendFlags := 0;
BlendFunction.SourceConstantAlpha := 0; // Start completely transparent
BlendFunction.AlphaFormat := AC_SRC_ALPHA;
Show;
// ... and action!
Ticks := 0;
while (BlendFunction.SourceConstantAlpha < 255) do
begin
while (Ticks = GetTickCount) do
Sleep(10); // Don't fade too fast
inc(BlendFunction.SourceConstantAlpha, (255-BlendFunction.SourceConstantAlpha) div 32+1); // Fade in
UpdateLayeredWindow(Handle, 0, nil, @BitmapSize, Bitmap.Canvas.Handle,
@BitmapPos, 0, @BlendFunction, ULW_ALPHA);
end;
···
I’ll better explain what’s going on inside the loop.
The throttle
The GetTickCount
stuff throttles the speed of the fade so it doesn’t progress too quickly on a fast machine. Since GetTickCount
has a resolution of approximately 16mS and the loop iterates 85 times [handwave], the whole fade takes at least 1.4 seconds (85*16mS) to complete. Use a multimedia timer (the timeGetTime
function in the mmSystem
unit) if you need better resolution.
The fade
Instead of just increasing the opacity in a linear fashion with a sequence of equal steps:
inc(BlendFunction.SourceConstantAlpha);
we start with a large step and continue with increasingly smaller steps using a simple algorithm that is known as exponential slide when applied to motion:
inc(BlendFunction.SourceConstantAlpha,
(255-BlendFunction.SourceConstantAlpha) div 32+1);
The idea behind an exponential slide is that with each step, we halve the remaining distance. In this case we accelerate the slide by dividing the remaining distance by 32 instead of 2. The +1 is to avoid getting trapped in Zeno’s dichotomy paradox. In my opinion the exponential slide gives the fade a much more organic feel.
Look mom, I made an anti-pattern
One bad side effect of the above implementation is that it uses busy waiting and in effect adds almost 2 useless seconds to your application startup time. If this is a problem you should move the whole loop into a low priority thread, but that, as they say, is left as an exercise to the reader.
Wrapping Up
That’s all for this time. I hope you enjoyed reading this and learned a bit in the process. I certainly did.
Please take a moment to rate the article and leave a comment to let me know what you think.