I'm trying to convert the HMENU
I get from IContextMenu.QueryContextMenu()
to a WPF ContextMenu
, so I can have a context menu with the styles I defined in my application instead of the plain old Windows menu style.
Most of it works, except for two items "Scan with Windows Defender..." and the Google Drive "Synchronize or backup this folder", which don't return their icons in the MENUITEMINFO
structure I get from GetMenuItemInfo()
. Icons do work for the other items.
I tried looking at the MIIM_DATA
too, but found nothing.
This is my code:
Public Function GetContextMenu(items As IEnumerable(Of Item)) As ContextMenu
Dim pidls(items.Count - 1) As IntPtr
Dim lastpidls(items.Count - 1) As IntPtr
For i = 0 To items.Count - 1
Functions.SHGetIDListFromObject(Marshal.GetIUnknownForObject(items(i).ShellItem2), pidls(i))
lastpidls(i) = Functions.ILFindLastID(pidls(i))
Next
Dim ptr As IntPtr
Me.ShellFolder.GetUIObjectOf(IntPtr.Zero, lastpidls.Length, lastpidls, GetType(IContextMenu).GUID, 0, ptr)
Dim contextMenu As IContextMenu = Marshal.GetTypedObjectForIUnknown(ptr, GetType(IContextMenu))
Marshal.QueryInterface(ptr, GetType(IContextMenu2).GUID, ptr)
Dim contextMenu2 As IContextMenu2, contextMenu3 As IContextMenu3
If Not ptr = IntPtr.Zero Then
contextMenu2 = CType(Marshal.GetObjectForIUnknown(ptr), IContextMenu2)
End If
Marshal.QueryInterface(ptr, GetType(IContextMenu3).GUID, ptr)
If Not ptr = IntPtr.Zero Then
contextMenu3 = CType(Marshal.GetObjectForIUnknown(ptr), IContextMenu3)
End If
Dim hMenu As IntPtr = Functions.CreatePopupMenu()
contextMenu.QueryContextMenu(hMenu, 0, 0, UInt32.MaxValue, CMF.CMF_NORMAL Or CMF.CMF_EXTENDEDVERBS)
If _firstContextMenuCall Then
' somehow very first call doesn't return all items
Functions.DestroyMenu(hMenu)
hMenu = Functions.CreatePopupMenu()
contextMenu.QueryContextMenu(hMenu, 0, 0, UInt32.MaxValue, CMF.CMF_NORMAL Or CMF.CMF_EXTENDEDVERBS)
End If
Dim getMenu As Func(Of IntPtr, List(Of Control)) =
Function(hMenu2 As IntPtr) As List(Of Control)
If Not contextMenu2 Is Nothing Then
contextMenu2.HandleMenuMsg(WM.INITMENUPOPUP, hMenu2, IntPtr.Zero)
End If
If Not contextMenu3 Is Nothing Then
Dim ptr2 As IntPtr
contextMenu3.HandleMenuMsg2(WM.INITMENUPOPUP, hMenu2, IntPtr.Zero, ptr2)
End If
Dim result As List(Of Control) = New List(Of Control)()
For i = 0 To Functions.GetMenuItemCount(hMenu2) - 1
Dim mii As MENUITEMINFO
mii.cbSize = CUInt(Marshal.SizeOf(mii))
mii.fMask = MIIM.MIIM_STRING
mii.dwTypeData = New String(" "c, 2048)
mii.cch = mii.dwTypeData.Length
Functions.GetMenuItemInfo(hMenu2, i, True, mii)
Dim header As String = mii.dwTypeData.Substring(0, mii.cch)
mii.fMask = MIIM.MIIM_BITMAP Or MIIM.MIIM_SUBMENU Or MIIM.MIIM_FTYPE
Functions.GetMenuItemInfo(hMenu2, i, True, mii)
Dim bitmapSource As BitmapSource
If Not IntPtr.Zero.Equals(mii.hbmpItem) Then
bitmapSource = Interop.Imaging.CreateBitmapSourceFromHBitmap(mii.hbmpItem, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions())
Else
bitmapSource = Nothing
End If
If mii.fType = MFT.SEPARATOR Then
result.Add(New Separator())
Else
Dim menuItem As MenuItem = New MenuItem() With {
.Header = header.Replace("&", "_"),
.Icon = New Image() With {.Source = bitmapSource}
}
If mii.hSubMenu Then
Dim subMenu As List(Of Control) = getMenu(mii.hSubMenu)
For Each subMenuItem In subMenu
menuItem.Items.Add(subMenuItem)
Next
End If
result.Add(menuItem)
End If
Next
Return result
End Function
Dim result2 As List(Of Control) = getMenu(hMenu)
Dim menuResult As ContextMenu = New ContextMenu()
For Each item In result2
menuResult.Items.Add(item)
Next
Functions.DestroyMenu(hMenu)
Return menuResult
End Function
EDIT: they are not owner-drawn. I've hooked up TrackPopupMenuEx()
with the WndProc
and checked the messages flowing though, and there is no WM_DRAWITEM
or WM_MEASUREITEM
message.
I just found out how to get the icons for these items.
This is the MENUITEMINFO
structure:
<StructLayout(LayoutKind.Sequential)>
Public Structure MENUITEMINFO
Public cbSize As UInteger
Public fMask As UInteger
Public fType As UInteger
Public fState As MFS
Public wID As Integer
Public hSubMenu As IntPtr
Public hbmpChecked As IntPtr
Public hbmpUnchecked As IntPtr
Public dwItemData As IntPtr
Public dwTypeData As String
Public cch As UInteger
Public hbmpItem As IntPtr
End Structure
I just found out it has 3 icons inside: hbmpItem
(the only one I was checking at first), but also hbmpChecked
and hbmpUnchecked
. Apparently some context menu handlers use hbmpUnchecked
instead of hbmpItem
. I found this in the sources of the 7-zip context menu handler.