TBBUTTON struct not working with SendMessage

Edit: The originally posted code was in error in assuming that Intptr could be coersed to work across the bitness boundary. That mistake has been corrected.

I have also expanded the tray button data retrieval to retrieve a the button icon if possible. The Code Project article: Shell Tray Info - Arrange your system tray icons was used as the basis for writing this .Net implementation.

Do note that retrieved Icon instances do not own their respective handles as those are still owned by the OS.


The TBBUTTON structure is a bit of pain as fields after fsStyle vary their size based on the OS bitness (32/64). The following works on my system Win 10 (64 bit) for both x86 and x64 compilations. I apologize for the length and apparent formatting (I use 2 char tab stops and SO really messes with the formatting of multiple tabs), but I wish to present all code used in the example.

First is my TBBUTTON declaration. It is defined as base class and a class for 32-bit and class for 64-bit OS. The base class has a factory method (TBBUTTON.CreateForOS) to return the proper implementation. I deal with the varying field sizes by declaring byte place-holders to receive the marshaled structure and re-assemble when needed.

Imports System.Runtime.InteropServices

' Ref: https://docs.microsoft.com/en-us/windows/desktop/api/commctrl/ns-commctrl-tbbutton
' For info on native type size: Windows Data Types
'   https://docs.microsoft.com/en-us/windows/desktop/WinProg/windows-data-types

'   typedef struct _TBBUTTON {
'       int       iBitmap;
'       int       idCommand;
'       Byte      fsState;
'       Byte      fsStyle;
'   #If ...
'       Byte      bReserved[6]; - 64 bit
'   #Else
'       BYTE      bReserved[2]; - 32 bit
'   #End If
'       DWORD_PTR dwData; ' DWORD_PTR = ULONG_PTR  32/64 bits OS=> 4/8 bytes
'       INT_PTR   iString; ' 32/64 bits OS => 4/8 bytes

' ref:  How to Display Tooltips for Buttons
'               https://docs.microsoft.com/en-us/windows/desktop/Controls/display-tooltips-for-buttons
'               Set the tooltip text as the iString member of the TBBUTTON structure for each button.
' so iString is a pointer to the Tooltip text

'   } TBBUTTON, *PTBBUTTON, *LPTBBUTTON;

<StructLayout(LayoutKind.Sequential)>
Friend MustInherit Class TBBUTTON

    Public iBitmap As Int32
    Public idCommand As Int32
    Public fsState As NativeMethods.ToolBars.TBSTATE
    Public fsStyle As Byte
    Protected bReserved0 As Byte
    Protected bReserved1 As Byte

    Public Shared ReadOnly Property Is64Bit As Boolean
        Get
            Return Environment.Is64BitOperatingSystem
        End Get
    End Property

    Public Shared Function CreateForOS() As TBBUTTON
        Dim ret As TBBUTTON = Nothing
        If AppBitnessMatchesOS() Then
            If Environment.Is64BitOperatingSystem Then
                ret = New TBBUTTON64
            Else
                ret = New TBBUTTON32
            End If
        Else
            Throw New Exception($"Application is {If(Environment.Is64BitProcess, 64, 32)} bits and OS is {If(Environment.Is64BitOperatingSystem, 64, 32)}. Bitnesses much match.")
        End If
        Return ret
    End Function

    Private Shared Function AppBitnessMatchesOS() As Boolean
        Return Environment.Is64BitProcess.Equals(Environment.Is64BitOperatingSystem)
    End Function

    Public ReadOnly Property MarshalSize As IntPtr
        Get
            Return New IntPtr(Marshal.SizeOf(Me))
        End Get
    End Property

    Public MustOverride ReadOnly Property Reserved As Byte()
    Public MustOverride ReadOnly Property DwData As IntPtr
    Public MustOverride ReadOnly Property IString As IntPtr
End Class

<StructLayout(LayoutKind.Sequential)>
Friend NotInheritable Class TBBUTTON32 : Inherits TBBUTTON
    Private _dwData As IntPtr
    Private _iString As IntPtr

    Public Overrides ReadOnly Property Reserved As Byte()
        Get
            Return New Byte() {bReserved0, bReserved1}
        End Get
    End Property

    Public Overrides ReadOnly Property DwData As IntPtr
        Get
            Return _dwData
        End Get
    End Property

    Public Overrides ReadOnly Property IString As IntPtr
        Get
            Return _iString
        End Get
    End Property
End Class

<StructLayout(LayoutKind.Sequential)>
Friend NotInheritable Class TBBUTTON64 : Inherits TBBUTTON
    Protected bReserved2 As Byte
    Protected bReserved3 As Byte
    Protected bReserved4 As Byte
    Protected bReserved5 As Byte
    Private _dwData As IntPtr
    Private _iString As IntPtr

    Public Overrides ReadOnly Property Reserved As Byte()
        Get
            Return New Byte() {bReserved0, bReserved1, bReserved2, bReserved3, bReserved4, bReserved5}
        End Get
    End Property

    Public Overrides ReadOnly Property DwData As IntPtr
        Get
            Return _dwData
        End Get
    End Property

    Public Overrides ReadOnly Property IString As IntPtr
        Get
            Return _iString
        End Get
    End Property
End Class

Next is my native methods class. This class declares various function overloads to let the interop-marshaling system perform the necessary allocations/conversions.

Imports System.Diagnostics.CodeAnalysis
Imports System.Runtime.ConstrainedExecution
Imports System.Runtime.InteropServices
Imports System.Security

Friend Class NativeMethods
    Public Const WM_User As Int32 = &H400
    Public Shared Sub FreeHGlobal(ptr As IntPtr)
        If ptr <> IntPtr.Zero Then
            Marshal.FreeHGlobal(ptr)
        End If
    End Sub

    Public Class ToolBars

#Region "Constants"
        ' values from CommCtrl.h
        ''' <summary>Retrieves a count of the buttons currently in the toolbar. </summary>
        Public Const TB_BUTTONCOUNT As Int32 = WM_User + 24
        Public Const TB_GETBUTTON As Int32 = WM_User + 23
        Public Const TB_DELETEBUTTON As Int32 = WM_User + 22

        Private Const TB_GETBUTTONINFOW As Int32 = WM_User + 63
        Private Const TB_SETBUTTONINFOW As Int32 = WM_User + 64
        Private Const TB_GETBUTTONINFOA As Int32 = WM_User + 65
        Private Const TB_SETBUTTONINFOA As Int32 = WM_User + 66

        ''' <summary> The cbSize and dwMask members of this structure must be filled in prior to sending this message.</summary>
        Public Const TB_GETBUTTONINFO As Int32 = TB_GETBUTTONINFOW
        ''' <summary> The cbSize and dwMask members of this structure must be filled in prior to sending this message.</summary>
        Public Const TB_SETBUTTONINFO As Int32 = TB_SETBUTTONINFOW

        Public Const TB_GETBUTTONTEXTA As Int32 = WM_User + 45
        Public Const TB_GETBUTTONTEXTW As Int32 = WM_User + 75
        Public Const TB_GETBUTTONTEXT As Int32 = TB_GETBUTTONTEXTW

        Public Const TB_GETSTRINGW As Int32 = WM_User + 91
        Public Const TB_GETSTRINGA As Int32 = WM_User + 92
        ''' <summary>This message returns the specified string from the toolbar's string pool. It does not necessarily correspond to the text string currently being displayed by a button.</summary>
        Public Const TB_GETSTRING As Int32 = TB_GETSTRINGW

        ''' <summary>wParam and lParam must be zero. returns handle to the image list, or NULL if no image list is set.</summary>
        Public Const TB_GETIMAGELIST As Int32 = WM_User + 49
        ''' <summary>Retrieves the index of the bitmap associated with a button in a toolbar. 
        ''' wParam=Command identifier of the button whose bitmap index is to be retrieved.</summary>
        Public Const TB_GETBITMAP As Int32 = WM_User + 44

        <DllImport("comctl32.dll", SetLastError:=True)>
        Public Shared Function ImageList_GetIcon(himl As IntPtr, imageIndex As Int32, flags As UInt32) As IntPtr
        End Function

        <DllImport("comctl32.dll", SetLastError:=True)>
        Public Shared Function ImageList_GetImageCount(himl As IntPtr) As Int32
        End Function

#End Region

        Public Enum TBSTATE As Byte
            CHECKED = &H1
            PRESSED = &H2
            ENABLED = &H4
            HIDDEN = &H8
            INDETERMINATE = &H10
            WRAP = &H20
            ELLIPSES = &H40
            MARKED = &H80
        End Enum
    End Class

    Public Class User32
#Region "Utility Methods"
        Public Shared Function GetNotificationAreaToolBarHandle() As IntPtr
            Dim hWndTray As IntPtr = FindWindow("Shell_TrayWnd", Nothing)
            If hWndTray <> IntPtr.Zero Then
                hWndTray = FindWindowEx(hWndTray, IntPtr.Zero, "TrayNotifyWnd", Nothing)
                If hWndTray <> IntPtr.Zero Then
                    hWndTray = FindWindowEx(hWndTray, IntPtr.Zero, "SysPager", Nothing)
                    If hWndTray <> IntPtr.Zero Then
                        hWndTray = FindWindowEx(hWndTray, IntPtr.Zero, "ToolbarWindow32", Nothing)
                        Return hWndTray
                    End If
                End If
            End If

            Return IntPtr.Zero
        End Function

        Public Shared Function GetTaskBarHandle() As IntPtr
            Dim hWndTray As IntPtr = FindWindow("Shell_TrayWnd", Nothing)
            If hWndTray <> IntPtr.Zero Then
                hWndTray = FindWindowEx(hWndTray, IntPtr.Zero, "TrayNotifyWnd", Nothing)
                If hWndTray <> IntPtr.Zero Then
                    'hWndTray = FindWindowEx(hWndTray, IntPtr.Zero, "SysPager", Nothing)
                    If hWndTray <> IntPtr.Zero Then
                        'hWndTray = FindWindowEx(hWndTray, IntPtr.Zero, "ToolbarWindow32", Nothing)
                    End If
                End If
            End If
            Return hWndTray
        End Function

#End Region

        <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
        Public Shared Function FindWindow(ByVal lpClassName As String, ByVal lpWindowName As String) As IntPtr
        End Function

        <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
        Public Shared Function FindWindowEx(ByVal hwndParent As IntPtr, ByVal hwndChildAfter As IntPtr, ByVal lpszClass As String, ByVal lpszWindow As String) As IntPtr
        End Function

        <DllImport("User32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
        Public Shared Function GetWindowThreadProcessId(ByVal hWnd As IntPtr, <System.Runtime.InteropServices.Out()> ByRef lpdwProcessId As Int32) As Int32
        End Function

#Region "SendMessage Overloads"

        <DllImport("User32.dll", CharSet:=CharSet.Unicode, EntryPoint:="SendMessage")>
        Public Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Int32, ByVal wParam As IntPtr, ByVal lParam As System.Text.StringBuilder) As Int32
        End Function

        <DllImport("User32.dll", CharSet:=CharSet.Unicode, EntryPoint:="SendMessage")>
        Public Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Int32, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
        End Function

        <DllImport("User32.dll", CharSet:=CharSet.Unicode)>
        Public Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Int32, ByVal wParam As Int32, ByVal lParam As Int32) As Int32
        End Function

        <DllImport("User32.dll", CharSet:=CharSet.Unicode)>
        Public Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Int32, ByVal wParam As Int32, ByVal lParam As IntPtr) As Boolean
        End Function

#End Region

        <StructLayout(LayoutKind.Sequential)>
        Friend Structure ICONINFO
            ''' <summary>Specifies whether this structure defines an icon or a cursor. A value of TRUE specifies an icon; FALSE specifies a cursor.</summary>
            Public fIcon As Boolean

            ''' <summary>Specifies the x-coordinate of a cursor's hot spot. If this structure defines an icon, the hot spot is always in the center of the icon, and this member is ignored.</summary>
            Public xHotspot As Int32

            ''' <summary>Specifies the y-coordinate of the cursor's hot spot. If this structure defines an icon, the hot spot is always in the center of the icon, and this member is ignored.</summary>
            Public yHotspot As Int32 ' 

            ''' <summary>(HBITMAP) Specifies the icon bitmask bitmap. If this structure defines a black and white icon, this bitmask is formatted so that the upper half is the icon AND bitmask and the lower half is the icon XOR bitmask. Under this condition, the height should be an even multiple of two. If this structure defines a color icon, this mask only defines the AND bitmask of the icon.</summary>
            Public hbmMask As IntPtr ' 

            ''' <summary>(HBITMAP) Handle to the icon color bitmap. This member can be optional if this structure defines a black and white icon. The AND bitmask of hbmMask is applied with the SRCAND flag to the destination; subsequently, the color bitmap is applied (using XOR) to the estination by using the SRCINVERT flag.</summary>
            Public hbmColor As IntPtr
        End Structure

        <DllImport("user32.dll")>
        Shared Function GetIconInfo(ByVal hIcon As IntPtr, ByRef piconinfo As ICONINFO) As Boolean
        End Function
    End Class

    Public Class Kernel32
        <DllImport("kernel32.dll", SetLastError:=True)>
        <ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)>
        <SuppressUnmanagedCodeSecurity>
        Public Shared Function CloseHandle(ByVal hObject As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

#Region "OpenProcess"
        <DllImport("kernel32.dll", SetLastError:=True)>
        Public Shared Function OpenProcess(ByVal processAccess As ProcessAccessFlags, ByVal bInheritHandle As Boolean, ByVal processId As Int32) As IntPtr
        End Function

        <Flags>
        Public Enum ProcessAccessFlags As UInt32
            All = &H1F0FFF
            Terminate = &H1
            CreateThread = &H2
            VirtualMemoryOperation = &H8
            VirtualMemoryRead = &H10
            VirtualMemoryWrite = &H20
            DuplicateHandle = &H40
            CreateProcess = &H80
            SetQuota = &H100
            SetInformation = &H200
            QueryInformation = &H400
            QueryLimitedInformation = &H1000
            Synchronize = &H100000
        End Enum
#End Region

#Region "ReadProcessMemory Overloads"
        <DllImport("kernel32.dll", SetLastError:=True)>
        Public Shared Function ReadProcessMemory(
            ByVal hProcess As IntPtr,
            ByVal lpBaseAddress As IntPtr,
            ByVal lpBuffer As IntPtr,
            <MarshalAs(UnmanagedType.SysInt)> ByVal iSize As IntPtr,
            <MarshalAs(UnmanagedType.SysInt)> ByRef lpNumberOfBytesRead As IntPtr) As Boolean
        End Function

        <DllImport("kernel32.dll", SetLastError:=True, EntryPoint:="ReadProcessMemory")>
        Public Shared Function ReadProcessMemory(
            ByVal hProcess As IntPtr,
            ByVal lpBaseAddress As IntPtr,
            ByRef lpBuffer As Int32,
            <MarshalAs(UnmanagedType.SysInt)> ByVal iSize As IntPtr,
            <MarshalAs(UnmanagedType.SysInt)> ByRef lpNumberOfBytesRead As IntPtr) As Boolean
        End Function

        <DllImport("kernel32.dll", SetLastError:=True, EntryPoint:="ReadProcessMemory")>
        Public Shared Function ReadProcessMemory(
            ByVal hProcess As IntPtr,
            ByVal lpBaseAddress As IntPtr,
            ByRef lpBuffer As Int64,
            <MarshalAs(UnmanagedType.SysInt)> ByVal iSize As IntPtr,
            <MarshalAs(UnmanagedType.SysInt)> ByRef lpNumberOfBytesRead As IntPtr) As Boolean
        End Function

        <DllImport("kernel32.dll", SetLastError:=True, EntryPoint:="ReadProcessMemory")>
        Public Shared Function ReadProcessMemory(
            ByVal hProcess As IntPtr,
            ByVal lpBaseAddress As IntPtr,
            ByVal lpBuffer As TBBUTTON,
            <MarshalAs(UnmanagedType.SysInt)> ByVal iSize As IntPtr,
            <MarshalAs(UnmanagedType.SysInt)> ByRef lpNumberOfBytesRead As IntPtr) As Boolean
        End Function

        <DllImport("kernel32.dll", SetLastError:=True, EntryPoint:="ReadProcessMemory")>
        Public Shared Function ReadProcessMemory(
            ByVal hProcess As IntPtr,
            ByVal lpBaseAddress As IntPtr,
            ByVal lpBuffer As TrayData,
            <MarshalAs(UnmanagedType.SysInt)> ByVal iSize As IntPtr,
            <MarshalAs(UnmanagedType.SysInt)> ByRef lpNumberOfBytesRead As IntPtr) As Boolean
        End Function

#End Region

#Region "VirtualAllocEx"
        <DllImport("kernel32.dll", SetLastError:=True, ExactSpelling:=True)>
        Shared Function VirtualAllocEx(ByVal hProcess As IntPtr,
                                                                     ByVal lpAddress As IntPtr,
                                                                     <MarshalAs(UnmanagedType.SysInt)> ByVal dwSize As IntPtr,
                                                                     <MarshalAs(UnmanagedType.U4)> ByVal flAllocationType As AllocationType,
                                                                     ByVal flProtect As MemoryProtection) As IntPtr
        End Function

        <Flags>
        Public Enum AllocationType As UInt32
            Commit = &H1000
            Reserve = &H2000
            Decommit = &H4000
            Release = &H8000
            Reset = &H80000
            Physical = &H400000
            TopDown = &H100000
            WriteWatch = &H200000
            LargePages = &H20000000
        End Enum

        <Flags>
        Public Enum MemoryProtection As UInt32
            Execute = &H10
            ExecuteRead = &H20
            ExecuteReadWrite = &H40
            ExecuteWriteCopy = &H80
            NoAccess = &H1
            [ReadOnly] = &H2
            ReadWrite = &H4
            WriteCopy = &H8
            GuardModifierflag = &H100
            NoCacheModifierflag = &H200
            WriteCombineModifierflag = &H400
        End Enum

#End Region

#Region "VirtualFreeEx"
        <DllImport("kernel32.dll")>
        Public Shared Function VirtualFreeEx(ByVal hProcess As IntPtr,
                                                ByVal lpAddress As IntPtr,
                                                <MarshalAs(UnmanagedType.SysInt)> ByVal dwSize As IntPtr,
                                                ByVal dwFreeType As FreeType) As Boolean
        End Function

        ''' <summary>helper method to release memory allocated with VirtualAllocEx</summary>
        ''' <param name="lpAddress">ptr received from VirtualAllocEx</param>
        ''' <param name="hProcess">ptr to process received from OpenProcess</param>
        Public Shared Sub ReleaseVirtualAlloc(ByRef lpAddress As IntPtr, hProcess As IntPtr)
            If lpAddress <> IntPtr.Zero Then
                VirtualFreeEx(hProcess, lpAddress, Nothing, FreeType.RELEASE)
                lpAddress = IntPtr.Zero
            End If
        End Sub

        <Flags()>
        Public Enum FreeType As UInt32
            DECOMMIT = &H4000
            RELEASE = &H8000
        End Enum
#End Region
    End Class
End Class

There are two additional support classes defined below.

Friend Class TrayButtonInfo
    Public Property Icon As Icon
    Public Property Index As Int32
    Public Property CommandID As Int32
    Public Property DisplayText As String = String.Empty
    Public Property ProcessFound As Boolean = False
    Public Property ProcessName As String = String.Empty
    Public Property ProcessID As Int32
    Public Property State As NativeMethods.ToolBars.TBSTATE
End Class

...

Imports System.Runtime.InteropServices

<StructLayout(LayoutKind.Sequential)>
Friend Class TrayData
    Public hWnd As IntPtr
    Public uID As Int32
    Public uCallbackMessage As UInt32
    Public Reservered0 As UInt32
    Public Reservered1 As UInt32
    Public hIcon As IntPtr

    Public ReadOnly Property OwningProcess As Process
        Get
            Dim ret As Process = Nothing
            If hWnd <> IntPtr.Zero Then
                Dim processIDOfButton As Int32
                Dim threadId As Int32 = NativeMethods.User32.GetWindowThreadProcessId(hWnd, processIDOfButton)
                Try ' Process.GetProcessById can throw an exception if the id is not found
                    ret = Process.GetProcessById(processIDOfButton)
                Catch ex As Exception
                    ' eat it
                End Try
            End If
            Return ret
        End Get
    End Property
End Class

Now for the actual example code. This code prepare a list of TrayButtonInfo instances that can be searched to find a matching button. It also shows an example of this search and how to delete the button. I tried to explain the code in the comments, but feel free to ask about anything that is not clear.

Imports System.Runtime.InteropServices

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim retrievedNotificationButtons As List(Of TrayButtonInfo) = PrepareButtonInfoList()

        ' Now we have filled retrievedNotificationButtons with the information for all the buttons
        ' "Realtek HD Audio Manager" is just for testing on my machine.
        ' apply criteria for finding the button you want to delete
        ' note that the button can be restored by restarting Explorer
        Dim targetBtn As TrayButtonInfo = retrievedNotificationButtons.FirstOrDefault(Function(info) info.DisplayText.StartsWith("Realtek HD Audio Manager"))
        If targetBtn IsNot Nothing Then ' delete the button
            ' the following statement will delete the button if uncommented
            'Dim toolBarHwnd As IntPtr = NativeMethods.User32.GetNotificationAreaToolBarHandle()
            'NativeMethods.User32.SendMessage(toolBarHwnd, NativeMethods.ToolBars.TB_DELETEBUTTON, targetBtn.Index, 0)
        End If
        DataGridView1.DataSource = retrievedNotificationButtons
    End Sub

    Private Function PrepareButtonInfoList() As List(Of TrayButtonInfo)
        Dim ret As List(Of TrayButtonInfo)

        ' create a TBButon structure appropriate for the OS bitness
        Dim btn As TBBUTTON = TBBUTTON.CreateForOS

        Dim toolBarProcessHandle As IntPtr
        Dim toolBarButtonProcessMemoryPtr As IntPtr

        ' obtain window handle of the notification area toolbar
        Dim toolBarHwnd As IntPtr = NativeMethods.User32.GetNotificationAreaToolBarHandle()

        ' obtain id of process that owns the notification area toolbar, threadId is of no consequence
        Dim proccessIDOwningToolBar As Int32
        Dim threadId As Int32 = NativeMethods.User32.GetWindowThreadProcessId(toolBarHwnd, proccessIDOwningToolBar)

        Try
            ' obtain handle to the toolbar process that will allow allocating,reading and writing
            toolBarProcessHandle = NativeMethods.Kernel32.OpenProcess(NativeMethods.Kernel32.ProcessAccessFlags.VirtualMemoryOperation Or
                                                                                                    NativeMethods.Kernel32.ProcessAccessFlags.VirtualMemoryRead Or
                                                                                                    NativeMethods.Kernel32.ProcessAccessFlags.VirtualMemoryWrite,
                                                                                                    False, proccessIDOwningToolBar)

            ' allocate memory in the toolbar process to hold the TBButton
            ' need ReadWrite access due to TB_GETBUTTON writing to the allocated memory
            toolBarButtonProcessMemoryPtr = NativeMethods.Kernel32.VirtualAllocEx(toolBarProcessHandle,
                                                                                                                            Nothing,
                                                                                                                            btn.MarshalSize,
                                                                                                                            NativeMethods.Kernel32.AllocationType.Commit,
                                                                                                                            NativeMethods.Kernel32.MemoryProtection.ReadWrite)

            ' now we can request the toolbar to fill the allocated memory with a TBButton structure
            ' for each button in the notifiaction area.

            ' determine how many toolbar buttons are visible notification area contains
            Dim buttonCount As Int32 = NativeMethods.User32.SendMessage(toolBarHwnd, NativeMethods.ToolBars.TB_BUTTONCOUNT, 0, 0)
            ret = New List(Of TrayButtonInfo)(buttonCount)

            For btnIndex As Int32 = 0 To buttonCount - 1

                Dim btnInfo As New TrayButtonInfo With {.Index = btnIndex}
                ret.Add(btnInfo)

                If NativeMethods.User32.SendMessage(toolBarHwnd, NativeMethods.ToolBars.TB_GETBUTTON, btnIndex, toolBarButtonProcessMemoryPtr) Then
                    ' the toolbar owning process has successfully filled toolBarButtonProcessMemoryPtr

                    ' use a customize ReadProcessMemory that takes a TBButtonBase instance as the destination buffer
                    If NativeMethods.Kernel32.ReadProcessMemory(toolBarProcessHandle, toolBarButtonProcessMemoryPtr, btn, btn.MarshalSize, Nothing) Then
                        ' btn has been loaded, get the data

                        btnInfo.CommandID = btn.idCommand
                        btnInfo.State = btn.fsState

                        ' Note that per the documentation, TBBUTTON.iString can contain a pointer the Tooltip Text
                        ' In testing it does, but I have not found out how to determine the length of the string
                        ' without the length, a guess on the size of process memory to read must be made and that
                        ' seems unwise when accessing memory.
                        ' GetButtonText use the TB_GETBUTTONTEXT message.  This message's documentation indicates
                        ' that the retrieved text may differ from the Tooltip text.  As Tooltip text can be provided
                        ' via several mechanisms, this makes sense.
                        btnInfo.DisplayText = GetButtonText(btnInfo.CommandID, toolBarHwnd, toolBarProcessHandle)

                        ' get the process pointed to by dwData
                        ' according to: Code Project article: A tool to order the window buttons in your taskbar
                        ' https://www.codeproject.com/Articles/10497/A-tool-to-order-the-window-buttons-in-your-taskbar
                        ' this is a pointer to the window handle of the process that owns the button
                        ' while I can find no documentation that this is true, it appears to work

                        GetButtonData(btn, toolBarProcessHandle, btnInfo)

                    End If ' ReadProcessMemoryToTBButton
                End If
            Next

        Finally ' cleanup handles
            If toolBarProcessHandle <> IntPtr.Zero Then
                NativeMethods.Kernel32.ReleaseVirtualAlloc(toolBarButtonProcessMemoryPtr, toolBarProcessHandle)
            End If
            If toolBarProcessHandle <> IntPtr.Zero Then
                NativeMethods.Kernel32.CloseHandle(toolBarProcessHandle)
                toolBarProcessHandle = IntPtr.Zero
            End If
        End Try

        Return ret
    End Function

    Private Function GetButtonText(CommandID As Int32, toolBarWindowHandle As IntPtr, toolBarProcessHandle As IntPtr) As String
        Dim ret As String = String.Empty
        '1st determine the number of characters to retrieve
        Dim lenText As Int32 = NativeMethods.User32.SendMessage(toolBarWindowHandle, NativeMethods.ToolBars.TB_GETBUTTONTEXT, New IntPtr(CommandID), IntPtr.Zero).ToInt32
        If lenText > 0 Then
            Dim ptrToText As IntPtr
            Dim localBuffer As IntPtr
            Try
                Dim numBytes As New IntPtr((lenText * 2) + 1) ' Unicode 2 bytes per character + 1 for null terminator
                'need to allocate the string in the process space
                ptrToText = NativeMethods.Kernel32.VirtualAllocEx(toolBarProcessHandle,
                                                                                        Nothing,
                                                                                        numBytes,
                                                                                        NativeMethods.Kernel32.AllocationType.Commit,
                                                                                        NativeMethods.Kernel32.MemoryProtection.ReadWrite)

                Dim receivedLen As Int32 = NativeMethods.User32.SendMessage(toolBarWindowHandle, NativeMethods.ToolBars.TB_GETBUTTONTEXT, New IntPtr(CommandID), ptrToText).ToInt32
                localBuffer = Marshal.AllocHGlobal(numBytes) ' allocate local buffer to receive bytes from process space
                If NativeMethods.Kernel32.ReadProcessMemory(toolBarProcessHandle, ptrToText, localBuffer, numBytes, Nothing) Then
                    ret = Marshal.PtrToStringUni(localBuffer)
                End If

            Finally ' release handles to unmanaged memory
                NativeMethods.Kernel32.ReleaseVirtualAlloc(ptrToText, toolBarProcessHandle)
                NativeMethods.FreeHGlobal(localBuffer)
            End Try
        End If
        Return ret
    End Function

    Private Sub GetButtonData(btn As TBBUTTON, toolBarProcessHandle As IntPtr, btnInfo As TrayButtonInfo)

        Dim data As TrayData
        Try
            data = New TrayData()
            Dim dataMarshalSize As New IntPtr(Marshal.SizeOf(data))
            If NativeMethods.Kernel32.ReadProcessMemory(toolBarProcessHandle, btn.DwData, data, dataMarshalSize, Nothing) Then

                ' use GetIconInfo to validate icon handle
                Dim iconInfo As New NativeMethods.User32.ICONINFO
                If NativeMethods.User32.GetIconInfo(data.hIcon, iconInfo) Then
                    btnInfo.Icon = Icon.FromHandle(data.hIcon)
                End If

                Using p As Process = data.OwningProcess
                    If p IsNot Nothing Then
                        btnInfo.ProcessFound = True
                        btnInfo.ProcessID = p.Id
                        btnInfo.ProcessName = p.ProcessName
                    End If
                End Using
            End If
        Catch ex As Exception
            Debug.Print(ex.Message)
        End Try

    End Sub

End Class

One thing that I observed in limited testing is that intermitantly, addition characters are post-pended onto the retrieved button text. I tried zeroing the memory but that did not help. Hence the use of the StartsWith when searching for a target button to delete.