.netvb.netwinapimarshallingreadprocessmemory

TBBUTTON struct not working with SendMessage


I'm trying to send the TB_GETBUTTON message to get info about the buttons inside this Toolbar control marked in red color:

enter image description here

( The System tray notification area )

The problem is that when I send the message, the Explorer refreshes itself, is very annonying because all the desktop refreshes, and also I'm not getting the proper values with the TBBUTTON structure definition that I'm using, I tested three different definitions, those with unions from pinvoke.net, and the one published here by @David Heffernan.

I'm running the code below in a 64-Bit Windows 10 and with the x64 config set in my project properties.

How can I fix the struct and the annonying system's refresh?.

These are the relevant definitions I'm using:

Const WM_USER As Integer = &H400
Const TB_BUTTONCOUNT As Integer = (WM_USER + 24)
Const TB_GETBUTTON As Integer = (WM_USER + 23)
' Toolbar values are defined in "CommCtrl.h" Windows SDK header files.

<StructLayout(LayoutKind.Sequential)>
Public Structure TBBUTTON64
    Public iBitmap As Integer
    Public idCommand As Integer
    Public fsState As Byte
    Public fsStyle As Byte
    <MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst:=6)> ' 6 on x64
    Public bReserved As Byte()
    Public dwData As UIntPtr
    Public iString As IntPtr
End Structure

<DllImport("User32.dll", SetLastError:=True)>
Public Shared Function SendMessage(ByVal hwnd As IntPtr,
                                   ByVal msg As Integer,
                                   ByVal wParam As IntPtr,
                                   ByVal lParam As IntPtr
) As IntPtr
End Function

<SuppressUnmanagedCodeSecurity>
<DllImport("User32.dll", SetLastError:=True, CharSet:=CharSet.Auto, BestFitMapping:=False, ThrowOnUnmappableChar:=True)>
Public Shared Function FindWindow(ByVal lpClassName As String,
                                  ByVal lpWindowName As String
) As IntPtr
End Function

<SuppressUnmanagedCodeSecurity>
<DllImport("User32.dll", SetLastError:=True, CharSet:=CharSet.Auto, BestFitMapping:=False, ThrowOnUnmappableChar:=True)>
Public Shared Function FindWindowEx(ByVal hwndParent As IntPtr,
                                    ByVal hwndChildAfter As IntPtr,
                                    ByVal strClassName As String,
                                    ByVal strWindowName As String
) As IntPtr
End Function

And this is the code to test them:

Dim tskBarHwnd As IntPtr =
    NativeMethods.FindWindow("Shell_TrayWnd", Nothing)

Dim systrayBarHwnd As IntPtr =
    NativeMethods.FindWindowEx(tskBarHwnd, IntPtr.Zero, "TrayNotifyWnd", Nothing)

Dim sysPagerHwnd As IntPtr =
    NativeMethods.FindWindowEx(systrayBarHwnd, IntPtr.Zero, "SysPager", Nothing)

Dim ntfyBarHwnd As IntPtr =
    NativeMethods.FindWindowEx(sysPagerHwnd, IntPtr.Zero, "ToolbarWindow32", Nothing)

Dim buttonCount As Integer =
    NativeMethods.SendMessage(ntfyBarHwnd, TB_BUTTONCOUNT, IntPtr.Zero, IntPtr.Zero).ToInt32()

For index As Integer = 0 To (buttonCount - 1)

    Dim btInfo As New TBBUTTON64
    Dim alloc As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(GetType(TBBUTTON64)))

    Marshal.StructureToPtr(btInfo, alloc, fDeleteOld:=True)
    NativeMethods.SendMessage(ntfyBarHwnd, TB_GETBUTTON, New IntPtr(index), alloc)
    Marshal.PtrToStructure(Of TBBUTTON64)(alloc)
    Marshal.FreeHGlobal(alloc)

    ' This line always prints "00000"
    Console.WriteLine(btInfo.iBitmap &
                      btInfo.fsState &
                      btInfo.fsStyle &
                      btInfo.idCommand &
                      btInfo.iString.ToInt32())
Next index

UPDATE (25-MARCH-2019)

I came back on this need because now I need to hide a sys-tray icon of a external app. So I started investigating again these days...

Please note the commentary did by @Remy Lebeau:

TB_GETBUTTON can be sent to another process. You just have to give it the address of a TBBUTTON that exists in the target process's address space. Use VirtualAllocEx() to allocate it, then send the message, then use ReadProcessMemory() to read its content.

I'm not sure at all how to reproduce the steps that he gave, but after investigating a lot I found a code that apparently does that, it seem to read the process memory to retrieve the icon text:

However, it is written in C#, using unsafe and fixed keywords and I'm not sure how to translate it entirely in a proper way. Also, and just as a personal opinion, I feel that code is not simplified in any way, and I see bad design practices with var nomenclatures like "b", "b2" and "b4" which I don't get their purpose at all...

And, if helpful, I also found this in C/C++:

In resume, what I'm asking for is to reproduce in VB.NET code the solution pointed by @Remy Lebeau, or to translate and simplify the C# code that I mentioned.

This is the best I can do by the moment with the help of a code converter, note that this code IS NOT working (it is broken / not fully converted to VB.NET):

Private Function GetTBButton(ByVal hToolbar As IntPtr, ByVal i As Integer, ByRef tbButton As ToolBarButton64, ByRef text As String, ByRef ipWindowHandle As IntPtr) As Boolean

    ' One page
    Const BUFFER_SIZE As Integer = &H1000

    Dim localBuffer(BUFFER_SIZE - 1) As Byte

    Dim processId As Integer = 0
    Dim threadId As Integer = NativeMethods.GetWindowThreadProcessId(hToolbar, processId)

    Dim hProcess As IntPtr = NativeMethods.OpenProcess(ProcessAccessRights.AllAccess, False, processId)
    If hProcess = IntPtr.Zero Then
        Debug.Assert(False)
        Return False
    End If

    Dim ipRemoteBuffer As UIntPtr = NativeMethods.VirtualAllocEx(hProcess, IntPtr.Zero, New UIntPtr(BUFFER_SIZE), MemoryAllocationType.Commit, MemoryProtectionOptions.ReadWrite)

    If ipRemoteBuffer = UIntPtr.Zero Then
        Debug.Assert(False)
        Return False
    End If

    ' TBButton
    'INSTANT VB TODO TASK: There is no equivalent to a 'fixed' block in VB:
    '       fixed (TBBUTTON* pTBButton = &tbButton)
    Dim ipTBButton As New IntPtr(pTBButton)

    Dim b As Integer = CInt(Math.Truncate(NativeMethods.SendMessage(hToolbar, TB.GETBUTTON, CType(i, IntPtr), ipRemoteBuffer)))
    If b = 0 Then
        Debug.Assert(False)
        Return False
    End If

    ' this is fixed
    Dim dwBytesRead As Int32 = 0
    Dim ipBytesRead As New IntPtr(& dwBytesRead)

    'INSTANT VB TODO TASK: There is no VB equivalent to 'sizeof':
    Dim b2 As Boolean = NativeMethods.ReadProcessMemory(hProcess, ipRemoteBuffer, ipTBButton, New UIntPtr(CUInt(Math.Truncate(Marshal.SizeOf(tbButton)))), ipBytesRead)

    If Not b2 Then
        Debug.Assert(False)
        Return False
    End If
    'INSTANT VB NOTE: End of the original C# 'fixed' block.

    ' button text
    'INSTANT VB TODO TASK: There is no equivalent to a 'fixed' block in VB:
    '       fixed (byte* pLocalBuffer = localBuffer)
    Dim ipLocalBuffer As New IntPtr(pLocalBuffer)

    Dim chars As Integer = CInt(Math.Truncate(NativeMethods.SendMessage(hToolbar, TB.GETBUTTONTEXTW, CType(tbButton.idCommand, IntPtr), ipRemoteBuffer)))
    If chars = -1 Then
        Debug.Assert(False)
        Return False
    End If

    ' this is fixed
    Dim dwBytesRead As Integer = 0
    Dim ipBytesRead As New IntPtr(& dwBytesRead)

    Dim b4 As Boolean = NativeMethods.ReadProcessMemory(hProcess, ipRemoteBuffer, ipLocalBuffer, New UIntPtr(BUFFER_SIZE), ipBytesRead)

    If Not b4 Then
        Debug.Assert(False)
        Return False
    End If

    text = Marshal.PtrToStringUni(ipLocalBuffer, chars)

    If text = " " Then
        text = String.Empty
    End If
    'INSTANT VB NOTE: End of the original C# 'fixed' block.

    NativeMethods.VirtualFreeEx(hProcess, ipRemoteBuffer, UIntPtr.Zero, MemoryFreeType.Release)
    NativeMethods.CloseHandle(hProcess)

    Return True

End Function

Which in theory it would be called like this:

Dim sysTrayHwnd As IntPtr = NotificationAreaUtil.Hwnd
Dim btIndex As Integer = 0
Dim tbButton As New ToolBarButton64() ' TBBUTTON struct for a x64 process
Dim text As String
Dim ipHwnd As IntPtr

GetTBButton(sysTrayHwnd, btIndex , tbButton, text, ipHwnd)

UPDATE (13-APRIL-2019)

I tried to convert the C/C++ solution provided by @RbMm in this answer, however, I'm getting a related memory error through a System.AccessViolationException when I try to marshal the TBBUTTON structure at this line:

...
Dim ptbi As ToolBarButtonInfo = Marshal.PtrToStructure(Of ToolBarButtonInfo)(remoteBaseAddress)
...

Note that to ensure that the root of the problem was not a bad TBBUTTONINFOW definition from my side, instead of using Marshal.PtrToStructure() I just used the Marshal.ReadInt32() function to try read a single field on the specific offset, and I get the same kind of error.

Probably I'm doing something wrong because I don't manage C/C++. Here is my attempt of code conversion in VB.NET:

( I will omit to share the P/Invoke definitions to simplify the code example )

Dim sysTray As IntPtr = NotificationAreaUtil.Hwnd
Dim pid As Integer
If (NativeMethods.GetWindowThreadProcessId(sysTray, pid) <> 0) Then
    Dim hProcess As IntPtr = NativeMethods.OpenProcess(ProcessAccessRights.VirtualMemoryOperation, False, pid)
    If (hProcess <> IntPtr.Zero) Then

        Dim hSection As IntPtr
        Dim pageSize As ULong = 81920 ' LARGE_INTEGER
        Dim viewSize As IntPtr ' SIZE_T
        Dim baseAddress As IntPtr ' PVOID
        Dim remoteBaseAddress As IntPtr ' PVOID

        If (NativeMethods.NtCreateSection(hSection, SectionAccessRights.AllAccess,
                                          IntPtr.Zero, pageSize,
                                          MemoryProtectionOptions.ReadWrite,
                                          SectionAttributes.Commit,
                                          IntPtr.Zero) = NTStatus.SUCCESS) Then

            If (NativeMethods.NtMapViewOfSection(hSection, NativeMethods.GetCurrentProcess(), baseAddress,
                                                 IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, viewSize,
                                                 ViewOfSectionInherit.ViewUnmap,
                                                 MemoryAllocationType.Default,
                                                 MemoryProtectionOptions.ReadWrite) = NTStatus.SUCCESS) Then


                If (NativeMethods.NtMapViewOfSection(hSection, hProcess, remoteBaseAddress,
                                                     IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, viewSize,
                                                     ViewOfSectionInherit.ViewUnmap,
                                                     MemoryAllocationType.Default,
                                                     MemoryProtectionOptions.ReadWrite) = NTStatus.SUCCESS) Then

                    Dim btIndex As Integer = 3 ' Button index from which I'll try to retrieve a valid TBBUTTONINFOW struct.
                    ' Const TBIF_BYINDEX As Integer = &H80000000
                    ' Const TBIF_TEXT As Integer = &H2 

                    If (NativeMethods.SendMessage(sysTray, ToolbarMessages.GetButtonInfoUnicode, New IntPtr(btIndex), remoteBaseAddress) <> IntPtr.Zero) Then

                        ' AT THIS LINE THROWS THE ACCESSVIOLATIONEXCEPTION.
                        Dim ptbi As ToolBarButtonInfo = Marshal.PtrToStructure(Of ToolBarButtonInfo)(remoteBaseAddress)

                        Console.WriteLine(ptbi.CommandId)
                        Console.WriteLine(Marshal.PtrToStringUni(ptbi.Text))

                    Else
                        Throw New Win32Exception(Marshal.GetLastWin32Error())

                    End If

                    NativeMethods.NtUnmapViewOfSection(hProcess, remoteBaseAddress)
                End If

                NativeMethods.NtUnmapViewOfSection(NativeMethods.GetCurrentProcess(), baseAddress)
            End If

            NativeMethods.NtClose(hSection)
        End If

        NativeMethods.CloseHandle(hProcess)
    End If

End If

And here is a code conversion(on-the-fly and not tested) to C# of the code above:

IntPtr sysTray = NotificationAreaUtil.Hwnd;
int pid = 0;
if (NativeMethods.GetWindowThreadProcessId(sysTray, pid) != 0)
{
    IntPtr hProcess = NativeMethods.OpenProcess(ProcessAccessRights.VirtualMemoryOperation, false, pid);
    if (hProcess != IntPtr.Zero)
    {

        IntPtr hSection = System.IntPtr.Zero;
        ulong pageSize = 81920; // LARGE_INTEGER
        IntPtr viewSize = System.IntPtr.Zero; // SIZE_T
        IntPtr baseAddress = System.IntPtr.Zero; // PVOID
        IntPtr remoteBaseAddress = System.IntPtr.Zero; // PVOID

        if (NativeMethods.NtCreateSection(hSection, SectionAccessRights.AllAccess, IntPtr.Zero, pageSize, MemoryProtectionOptions.ReadWrite, SectionAttributes.Commit, IntPtr.Zero) == NTStatus.SUCCESS)
        {

            if (NativeMethods.NtMapViewOfSection(hSection, NativeMethods.GetCurrentProcess(), baseAddress, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, viewSize, ViewOfSectionInherit.ViewUnmap, MemoryAllocationType.Default, MemoryProtectionOptions.ReadWrite) == NTStatus.SUCCESS)
            {


                if (NativeMethods.NtMapViewOfSection(hSection, hProcess, remoteBaseAddress, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, viewSize, ViewOfSectionInherit.ViewUnmap, MemoryAllocationType.Default, MemoryProtectionOptions.ReadWrite) == NTStatus.SUCCESS)
                {

                    int btIndex = 3; // Button index from which I'll try to retrieve a valid TBBUTTONINFOW struct.

                    if (NativeMethods.SendMessage(sysTray, ToolbarMessages.GetButtonInfoUnicode, new IntPtr(btIndex), remoteBaseAddress) != IntPtr.Zero)
                    {

                        // AT THIS LINE THROWS THE ACCESSVIOLATIONEXCEPTION.
                        ToolBarButtonInfo ptbi = Marshal.PtrToStructure<ToolBarButtonInfo>(remoteBaseAddress);

                        Console.WriteLine(ptbi.CommandId);
                        Console.WriteLine(Marshal.PtrToStringUni(ptbi.Text));

                    }
                    else
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error());

                    }

                    NativeMethods.NtUnmapViewOfSection(hProcess, remoteBaseAddress);
                }

                NativeMethods.NtUnmapViewOfSection(NativeMethods.GetCurrentProcess(), baseAddress);
            }

            NativeMethods.NtClose(hSection);
        }

        NativeMethods.CloseHandle(hProcess);
    }

}

Solution

  • 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://learn.microsoft.com/en-us/windows/desktop/api/commctrl/ns-commctrl-tbbutton
    ' For info on native type size: Windows Data Types
    '   https://learn.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://learn.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.