Visual Basic Subclassing Routines
Shell_NotifyIcon: Use SetTimer to Define Balloon Tip Life
Posted:   Thursday June 17, 2004
Updated:   Monday December 26, 2011
Applies to:   VB5, VB6
Developed with:   VB6, Windows XP
OS restrictions:   Windows 2000 or Windows XP
Author:   VBnet - Randy Birch


CreateWindowEx: 21st Century ToolTips for VB - The Basics
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
SendMessage: Add Balloon Tips to a Combo Edit Box

SendMessage: Add Balloon Tips to a Text Box

Windows 2000 or XP (Shell version 5 or better)

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.

Shell_NotifyIcon: Display Systray Balloon Tips shows how easy it is to use balloon tips on 2000 or XP. Like Shell_NotifyIcon: Respond to Systray Balloon Tip Clicks, this demo uses subclassing to enable detection of the user's clicking on the balloon tip in order to provide a means to respond to the click, as well as code that detects when the balloon tip is shown and if it was closed by the timeout expiring or the X being pressed. It adds

The NOTIFYICONDATA structure contains two members used with later (post Windows 2000) operating systems - uTimeout and uVersion.  Unfortunately, these are contained in a C UNION that in VB is represented as the same Long variable:

typedef struct _NOTIFYICONDATA {
    DWORD cbSize;
    HWND hWnd;
    UINT uID;
    UINT uFlags;
    UINT uCallbackMessage;
    HICON hIcon;
    TCHAR szTip[64];
    DWORD dwState;
    DWORD dwStateMask;
    TCHAR szInfo[256];
    union {
        UINT uTimeout;
        UINT uVersion;
    TCHAR szInfoTitle[64];
    DWORD dwInfoFlags;
    GUID guidItem;

As the definition of a UNION is unavailable to VB developers outside a typelib, each of uTimeout and uVersion occupies the same data variable. Now if one was to read the MSDN closely, it would appear that the uVersion would only be used during the NOTIFYICON_VERSION call to Shell_NotifyIcon, and that subsequent calls could use this same variable for assigning a custom timeout value. While this may be the case with C, it does not work with VB.

This demo therefore takes a different approach to enabling the display (and closure) of a systray balloon tip programmatically using an API timer created by SetTimer. The principle is straightforward:

  • the application adds an icon to the systray using Shell_NotifyIcon
  • the app then calls Shell_NotifyIcon again to indicate the OS features to enable
  • the app then enters a subclassing routine to trap the user's interaction with the balloon
  • when the WindowProc's NIN_BALLOONSHOW message fires as a result of the pressing of Command2 (indicating the balloon tip has been displayed), the subclassing routine makes a call to SetTimer to create an API timer.  The interval for this demo is set to 7000 milliseconds (7 seconds) through the APP_TIMER_MILLISECONDS constant.

With this set up the user can interact with (or ignore) the balloon tip.

  • If the user clicks or closes the tip the subclassing indicates the event.
  • If APP_TIMER_MILLISECONDS is less than the system's automatic timeout value, which the MSDN indicates can be between 10 an 30 seconds, SetTimer's TimerProc routine will fire when APP_TIMER_MILLISECONDS expires and TimerProc makes the call to hide the tip.
  • If the SetTimer delay interval is greater than the Windows default interval the balloon tip is closed by the system.  Therefore this implementation is designed to force the closure of the balloon before the system timeout interval would fire.

To assist in understanding the order of events, there are a number of Debug.Print statements in the code.

Windows allows no more than one taskbar balloon tip to be displayed at any given moment. If an application attempts to display a balloon tip when one is already being displayed, the balloon tip will not appear until the existing balloon tip has been visible for at least the system-minimum timeout value (typically 10 seconds). For example, a balloon tip from another application having a uTimeout value of 30 seconds has been visible for seven seconds when your application attempts to display its balloon tip. If the system-minimum timeout is ten seconds, the first balloon tip displays for an additional three seconds before being replaced by your balloon tip. If your application expected your balloon tip to have been on-screen for 3 seconds before it is actually shown, you may invoke code to close the tip too quickly, etc. or make incorrect assumptions about the user's interest in the message. Therefore, to accommodate this balloon tips raise a message when they are actually shown, and, in response to this your app can begin to take whatever action is appropriate.

Similarly, to assist in determining a course of action balloon tips also send a message when it times out without user interaction (NIN_BALLOONTIMEOUT). This message is also sent when the user dismisses the balloon tip using its X button.

With the SetTimer closure routine an application does not receive the system's NIN_BALLOONTIMEOUT message when the TimerProc has invoked the closure of the tip. Instead, the subclassing receives a NIN_BALLOONHIDE message. (This message is also received when the the systray icon is removed during the display of a balloon tip).

A complete discussion of the structure and parameters for balloon tips can be found on the Shell_NotifyIcon: Windows Systray NOTIFYICONDATA Overview page.

 BAS Module
Add the following code to a BAS module:

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 holds the address
'of the default window message processing
'procedure returned by SetWindowLong
Public defWindowProc As Long

'flag preventing re-creating the timer
Private tmrRunning As Boolean

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

'general windows messages
Private Const WM_USER As Long = &H400
Private Const WM_NOTIFY As Long = &H4E
Private Const WM_COMMAND As Long = &H111
Public Const WM_CLOSE As Long = &H10
Private Const WM_TIMER = &H113

'mouse constants for the callback
Private Const WM_LBUTTONDOWN As Long = &H201
Private Const WM_LBUTTONUP As Long = &H202
Private Const WM_LBUTTONDBLCLK As Long = &H203
Private Const WM_MBUTTONDOWN As Long = &H207
Private Const WM_MBUTTONUP As Long = &H208
Private Const WM_MBUTTONDBLCLK As Long = &H209
Private Const WM_RBUTTONDOWN As Long = &H204
Private Const WM_RBUTTONUP As Long = &H205
Private Const WM_RBUTTONDBLCLK As Long = &H206

'WM_MYHOOK is a private message the shell_notify api 
'will pass to WindowProc when the systray icon is acted upon
Private Const WM_APP As Long = &H8000&
Public Const WM_MYHOOK As Long = WM_APP + &H15

'ID constant representing this
'application in the systray. Use 
'a unique ID for each systray icon 
'your app will add in order to 
'differentiate between icons selected. 
'The ID is returned in wParam.
Private Const APP_SYSTRAY_ID = 999

'ID constant representing this
'application for SetTimer
Public Const APP_TIMER_EVENT_ID As Long = 998

'const holding number of milliseconds to timeout
'7000=7 seconds
Public Const APP_TIMER_MILLISECONDS As Long = 7000

'balloon tip notification messages
Private Const NIN_BALLOONSHOW = (WM_USER + 2)
Private Const NIN_BALLOONHIDE = (WM_USER + 3)

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


'shell_notify flags
Private Const NIF_MESSAGE = &H1
Private Const NIF_ICON = &H2
Private Const NIF_TIP = &H4
Private Const NIF_STATE = &H8
Private Const NIF_INFO = &H10
'shell_notify messages
Private Const NIM_ADD = &H0
Private Const NIM_MODIFY = &H1
Private Const NIM_DELETE = &H2
Private Const NIM_SETFOCUS = &H3
Private Const NIM_SETVERSION = &H4
Private Const NIM_VERSION = &H5
'shell_notify styles
Private Const NIS_HIDDEN = &H1
Private Const NIS_SHAREDICON = &H2

'shell_notify icon flags
Private Const NIIF_NONE = &H0
Private Const NIIF_INFO = &H1
Private Const NIIF_WARNING = &H2
Private Const NIIF_ERROR = &H3
Private Const NIIF_GUID = &H5
Private Const NIIF_ICON_MASK = &HF
Private Const NIIF_NOSOUND = &H10

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

  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

Private 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
Private Declare Function SetWindowLong Lib "user32" _
   Alias "SetWindowLongA" _
   (ByVal hwnd As Long, _
    ByVal nIndex As Long, _
    ByVal dwNewLong As Any) As Long

Private 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

Private Declare Function SetTimer Lib "user32" _
  (ByVal hwnd As Long, _
   ByVal nIDEvent As Long, _
   ByVal uElapse As Long, _
   ByVal lpTimerFunc As Long) As Long
Private Declare Function KillTimer Lib "user32" _
  (ByVal hwnd As Long, _
   ByVal nIDEvent As Long) As Long

Private Declare Function Shell_NotifyIcon Lib "shell32.dll" _
   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)

Private 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

Private Sub SetShellVersion()

   Select Case True
      Case IsShellVersion(6)
      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

Public Sub ShellTrayIconAdd(hwnd As Long, _
                            hIcon As StdPicture, _
                            sToolTip As String)
   If NOTIFYICONDATA_SIZE = 0 Then SetShellVersion
   With nid
      .hwnd = hwnd
      .dwState = NIS_SHAREDICON
      .hIcon = hIcon
      .szTip = sToolTip & vbNullChar
      .uTimeoutAndVersion = NOTIFYICON_VERSION
      .uCallbackMessage = WM_MYHOOK
   End With
  'add the icon ...
   If Shell_NotifyIcon(NIM_ADD, nid) = 1 Then
     '... and inform the system of the
     'NOTIFYICON version in use
      Call Shell_NotifyIcon(NIM_SETVERSION, nid)
     'prepare to receive the systray messages
      Call SubClass(hwnd)
   End If
End Sub

Public Sub ShellTrayIconRemove(hwnd As Long)

   If NOTIFYICONDATA_SIZE = 0 Then SetShellVersion
   With nid
      .hwnd = hwnd
   End With
   If tmrRunning Then Call TimerStop(hwnd)
   Call Shell_NotifyIcon(NIM_DELETE, nid)

End Sub

Private Sub ShellTrayBalloonTipClose(hwnd As Long)

   If NOTIFYICONDATA_SIZE = 0 Then SetShellVersion
   With nid
      .hwnd = hwnd
      .uFlags = NIF_TIP Or NIF_INFO
      .szTip = vbNullChar
      .uTimeoutAndVersion = NOTIFYICON_VERSION
   End With
   Call Shell_NotifyIcon(NIM_MODIFY, nid)
End Sub

Public Sub ShellTrayBalloonTipShow(hwnd As Long, _
                                   nIconIndex As Long, _
                                   sTitle As String, _
                                   sMessage As String)

   If NOTIFYICONDATA_SIZE = 0 Then SetShellVersion
   With nid
      .hwnd = hwnd
      .uFlags = NIF_INFO
      .dwInfoFlags = nIconIndex
      .szInfoTitle = sTitle & vbNullChar
      .szInfo = sMessage & vbNullChar
   End With

   Call Shell_NotifyIcon(NIM_MODIFY, nid)

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

Public Sub UnSubClass(hwnd As Long)

  'restore the default message handling
  'before exiting
   If defWindowProc <> 0 Then
      SetWindowLong hwnd, GWL_WNDPROC, defWindowProc
      defWindowProc = 0
   End If
End Sub

Private Sub TimerBegin(ByVal hwndOwner As Long, ByVal dwMilliseconds As Long)

   If Not tmrRunning Then

      If dwMilliseconds <> 0 Then

        'SetTimer returns the event ID we
        'assign if it starts successfully,
        'so this is assigned to the Boolean
        'flag to indicate the timer is running.
         tmrRunning = SetTimer(hwndOwner, _
                               APP_TIMER_EVENT_ID, _
                               dwMilliseconds, _
                               AddressOf TimerProc) = APP_TIMER_EVENT_ID
         Debug.Print "timer started"

      End If

   End If

End Sub

Public Function TimerProc(ByVal hwnd As Long, _
                          ByVal uMsg As Long, _
                          ByVal idEvent As Long, _
                          ByVal dwTime As Long) As Long

   Select Case uMsg
      Case WM_TIMER

         If idEvent = APP_TIMER_EVENT_ID Then
            If tmrRunning = True Then
               Debug.Print "timer proc fired"
               Debug.Print "  shutting down balloon"
               Call TimerStop(hwnd)
               Call ShellTrayBalloonTipClose(Form1.hwnd)
            End If  'tmrRunning
         End If  'idEvent

      Case Else
   End Select

End Function

Private Sub TimerStop(ByVal hwnd As Long)

   If tmrRunning = True Then

      Debug.Print "timer stopped"
      Call KillTimer(hwnd, APP_TIMER_EVENT_ID)
      tmrRunning = False

   End If

End Sub

Public Function WindowProc(ByVal hwnd As Long, _
                           ByVal uMsg As Long, _
                           ByVal wParam As Long, _
                           ByVal lParam As Long) As Long

  'If the handle returned is to our form,
  'call a message handler to deal with
  'tray notifications. If it is a general
  'system message, pass it on to
  'the default window procedure.
  'If destined for the form and equal to
  'our custom hook message (WM_MYHOOK),
  'examining lParam reveals the message
  'generated, to which we react appropriately.
   On Error Resume Next
   Select Case hwnd
     'form-specific handler
      Case Form1.hwnd
         Select Case uMsg
          'check uMsg for the application-defined
          'identifier (NID.uID) assigned to the
          'systray icon in NOTIFYICONDATA (NID).
           'WM_MYHOOK was defined as the message sent
           'as the .uCallbackMessage member of
           'NOTIFYICONDATA the systray icon
            Case WM_MYHOOK
              'lParam is the value of the message
              'that generated the tray notification.
               Select Case lParam
                  Case WM_RBUTTONUP

                 'This assures that focus is restored to
                 'the form when the menu is closed. If the
                 'form is hidden, it (correctly) has no effect.
                  Call SetForegroundWindow(Form1.hwnd)
                  Form1.PopupMenu Form1.zmnuSysTrayDemo
                  Case NIN_BALLOONSHOW
                    'the balloon tip has just appeared so
                    'set the timer to automatically close it
                     Call TimerBegin(hwnd, APP_TIMER_MILLISECONDS)
                     Debug.Print "NIN_BALLOONSHOW"
                  Case NIN_BALLOONHIDE
                    'the balloon tip has just been hidden,
                    'either because of a user-click, the
                    'system timeout being reached, or our
                    'SetTimer timeout expiring, so ensure
                    'the timer has stopped.
                     Call TimerStop(hwnd)
                     Debug.Print "NIN_BALLOONHIDE"

                  Case NIN_BALLOONUSERCLICK
                    'the balloon tip was clicked so
                    'ensure the timer won't fire
                     Call TimerStop(hwnd)
                     Debug.Print "NIN_BALLOONUSERCLICK"
                  Case NIN_BALLOONTIMEOUT
                    'the system timeout has been reached
                    'which causes the system to close the
                    'tip without intervention. The timer
                    'must also be stopped now. Note that
                    'this message does not fire if the
                    'balloon tip is closed through our
                    'SetTimer method!
                     Call TimerStop(hwnd)
                     Debug.Print "NIN_BALLOONTIMEOUT"
               End Select
           'handle any other form messages by
           'passing to the default message proc
            Case Else
               WindowProc = CallWindowProc(defWindowProc, _
                                            hwnd, _
                                            uMsg, _
                                            wParam, _
               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, _
   End Select
End Function

 Form Code
To a form add a text box (Text1, multiline), four option buttons in a control array (Option1(0) through Option1(3)), and two command buttons (Command1, Command2). Also add a top-level menu named zmnuSysTrayDemo with four menu items in a menu array - mnuFile(0) through mnuFile(3).  mnuFile(2) is a separator. Add the following code to the form:

Option Explicit
Private Sub Form_Load()

   Text1.Text = "The balloon tip shown in this demo " & _
                "will automatically disappear when " & _
                "the assigned time has elapsed. For " & _
                "this demo the balloon will close in " & _
                "7000 milliseconds (AKA seven seconds)." & _
                vbCrLf & vbCrLf & _
                "Clicking the balloon tip will cause it to close immediately."

   Command1.Caption = "Add Systray Icon"
   Command2.Caption = "Show Balloon Tip"
   Command2.Enabled = False
   Option1(0).Caption = "no icon"
   Option1(1).Caption = "information icon"
   Option1(2).Caption = "warning icon"
   Option1(3).Caption = "error icon"         
   Option1(1).Value = True   
   Option1(1).Value = True
End Sub

Private Sub Form_Unload(Cancel As Integer)

  'Remove the icon added to the
  'taskbar and remove subclassing
   If defWindowProc <> 0 Then
      Call ShellTrayIconRemove(Me.hwnd)
      UnSubClass Me.hwnd
   End If
  'ensure unloading proceeds
   Cancel = False
End Sub

Private Sub Command1_Click()
   Dim sToolTip As String

   sToolTip = "This is the VBnet Balloon Tip Demo"
   Call ShellTrayIconAdd(Me.hwnd, Me.Icon, sToolTip)

   Command2.Enabled = True
End Sub

Private Sub Command2_Click()
   Dim nIconIndex As Long
   Dim sTitle As String
   Dim sMessage As String
   nIconIndex = GetSelectedOptionIndex()
   sTitle = "VBnet Balloon Tip Demo"
   sMessage = Text1.Text

   Call ShellTrayBalloonTipShow(Me.hwnd, nIconIndex, sTitle, sMessage)
End Sub

Private Sub mnuFile_Click(Index As Integer)

  'code simulating reaction
  'to a menu click
   Select Case Index
      Case 0, 1:
         MsgBox "Called from File " & mnuFile(Index).Caption
      Case 3:
        '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(Me.hwnd, WM_CLOSE, 0&, ByVal 0&)
      Case Else
   End Select
End Sub

Private Function GetSelectedOptionIndex() As Long

  'returns the selected item index from
  'an option button array. Use in place
  'of multiple If...Then statements!
  'If your array contains more elements,
  'just append them to the test condition,
  'setting the multiplier to the button's
  'negative -index.
   GetSelectedOptionIndex = Option1(0).Value * 0 Or _
                            Option1(1).Value * -1 Or _
                            Option1(2).Value * -2 Or _
                            Option1(3).Value * -3
End Function
Remember this is a subclassed app, so don't hit VB's 'End' button.


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