Skip to content

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