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 Programmer Interface - this is essentially a list of
all of the functions and methods that are available to us 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 cu
rrent 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):
Public Const IMAGE_BITMAP = 0
Public Const IMAGE_ICON = 1
Public Const IMAGE_CURSOR = 2
Public Const 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 is 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.
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 Tools->Options 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 old
handler 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
SetWindowLong A 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)
Copyright 2002 Roger Light