赞
踩
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.
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.
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.
With me so far? OK, let’s try it out:
- // Name Type Filename
- SPLASH RCDATA "splash.bmp"
{$R 'splash.res' 'splash.rc'}
- 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 that LoadFromResourceName
is hardwired to only load theBITMAP
resource type and thus can’t be used to load our RCDATA
resource.
splash.rc
resource script into the splash.res
resource file and link it into the application.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:
Shift+Ctrl+E
).Ctrl+Click
on the layer or Right-click on the layer+Select Pixels).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.
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:
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 the GraphicConfiguration.inc
file to disable everything but PNG support to save space. Basically only thePortableNetworkGraphic
conditional should be defined.
splash.rc
resource script to include the PNG bitmap:- // Name Type Filename
- SPLASH RCDATA "splash.png"
TPNGGraphic
object instead of a TBitmap
:
- ···
- // Bitmap := TBitmap.Create;
- Bitmap := TPNGGraphic.Create;
- try
- ···
Because TPNGGraphic
descends from TBitmap
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.
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:
GraphicEx
unit and include the GdipApi
, GdipObj
and ActiveX
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.
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.
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:
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;
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
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).
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 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.
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.
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.
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.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。