Creating Simple Task Tray Apps for Windows 95/NT 4.0 with Delphi

Posted: (EET/GMT+2)

 

Windows 95 and NT 4.0 include an interesting feature named taskbar notification area, or task tray for short. This area, usually located in the right side of the task bar, can contain small icons. These icons could hide simple applications only starting a larger one, or they could be full blown applications with menus and dialog boxes. In this How To we create a small task tray application with Delphi 2.0 which displays a menu of almost all of the shortcuts on the Windows desktop.

Before proceeding, please see note about recuired interface units.

Technically, a task tray application is much like an ordinary application. It has a message loop, and it handles it's functions by responding to certain messages sent by Windows itself. For example, our "main" procedure is like this:

    Procedure RunTrayApplication;
    Var Msg : TMsg;
    Begin
      CreateWindow;
      AddTrayIcon;
      While GetMessage(Msg,0,0,0) do Begin
        TranslateMessage(Msg);
        DispatchMessage(Msg);
      End;
      DeleteTrayIcon;
    End;
    


As you can see, all we need to do is create a window, register an icon on the task tray, sit in a message loop, and terminate gracefully. Of course, additional coding is required, but it really isn't something to worry about.

Let's start with the window creation. Actually, this window is not the window which can be seen on the task tray when the application is ready. Instead, this window is only a message processor, or something like a "parent". The Windows shell handles the creation of messages (for example, a mouse click) and the send the message to our window.

    Procedure CreateWindow;
    Var
      WC : TWndClass;
      W  : hWnd;
    
    Begin
      With WC do Begin
        Style := 0;
        lpfnWndProc := @WndProc;
        cbClsExtra := 0;
        cbWndExtra := 0;
        hIcon := 0;
        hCursor := 0;
        hbrBackground := 0;
        lpszMenuName := nil;
        lpszClassName := 'MyTrayIconClass';
        hInstance := System.hInstance;
      end;
      RegisterClass(WC);
      W := Windows.CreateWindow('MyTrayIconClass','MyVeryOwnTrayIconWindow',
                                ws_OverlappedWindow,0,0,0,0,0,0,hInstance,nil);
      ShowWindow(W,sw_Hide);
      UpdateWindow(W);
      MainWindow := W;
    End;
    


The window is created using normal Windows functions. Notice that the window style is "ws_OverlappedWindow", but it's size is zero and that it is hidden so that it won't show up. Creating an overlapped window with no parent assures that our app will be shown on the Close Proram list when Ctrl+Alt+Del is pressed (in Windows 95).

Next step is to add (or register) our icon with the shell. This is done with the Shell_NotifyIcon API function. This message is actually a three-purpose function. This time we only need to use the add feature:

    Procedure AddTrayIcon;
    Var IconData : TNotifyIconData;
    Begin
      With IconData do Begin
        cbSize := SizeOf(IconData);
        Wnd := MainWindow;
        uID := 0;
        uFlags := nif_Icon Or nif_Message Or nif_Tip;
        uCallBackMessage := wm_MyCallBack;
        hIcon := LoadIcon(hInstance,'MYICON');
        StrCopy(szTip,PChar(TrayIconTip));
      End;
      Shell_NotifyIcon(nim_Add,@IconData);
    End;
    


The most important thing is the TNotifyIconData data structure. It is a parameter passing record, and for us, we need to set the window handle (which defines the window handling the messages), the callback message number (we define this - I'll return to this later), an icon, and of course, a tool tip for our icon. After the data has been set, we are ready to add the icon to the task bar. To achieve this, nim_Add code must be used.

Now that we've added our icon, we need to decide how we are going to handle the messages. Deciding the callback message number (see above) is simple:

    Const
      wm_MyCallback = wm_User+1000;
    
      cm_Exit       = 100; { we worry about... }
      cm_About      = 101; { ...these later    }
    


The actual window procedure is quite normal. Several Windows messages (like wm_NCCreate) must be processed. However, for us, the far most important messages are the wm_MyCallback and wm_Command:

    Function WndProc(Window : hWnd; Msg,WParam,LParam : Integer)
                    : Integer; StdCall;
    Begin
      Result := 0;
      Case Msg of
        wm_NCCreate   : Result := 1;
        wm_Destroy    : PostQuitMessage(0);
        wm_Command    : Begin { a command was chosen from the popup menu }
                          If (WParam = cm_Exit) Then
                            PostMessage(Window,wm_Destroy,0,0)
                          Else If (WParam = cm_About) Then
                            MessageBox(0,'Shell Test Copyright © '+
                                       'Jani Järvinen 1996.',
                                       'About Shell Test',mb_OK)
                          Else OpenDesktopIcon(WParam-cm_About);
                        End;
        wm_MyCallback : Begin { our icon was clicked }
                          If (LParam = wm_LButtonDown) Then
                            ShowIconPopupMenu
                          Else If (LParam = wm_RButtonDown) Then
                            ShowAboutPopupMenu;
                        End;
        Else Result := DefWindowProc(Window,Msg,WParam,LParam);
      End;
    End;
    


As you can see, the shell notifies us when the user clicks our icon. Notice that we won't get a normal wm_LButtonDown message. Instead, we get our own wm_MyCallback message, and the actual message is in the LParam parameter.

When the user clicks the left mouse button on our icon, we create menu of the shortcut icons on the desktop:

    Type
      TIconData = Array[1..100] of String;
    
    Var
      IconData   : TIconData;
    
    Procedure ShowIconPopupMenu;
    Var
      ShellFolder : IShellFolder;
      EnumIDList  : IEnumIDList;
      Result      : hResult;
      Dummy       : ULong;
      ItemIDList  : TItemIDList;
      Pntr        : PItemIDList;
      StrRet      : TStrRet;
      PopupMenu   : hMenu;
      ItemID      : Integer;
      Pos         : TPoint;
    
      Procedure AddToMenu(Item : String);
      Var S : String;
      Begin
        IconData[ItemID-cm_About] := Item;
        S := ExtractFileName(Item);
        If (System.Pos('.',S) <> 0) Then SetLength(S,System.Pos('.',S)-1);
        AppendMenu(PopupMenu,mf_Enabled Or mf_String,ItemID,PChar(S));
        Inc(ItemID);
      End;
    
    begin
      PopupMenu := CreatePopupMenu;
      ItemID := cm_About+1;
      SHGetDesktopFolder(ShellFolder);
      ShellFolder.EnumObjects(MainWindow,SHCONTF_NONFOLDERS,EnumIDList);
      Pntr := @ItemIDList;
      Result := EnumIDList.Next(1,Pntr,Dummy);
      While (Result = NoError) do Begin
        ShellFolder.GetDisplayNameOf(Pntr,SHGDN_FORPARSING,@StrRet);
        With StrRet do AddToMenu(String(CStr));
        Result := EnumIDList.Next(1,Pntr,Dummy);
      End;
      EnumIDList.Release;
      ShellFolder.Release;
    
      GetCursorPos(Pos);
      AppendMenu(PopupMenu,mf_Separator,0,'');
      AppendMenu(PopupMenu,mf_Enabled Or mf_String,cm_Exit,'E&xit');
      SetForegroundWindow(MainWindow);
      TrackPopupMenu(PopupMenu,tpm_LeftAlign Or tpm_LeftButton,
                     Pos.X,Pos.Y,0,MainWindow,nil);
      DestroyMenu(PopupMenu);
    end;
    


This somewhat complicated procedure can be divided into two different parts: enumerating the items on the desktop and displaying the actual menu.

Enumerating the items is done using shell interfaces*. First, we get an instance of the IShellFolder interface for the desktop using the SHGetDesktopForlder API call. Using this interface, we can get an instance of still another interface, IEnumIDList. This interface is used to do the actual item enumeration. We simply call a function repeatedly until an error value is returned (ie. all items have been enumerated). When we get an item, we add it to our menu using the custom AddToMenu function. (Note that this enumeration is not complere. It skips for example the "My Computer" and "Recycle Bin" icons. This is by design.)

After the items have been enumerated and the menu is created, we are ready to run the menu. Note that we save the found desktop items to a global list. Also, every menu item has its own item ID. This ensures that we can get an index to our data array using simple substraction. Thus the call

OpenDesktopIcon(WParam-cm_About)
above. Of course, WParam holds the item ID of the menu item user clicked.

Now the only interesting procedure we haven't covered yet is how to actually run the selected desktop icon. Looking at the previous code, it might seem that running an "icon" would be difficult. Fortunately, this is very simple:

    Procedure OpenDesktopIcon(Number : Integer);
    Var
      S : String;
      I : Integer;
    
    begin
      S := IconData[Number];
      I := ShellExecute(0,nil,PChar(S),nil,nil,sw_ShowNormal);
      If (I < 32) Then Begin
        S := 'Could not open selected item "'+S+'". '+
             'Result was: '+IntToStr(I)+'.';
        MessageBox(0,PChar(S),'Shell Test',mb_OK);
      End;
    end;
    


Above, the Win32 API function ShellExecute does all the dirty work like resolving the possible shortcuts. The important thing to note in the call is the second parameter. It is the operation we wish to do for the file. The parameter could be "open" or "print" or - as we have it - nil. It would sound reasonable if the "open" parameter worked, but it doesn't. If anybody knows why it must be nil, please mail me. Reasons aside, simply pass in nil, and everything is OK.

Having got this far, you should now have an idea how to create a simple task tray application using Delphi - without creating a 200k EXE file. Note that the actual application implements some more features, like an about box, but that's nothing spectacular.

You can donwload the whole application below. Please see note below. Also, be sure to read the acomppanying read-me file in the archive.


Note: The Delphi interface files don't include all the functions and objects needed in this How To. However, a file named "shellobj.zip" is released as public domain, and it is used by this application. This file is included in the archive below. You can also download it from the original site.



Code for this How To

  • Environment: Windows 95 or NT 4.0
  • Compiler: Delphi 2.0 for Win32
  • Memory: 4 MB RAM, 200k disk
  • Filename: tasktray.zip
  • Size: 32 568 bytes, 30kB
  • Date (M/D/Y): 12/26/96 01:00AM
  • Download time: 14.4k: about 30 seconds, 28.8k: about 15 seconds
  • Distribution: Freeware
  • Registration price: none
  • Special: Includes "shellobj.zip".
  • Download!


Questions? Comments? Give feedback. Or just fill in a form.