jueves, 30 de junio de 2016

Creating a Yosemite shadow for borderless form in Delphi [part 2]


Now that we have our shadow picture, it is time to create our project, so with no further ado let’s create a new VCL Form application in Delphi, and add another VCL form, that will be our shadow form which will be an alpha layered window, that’s because we will use the main form as the content holder, and VCL controls are not compatible with alpha layered windows, neither can be translucent, so that’s why we need two forms.

For the sake of small footprint of the pictures, I cropped them to 252x252 resulting in the following:

macosxmacosx2

Name the second form as frmShadow and then add those two pictures as resources: Project->Resources and images

image

Identify them as SHADOWACTIVE and SHADOWINACTIVE accordingly.

The goal is to load those two pictures in memory, crop the TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom and BottomRight parts of each and create another one that will fit the main form, updating it when we resize our main form too.

imageThe sides will be stretched to the new size, but the corners will need to be static sizes. However, to make it work properly we need to limit the minimum size of our window at least to 126 x 126.

Another thing to consider is the coordinates of each border in order to map correctly the shadow position. Just knowing width and height of the top and bottom corners will suffice, as for this demo at least. However, we must know the margins too.

Lets edit our frmShadow class in FormShadow.pas

We are going to use GDIPLus library to redraw the shadow, so add in Uses GDIPAPI , GDIPOBJ  and ActiveX too.

Add these variables in the private section of our frmShadow class:

{ Private declarations }
shadowActive, shadowInactive: TGPBitmap;
shadowState: boolean;
shadowActiveMargin, shadowInactiveMargin: TRect; // each shadow has different margins
shadowBorderSize: Integer; //width x height (square) to pick on each corner for drawing

First two Bitmaps will hold those two pictures to use it as a base template.

shadowState will be used to tell which shadow to use

shadowActiveMargin and shadowInactiveMargin will hold information of the margins of the sides of our pictures, so we will know what parts of the pictures are the shadow to copy

shadowBorderSize and integer, which would be replace by a constant, and it will hold a minimum square area to copy from the templates, in our example will set it to 100x100, which is enough to get the borders and sides of the shadow templates

We will also need another public variable that will be used to tell our main form where to align the shadow form.

{Public declarations}
Margins: TRect;

Now, we will add two procedures in our private section

procedure SetShadowState(const value: boolean);
procedure UpdateShadow;

And a public property to switch shadows from the another form

published
     property ActivateShadow: boolean read shadowState write SetShadowState;

The procedure SetShadowState will be called by ActivateShadow, when we assign it a value: True or False, which means that if set to True, our form will use the bold shadow and if it is False, it will use the light shadow. Afterwards it will call UpdateShadow procedure to redraw this form using the selected shadow.

procedure TfrmShadow.SetShadowState(const value: boolean);
begin
     shadowState := value;
     UpdateShadow;
end;

To continue with UpdateShadow, first we must load those pictures from our resources, set our shadow form style and the private variables’ values. Before UpdateShadow code, let’s modify the FormCreate method.

procedure TfrmShadow.FormCreate(Sender: TObject);
var
   Stream: TStream;
   StreamAdapter: IStream;
begin

  // Load both pictures
   Stream := TResourceStream.Create(HINSTANCE, 'SHADOWACTIVE', RT_RCDATA);
   try
     StreamAdapter := TStreamAdapter.Create(Stream);
     try
       shadowActive := TGPBitmap.Create(StreamAdapter);
     finally
       StreamAdapter := nil;
     end;
   finally
     FreeAndNil(Stream);
   end;
  //set its correspondant margins and borders
   // this can be extended to use config settings and external themes
   // but for now it will only be static values

   shadowBorderSize := 100;
   shadowActiveMargin := Rect(48, 25, 48, 73);

  Stream := TResourceStream.Create(HINSTANCE, 'SHADOWINACTIVE', RT_RCDATA);
   try
     StreamAdapter := TStreamAdapter.Create(Stream);
     try
       shadowInactive := TGPBitmap.Create(StreamAdapter);
     finally
       StreamAdapter := nil;
     end;
   finally
     FreeAndNil(Stream);
   end;

  shadowInactiveMargin := Rect(36, 27, 36, 43);

  //let's calculate the maximum margin
   //Margins.Left := Max(shadowActiveMargin.Left, shadowInactiveMargin.Left);
   //but… since we already know, just write them

   Margins := Rect(48, 27, 48, 73);

  BorderStyle := bsNone;
   SetWindowLong(Handle, GWL_EXSTYLE, GetWindowLong(Handle, GWL_EXSTYLE) or WS_EX_LAYERED or WS_EX_TRANSPARENT or WS_EX_TOOLWINDOW or WS_EX_NOACTIVATE);
   ActivateShadow := True;  
end;


procedure TfrmShadow.FormDestroy(Sender: TObject);
begin
   FreeAndNil(shadowActive);
   FreeAndNil(shadowInactive);
end;

As you can read the previous code, we loaded our two shadow templates from the resources of our executable, to shadowActive and shadowInactive, then set the shadowBorderSize to 100 (100x100), and also the margins for both pictures and a global margin which will keep the maximum margins between those two margins.

image

shadowActiveMargin and shadowInactiveMargin have different margins from the sides, and Margins will consider the maximum margins, this will be useful as offset to draw the shadows in the correct X,Y position when switched.

Of course our frmShadow form will not have border style (bsNone), and with SetWindowLong we establish it as WS_EX_LAYERED and WS_EX_TRANSPARENT primarily, the first style is required to draw with a Blending Function our alpha channeled shadow picture and the second style to make it unresponsive to user interaction (no clicks to answer, it will allow us to avoid focusing this form). The other ones are optional.

We set the bold shadow by default with ActivateShadow := True; and we should end this form deleting those two pictures that we created on memory.

procedure TfrmShadow.FormResize(Sender: TObject);
begin
   UpdateShadow;
end;

If our shadow form is resized, we need to update the shadow with a new size too.

Finally, the most important part, UpdateShadow procedure, the only responsible to create our resized shadow:

procedure TfrmShadow.UpdateShadow;
type
   pTGPBitmap = ^TGPBitmap;
var
   BlendFunction: TBlendFunction;
   Bitmap: TBitmap;
   BitmapPoint: TPoint;
   BitmapHandle: HBITMAP;
   BitmapSize: TSize;
   Bmp: TGPGraphics;
   Buf: TGPBitmap;
   R: TGPRect;
   ShadowPicture: pTGPBitmap;
   ShadowSize: TSize;
   MarginOffset: TRect;
begin
   Bitmap := TBitmap.Create;
   try
     Buf := TGPBitmap.Create(ClientWidth, ClientHeight, 0, PixelFormat32bppARGB, nil);
     try
       Bmp := TGPGraphics.Create(Buf);
       try
         Bmp.SetPixelOffsetMode(PixelOffsetModeHalf); // to fix bad stretching
         Bmp.Clear(MakeColor(0, 0, 0, 0));
         Bmp.SetInterpolationMode(InterpolationModeNearestNeighbor);
         //choose which of those two shadows we will use
         if shadowState then
         begin
           MarginOffset := Rect( Margins.Left - shadowActiveMargin.Left,
           Margins.Top - shadowActiveMargin.Top,
           Margins.Right - shadowActiveMargin.Right,
           Margins.Bottom - shadowActiveMargin.Bottom);
           ShadowPicture := @ShadowActive;
           ShadowSize.cx := shadowActive.GetWidth;
           ShadowSize.cy := shadowActive.GetHeight;
         end
         else
         begin
           MarginOffset := Rect( Margins.Left - shadowInactiveMargin.Left,
           Margins.Top - shadowInactiveMargin.Top,
           Margins.Right - shadowInactiveMargin.Right,
           Margins.Bottom - shadowInactiveMargin.Bottom);
           ShadowPicture := @ShadowInactive;
           ShadowSize.cx := shadowInactive.GetWidth;
           ShadowSize.cy := shadowInactive.GetHeight;
         end;

       //TopLeft
         R.X := MarginOffset.Left; R.Y := MarginOffset.Top;
         R.Width := shadowBorderSize; R.Height := shadowBorderSize;
         Bmp.DrawImage(ShadowPicture^, R, 0, 0, shadowBorderSize, shadowBorderSize, UnitPixel);
         //Top
         R.X := MarginOffset.Left + shadowBorderSize; R.Y := MarginOffset.Top;
         R.Width := ClientWidth - MarginOffset.Left - MarginOffset.Right - shadowBorderSize * 2; R.Height := shadowBorderSize;
         Bmp.DrawImage(ShadowPicture^, R, shadowBorderSize, 0, shadowBorderSize, shadowBorderSize, UnitPixel);
        //TopRight
         R.X := ClientWidth - shadowBorderSize - MarginOffset.Right; R.Y := MarginOffset.Top;
         R.Width := shadowBorderSize; R.Height := shadowBorderSize;
         Bmp.DrawImage(ShadowPicture^, R, ShadowSize.cx - shadowBorderSize ,0, shadowBorderSize, shadowBorderSize, UnitPixel);
        //Left
         R.X := MarginOffset.Left; R.Y := MarginOffset.Top + shadowBorderSize;
         R.Width := shadowBorderSize; R.Height := ClientHeight - MarginOffset.Top - MarginOffset.Bottom - shadowBorderSize * 2;
         Bmp.DrawImage(ShadowPicture^, R, 0, shadowBorderSize, shadowBorderSize, ShadowSize.cy - shadowBorderSize*2, UnitPixel);
         //right
         R.X := ClientWidth - shadowBorderSize - MarginOffset.Right; R.Y := MarginOffset.Top + shadowBorderSize;
         R.Width := shadowBorderSize; R.Height := ClientHeight - MarginOffset.Top - MarginOffset.Bottom - shadowBorderSize * 2;
         Bmp.DrawImage(ShadowPicture^, R, ShadowSize.cx - shadowBorderSize, shadowBorderSize, shadowBorderSize, ShadowSize.cy - shadowBorderSize * 2, UnitPixel);
         //BottomLeft
         R.X := MarginOffset.Left; R.Y := ClientHeight - shadowBorderSize - MarginOffset.Bottom;
         R.Width := shadowBorderSize; R.Height := shadowBorderSize;
         Bmp.DrawImage(ShadowPicture^, R, 0, ShadowSize.cy - shadowBorderSize,shadowBorderSize, shadowBorderSize, UnitPixel);
        //Bottom
         R.X := MarginOffset.Left + shadowBorderSize; R.Y := ClientHeight - shadowBorderSize - MarginOffset.Bottom;
         R.Width := ClientWidth - MarginOffset.Left - MarginOffset.Right - shadowBorderSize * 2; R.Height := shadowBorderSize;
         Bmp.DrawImage(ShadowPicture^, R, shadowBorderSize, ShadowSize.cy - shadowBorderSize, ShadowSize.cy - shadowBorderSize*2, shadowBorderSize, UnitPixel);
         //BottomRight
         R.X := ClientWidth - shadowBorderSize - MarginOffset.Right; R.Y := ClientHeight - shadowBorderSize - MarginOffset.Bottom;
         R.Width := shadowBorderSize; R.Height := shadowBorderSize;
         Bmp.DrawImage(ShadowPicture^, R, ShadowSize.cx - shadowBorderSize, ShadowSize.cy - shadowBorderSize, shadowBorderSize, shadowBorderSize, UnitPixel);

      finally
         FreeAndNil(Bmp);
       end;

      Buf.GetHBITMAP(MakeColor(0, 0, 0, 0), BitmapHandle);
     finally
       FreeAndNil(Buf);
     end;

    Bitmap.Handle := BitmapHandle;

    BitmapSize.cx := Bitmap.Width;
     BitmapSize.cy := Bitmap.Height;

    BlendFunction.BlendOp := AC_SRC_OVER;
     BlendFunction.BlendFlags := 0;
     BlendFunction.SourceConstantAlpha := 255;
     BlendFunction.AlphaFormat := AC_SRC_ALPHA;

    BitmapPoint := Point(0, 0);

    UpdateLayeredWindow(Handle, 0, nil, @BitmapSize, Bitmap.Canvas.Handle,
          @BitmapPoint, 0, @BlendFunction, ULW_ALPHA);
   finally
     FreeAndNil(Bitmap);
   end;
end;

Basically it creates a new picture with the dimensions of our frmShadow form with ARGB pixel format, we must also remember that this shadow form will always be larger than the main form.

According to the selected shadow (shadowState), we will set a temporary variable called MarginOffset which will hold a difference of pixels between the global margin (Margins) and the selected shadow margin, and assign the selected picture using a pointer, and also find out its width and height in ShadowSize local variable.

Having that information, the next part will consist of drawing in our Bmp graphics using the previously gathered information, shifting with MarginOffset to get the correct X,Y coordinates to draw from/to, destination canvas and our template canvas.

R: holds the destination rectangle where we will paint on our frmShadow’s new bitmap.
ShadowPicture: points to the selected shadow template picture.
The Bmp.DrawImage variant method used is:

Bmp.DrawImage(sourcepicture, destination rectangle, sourcepicture left, sourcepicture top, sourcepicture width, sourcepicture height, UnitPixel);

In other words, we will copy parts of our source picture into the new one, TopLeft, Top, TopRight, and so on. Top, Left, Right and Bottom drawing will stretch the drawing so it will fill the sides completely.

Once created that picture, we proceed with the conventional way to update a layered window, with the UpdateLayeredWindow WinAPI function, which draws the created new shadow picture in our entire shadow form.

Finally, we should add a global variable for out FormShadow unit, rght nex to our frmShadow variable:

var
   frmShadow: TfrmShadow;
   frmShadowEnabled : Boolean = False;

False by default, it will be useful to tell our calling form that frmShadow formclass is ready.

That’s it for our FormShadow unit/form, now is time to use it from our main form.

<— Creating a Yosemite shadow for borderless form in Delphi [part 1]