0

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.

Ken White
  • 123,280
  • 14
  • 225
  • 444
GTAVLover
  • 1,407
  • 3
  • 22
  • 41

1 Answers1

1

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. 
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • I know it, but I declared them manually because `DUMMYUNIONNAME` is not declared in `ShellAPI` Version of Windows 7 (this PC) and it is the new structure of `NotifyIconData` as I know, can you show me how to clear `szTip` and other fields ? I also added all other parts of my code now. – GTAVLover Jul 29 '16 at 16:12
  • `DUMMYUNIONNAME` is very useful and I added it because it can determine System Tray Icon's Balloons' Display Durations........ :) – GTAVLover Jul 29 '16 at 16:37
  • You don't need to redeclare the entire `Shell_NotifyIcon()` API just to use the versioning feature. It is possible to use it in Delphi 2009 even though the `uVersion` field does not exist (it was added in Delphi 2010). I have updated my answer to show this. – Remy Lebeau Jul 29 '16 at 18:16
  • Thank you , this seems to work with my code. I will try this and notify you soon. – GTAVLover Jul 30 '16 at 00:08
  • I added the code properly, but why I'm getting the error `TMessage does not contain a member named 'Message'` ? I added `Messages` to the uses list when I started creating this Project, not Today.......Why that? – GTAVLover Jul 30 '16 at 01:14
  • @GTAVLover: it was a typo in my example, I have fixed it. – Remy Lebeau Jul 30 '16 at 01:27