I'm trying to send the TB_GETBUTTON message to get info about the buttons inside this Toolbar control marked in red color:
( 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);
}
}
You can't send the
TB_GETBUTTON
message to windows in another process and get valid results, becauseTB_GETBUTTON
needs to manipulate a pointer to a struct, and window messages that do that have to marshal the struct. Very few windows messages do that.TB_GETBUTTONCOUNT
works because it doesn't need to be marshaled.we can send
TB_GETBUTTONINFOW
for get the button text. if we know what text must be for button - we can detect it by text and delete it viaTB_DELETEBUTTON
. the pointer toTBBUTTONINFOW
of course must be valid in context of process, which own the hwnd. the pszText insideTBBUTTONINFOW
also of course must be valid in context of target process.for implement this - the best create section and map it in self and target process. this let easy manipulate with memory.
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
Next is my native methods class. This class declares various function overloads to let the interop-marshaling system perform the necessary allocations/conversions.
There are two additional support classes defined below.
...
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.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.