Search code examples
delphi-xetrayicon

EOutOfResources exception when trying to restore tray icon


I'm getting an EOutOfResources exception 'Cannot remove shell notification icon' when trying to implement code to restore the tray icon after an Explorer crash/restart. My code is based on the old solution found here. The exception occurs when trying to hide the trayicon. Why does the Delphi XE code below not work?

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ImgList, ExtCtrls;

type
  TForm1 = class(TForm)
    TrayIcon1: TTrayIcon;
    ImageListTray: TImageList;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  protected
    procedure WndProc(var Message: TMessage); Override;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  msgTaskbarRestart : Cardinal; {custom systemwide message}  

implementation

{$R *.dfm}

//ensure systray icon recreated on explorer crash
procedure TForm1.FormCreate(Sender: TObject);
begin
  msgTaskbarRestart := RegisterWindowMessage('TaskbarCreated');
end;

procedure TForm1.WndProc(var Message: TMessage);
begin
  if (msgTaskbarRestart <> 0) and (Message.Msg = msgTaskbarRestart) then begin 
    TrayIcon1.Visible := False; {Destroy the systray icon here}//EOutOfResources exception here
    TrayIcon1.Visible := True;  {Replace the systray icon}
    Message.Result := 1;
  end;
  inherited WndProc(Message);
end;

end.

Solution

  • The TTrayIcon.Visible property setter raises EOutOfResources when a NIM_DELETE request fails:

    procedure TCustomTrayIcon.SetVisible(Value: Boolean);
    begin
      if FVisible <> Value then
      begin
        FVisible := Value;
        ...
    
        if not (csDesigning in ComponentState) then
        begin
          if FVisible then
            ...
          else if not (csLoading in ComponentState) then
          begin
            if not Refresh(NIM_DELETE) then
              raise EOutOfResources.Create(STrayIconRemoveError); // <-- HERE
          end;
          ...
        end;
      end;
    end;
    

    Where Refresh() is just a call to the Win32 Shell_NotifyIcon() function:

    function TCustomTrayIcon.Refresh(Message: Integer): Boolean;
      ...
    begin
      Result := Shell_NotifyIcon(Message, FData);
      ...
    end;
    

    When you receive the TaskbarCreated message, your previous icons are no longer present in the Taskbar, so Shell_NotifyIcon(NIM_DELETE) returns False. When the Taskbar is (re-)created, you are not supposed to try to remove old icons at all, only re-add new icons with Shell_NotifyIcon(NIM_ADD) as needed.

    TTrayIcon has a public Refresh() method, but that uses NIM_MODIFY instead of NIM_ADD, so that will not work in this situation, either:

    procedure TCustomTrayIcon.Refresh;
    begin
      if not (csDesigning in ComponentState) then
      begin
        ...
        if Visible then
          Refresh(NIM_MODIFY);
      end;
    end;
    

    However, you don't actually need to handle the TaskbarCreated message manually when using TTrayIcon, because it already handles that message internally for you, and it will call Shell_NotifyIcon(NIM_ADD) if Visible=True:

    procedure TCustomTrayIcon.WindowProc(var Message: TMessage);
      ...
    begin
      case Message.Msg of
        ...
      else
        if (Cardinal(Message.Msg) = RM_TaskBarCreated) and Visible then
          Refresh(NIM_ADD); // <-- HERE
      end;
    end;
    
    ...
    
    initialization
      ...
      TCustomTrayIcon.RM_TaskBarCreated := RegisterWindowMessage('TaskbarCreated');
    end.
    

    If, for some reason, that is not working correctly, and/or you need to handle TaskbarCreated manually, then I would suggest calling the protected TCustomTrayIcon.Refresh() method directly, eg:

    type
      TTrayIconAccess = class(TTrayIcon)
      end;
    
    procedure TForm1.WndProc(var Message: TMessage);
    begin
      if (msgTaskbarRestart <> 0) and (Message.Msg = msgTaskbarRestart) then begin 
        if TrayIcon1.Visible then begin
          // TrayIcon1.Refresh;
          TTrayIconAccess(TrayIcon1).Refresh(NIM_ADD);
        end;
        Message.Result := 1;
      end;
      inherited WndProc(Message);
    end;
    

    Otherwise, simply don't use TTrayIcon at all. It is known to be buggy. I have seen a lot of people have a lot of problems with TTrayIcon over the years. I would suggest using Shell_NotifyIcon() directly instead. I have never had any problems using it myself.