NotifyIconEx

–This page is mostly just here for archival purposes. It’s way outdated–

In Visual Basic.NET, click “Project”, “Add User Control…” then put all this stuff in it. Then under the Toolbox in the form Designer, you should have a new thing under My User Controls. NICE!

Imports System
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Runtime.InteropServices
Imports System.ComponentModel
Imports System.Reflection
 
Public Class NotifyIconEx
    Inherits System.ComponentModel.Component
 
#Region "Notify Icon Target Window"
    Private Class NotifyIconTarget
        Inherits System.Windows.Forms.Form
 
        Public Sub NotifyIconTarget()
            Me.Text = "Hidden NotifyIconTarget Window"
        End Sub
 
        Protected Overrides Sub DefWndProc(ByRef msg As System.Windows.Forms.Message)
            If (msg.Msg = &H400) Then '// WM_USER
                Dim msgId As Integer = msg.LParam.ToInt32
                Dim id As Integer = msg.WParam.ToInt32
 
                Select Case msgId
                    Case &H201 ' WM_LBUTTONDOWN
 
                    Case &H202 ' WM_LBUTTONUP
                        'If (ClickNotify) Then
                        RaiseEvent ClickNotify(Me, id)
                        'End If
 
                    Case &H203 ' WM_LBUTTONDBLCLK
                        'If (DoubleClickNotify <> null) Then
                        RaiseEvent DoubleClickNotify(Me, id)
                        'End If
 
                    Case &H205 ' WM_RBUTTONUP
                        'If (RightClickNotify <> null) Then
                        RaiseEvent RightClickNotify(Me, id)
                        'End If
 
                    Case &H200 ' WM_MOUSEMOVE
 
                    Case &H402 ' NIN_BALLOONSHOW
 
                        ' this should happen when the balloon is closed using the x
                        ' - we never seem to get this message!
                    Case &H403 ' NIN_BALLOONHIDE
 
                        ' we seem to get this next message whether the balloon times
                        ' out or whether it is closed using the x
                    Case &H404 ' NIN_BALLOONTIMEOUT
 
                    Case &H405 ' NIN_BALLOONUSERCLICK
                        'If (ClickBalloonNotify <> Nothing) Then
                        RaiseEvent ClickBalloonNotify(Me, id)
                        'End If
                End Select
 
            ElseIf (msg.Msg = &HC086) Then ' WM_TASKBAR_CREATED
 
                    'If (TaskbarCreated <> Nothing) Then
                    RaiseEvent TaskbarCreated(Me, System.EventArgs.Empty)
                    'End If
 
                Else
                    MyBase.DefWndProc(msg)
                End If
        End Sub
 
        Public Delegate Sub NotifyIconHandler(ByVal sender As Object, ByVal id As Long)
 
        Public Event ClickNotify As NotifyIconHandler
        Public Event DoubleClickNotify As NotifyIconHandler
        Public Event RightClickNotify As NotifyIconHandler
        Public Event ClickBalloonNotify As NotifyIconHandler
        Public Event TaskbarCreated As EventHandler
    End Class
#End Region
 
#Region "Platform Invoke"
 
    <StructLayout(LayoutKind.Sequential)> Private Structure NotifyIconData
        Public cbSize As Integer ' DWORD
        Public hWnd As System.IntPtr ' HWND
        Public uID As Integer ' UINT
        Public uFlags As NotifyFlags ' UINT
        Public uCallbackMessage As Integer ' UINT
        Public hIcon As System.IntPtr ' HICON
        <MarshalAs(UnmanagedType.ByValTStr, sizeconst:=128)> Public szTip As String
        Public dwState As NotifyState ' DWORD
        Public dwStateMask As NotifyState ' DWORD
        <MarshalAs(UnmanagedType.ByValTStr, sizeconst:=256)> Public szInfo As String
        Public uTimeoutOrVersion As Integer ' UINT
        <MarshalAs(UnmanagedType.ByValTStr, sizeconst:=64)> Public szInfoTitle As String
        Public dwInfoFlags As NotifyInfoFlags ' DWORD
    End Structure
 
    <StructLayout(LayoutKind.Sequential)> Private Structure POINT
        Public x As Integer
        Public y As Integer
    End Structure
 
    Private Overloads Declare Function Shell_NotifyIcon Lib "shell32.dll" (ByVal cmd As NotifyCommand, ByRef data As NotifyIconData) As Boolean
    Private Overloads Declare Function TrackPopupMenuEx Lib "user32.dll" (ByVal hMenu As System.IntPtr, ByVal uFlags As Long, ByVal x As System.Int32, ByVal y As System.Int32, ByVal hwnd As System.IntPtr, ByVal ignore As System.IntPtr) As System.Int32
    Private Overloads Declare Function GetCursorPos Lib "user32.dll" (ByRef point As POINT) As System.Int32
    Private Overloads Declare Function SetForegroundWindow Lib "user32.dll" (ByVal hWnd As System.IntPtr) As System.Int32
    Private Overloads Declare Function PostMessage Lib "user32.dll" Alias "PostMessageA" (ByVal hwnd As System.IntPtr, ByVal wMsg As Integer, ByVal wParam As System.IntPtr, ByVal lParam As System.IntPtr) As Integer
#End Region
 
    Public Enum NotifyInfoFlags As Integer
        [Error] = &H3
        Info = &H1
        None = &H0
        Warning = &H2
    End Enum
    Private Enum NotifyCommand As Integer
        Add = &H0
        Delete = &H2
        Modify = &H1
    End Enum
    Private Enum NotifyFlags As Integer
        Message = &H1
        Icon = &H2
        Tip = &H4
        Info = &H10
        State = &H8
    End Enum
    Private Enum NotifyState As Integer
        Hidden = &H1
    End Enum
 
    Private m_id As Long = 0 ' each icon in the notification area has an id
    Private m_handle As IntPtr ' save the handle so that we can remove icon
    Private Shared m_messageSink As NotifyIconTarget = New NotifyIconTarget
    Private Shared m_nextId As Long = 1
    Private m_text As String = ""
    Private m_icon As Icon = Nothing
    Private m_contextMenu As ContextMenu = Nothing
    Private m_visible As Boolean = False
    Private m_doubleClick As Boolean = False ' fix for extra mouse up message we want to discard
 
    Public Event Click As EventHandler
    Public Event DoubleClick As EventHandler
    Public Event BalloonClick As EventHandler
 
#Region "Properties"
    Public Property Text() As String
        Set(ByVal Value As String)
            If (m_text <> Value) Then
                m_text = Value
                CreateOrUpdate()
            End If
        End Set
        Get
            Return m_text
        End Get
    End Property
    Public Property Icon() As Icon
        Set(ByVal Value As Icon)
            m_icon = Value
            CreateOrUpdate()
        End Set
        Get
            Return m_icon
        End Get
    End Property
    Public Property ContextMenu() As ContextMenu
        Set(ByVal Value As ContextMenu)
            m_contextMenu = Value
        End Set
        Get
            Return m_contextMenu
        End Get
    End Property
    Public Property Visible() As Boolean
        Set(ByVal Value As Boolean)
            If (m_visible <> Value) Then
                m_visible = Value
                CreateOrUpdate()
            End If
        End Set
        Get
            Return m_visible
        End Get
    End Property
#End Region
 
    Public Sub NotifyIconEx()
    End Sub
 
    ' this method adds the notification icon if it has not been added and if we
    ' have enough data to do so
    Private Sub CreateOrUpdate()
 
        If (Me.DesignMode) Then
            Return
        End If
 
        If (m_id = 0) Then
            If (Not m_icon Is Nothing) Then
                ' create icon using available properties
                Create(m_nextId)
                m_nextId += 1
            End If
        Else
            ' update notify icon
            Update()
        End If
    End Sub
 
    Private Sub Create(ByVal id As Long)
        Dim Data As NotifyIconData = New NotifyIconData
 
        Data.cbSize = Marshal.SizeOf(Data)
 
        m_handle = m_messageSink.Handle
        Data.hWnd = m_handle
        m_id = id
        Data.uID = m_id
 
        Data.uCallbackMessage = &H400
        Data.uFlags = NotifyFlags.Icon Or NotifyFlags.Message Or NotifyFlags.Tip
 
        Data.hIcon = m_icon.Handle ' this should always be valid
 
        Data.szTip = m_text
 
        If (m_visible = False) Then
            Data.dwState = NotifyState.Hidden
        End If
        Data.dwStateMask = NotifyState.Hidden
 
        Dim ret As Boolean = Shell_NotifyIcon(NotifyCommand.Add, Data)
 
        ' add handlers
        AddHandler m_messageSink.ClickNotify, AddressOf OnClick
        AddHandler m_messageSink.DoubleClickNotify, AddressOf OnDoubleClick
        AddHandler m_messageSink.RightClickNotify, AddressOf OnRightClick
        AddHandler m_messageSink.ClickBalloonNotify, AddressOf OnClickBalloon
        AddHandler m_messageSink.TaskbarCreated, AddressOf OnTaskbarCreated
    End Sub
 
    ' update an existing icon
    Private Sub Update()
 
        Dim data As NotifyIconData = New NotifyIconData
        data.cbSize = Marshal.SizeOf(data)
 
        data.hWnd = m_messageSink.Handle
        data.uID = m_id
 
        data.hIcon = m_icon.Handle ' this should always be valid
 
        data.szTip = m_text
        data.uFlags = NotifyFlags.Icon Or NotifyFlags.Tip Or NotifyFlags.State
        data.dwStateMask = NotifyState.Hidden
 
        If (m_visible = False) Then
            data.dwState = NotifyState.Hidden
        End If
 
        Dim ret As Boolean = Shell_NotifyIcon(NotifyCommand.Modify, data)
    End Sub
 
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        Remove()
        MyBase.Dispose(disposing)
    End Sub
 
    Public Sub Remove()
 
        If (m_id <> 0) Then
 
            ' remove the notify icon
            Dim data As NotifyIconData = New NotifyIconData
            data.cbSize = Marshal.SizeOf(data)
 
            data.hWnd = m_handle
            data.uID = m_id
 
            Shell_NotifyIcon(NotifyCommand.Delete, data)
 
            m_id = 0
        End If
    End Sub
 
    Public Sub ShowBalloon(ByVal title As String, ByVal text As String, ByVal type As NotifyInfoFlags, ByVal timeoutInMilliSeconds As Integer)
        If (timeoutInMilliSeconds < 0) Then
            Throw New ArgumentException("The parameter must be positive", "timeoutInMilliseconds")
        End If
 
        Dim data As NotifyIconData = New NotifyIconData
        data.cbSize = Marshal.SizeOf(data)
 
        data.hWnd = m_messageSink.Handle
        data.uID = m_id
 
        data.uFlags = NotifyFlags.Info
        data.uTimeoutOrVersion = timeoutInMilliSeconds ' this value does not seem to work - any ideas?
        data.szInfoTitle = title
        data.szInfo = text
        data.dwInfoFlags = type
 
        Dim ret As Boolean = Shell_NotifyIcon(NotifyCommand.Modify, data)
    End Sub
 
#Region "Message Handlers"
 
    Private Sub OnClick(ByVal sender As Object, ByVal id As Long)
        If (id = m_id) Then
            If (m_doubleClick = False) Then
                RaiseEvent Click(Me, EventArgs.Empty)
                m_doubleClick = False
            End If
        End If
    End Sub
 
    Private Sub OnRightClick(ByVal sender As Object, ByVal id As Long)
        If (id = m_id) Then
            ' show context menu
            If (Not m_contextMenu Is Nothing) Then
                Dim point As Point = New Point
                GetCursorPos(point)
                SetForegroundWindow(m_messageSink.Handle) ' this ensures that if we show the menu and then click on another window the menu will close
 
                ' call non public member of ContextMenu
                m_contextMenu.GetType().InvokeMember("OnPopup", BindingFlags.NonPublic Or BindingFlags.InvokeMethod Or BindingFlags.Instance, Nothing, m_contextMenu, New Object() {System.EventArgs.Empty})
                TrackPopupMenuEx(m_contextMenu.Handle, 64, point.x, point.y, m_messageSink.Handle, IntPtr.Zero)
 
                PostMessage(m_messageSink.Handle, 0, IntPtr.Zero, IntPtr.Zero)
            End If
        End If
    End Sub
 
    Private Sub OnDoubleClick(ByVal sender As Object, ByVal id As Long)
        If (id = m_id) Then
            m_doubleClick = True
            'If (Not DoubleClick Is Nothing) Then
            RaiseEvent DoubleClick(Me, EventArgs.Empty)
            'End If
        End If
    End Sub
 
    Private Sub OnClickBalloon(ByVal sender As Object, ByVal id As Long)
        If (id = m_id) Then
            'If (BalloonClick <> Nothing) Then
            RaiseEvent BalloonClick(Me, EventArgs.Empty)
            'End If
        End If
    End Sub
 
    Private Sub OnTaskbarCreated(ByVal sender As Object, ByVal e As EventArgs)
        If (m_id <> 0) Then
            Create(m_id) ' keep the id the same
        End If
    End Sub
#End Region
End Class