4
votes

I am minimizing a form to system tray (display a tray icon) while keeping its taskbar button when it is not minimized. This implies removing the taskbar button when the form is minimized and restoring it otherwise.

The simplest way to achieve this is to hide/show the form, a minimized window does not show anyway.

type
  TForm1 = class(TForm)
    TrayIcon1: TTrayIcon;
    procedure TrayIcon1DblClick(Sender: TObject);
  protected
    procedure WMSize(var Message: TWMSize); message WM_SIZE;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.WMSize(var Message: TWMSize);
begin
  inherited;
  case Message.SizeType of
    SIZE_MINIMIZED:
      if not TrayIcon1.Visible then begin
        TrayIcon1.Visible := True;
        Hide;
      end;
    SIZE_RESTORED, SIZE_MAXIMIZED:
      if TrayIcon1.Visible then begin
        Show;
        Application.BringToFront;
        TrayIcon1.Visible := False;
      end;
  end;
end;

procedure TForm1.TrayIcon1DblClick(Sender: TObject);
begin
  Show;
  WindowState := wsNormal;
end;


The above application introduces a visual glitch when "Animate windows when minimizing and maximizing" setting of the OS is on (accessible through 'SystemPropertiesPerformance.exe'). The minimize window animation is skipped. It appears that the animation actually takes place after the window is minimized. In the code, the window is already hidden by then.


One solution could be to have a notification when the window manager is done with the animation and hiding the form after that. I can't find any. When, for instance, you use the taskbar button minimizing the window, the last message sent is the WM_SYSCOMMAND, which doesn't lead to any progress if I move the code (not to mention that a window can be minimized through a ShowWindow and others).


Another solution might involve to know how long the animation takes place. SystemParametersInfo doesn't have it. Similar question here tries to deal with the animation displayed when a window is first shown, although that animation seems to be related with DWM and minimize/maximize animation precedes DWM. No conclusive solution there either. Like in that question, a 250ms delay seems to work fine. But I'm not sure this is a universally sound delay. I'm not even sure a discrete delay would be definitive, perhaps a stutter would cause it to extent (not that it would matter much, but anyway...).


I also tried to actually remove the taskbar button, without hiding the form. But it's more clumsy and it doesn't change the output: the animation is skipped.

1
AnimateWindow()?andlabs
@andlabs - It does animate a window in place. I mean it cannot be used to minimize a window.Sertac Akyuz
@Sertac Akyuz, probably CBTProc callback function can help. Especially HCBT_MINMAX. You also could make some research in using Event Constans - EVENT_SYSTEM_MINIMIZESTART, EVENT_SYSTEM_MINIMIZEEND.Josef Švejk
15 years ago I used DrawAnimatedRects to mimick the animation (IDANI_CAPTION is 3) - maybe this helps.AmigoJack
@Amigo - That's interesting, it may help if it's synchronous like AnimateWindow. I'll try to see how it'll look like.Sertac Akyuz

1 Answers

1
votes

Comment about DrawAnimatedRects (which draws no animation when Aero is on) convinced me to go slightly undocumented until I have a better alternative. Methods using DrawAnimatedRects have to determine where to minimize, that's where they use undocumented system tray window class name.

The below code goes undocumented when removing the taskbar button of the form, in particular with the use of the GWLP_HWNDPARENT index of SetWindowLongPtr. In any case, removing the taskbar button is not clumsy as in transforming the window to a tool window and the animation goes smooth.

The code falls back to a timer which hides the form in case removing the taskbar button fails.

type
  TForm1 = class(TForm)
    TrayIcon1: TTrayIcon;
    Timer1: TTimer;
    procedure TrayIcon1DblClick(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  protected
    procedure WMSize(var Message: TWMSize); message WM_SIZE;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

function ShowTaskbarButton(Wnd: HWND; Show: Boolean = True;
    OwnerWnd: HWND = 0): Boolean;
var
  ExStyle, HWndParent: LONG_PTR;
  IsToolWindow: Boolean;
begin
  HwndParent := GetWindowLongPtr(Wnd, GWLP_HWNDPARENT);
  ExStyle := GetWindowLongPtr(Wnd, GWL_EXSTYLE);
  Result := Show = (HWndParent = 0) and (ExStyle and WS_EX_APPWINDOW <> 0);

  if not Result then begin
    IsToolWindow := ExStyle and WS_EX_TOOLWINDOW <> 0;
    if IsToolWindow then begin
      ShowWindow(Wnd, SW_HIDE);
      ShowWindowAsync(Wnd, SW_SHOW);
    end;
    SetLastError(0);
    if Show then
      SetWindowLongPtr(Wnd, GWL_EXSTYLE, ExStyle or WS_EX_APPWINDOW)
    else
      SetWindowLongPtr(Wnd, GWL_EXSTYLE, ExStyle and not WS_EX_APPWINDOW);
    if not IsToolWindow and (GetLastError = 0) then
      SetWindowLongPtr(Wnd, GWLP_HWNDPARENT, OwnerWnd);

    Result := GetLastError = 0;
  end;
end;

procedure TForm1.WMSize(var Message: TWMSize);
begin
  inherited;
  case Message.SizeType of
    SIZE_MINIMIZED:
      if not TrayIcon1.Visible then begin
        if not ShowTaskbarButton(Handle, False, Application.Handle) then
          Timer1.Enabled := True;   // fall back
        TrayIcon1.Visible := True
      end;
    SIZE_RESTORED, SIZE_MAXIMIZED:
      if TrayIcon1.Visible then begin
        ShowTaskbarButton(Handle);
        Application.BringToFront;
        TrayIcon1.Visible := False;
      end;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Timer1.Interval := 250;
  Timer1.Enabled := False;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Hide;
  Timer1.Enabled := False;
end;

procedure TForm1.TrayIcon1DblClick(Sender: TObject);
begin
  ShowTaskbarButton(Handle);
  if not Showing then   // used timer to hide
    Show;
  WindowState := wsNormal;
end;