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:
    ···
    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 the PortableNetworkGraphic conditional should be defined.

  2. Next we modify the splash.rc resource script to include the PNG bitmap:
    1
    2
    
    // Name                 Type	Filename
    SPLASH                  RCDATA  "splash.png"
  3. Finally we modify the code to use a 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.

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+ wrapper implement 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 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.

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

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

  1. 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;
  2. 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 your copy 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 ever feels 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. The UpdateLayeredWindow 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:

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 twinkle twinkle the article and leave a comment to let me know what you think.

Credits

References

API documentation

Libraries

Algorithms

Requirements

The source code that accompany this article are known to be compatible with the following versions of Delphi:

D1 D2 D3 D4 D5 D6 D7 D2005 D2006 D2007
Fail Fail Fail Fail Pass Unknown Pass Unknown Pass Pass

License

Creative Commons License
This work is licensed under a
Creative Commons Attribution-Share Alike 3.0 Unported License.

Download

download
Download: Alpha Blended Splash Form - Part 2
Version: 1.1
Updated: 26 June, 2008
Size: 477.05 KB
Notes: Incudes source and sample images.
Downloads: 14,739