6

Is it possible to remove NotifyIcon from the notification area (system tray) when an app terminates abruptly?

if no, how can I remove it when the app runs for the next time?

DelPhi
  • 83
  • 1
  • 5
  • 2
    Please define "terminates abruptly". – David Heffernan Oct 04 '13 at 13:25
  • 2
    When the app crashes or unexpectedly shuts down. – DelPhi Oct 04 '13 at 13:41
  • 1
    In those situations you still have the opportunity to remove the notification icon gracefully. I assume you are using try/finally properly. It's really just forceful termination (TerminateProcess) that you cannot defend against. – David Heffernan Oct 04 '13 at 13:45
  • 2
    There's this, but it's not in Delphi: http://stackoverflow.com/questions/74723/can-you-send-a-signal-to-windows-explorer-to-make-it-refresh-the-systray-icons – Marcus Adams Oct 04 '13 at 15:09
  • In fact, I had never thought of it before, but seeing your user name made me realise it: Delphi is actually the gradient of a typical scalar field. – Andreas Rejbrand Oct 04 '13 at 15:29
  • Good find, @Marcus. Doesn't matter if it's not Delphi. The Windows API is the same in any language. – Rob Kennedy Oct 04 '13 at 15:53

3 Answers3

5

Abruptly? No. Your program has ceased to exist, so there's no opportunity to run any code to tell the shell that it should remove the icon.

To remove the icon, move your mouse over it. The shell will try to notify your program, realize there's nothing there anymore, and remove the icon by itself.

Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
3

On Windows 7 and later, notify icons can be identified by a user-defined GUID. On earlier versions, they are identified by a combination of HWND and ID number instead. Since your app is not guaranteed to get the same HWND value the next time it runs, the only way you can do anything to an old icon that is identified by HWND is if you remembered the previous HWND value so you can use it to remove the old icon, before then using a new HWND to add a new icon. But with a GUID-identified icon, the GUID needs to be persistent (as it is stored in the Registry to store app settings associated with the icon), so you should be able to simply keep updating the existing icon as needed, or remove it if desired.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • It's not clear from the documentation whether that GUID really identifies a single icon, or just a single *type* of icon. That is, if multiple instances of a program run simultaneously, must they all use distinct GUIDs for their icons? Or is it just that an instance that shows multiple icons must use a different GUID for each icon? I would expect the latter since the former would be difficult to implement. And if that's true, then a later instance of the program would still be unable to clean up the icon of the previous crashed instance. – Rob Kennedy Oct 04 '13 at 16:03
  • The docs suggest that the guid uniquely identifies a particular icon for a given copy of the app's executable, not for a given process of the app like with the HWND+ID combo. The docs do say that multiple icons in an app need to use separate guids. Also that the guids are stored in the Registry and it stores the app's full path with each guid, so side-by-side installations must use different guids for the same icon in each installation. If the app moves to a new path, the guids(s) of the old path must be unregistered so the new path can then be associated with the guid(s). – Remy Lebeau Oct 04 '13 at 18:58
  • So under those conditions, it does sound like a later instance of the app process can take control of a previous app process's icons, since they are not tied to any given process when identified by a guid. But I may be wrong. I still use the HWND+ID combo in my apps, they haven't been updated to use guids yet. – Remy Lebeau Oct 04 '13 at 18:59
  • I just ran a test. On Win7 with one icon identified by guid, running multiple instances of that app, only one copy of the icon showed up in the system tray. So that does suggest one icon instance per guid, and multiple app instances can share that one icon instance. – Remy Lebeau Oct 04 '13 at 19:28
0

FWIW, since code doesn't exist so far, I thought I'd throw this in. I don't know if it will help or not for the OP, but it should be good guidance in the right direction.

unit csystray;
  { removes dead system tray icons, by Glenn1234 @ stackoverflow.com
    since this uses "less than supported by Microsoft" means, it may
    not work on all operating system.  It was tested on Windows XP }
interface
  uses commCtrl, shellapi, windows;
type
  TTrayInfo = packed record
    hWnd: HWnd;
    uID: UINT;
    uCallBackMessage: UINT;
    Reserved1: array[0..1] of longint;
    Reserved2: array[0..2] of longint;
    hIcon: HICON;
  end;
  PTBButton = ^TTBButton;
  _TBBUTTON = packed record
    iBitmap: Integer;
    idCommand: Integer;
    fsState: Byte;
    fsStyle: Byte;
    bReserved: array[1..2] of Byte;
    dwData: Longint;
    iString: Integer;
  end;
  TTBButton = _TBBUTTON;

procedure RemoveStaleTrayIcons;

implementation

procedure RemoveStaleTrayIcons;
const
  VMFLAGS = PROCESS_VM_OPERATION or PROCESS_VM_READ OR PROCESS_VM_WRITE;
var
  ProcessID: THandle;
  ProcessHandle: THandle;
  trayhandle: HWnd;
  ExplorerButtonInfo: Pointer;
  i: integer;
  ButtonCount: Longint;
  BytesRead: Longint;
  ButtonInfo: TTBButton;
  TrayInfo: TTrayInfo;
  ClassNameA: Array[0..255] of char;
  outlen: integer;
  TrayIconData: TNotifyIconData;
begin
  // walk down the window hierarchy to find the notification area window
  trayhandle := FindWindow('Shell_TrayWnd', '');
  trayhandle := FindWindowEx(trayhandle, 0, 'TrayNotifyWnd', nil);
  trayhandle := FindWindowEx(trayhandle, 0, 'SysPager', nil);
  trayhandle := FindWindowEx(trayhandle, 0, 'ToolbarWindow32', nil);
  if trayhandle = 0 then exit;
  // find the notification area process and open it up for reading.
  GetWindowThreadProcessId(trayhandle, @ProcessID);
  ProcessHandle := OpenProcess(VMFLAGS, false, ProcessID);
  ExplorerButtonInfo := VirtualAllocEx(ProcessHandle, nil, Sizeof(TTBButton),
       MEM_RESERVE or MEM_COMMIT, PAGE_READWRITE);
  // the notification area is a tool bar.  Get the number of buttons.
  ButtonCount := SendMessage(trayhandle, TB_BUTTONCOUNT, 0, 0);
  if ExplorerButtonInfo <> nil then
    try
      // iterate the buttons & check.
      for i := (ButtonCount - 1) downto 0 do
        begin
          // get button information.
          SendMessage(trayhandle, TB_GETBUTTON, i, LParam(ExplorerButtonInfo));
          ReadProcessMemory(ProcessHandle, ExplorerButtonInfo, @ButtonInfo,
             Sizeof(TTBButton), BytesRead);
          // if there's tray data, read and process
          if Buttoninfo.dwData <> 0 then
            begin
              ReadProcessMemory(ProcessHandle, PChar(ButtonInfo.dwData),
                                @TrayInfo, Sizeof(TTrayInfo), BytesRead);
              // here's the validation test, this fails if the master window is invalid
              outlen := GetClassName(TrayInfo.hWnd, ClassNameA, 256);
              if outlen < 1 then
                begin
                  // duplicate the shell icon removal, i.e. my component's DeleteTray
                  TrayIconData.cbSize := sizeof(TrayIconData);
                  TrayIconData.Wnd := TrayInfo.hWnd;
                  TrayiconData.uID := TrayInfo.uID;
                  TrayIconData.uCallbackMessage := TrayInfo.uCallBackMessage;
                  Shell_NotifyIcon(NIM_DELETE, @TrayIconData);
                end;
            end;
        end;
    finally
      VirtualFreeEx(ProcessID, ExplorerButtonInfo, Sizeof(TTBButton), MEM_RELEASE);
    end;
end;

end.
Glenn1234
  • 2,542
  • 1
  • 16
  • 21
  • 1
    I wouldn't say that digging into another process's memory to fetch undocumented information is ever really "guidance in the right direction." – Rob Kennedy Oct 05 '13 at 21:36