Visual Basic Subclassing Routines
Shell_NotifyIcon: Add Icon to Windows System Tray
     
Posted:   Monday April 13, 1998
Updated:   Monday December 26, 2011
     
Applies to:   VB5, VB6
Developed with:   VB6, Windows 98
OS restrictions:   None
Author:   VBnet - Randy Birch
     

Related:  

Shell_NotifyIcon: Windows Systray NOTIFYICONDATA Overview

Shell_NotifyIcon: Add Icon to Windows System Tray
Shell_NotifyIcon: Respond to Systray Icon/Menu Interaction
Shell_NotifyIcon: Respond to Systray Icon/Menu Interaction in a MDI App

Shell_NotifyIcon: Animate the System Tray Icon

Shell_NotifyIcon: Display Systray Balloon Tips
Shell_NotifyIcon: Respond to Systray Balloon Tip Clicks
Shell_NotifyIcon: Use SetTimer to Define Balloon Tip Life

     
 Prerequisites
VB5 or VB6 to support AddressOf. 

This demo contains new code added January 2003 to properly determine the Shell32.dll version and use the appropriately-sized NOTIFYICONDATA structure. Although this will handle the display of the systray icon across Windows versions, application designers targeting Windows 2000 and XP should nonetheless take appropriate steps to ensure their app degrades gracefully to utilize only the functionality provided in earlier system's shell versions. For information concerning using the systray across all Windows versions it is strongly recommended you refer to Shell_NotifyIcon: Windows Systray NOTIFYICONDATA Overview.


7900 bytesEver since Windows 95 hit the street, one of the most popular How-Do-I questions concerns placing an icon into the systray area of the screen. Until VB5 offered the AddressOf operator, responding to mouse actions on the icon was a haphazard affair. Those methods still widely posted to newsgroups and found in archaic zip files use a 'magic number' representing screen coordinates that really only works for some screen resolutions. Other code around relies on third-party message interceptors such as Subclass.ocx (an excellent control), MsgBlaster and Desaware's Spy.

This, and the accompanying advanced page, discusses adding, manipulating, deleting and responding to events generated by an icon placed into the system tray by your application.  This part - the basics - shows the minimal code to achieve this functionality.  Advanced lessons provide mini-apps that actually do something. 

Because this app involves subclassing I can guarantee that at some point in testing the code you will GPF. So save your project often, and _always_ use the  Start with Full Compile command to run the app (I replaced my regular VB Run button with this command).

Due to the subclassing necessary to implement this mechanism, coding errors are unforgiving and un-editable once the app has started. Should you find you're locked up, double click on the VB toolbar. This will bring up the customization dialog, which you then cancel. Now, at least, you'll be able to hit the VB stop button, instead of requiring the three-finger salute.

A subclassed application can not be closed via a popup menu shown by the sys tray without generating a GPF. Therefore the code shown here uses another technique to allow application termination - the menu event posts to the form a WM_CLOSE message. This message is processed after the menu event has completed, and results in a UnloadMode message similar to pressing the X button or selecting the system menu's Close option. The Unload event fires, and subclassing is safely terminated. Be sure to follow the instructions below in creating the menu as the illustration above is missing the last required menu item.

 BAS Module 1 Code - systray.bas
To provide for reusability with the advanced pages, there are two BAS modules that need to be created. Place the following code into the general declarations area of bas module 1 (systray.bas):

Option Explicit
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Copyright ©1996-2011 VBnet/Randy Birch, All Rights Reserved.
' Some pages may also contain other copyrights by the author.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Distribution: You can freely use this code in your own
'               applications, but you may not reproduce 
'               or publish this code on any web site,
'               online service, or distribute as source 
'               on any media without express permission.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'Required Public constants, types & declares
'for the Shell_Notify API method
Public Const NIM_ADD As Long = &H0
Public Const NIM_MODIFY As Long = &H1
Public Const NIM_DELETE As Long = &H2
Public Const NIF_ICON As Long = &H2     'adding an ICON
Public Const NIF_TIP As Long = &H4      'adding a TIP
Public Const NIF_MESSAGE As Long = &H1  'want return messages

'rodent constant we'll need for the callback
Public Const WM_LBUTTONDOWN As Long = &H201
Public Const WM_LBUTTONUP As Long = &H202
Public Const WM_LBUTTONDBLCLK As Long = &H203

Public Const WM_MBUTTONDOWN As Long = &H207
Public Const WM_MBUTTONUP As Long = &H208
Public Const WM_MBUTTONDBLCLK As Long = &H209

Public Const WM_RBUTTONDOWN As Long = &H204
Public Const WM_RBUTTONUP As Long = &H205
Public Const WM_RBUTTONDBLCLK As Long = &H206

'shell version / NOTIFYICONDATA struct size constants
Public Const NOTIFYICONDATA_V1_SIZE As Long = 88  'pre-5.0 structure size
Public Const NOTIFYICONDATA_V2_SIZE As Long = 488 'pre-6.0 structure size
Public Const NOTIFYICONDATA_V3_SIZE As Long = 504 '6.0+ structure size
Public NOTIFYICONDATA_SIZE As Long

Public Type GUID
   Data1 As Long
   Data2 As Integer
   Data3 As Integer
   Data4(7) As Byte
End Type

Public Type NOTIFYICONDATA
   cbSize As Long
   hWnd As Long
   uID As Long
   uFlags As Long
   uCallbackMessage As Long
   hIcon As Long
   szTip As String * 128
   dwState As Long
   dwStateMask As Long
   szInfo As String * 256
   uTimeoutAndVersion As Long
   szInfoTitle As String * 64
   dwInfoFlags As Long
   guidItem As GUID
End Type

Public NID As NOTIFYICONDATA

Public Declare Function Shell_NotifyIcon Lib "shell32" _
   Alias "Shell_NotifyIconA" _
   (ByVal dwMessage As Long, _
   lpData As NOTIFYICONDATA) As Long
   
Private Declare Function GetFileVersionInfoSize Lib "version.dll" _
   Alias "GetFileVersionInfoSizeA" _
  (ByVal lptstrFilename As String, _
   lpdwHandle As Long) As Long

Private Declare Function GetFileVersionInfo Lib "version.dll" _
   Alias "GetFileVersionInfoA" _
  (ByVal lptstrFilename As String, _
   ByVal dwHandle As Long, _
   ByVal dwLen As Long, _
   lpData As Any) As Long
   
Private Declare Function VerQueryValue Lib "version.dll" _
   Alias "VerQueryValueA" _
  (pBlock As Any, _
   ByVal lpSubBlock As String, _
   lpBuffer As Any, _
   nVerSize As Long) As Long

Private Declare Sub CopyMemory Lib "kernel32" _
   Alias "RtlMoveMemory" _
  (Destination As Any, _
   Source As Any, _
   ByVal Length As Long)


Public Function IsShellVersion(ByVal version As Long) As Boolean

  'returns True if the Shell version
  '(shell32.dll) is equal or later than
  'the value passed as 'version'
   Dim nBufferSize As Long
   Dim nUnused As Long
   Dim lpBuffer As Long
   Dim nVerMajor As Integer
   Dim bBuffer() As Byte
   
   Const sDLLFile As String = "shell32.dll"
   
   nBufferSize = GetFileVersionInfoSize(sDLLFile, nUnused)
   
   If nBufferSize > 0 Then
    
      ReDim bBuffer(nBufferSize - 1) As Byte
    
      Call GetFileVersionInfo(sDLLFile, 0&, nBufferSize, bBuffer(0))
    
      If VerQueryValue(bBuffer(0), "\", lpBuffer, nUnused) = 1 Then
         
         CopyMemory nVerMajor, ByVal lpBuffer + 10, 2
        
         IsShellVersion = nVerMajor >= version
      
      End If  'VerQueryValue
   End If  'nBufferSize
  
End Function


Public Sub SetShellVersion()

   Select Case True
      Case IsShellVersion(6)
         NOTIFYICONDATA_SIZE = NOTIFYICONDATA_V3_SIZE '6.0+ structure size
      
      Case IsShellVersion(5)
         NOTIFYICONDATA_SIZE = NOTIFYICONDATA_V2_SIZE 'pre-6.0 structure size
      
      Case Else
         NOTIFYICONDATA_SIZE = NOTIFYICONDATA_V1_SIZE 'pre-5.0 structure size
   End Select

End Sub
 BAS Module 2 Code - winproc.bas
Place the following code into the general declarations area of bas module 2 (winproc.bas):

Option Explicit
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Copyright ©1996-2011 VBnet/Randy Birch, All Rights Reserved.
' Some pages may also contain other copyrights by the author.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Distribution: You can freely use this code in your own
'               applications, but you may not reproduce 
'               or publish this code on any web site,
'               online service, or distribute as source 
'               on any media without express permission.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'defWindowProc: Variable to hold the ID of the
'               default window message processing
'               procedure. Returned by SetWindowLong.
Public defWindowProc As Long

'isSubclassed: flag indicating that subclassing
'              has been done. Provides the means
'              to call the correct message-handler.
Public isSubclassed As Boolean

'Get/SetWindowLong messages
Public Const GWL_WNDPROC As Long = (-4)
Public Const GWL_HWNDPARENT As Long = (-8)
Public Const GWL_ID As Long = (-12)
Public Const GWL_STYLE As Long = (-16)
Public Const GWL_EXSTYLE As Long = (-20)
Public Const GWL_USERDATA As Long = (-21)

'general windows messages
Public Const WM_USER As Long = &H400
Private Const WM_APP As Long = &H8000&
Public Const WM_MYHOOK As Long = WM_APP + &H15
Public Const WM_NOTIFY As Long = &H4E
Public Const WM_COMMAND As Long = &H111
Public Const WM_CLOSE As Long = &H10

Public Declare Function SetForegroundWindow Lib "user32" _
   (ByVal hwnd As Long) As Long
   
Public Declare Function PostMessage Lib "user32" _
   Alias "PostMessageA" _
   (ByVal hwnd As Long, _
    ByVal wMsg As Long, _
    ByVal wParam As Long, _
    lParam As Any) As Long
    
Public Declare Function SetWindowLong Lib "user32" _
   Alias "SetWindowLongA" _
   (ByVal hwnd As Long, _
    ByVal nIndex As Long, _
    ByVal dwNewLong As Any) As Long

Public Declare Function CallWindowProc Lib "user32" _
   Alias "CallWindowProcA" _
   (ByVal lpPrevWndFunc As Long, _
    ByVal hwnd As Long, _
    ByVal uMsg As Long, _
    ByVal wParam As Long, _
    ByVal lParam As Long) As Long
                            
                            
'our own window message procedure
Public Function WindowProc(ByVal hwnd As Long, _
                           ByVal uMsg As Long, _
                           ByVal wParam As Long, _
                           ByVal lParam As Long) As Long

  'window message procedure
  '
  'If the handle returned is to our form,
  'call a form-specific message handler to
  'deal with the tray notifications.  If it
  'is a general system message, pass it on to
  'the default window procedure.
  '
  'If it is ours, we look at lParam for the
  'message generated, and react appropriately.
   On Error Resume Next
  
   Select Case hwnd
   
     'form-specific handler
      Case Form1.hwnd
         
         Select Case uMsg
           'WM_MYHOOK was defined as
           'the .uCallbackMessage
           'message of NOTIFYICONDATA
            Case WM_MYHOOK

              'maintain focus on the app
              'window to assure the menu
              'disappears should the mouse
              'be clicked outside the menu
               Call SetForegroundWindow(Form1.hWnd)

              'lParam is the value of the message
              'that generated the tray notification.
               Select Case lParam
                  Case WM_RBUTTONUP:

                    'show the menu                  
                     With Form1
                        .PopupMenu .zmnuDemo
                     End With                  
                  
               End Select
            
           'handle any other form messages by
           'passing to the default message proc
            Case Else
            
               WindowProc = CallWindowProc(defWindowProc, _
                                            hwnd, _
                                            uMsg, _
                                            wParam, _
                                            lParam)
               Exit Function
            
         End Select

     
     'this takes care of messages when the
     'handle specified is not that of the form
      Case Else
      
          WindowProc = CallWindowProc(defWindowProc, _
                                      hwnd, _
                                      uMsg, _
                                      wParam, _
                                      lParam)
   End Select
   
End Function
 Form Code
To a form (Form1), add a parent menu bar item named 'zmnuDemo', and under it a menu array (mFile(0) - mFile(6)). Items 0, 1 and 3 are dummy items that simply show a message box and can contain any caption; items 2 and 4 are separators, and item 5 terminates the tray icon. Note: there is an *additional* menu item command that is used and required in the demo but which is NOT shown in the illustration - add it as mFile(6)  - "Exit Demo". The code behind this file item shows how to properly terminate an application when a Close (Exit) command is invoked from a systray popup menu command.

Note too that for demo purposes I've left the hidden popup menu visible on the form. You can hide it (visible=False) if you prefer.

Add a command button (Command1) and a label (Label1). In addition, add a 16x16 icon to the form's Icon property --- this icon will be used as the icon displayed in the taskbar. Finally, add the following code to the form:


Option Explicit

Private Sub Form_Load()
     
  'centre this form
   Me.Move (Screen.Width - Me.Width) \ 2, (Screen.Height - Me.Height) \ 2
   
   Dim msg As String
   msg = "Systray ToolTip && Popup Menu Demo." & vbCrLf & vbCrLf
   msg = msg & "Move the mouse over the systray 'drive'"
   msg = msg & " icon. The tooltip will appear." & vbCrLf & vbCrLf
   msg = msg & "Right-clicking displays the popup menu. "
   msg = msg & "Choose an option."
   
   Label1.Caption = msg

  'add an icon to the system tray. If is
  'is successful (returns 1) then subclass
  'to intercept messages
   If ShellTrayAdd = 1 Then
   
     'prepare to receive the systray messages
      SubClass Form1.hwnd
      
   End If

End Sub


Private Sub Form_Unload(Cancel As Integer)

  'Remove the icon added to the taskbar
   ShellTrayRemove
   
  'remove subclassing
   UnSubClass
       
  'assure unloading proceeds
   Cancel = False
    
End Sub


Private Sub Command1_Click()

  'call the unload event. This MUST execute
  'to un-subclass the form before exiting.
  'If you GPF on exiting, this is the reason!
  'Always save the project before running.
   Unload Me

End Sub


Private Sub mFile_Click(Index As Integer)

  'code demonstrating typical reaction to
  'the menu clicks.  
   Select Case Index
      
      Case 0, 1, 3:
         
         MsgBox "Called from File " & mFile(Index).Caption
      
      Case 5: ShellTrayRemove
      Case 6:
         
        'Executing 'Unload Me' from within a
        'menu event invoked from a systray icon
        'will cause a GPF. The proper way to
        'terminate under these circumstances
        'is to send a WM_CLOSE message to the
        'form. The form will process the
        'message as though the user had selected
        'Close from the sysmenu, invoking the
        'normal chain of shutdown events, removing
        'the tray icon, terminating the subclassing
        'cleanly and ultimately preventing the GPF.
        '
        'This code can also be called directly from
        'the form's menu as well, so no special coding
        'is required to differentiate between an end
        'command from a popup systray menu, or from
        'a normal form menu.
        '
        'The UnloadMode of QueryUnload/UnloadMode
        'will equal vbFormControlMenu when this
        'close method is used.
         Call PostMessage(Form1.hwnd, WM_CLOSE, 0&, ByVal 0&)
      
      Case Else
   End Select
              
End Sub


Public Function ShellTrayAdd() As Long

 'prepare the NOTIFYICONDATA type with the
 'required parameters:
 
 '.cbSize: Size of this structure, in bytes.
 '
 '.hwnd:   Handle of the window that will receive
 '         notification messages associated with
 '         an icon in the taskbar status area.
 '
 'uID:     Application-defined identifier of
 '         the taskbar icon. In an application
 '         with a single tray icon, this can be
 '         an arbitrary number.  For apps with
 '         multiple icons, each icon ID must be
 '         different as this member identifies
 '         which of the icons was selected.
 '
 '.uFlags: flags that indicate which of the other
 '         members contain valid data. This member
 '         can be a combination of the following:
 '         NIF_ICON    hIcon member is valid.
 '         NIF_MESSAGE uCallbackMessage member is valid.
 '         NIF_TIP     szTip member is valid.
 '
 'uCallbackMessage: Application-defined message identifier.
 '         The system uses this identifier for
 '         notification messages that it sends
 '         to the window identified in hWnd.
 '         These notifications are sent when a
 '         mouse event occurs in the bounding
 '         rectangle of the icon. (Note: 'callback'
 '         is a bit misused here (in the context of
 '         other callback demonstrations); there is
 '         no systray-specific callback defined -
 '         instead the form itself must be subclassed
 '         to respond to this message.
 '
 'hIcon:   Handle to the icon to add, modify, or delete.
 '
 'szTip:   Tooltip text to display for the icon. Must
 '         be terminated with a Chr$(0).
 
 'Shell_NotifyIcon messages:
 'dwMessage: Message value to send. This parameter
 '           can be one of these values:
 '           NIM_ADD     Adds icon to status area
 '           NIM_DELETE  Deletes icon from status area
 '           NIM_MODIFY  Modifies icon in status area
 '
 'pnid:      Address of the prepared NOTIFYICONDATA.
 '           The content of the structure depends
 '           on the value of dwMessage.

   If NOTIFYICONDATA_SIZE = 0 Then SetShellVersion
    
   With NID
      .cbSize = NOTIFYICONDATA_SIZE
      .hwnd = Form1.hwnd
      .uID = 125&
      .uFlags = NIF_ICON Or NIF_TIP Or NIF_MESSAGE
      .uCallbackMessage = WM_MYHOOK
      .hIcon = Me.Icon
      .szTip = "VBnet System Tray Callback Demo" & Chr$(0)
    End With
   
    ShellTrayAdd = Shell_NotifyIcon(NIM_ADD, NID)

End Function


Private Sub ShellTrayRemove()

  'Remove the icon from the taskbar
   Call Shell_NotifyIcon(NIM_DELETE, NID)
   
End Sub


Private Sub UnSubClass()

  'restore the default message handling
  'before exiting   
   If defWindowProc Then
      SetWindowLong Form1.hWnd, GWL_WNDPROC, defWindowProc
      defWindowProc = 0
   End If
   
End Sub


Private Sub SubClass(hwnd As Long)

  'assign our own window message
  'procedure (WindowProc)
  
   On Error Resume Next
   defWindowProc = SetWindowLong(hwnd, GWL_WNDPROC, AddressOf WindowProc)
   
End Sub
 Comments
Double check all your code. In particular, should you have chosen to use different form, menu or control names, or changed the names of routines, you must assure that every occurrence in the project is correct. Save the project, then run. On starting the form's icon will appear in the taskbar.  By leaving the mouse over the icon, the icon's ToolTip will appear. Right-clicking over the icon will display the popup menu listing the drives and drive types available on the system. Other menu options will display a placeholder messagebox, and selecting Close Tray Icon will remove the icon from the tray.

Also see related topic: Shell_NotifyIcon: Respond to Systray Icon/Menu Interaction


 
 

PayPal Link
Make payments with PayPal - it's fast, free and secure!

 
 
 
 

Copyright ©1996-2011 VBnet and Randy Birch. All Rights Reserved.
Terms of Use  |  Your Privacy

 

Hit Counter