Borderless TForm with drop shadow

willw picture willw · Aug 20, 2010 · Viewed 7.7k times · Source

I have made a TForm derivative that acts like the drop down part of a combo, or a hint window, or a popup menu - a temporary thing. It has no caption - its BorderStyle is set to bsNone. The form is displayed non-modally using Show, having set its position.

To make it stand out, it needs a drop shadow around its border. However, a consequence of setting its border to bsNone is that the drop shadow disappears.

Various Google sources suggest variations of this:

procedure TdlgEditServiceTask.CreateParams(var Params: TCreateParams);
const
  CS_DROPSHADOW = $00020000;
begin
  inherited;
  { Enable drop shadow effect on Windows XP and later }
  if (Win32Platform = VER_PLATFORM_WIN32_NT) and
     ((Win32MajorVersion > 5) or
      ((Win32MajorVersion = 5) and (Win32MinorVersion >= 1))) then
    Params.WindowClass.Style := Params.WindowClass.Style or
             CS_DROPSHADOW;
end;

but it doesn't work - the shadow is not displayed (unless I also set a resizable border with WS_THICKFRAME set, which looks terrible). This is a popup window, not a child window, so I don't see why it should fail.

Suggestions please?

NB: this is a similar question to this question, which remains unanswered.

NB2: There is an obscure VCL component called TShadowWindow that looks like it will do the right thing, but turns out to be too crudely written to be practical.

Update: Following Andreas' comments below, I have investigated this further, and found some niceties.

Under Windows 7, I discovered that the shadow does not appear when the popup window if it is over another window from the same application.

Here is a simple Delphi app, which uses CreateParams on a popup window to request a shadow as described above.

Windows 7 with shadow only over desktop

See how the drop shadow appears where it extends beyond the main window?

But I want to use the borderless window as a popup over the main window. The drop shadow distinguishes the popup from the window underneath. All my description up above refers to this circumstance. Obviously some Windows mechanism is interfering here.

I have also tried the same application under Windows XP. Here is how it looks.

Same application under XP

This works correctly with shadow everywhere*. Gah!

So it would seem to be a Vista/W7 thing, as Andreas suggests.

(*An earlier version of this text and screendump suggested that no shadow appeared. However, this turned out to be because I had the Windows XP display option 'Shadows under menus' turned off. Duh.)

Answer

willw picture willw · Aug 21, 2010

Found it! Here is the proof:

alt text

As you can see, the drop shadow now shows properly over the form.

The problem was one of Z-order. It turns out that the shadow is itself a separate window maintained by Windows itself. In Windows 7, it seems to show the shadow underneath the main window. In order to get it to display properly, one needs to move it up.

A genius called Łukasz Płomiński explained this in a thread in the Embarcadero newsgroup. Here is his code to sort it out:

procedure TForm1.FixSysShadowOrder;

  function FindSysShadowOrderProc(WindowHandle: HWND; // handle to window
    Form: TForm1 // application-defined value, 32-bit
    ): BOOL; stdcall;
  var
    Buffer: array [0 .. 255] of char;
    Rect: TRect;
  begin
    Result := True;
    if IsWindowVisible(WindowHandle) then
    begin
      // this code  search for SysShadow window created for this window.
      GetClassName(WindowHandle, Buffer, 255);
      if 0 <> AnsiStrComp(Buffer, PChar('SysShadow')) then
        Exit;

      GetWindowRect(WindowHandle, Rect);
      if (Rect.Left <> Form.Left) or (Rect.Top <> Form.Top) then
        Exit;

      Form.FSysShadowHandle := WindowHandle;
      // stop enumeration
      Result := False;
    end;
  end;

begin
  if not(csDesigning in ComponentState) and
    ((GetClassLong(Handle, GCL_STYLE) and CS_DROPSHADOW) = CS_DROPSHADOW)
    and IsWindowVisible(Handle) then
  begin
    // for speed, proper SysShadow handle is cached
    if FSysShadowHandle = 0 then
      EnumThreadWindows(GetCurrentThreadID(), @FindSysShadowOrderProc,
        lParam(Self));
    // if SysShadow exists, change its z-order, and place it directly below this window
    if FSysShadowHandle <> 0 then
      SetWindowPos(FSysShadowHandle, Handle, 0, 0, 0, 0,
        SWP_NOACTIVATE or SWP_NOMOVE or SWP_NOOWNERZORDER or SWP_NOSIZE);
  end;
end;

You have to work out when to call FixSysShadowOrder(), because Z orders change, and it won't stay right. Łukasz suggested calling it in an idle routine (for example when updating an Action), and on receipt of WM_WINDOWPOSCHANGED message.