delphidelphi-2009system-trayballoon-tip

Display Multiple Balloons conditionally using Windows API - System Tray Icon in Delphi


I wrote a program that can identify outdated software in a Windows System and update them by interacting with the user.

It has a Software Updater Program which displays a System Tray Icon and show Balloon Tips about Available / Downloading Updates and Software installed in the System.

The problem is It can't show multiple Balloon Tips when each task is processing by it. Such as, when an update is available for a Software, it should remember user showing a balloon like An update for Software Name is available. and when user choose to download and minimize it to system tray again, the balloon tip should again show something like Updates are downloading...Click to view the Progress of Downloads.

However I like to know how can I do this by using only one System Tray Icon?

Can I use the NIM_MODIFY Flag again and again to change the Balloon Tip according to the current state of the Program?

I searched about this and I found some examples, but for Visual Studio and C++.

That's how I tried to show Multiple Tips when the Program is running:

unit MainForm-1;

...

const
  NIF_INFO = $10;
  NIF_MESSAGE = 1;
  NIF_ICON = 2;
  NOTIFYICON_VERSION = 3;
  NIF_TIP = 4;
  NIM_SETVERSION = $00000004;
  NIM_SETFOCUS = $00000003;
  NIIF_INFO = $00000001;
  NIIF_WARNING = $00000002;
  NIIF_ERROR = $00000003;

  NIN_BALLOONSHOW = WM_USER + 2;
  NIN_BALLOONHIDE = WM_USER + 3;
  NIN_BALLOONTIMEOUT = WM_USER + 4;
  NIN_BALLOONUSERCLICK = WM_USER + 5;
  NIN_SELECT = WM_USER + 0;
  NINF_KEY = $1;
  NIN_KEYSELECT = NIN_SELECT or NINF_KEY;

  TRAY_CALLBACK = WM_USER + $7258;

  PNewNotifyIconData = ^TNewNotifyIconData;
  TDUMMYUNIONNAME    = record
  case Integer of
       0: (uTimeout: UINT);
       1: (uVersion: UINT);
  end;

  TNewNotifyIconData = record
  cbSize: DWORD;
  Wnd: HWND;
  uID: UINT;
  uFlags: UINT;
  uCallbackMessage: UINT;
  hIcon: HICON;
  szTip: array [0..127] of Char;
  dwState: DWORD; /
  dwStateMask: DWORD; 
  szInfo: array [0..255] of Char; 
  DUMMYUNIONNAME: TDUMMYUNIONNAME;
  szInfoTitle: array [0..63] of Char; 
  dwInfoFlags: DWORD;   
end;

type
  MainForm-1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
private
    IconData: TNewNotifyIconData;
    procedure SysTrayIconMessageHandler(var Msg: TMessage); message TRAY_CALLBACK;
    procedure AddSysTrayIcon;
    procedure ShowBalloonTips;
    procedure DeleteSysTrayIcon;
public
end;

var
  MainForm-1: TForm;

implementation

uses
ShellAPI...,.....,;

procedure MainForm-1.SysTrayIconMessageHandler(var Msg: TMessage);
begin
  case Msg.lParam of
  WM_MOUSEMOVE:;
  WM_LBUTTONDOWN:;
  WM_LBUTTONUP:;
  WM_LBUTTONDBLCLK:;
  WM_RBUTTONDOWN:;
  WM_RBUTTONUP:;
  WM_RBUTTONDBLCLK:;
  NIN_BALLOONSHOW:;
  NIN_BALLOONHIDE:;
  NIN_BALLOONTIMEOUT:
  NIN_BALLOONUSERCLICK:;
 end;
end;

procedure MainForm-1.AddSysTrayIcon;
begin
  IconData.cbSize := SizeOf(IconData);
  IconData.Wnd := AllocateHWnd(SysTrayIconMessageHandler);
  IconData.uID := 0;
  IconData.uFlags := NIF_ICON or NIF_MESSAGE or NIF_TIP;
  IconData.uCallbackMessage := TRAY_CALLBACK;
  IconData.hIcon := Application.Icon.Handle;
  IconData.szTip := 'Software Updater is running';
  if not Shell_NotifyIcon(NIM_ADD, @IconData) then
  ShowMessage('System Tray Icon cannot be created.');
end;

procedure MainForm-1.DisplayBalloonTips;
var
  TipInfo, TipTitle: string;
begin
  IconData.cbSize := SizeOf(IconData);
  IconData.uFlags := NIF_INFO;
  if ssHelperState = UpdatesAvailable then TipInfo := 'Updates are available to the programs installed on your Computer' + ' Click to see details.';
  if ssHelperState = UpdatesDownloading then TipInfo := 'Updates are downloading in the background. Click to view the details.';
  strPLCopy(IconData.szInfo, TipInfo, SizeOf(IconData.szInfo) - 1);
  IconData.DUMMYUNIONNAME.uTimeout := 2500;
  if ssHelperState = UpdatesAvailable then TipTitle := 'Updates are Available...';
  if ssHelperState = UpdatesDownloading then TipTitle := 'Downloading the Updates...';
  strPLCopy(IconData.szInfoTitle, TipTitle, SizeOf(IconData.szInfoTitle) - 1);
  IconData.dwInfoFlags := NIIF_INFO; 
  Shell_NotifyIcon(NIM_MODIFY, @IconData);
  {Following code is for testing purpose.}
  IconData.DUMMYUNIONNAME.uVersion := NOTIFYICON_VERSION;
  if not Shell_NotifyIcon(NIM_SETVERSION, @IconData) then
  ShowMessage('Setting the Version is Failed.');
end;

procedure MainForm-1.DeleteSysTrayIcon;
begin
  DeallocateHWnd(IconData.Wnd);
  if not Shell_NotifyIcon(NIM_DELETE, @IconData) then
  ShowMessage('Unable to delete System Tray Icon.');
end;

procedure MainForm-1.FormCreate(Sender: TObject);
begin
  AddSysTrayIcon;
  ShowBalloonTips;
end;

procedure MainForm-1.FormDestroy(Sender: TObject);
begin
  DeleteSysTrayIcon;
end;
...
end. 

But, this is failing and I keep getting the same Balloon Tip (First One) again and again when the Program is running.......

I don't know how to use NIN_BALLOONSHOW and NIN_BALLOONHIDE Flags correctly. So, Thanks in Advance for Your Important Help.


Solution

  • Why are you declaring everything manually? Delphi 2009 already has declarations for the Shell_NotifyIcon() API. They are in the ShellAPI unit. It declares just about everything you are trying to use, except for the uVersion field (that was added in Delphi 2010). You are not using the guidItem and hBalloonIcon fields, so let's not worry about them here. The uTimeout field exists, and since it is wrapped in a union with uVersion, the data size does not change, so you can just use uTimeout when you want to use uVersion (or you can define your own union and type-cast the field, but that is overkill). You certainly do not need to redeclare the entire API.

    You are reusing the same IconData variable each time you call Shell_NotifyIcon(), which is fine, but you are not clearing the szTip and szInfoTitle fields if your helper state is not UpdatesAvailable or UpdatesDownloading, so the tray icon keeps displaying the last tip/balloon you have set. You need to clear those fields when you don't need tips/balloons anymore.

    NIN_BALLOONSHOW and NIN_BALLOONHIDE are not flags. They are notifications that are sent to your tray icon's registered HWND. To receive the notifications, you need to fill in the Wnd and uCallbackMessage fields and enable the NIF_MESSAGE flag.

    Also, you need to handle the WM_TASKBARCREATED message. If Explorer gets restarted for any reason (crashes, or is killed by the user), the Taskbar gets re-created, so you have to re-add your tray icon again.

    Also, make sure your message handler passes any unhandled window messages to DefWindowProc(), or you can lock up the system, or at least your app.

    And lastly, Delphi 2009 is a Unicode version of Delphi, but there are some sections of your code that are not handling Unicode correctly. Specifically, when populating szTip and szInfoTitle using StrPLCopy(), you need to use Length() instead of SizeOf(). The copy is expressed in number of characters, not number of bytes.

    With that said, try something more like this:

    unit MainForm1;
    
    interface
    
    uses
      ..., ShellAPI;
    
    type
      eHelperState = (Idle, UpdatesAvailable, UpdatesDownloading);
    
      MainForm = class(TForm)
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
      private
        TaskbarCreatedMsg: UINT;
        IconData: NOTIFYICONDATA;
        IconAdded: Boolean;
        ssHelperState: eHelperState;
        procedure SysTrayIconMessageHandler(var Message: TMessage);
        procedure AddSysTrayIcon;
        procedure ShowBalloonTips;
        procedure DeleteSysTrayIcon;
        procedures SetHelperState(NewState: eHelperState);
        ...
    end;
    
    var
      MainForm: TForm;
    
    implementation
    
    const
      TRAY_CALLBACK = WM_USER + $7258;
      {$IF RTLVersion < 21}
      NOTIFYICON_VERSION_4 = 4;
      {$IFEND}
    
    procedure MainForm.FormCreate(Sender: TObject);
    begin
      TaskbarCreatedMsg := RegisterWindowMessage('TaskbarCreated');
      IconData.cbSize := SizeOf(IconData);
      IconData.Wnd := AllocateHWnd(SysTrayIconMessageHandler);
      IconData.uID := 1;
      AddSysTrayIcon;
    end;
    
    procedure MainForm.FormDestroy(Sender: TObject);
    begin
      DeleteSysTrayIcon;
      DeallocateHWnd(IconData.Wnd);
    end;
    
    procedure MainForm.AddSysTrayIcon;
    begin
      IconData.uFlags := NIF_ICON or NIF_MESSAGE or NIF_TIP;
      IconData.uCallbackMessage := TRAY_CALLBACK;
      IconData.hIcon := Application.Icon.Handle;
      StrLCopy(IconData.szTip, 'Software Updater is running', Length(IconData.szTip));
    
      IconAdded := Shell_NotifyIcon(NIM_ADD, @IconData);
      if not IconAdded then
      begin
        ShowMessage('Unable to add System Tray Icon.');
        Exit;
      end;
    
      if CheckWin32Version(5, 0) then
      begin
        IconData.{$IF RTLVersion >= 21}uVersion{$ELSE}uTimeout{$IFEND} := NOTIFYICON_VERSION_4;
        if not Shell_NotifyIcon(NIM_SETVERSION, @IconData) then
          ShowMessage('Unable to set version for System Tray Icon.');
      end;
    end;
    
    procedure MainForm.DisplayBalloonTips;
    var
      Tip, InfoText, InfoTitle: string;
    begin
      if not IconAdded then Exit;
    
      case ssHelperState of
        UpdatesAvailable: begin
          Tip := 'Updates are Available. Click to see details.';
          InfoText := 'Updates are available to the programs installed on your Computer. Click to see details.';
          InfoTitle := 'Updates are Available';
        end;
        UpdatesDownloading: begin
          Tip := 'Downloading Updates. Click to see details.';
          InfoText := 'Updates are downloading in the background. Click to see details.';
          InfoTitle := 'Downloading Updates';
        end;
      else
        Tip := 'Software Updater is running';
      end;
    
      IconData.uFlags := NIF_TIP or NIF_INFO;
      StrPLCopy(IconData.szTip, Tip, Length(IconData.szTip));
      StrPLCopy(IconData.szInfo, InfoText, Length(IconData.szInfo));
      StrPLCopy(IconData.szInfoTitle, InfoTitle, Length(IconData.szInfoTitle));
      IconData.uTimeout := 2500;
      IconData.dwInfoFlags := NIIF_INFO; 
    
      if not Shell_NotifyIcon(NIM_MODIFY, @IconData) then
        ShowMessage('Unable to update System Tray Icon.')
    end;
    
    procedure MainForm.DeleteSysTrayIcon;
    begin
      if IconAdded then
      begin
        IconAdded := False;
        if not Shell_NotifyIcon(NIM_DELETE, @IconData) then
          ShowMessage('Unable to delete System Tray Icon.');
      end;
    end;
    
    procedures MainForm.SetHelperState(NewState: eHelperState);
    begin
      if ssHelperState <> NewState then
      begin
        ssHelperState := NewState;
        DisplayBalloonTips;
      end;
    end;
    
    procedure MainForm.SysTrayIconMessageHandler(var Message: TMessage);
    begin
      if Message.Msg = TRAY_CALLBACK then
      begin
        case LOWORD(Message.LParam) of
          WM_MOUSEMOVE: begin
            //...
          end;
    
          WM_LBUTTONDBLCLK,
          NIN_BALLOONUSERCLICK: begin
            // display status window...
          end;
    
          WM_CONTEXTMENU,
          NIN_KEYSELECT,
          NIN_SELECT: begin
            // display popup menu at coordinates specified by Msg.WParam...
         end;
    
          NIN_BALLOONSHOW:;
          NIN_BALLOONHIDE:;
          NIN_BALLOONTIMEOUT:;
        end;
      end
      else if (Message.Msg = TaskbarCreatedMsg) and (TaskbarCreatedMsg <> 0) then
      begin
        IconAdded := False;
        AddSysTrayIcon;
        DisplayBalloonTips;
      end
      else begin
        Message.Result := DefWindowProc(IconData.Wnd, Message.Msg, Message.WParam, Message.LParam);
      end;
    end;
    
    ...
    
    end.