Using the System Tray in VB

After reading this tutorial you should be able to use all features of the System Tray in your VB applications with ease. Because of the nature of using the System Tray in VB we also cover some slightly more complicated topics such as sub classing.


Setting Up

This project requires the use of the component library Microsoft Windows Common Controls 5.0 or 6.0. To add one of these to your project go to Project->Components and select the library from the list. We will be creating a standard exe project with a form and a module file.

Adding an Icon

VB (as of version 6) has no built in support for using the system tray, so we will have to resort to using what is known as the Windows API. An API is an Application Programming Interface - this is essentially a list of all of the functions and methods that are available to us, in this case under Windows. To add a system tray icon, we need to use one of these functions: Shell_NotifyIconA(). To use this function in VB add a module file to your project called modSystemTray and add the code:

Public Declare Function Shell_NotifyIconA Lib "shell32.dll" (ByVal dwMessage As Long, lpData As NOTIFYICONDATA) As Long

Public Type NOTIFYICONDATA
cbSize As Long ' Size of the NotifyIconData structure
hWnd As Long ' Window handle of the window processing the icon events
uID As Long ' Icon ID (to allow multiple icons per application)
uFlags As Long ' NIF Flags
uCallbackMessage As Long ' The message received for the system tray icon if NIF_MESSAGE specified. Can be in the range 0x0400 through 0x7FFF (1024 to 32767)
hIcon As Long ' The memory location of our icon if NIF_ICON is specifed
szTip As String * 64 ' Tooltip if NIF_TIP is specified (64 characters max)
End Type

' Shell_NotifyIconA() messages
Public Const NIM_ADD = &H0 ' Add icon to the System Tray
Public Const NIM_MODIFY = &H1 ' Modify System Tray icon
Public Const NIM_DELETE = &H2 ' Delete icon from System Tray

' NotifyIconData Flags

Public Const NIF_MESSAGE = &H1 ' uCallbackMessage in NOTIFYICONDATA is valid
Public Const NIF_ICON = &H2 ' hIcon in NOTIFYICONDATA is valid
Public Const NIF_TIP = &H4 'szTip in NOTIFYICONDATA is valid

This probably requires some more explanation. Shell_NotifyIcon() is the function that we use to interact with the system tray. Its first argument is one of NIM_ADD,. NIM_MODIFY and NIM_DELETE, telling the function what we want to do to the icon in question. The second argument describes the icon that we are going to perform the operation on.

So, on to adding an icon. Add a command button called cmdAddIcon to your form (which I am assuming is called frmSystray) and the add this code to the form:

Private Sub AddTrayIcon()
Dim nid As NOTIFYICONDATA

' nid.cdSize is always Len(nid)
nid.cbSize = Len(nid)
' Parent window - this is the window that will process the icon events
nid.hWnd = frmSystray.hWnd
' Icon identifier
nid.uID = 0
' We want to receive messages, show the icon and have a tooltip
nid.uFlags = NIF_MESSAGE Or NIF_ICON Or NIF_TIP
' The message we will receive on an icon event
nid.uCallbackMessage = 1024
' The icon to display
nid.hIcon = frmSystray.Icon
' Our tooltip
nid.szTip = "Always terminate the tooltip with vbNullChar" & vbNullChar

' Add the icon to the System Tray
Shell_NotifyIconA NIM_ADD, nid

' Prevent further adding
cmdAddIcon.Enabled = False
End Sub

Private Sub cmdAddIcon_Click()
AddTrayIcon
End Sub

The flags in nid.uFlags are used to indicate which data is valid. This means that later on if we wanted to change just the tooltip then we would set nid.uFlags = NIF_TIP and Shell_NotifyIconA would know to only look at szTip, not hIcon or uCallbackMessage.

You can run this program as is, although it is not really good practise - we should really remove the icon from the tray as well when the program ends.

Removing an Icon

Because we only have one function for the system tray, removing an icon is very similar to adding an icon. Add a command button called cmdRemoveIcon to your form and then add the following code:

Private Sub RemoveTrayIcon()
Dim nid As NOTIFYICONDATA

nid.hWnd = Me.hWnd
nid.cbSize = Len(nid)
nid.uID = 0 ' The icon identifier we set earlier

' Delete the icon
Shell_NotifyIconA NIM_DELETE, nid

tmrFlash.Enabled = False
cmdRemoveIcon.Enabled = False
cmdAddIcon.Enabled = True
End Sub

Private Sub cmdRemoveIcon_Click()
RemoveTrayIcon
End Sub

Private Sub Form_Unload(Cancel As Integer)
RemoveTrayIcon
End Sub

You may also wish to add this line to the end of AddTrayIcon():

cmdRemoveIcon.Enabled = True

You may have noticed that the icon will be removed twice if we have removed the icon manually by clicking on cmdRemoveIcon and then go on to quit the program. This is not a problem as Shell_NotifyIconA will just return an error but do nothing more. On success, Shell_NotifyIconA returns a 1. On error it returns 0.

Note also the use of Me. Me refers to the current object - within a form it will refer to that form.

Loading a Second Icon

So far we have just been using a single icon. If we want to use more than one icon available in the system tray (either to have multiple icons, or to use dirrent images on a single icon) what can we do? I can think of three solutions. The first, as tends to happen, is easy but not very good - we could add further forms to our project and give them the necessary icons. This is bad because our program will quickly grow in size with each new form added. It is also extremely untidy and inelegant. The second solution is probably my preferred solution and involves the use of an ImageList control from the Microsoft Windows Common Controls library.

Add an ImageList called ilstIcons to your form and select it. In the properties list on the right hand side, you should be able to see an entry "(Custom)". Select this item and click the button that appears. This should bring up the Property Pages for the ImageList (property pages will be covered in a later tutorial). To add some icons to the ImageList, select size as 16 x 16 then go to the Images tab. Click "Insert Picture" to add icons to the list - make sure that *.ico is shown in the file filter box. It is now possible to access this icon with eg.

ilstIcons.ListImages(1).IconExtract

This will return a handle to the first icon held in the ImageList.

The third method delves into the Windows API once more to load the icons at run time. This method would be useful for allowing user defined tray icons. Add the following code to modSystray:

Public Declare Function LoadImage Lib "user32" Alias "LoadImageA"(ByVal hInst As Long, ByVal lpsz As String, ByVal dwImageType As Long, ByVal dwDesiredWidth As Long, ByVal dwDesiredHeight As Long, ByVal dwFlags As Long ) As Long
Public Declare Function DeleteObject Lib "gdi32" (ByVal hObject As Long) As Long

LoadImage() can be used to load a number of different types of images - most important for us, it can load icons. DeleteObject() is used to free all resources associated with an object - the icon image in our case.

We need to be able to specify what type of image to load (the argument dwImageType in LoadImage):

PublicConst IMAGE_BITMAP = 0
PublicConst IMAGE_ICON = 1
PublicConst IMAGE_CURSOR = 2
PublicConst IMAGE_ENHMETAFILE = 3

Lastly, we need to specify that the icon should be loaded from a file (the argument dwFlags in LoadImage):

Public Const LR_LOADFROMFILE = &H10 ' Not NT

Unfortunately, LR_LOADFROMFILE is not supported in Windows NT.

To use LoadImage we would enter the following:

Dim hIcon As Long
hIcon = LoadImage(0&, IconPath, IMAGE_ICON, 16, 16, LR_LOADFROMFILE)

Where hIcon is the memory address of the loaded icon, IconPath is the path of the icon we wish to load and 16 gives the dimensions of the icon.

When we have finished with the icon, we must delete it to free up the memory it uses:

DeleteObject hIcon
Modifying an Icon

Now that we have more than one icon available to our project it is time to look at modifying our dray icon. A not uncommon use of modifying the tray icon is to flash the icon to indicate to the user that there is something for their attention. We will look at how this can be done. I have created a completely transparent icon for the time when apparently no icon is to be displayed - it is part of the project available for download here.

You can use either of the methods described above for loading the second icon but I am going to use LoadImage as it is a little more complicated and hence better if it is explained in more depth. Both methods are shown in the download.

Add the next lines to the top of frmSystray, just under Option Explicit. This will make them available to all functions in the form. If you do not have an Option Explicit line, you should do! Go to the Tools->Options menu and in the Editor tab make sure that "Require Variable Declaration" is checked. This will add Option Explicit to further forms but you must still add it to any existing forms. Setting this option means that all variables must be declared before they are used which prevents mistyped variables, e.g. typing RecieveBuffer instead of ReceiveBuffer. If Option Explicit is not set, this would go unnoticed and be very hard to debug.

Private SolidIcon As Boolean
Private hTransIcon As Long

We will use SolidIcon to store whether the solid (opaque) icon is displayed or whether the transparent icon is displayed.

In the Form_Load event add the following line above the AddTrayIcon line:

hTransIcon = LoadImage(0&, App.Path & "\trans.ico", IMAGE_ICON, 16, 16, LR_LOADFROMFILE)

This requires that the file "trans.ico" is in the same path as our program.

Add

SolidIcon = True ' the icon is displayed - not transparent

to the end of AddTrayIcon.

Now add a timer control called tmrFlash to frmSystray. In the properties for tmrFlash, set Enabled to False and interval to 500 - this is the rate in milliseconds at which the timer will generate an event.

Add a command button called cmdFlashIcon to the form. In the cmdFlashIcon_Click event add the following code:

Private Sub cmdFlashIcon_Click()
If tmrFlash.Enabled = True Then
tmrFlash.Enabled = False
Else
tmrFlash.Enabled = True
End If
End Sub

Clicking on this button will then enable/disable the timer.

Now to add some code to the timer event.

Private Sub tmrFlash_Timer()
Dim nid As NOTIFYICONDATA

If SolidIcon = True Then
nid.hIcon = hTransIcon
SolidIcon = False
Else
nid.hIcon = frmSystray.Icon
SolidIcon = True
End If

nid.cbSize = Len(nid)
nid.hWnd = frmSystray.hWnd
nid.uID = 0
nid.uFlags = NIF_MESSAGE Or NIF_ICON
nid.uCallbackMessage = 1024

Shell_NotifyIconA NIM_MODIFY, nid
End Sub

This is the code that does the work for flashing the icon. Again it is very similar to both adding and deleting icons.

To ensure that the program exits properly if the timer is running, you should add the following line to the Form_Unload event:

tmrFlash.Enabled = False
Icon Events

All the work we have done so far is very pretty, but not particularly useful. We want to be able to receive information about how the user is interacting with our icon. The events generated by our icon are all sent the window we specified when it was added. At the moment we have no way of processing these events. The method by which we can see these events is known as subclassing. In real terms what this means is that we replace the default window event handler function with one of our own which processes the system tray events and then passes control back to the default function. This is not without risk - if you are running your program within VB and it comes across an error, the window events will no longer be processed because your program has stopped - this makes VB freeze up. Also, in the same scenario if you press the Stop button in VB rather than closing your program properly, it will cause VB to crash. In other words - be careful!

Add the following code to modSystray:

Public Declare Function SetWindowLongA Lib "user32" (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public Declare Function CallWindowProcA Lib "user32" (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal Msg As Long , ByVal wParam As Long , ByVal lParam As Long) As Long

' The events sent appear in lParam and are as follows:
Private Const MOUSE_MOVE = 512
Private Const MOUSE_LEFT_DOWN = 513
Private Const MOUSE_LEFT_UP = 514
Private Const MOUSE_LEFT_DBLCLICK = 515
Private Const MOUSE_RIGHT_DOWN = 516
Private Const MOUSE_RIGHT_UP = 517
Private Const MOUSE_RIGHT_DBLCLICK = 518
Private Const MOUSE_MIDDLE_DOWN = 519
Private Const MOUSE_MIDDLE_UP = 520
Private Const MOUSE_MIDDLE_DBLCLICK = 521

Public Const GWL_WNDPROC = -4

Public OldWindowProc As Long

Public Function WindowProc(ByVal hWnd As Long,
ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
' Our processing goes here

' Pass the event onto the default window handler so that all other events get handled correctly
WindowProc = CallWindowProcA(OldWindowProc, hWnd, Msg, wParam, lParam)
End Function

Add these lines to the end of AddTrayIcon:

' Set our WindowProc as the event handler for frmSystray.
' Save the address of the oldhandler in OldWindowProc
OldWindowProc = SetWindowLongA(Me.hWnd, GWL_WNDPROC, AddressOf WindowProc)

And these to the end of RemoveTrayIcon:

If OldWindowProc <>0 Then
' Set the window event handler to the previous
SetWindowLongA Me.hWnd, GWL_WNDPROC, OldWindowProc
OldWindowProc = 0
End If

Now, as the code runs the first part of the new code to be processed will be the new line in AddTrayIcon. SetWindowLongA can be used in a number of different ways, as defined by nIndex. In this case we are using it to change the event handler function. AddressOf is a very useful operator - it returns the address of a function. The function must be in a module though. SetWindowLongA returns the address of the default window handler - we need this later so save it in the global variable OldWindowProc.

When we come to remove the icon, we have no longer any need to process events ourselves and anyway we should reset the window handler before the program quits. This is what the code in RemoveTrayIcon does - sets the window handler back to the default.

So onto the WindowProc function. This function must have the exact arguments as shown because it is called by a Windows function that expects exactly that format. The line of code at the end of the function calls the default window handler and sets the return value of WindowProc to be the return value of CallWindowProcA. You do not need to modify this part in any way - it will be the same for any program that you write using subclassing.

When WindowProc is called, the argument Msg contains the message number of the event that has fired. The only one that we are intererested in, however, is the number that we specified when we added the icon in nid.uCallbackMessage. When this event does occur, the argument lParam will hold the type of event that has happened as in MOUSE_... above and wParam indicates the affected icon - this is the number of the icon that we set in nid.uID.

Select your form and then go to the menu Tools->Menu Editor. Add a menu called mnuSysTray and make it invisible - it does not matter what the caption is. Now add two more items called mnuSysTrayHide and mnuSysTrayShow with captions Hide and Show respectively. For each of these two items, click on the right arrow button so that three dots ... appear before their caption in the list - this means that they belong to mnuSysTray. Add the following code to the form:

Private Sub mnuSysTrayHide_Click()
Me.Hide
End Sub

Private Sub mnuSysTrayShow_Click()
Me.Show
End Sub

We are going to use this as a pop up menu. So, in WindowProc add the following:

If Msg = 1024 And wParam = 0 Then ' SysTray event for icon number 0
If lParam = MOUSE_RIGHT_UP Then
frmSystray.PopupMenu frmSystray.mnuSystemTray
End If
End If

Now try running the program and right clicking on the icon that appears.

The End

This concludes my introduction to using the System Tray in VB. The project I created for this tutorial including all tidied up code is available here: Tutorial Files (12KB)

Valid XHTML 1.0

Valid CSS!