当前位置:   article > 正文

Alpha Blended Splash Screen in Delphi - Part 2_fade too fast

fade too fast

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.exethat 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.

A resource script in the Delphi Project Manager

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:

  1. Create a text file with the following content and save it as splash.rc.
    1. // Name Type Filename
    2. SPLASH RCDATA "splash.bmp"

  2. 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'}

  3. Modify the application to load the bitmap from a resource instead of from an external file:
    1. procedure TFormSplash.Execute;
    2. var
    3. Stream: TStream;
    4. ···
    5. begin
    6. ···
    7. Bitmap := TBitmap.Create;
    8. try
    9. // Bitmap.LoadFromFile('splash.bmp');
    10. Stream := TResourceStream.Create(HInstance, 'SPLASH', RT_RCDATA);
    11. try
    12. Bitmap.LoadFromStream(Stream);
    13. finally
    14. Stream.Free;
    15. end;
    16. ···


  4. 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.

  5. Save, compile and run.
    Once you compile the project, Delphi should automatically compile thesplash.rc resource script into the splash.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:

  1. procedure PremultiplyBitmap(Bitmap: TBitmap);
  2. var
  3. Row, Col: integer;
  4. p: PRGBQuad;
  5. begin
  6. for Row := 0 to Bitmap.Height-1 do
  7. begin
  8. Col := Bitmap.Width;
  9. p := Bitmap.ScanLine[Row];
  10. while (Col > 0) do
  11. begin
  12. p.rgbBlue := p.rgbBlue * p.rgbReserved div 255;
  13. p.rgbGreen := p.rgbGreen * p.rgbReserved div 255;
  14. p.rgbRed := p.rgbRed * p.rgbReserved div 255;
  15. inc(p);
  16. dec(Col);
  17. end;
  18. end;
  19. 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:

  1. procedure PremultiplyBitmap(Bitmap: TBitmap);
  2. var
  3. Row, Col: integer;
  4. p: PRGBQuad;
  5. PreMult: array[byte, byte] of byte;
  6. begin
  7. // precalculate all possible values of a*b
  8. for Row := 0 to 255 do
  9. for Col := Row to 255 do
  10. begin
  11. PreMult[Row, Col] := Row*Col div 255;
  12. if (Row <> Col) then
  13. PreMult[Col, Row] := PreMult[Row, Col]; // a*b = b*a
  14. end;
  15. for Row := 0 to Bitmap.Height-1 do
  16. begin
  17. Col := Bitmap.Width;
  18. p := Bitmap.ScanLine[Row];
  19. while (Col > 0) do
  20. begin
  21. p.rgbBlue := PreMult[p.rgbReserved, p.rgbBlue];
  22. p.rgbGreen := PreMult[p.rgbReserved, p.rgbGreen];
  23. p.rgbRed := PreMult[p.rgbReserved, p.rgbRed];
  24. inc(p);
  25. dec(Col);
  26. end;
  27. end;
  28. 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:

  1. Open or create a transparent image in PhotoShop.
  2. Merge Visible Layers (Shift+Ctrl+E).
    This flattens the image while keeping the transparency.
  3. Auto-select the Image Layer (Ctrl+Click on the layer or Right-click on the layer+Select Pixels).
  4. Switch to the Channels tab and Save selection as channel (PhotoShop - The “Save selection as channel” button).
  5. 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:

  1. ···
  2. Bitmap.LoadFromStream(Stream);
  3. finally
  4. Stream.Free;
  5. end;
  6. ASSERT(Bitmap.PixelFormat = pf32bit, 'Wrong bitmap format - must be 32 bits/pixel');
  7. // Perform run-time premultiplication
  8. PremultiplyBitmap(Bitmap);
  9. // Resize form to fit bitmap
  10. ClientWidth := Bitmap.Width;
  11. ClientHeight := Bitmap.Height;
  12. ···


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:

  1. First we have to include the GraphicEx unit:
    1. ···
    2. implementation
    3. uses
    4. GraphicEx;
    5. ···
  2. 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.

  3. Next we modify the splash.rc resource script to include the PNG bitmap:
    1. // Name Type Filename
    2. SPLASH RCDATA "splash.png"

  4. Finally we modify the code to use a TPNGGraphic object instead of a TBitmap:
    1. ···
    2. // Bitmap := TBitmap.Create;
    3. Bitmap := TPNGGraphic.Create;
    4. try
    5. ···

  5. Because TPNGGraphic descends from TBitmap we can leave the rest of the code alone.

Alpha Blended Glass Orbs - Hmmmmm. Nice!

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:

  1. Remove the GraphicEx unit and include the GdipApiGdipObj and ActiveX units instead:
    1. ···
    2. implementation
    3. uses
    4. GdipApi, GdipObj, ActiveX;
    5. ···

    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.

  2. Next, modify the TFormSplash.Execute method to use GDI+:
    1. procedure TFormSplash.Execute;
    2. var
    3. Stream: TStream;
    4. PNGBitmap: TGPBitmap;
    5. BitmapHandle: HBITMAP;
    6. StreamAdapter: IStream;
    7. Bitmap: TBitmap;
    8. ···
    9. begin
    10. ···
    11. Bitmap := TBitmap.Create;
    12. try
    13. // Load the PNG from a resource
    14. Stream := TResourceStream.Create(HInstance, 'SPLASH', RT_RCDATA);
    15. try
    16. // Wrap the VCL stream in a COM IStream
    17. StreamAdapter := TStreamAdapter.Create(Stream);
    18. try
    19. // Create and load a GDI+ bitmap from the stream
    20. PNGBitmap := TGPBitmap.Create(StreamAdapter);
    21. try
    22. // Convert the PNG to a 32 bit GDI bitmap
    23. PNGBitmap.GetHBITMAP(MakeColor(0,0,0,0), BitmapHandle);
    24. // Wrap the bitmap in a VCL TBitmap
    25. Bitmap.Handle := BitmapHandle;
    26. finally
    27. PNGBitmap.Free;
    28. end;
    29. finally
    30. StreamAdapter := nil;
    31. end;
    32. finally
    33. Stream.Free;
    34. end;
    35. ASSERT(Bitmap.PixelFormat = pf32bit, 'Wrong bitmap format - must be 32 bits/pixel');
    36. // Perform run-time premultiplication
    37. PremultiplyBitmap(Bitmap);
    38. ···


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.

Woops

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.statmethod. 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:

  1. Add the following code just above TFormSplash.Execute:
    1. type
    2. TFixedStreamAdapter = class(TStreamAdapter)
    3. public
    4. function Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult; override; stdcall;
    5. end;
    6. function TFixedStreamAdapter.Stat(out statstg: TStatStg; grfStatFlag: Integer): HResult;
    7. begin
    8. Result := inherited Stat(statstg, grfStatFlag);
    9. statstg.pwcsName := nil;
    10. end;


  2. Modify TFormSplash.Execute to use the new class and Bob’s your uncle:
      
    1. ···
    2. // Wrap the VCL stream in a COM IStream
    3. StreamAdapter := TFixedStreamAdapter.Create(Stream);
    4. ···

[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 of gdiplus.dll is in the same folder as your application,not the system32 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.

Fading

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:

  1. procedure TFormSplash.Execute;
  2. var
  3. Ticks: DWORD;
  4. ···
  5. begin
  6. ···
  7. // Setup alpha blending parameters
  8. BlendFunction.BlendOp := AC_SRC_OVER;
  9. BlendFunction.BlendFlags := 0;
  10. BlendFunction.SourceConstantAlpha := 0; // Start completely transparent
  11. BlendFunction.AlphaFormat := AC_SRC_ALPHA;
  12. Show;
  13. // ... and action!
  14. Ticks := 0;
  15. while (BlendFunction.SourceConstantAlpha < 255) do
  16. begin
  17. while (Ticks = GetTickCount) do
  18. Sleep(10); // Don't fade too fast
  19. inc(BlendFunction.SourceConstantAlpha, (255-BlendFunction.SourceConstantAlpha) div 32+1); // Fade in
  20. UpdateLayeredWindow(Handle, 0, nil, @BitmapSize, Bitmap.Canvas.Handle,
  21. @BitmapPos, 0, @BlendFunction, ULW_ALPHA);
  22. end;
  23. ···


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 timeGetTimefunction 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:

  1. inc(BlendFunction.SourceConstantAlpha,
  2. (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 twinkle twinkle the article and leave a comment to let me know what you think.


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/75077
推荐阅读
  

闽ICP备14008679号