Dot-Net

TBBUTTON 結構不適用於 SendMessage

  • May 1, 2019

我正在嘗試發送**TB_GETBUTTON**消息以獲取有關此工具欄控制項內以紅色標記的按鈕的資訊:

在此處輸入圖像描述

(系統托盤通知區域)

問題是當我發送消息時,資源管理器會自行刷新,這很煩人,因為所有桌面都會刷新,而且我使用的**TBBUTTON結構定義沒有得到正確的值,我測試了三個不同的定義,來自pinvoke.net的工會,以及@David Heffernan在此**發布的工會。

我在 64 位 Windows 10 中執行下面的程式碼,並在我的項目屬性中設置了 x64 配置。

如何修復結構和煩人的系統刷新?

這些是我正在使用的相關定義:

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

這是測試它們的程式碼:

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

更新(2019 年 3 月 25 日)

我回到了這個需求,因為現在我需要隱藏外部應用程序的系統托盤圖示。所以我這幾天又開始調查了……

請注意**@Remy Lebeau**的評論:

TB_GETBUTTON 可以發送到另一個程序。您只需為其提供目標程序地址空間中存在的 TBBUTTON 的地址。使用 VirtualAllocEx() 分配它,然後發送消息,然後使用 ReadProcessMemory() 讀取其內容。

我完全不確定如何重現他給出的步驟,但經過大量調查後,我發現了一個顯然可以做到這一點的程式碼,它似乎讀取了程序記憶體來檢索圖示文本:

但是,它是用 C# 編寫的,使用unsafeandfixed關鍵字,我不確定如何以正確的方式完全翻譯它。另外,就個人觀點而言,我覺得程式碼沒有以任何方式簡化,並且我看到使用“b”、“b2”和“b4”等 var 命名法的糟糕設計實踐,我不明白它們的目的全部…

而且,如果有幫助,我還在 C/C++ 中找到了這個:

在簡歷中,我要求的是在 VB.NET 程式碼中重現**@Remy Lebeau**指出的解決方案,或者翻譯和簡化我提到的 C# 程式碼。

這是我目前在程式碼轉換器的幫助下能做的最好的事情,請注意此程式碼不起作用(它已損壞/未完全轉換為 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

理論上它會這樣稱呼:

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)

更新(2019 年 4 月 13 日)

我嘗試在答案中轉換@RbMm 提供的 C/C++ 解決方案,但是,當我嘗試在此行編組 TBBUTTON 結構時,我通過System.AccessViolationException收到相關的記憶體錯誤:

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

請注意,為了確保問題的根源不是我這邊的錯誤 TBBUTTONINFOW 定義,Marshal.PtrToStructure()我只是使用該Marshal.ReadInt32()函式嘗試讀取特定偏移量上的單個欄位,而不是使用該函式,我得到了同樣的錯誤。

可能我做錯了什麼,因為我不管理 C/C++。這是我在 VB.NET 中進行程式碼轉換的嘗試:

(我將省略共享 P/Invoke 定義以簡化程式碼範例)

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

這是上面程式碼到 C# 的程式碼轉換(即時且未經測試):

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);
   }

}

編輯:最初發布的程式碼錯誤地假設 Intptr 可以被強制跨越位數邊界工作。該錯誤已得到糾正。

如果可能,我還擴展了托盤按鈕數據檢索以檢索按鈕圖示。程式碼項目文章:Shell Tray Info - 排列系統托盤圖示被用作編寫此 .Net 實現的基礎。

請注意,檢索到的 Icon 實例不擁有它們各自的句柄,因為這些句柄仍歸作業系統所有。


TBBUTTON 結構有點痛苦,因為欄位fsStyle根據作業系統位數(32/64)改變其大小。以下適用於 x86 和 x64 編譯的系統 Win 10(64 位)。對於長度和明顯的格式,我深表歉意(我使用 2 個字元製表位,所以確實混淆了多個製表符的格式),但我希望展示範例中使用的所有程式碼。

首先是我的 TBBUTTON 聲明。它被定義為基類和 32 位的類和 64 位作業系統的類。基類有一個工廠方法 ( TBBUTTON.CreateForOS) 來返回正確的實現。我通過聲明字節佔位符來處理不同的欄位大小以接收編組結構並在需要時重新組裝。

導入 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

接下來是我的本地方法類。此類聲明了各種函式重載,以讓互操作封送系統執行必要的分配/轉換。

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

下面定義了兩個額外的支持類。

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

現在是實際的範常式式碼。此程式碼準備了一個TrayButtonInfo實例列表,可以搜尋這些實例以找到匹配的按鈕。它還顯示了此搜尋的範例以及如何刪除按鈕。我試圖在評論中解釋程式碼,但請隨時詢問任何不清楚的地方。

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

我在有限的測試中觀察到的一件事是,附加字元會間歇性地附加到檢索到的按鈕文本上。我嘗試將記憶體歸零,但這沒有幫助。因此StartsWith在搜尋要刪除的目標按鈕時使用。

引用自:https://stackoverflow.com/questions/39666413