.netwindowsvb.netwinapicom

How to get the icon for the "Scan with Windows Defender..." shell context menu item?


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.


Solution

  • 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.