SetMenuItemImage and GetMenuItemImage [Windows only]

Share your advanced PureBasic knowledge/code with the community.
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

SetMenuItemImage and GetMenuItemImage [Windows only]

Post by Zapman »

[Edit 02 feb. 2025]This code has been completely rewritten to be more powerful. Some bugs have been fixed. To get a better version, please go to https://www.purebasic.fr/english/viewtopic.php?t=86194

Have you dreamed of a SetMenuItemImage function?
Have you dreamed of displaying icons for the main menu bar titles?
Have you dreamed to change the icon of a menu item without having to distroy and rebuild the menu?
Have you dreamed to retreive the ImageID of a menu item without having to store it by yourself?
Have you dreamed of doing all that without having to go through an 'OwnerDrawn' menu?
This code is for you!

Code: Select all

;
; *****************************************************************************
;
;                       Set-GetImageTo-FromMenuItem.pbi
;               Release 1.1 - Zapman - Jan 2025 - Windows only
;
;                         Thanks to the forum members
;                          for testing and debugging.
;                            (idle is a real boss)
;
;  This file should be saved under the name "Set-GetImageTo-FromMenuItem.pbi".
;
;     This library offers functions to set of get images to/from menu items.
; Its goal is to overcome the limitations of the current PureBasic (6.12) functions:
; • It is not possible to change the icon of a menu item after assigning it.
;   You have to destroy the menu and rebuild it entirely to be able to change one
;   of its icons.
; • It is not possible to retrieve the ImageID of a menu icon. You have to memorize
;   it in a variable at the time you assign it to be able to retrieve it later.
; • It is not possible to assign an icon to an item of the main menu of a window,
;   if this item has subitems.
;
; Now, you can do all of that!
;
;
;           The last part of this file includes a demonstration code.
;
;                The functions of this library are duplicated
;                in the SetGadgetItemEx.pbi Zapman library.
;    So, if you include SetGadgetItemEx.pbi in your project, you don't need to
;                   (and should not) include also this one.
;
;
; *****************************************************************************
;-                     1. FIRST PART: SOME EXPLANATIONS
; _____________________________________________________________________________
;
;             A brief description of how PureBasic handles menus
;                and the hassle of trying to manipulate them:
; _____________________________________________________________________________
; • Windows has a native way to handle menu icons: To assign an icon, you have
;   to enter its ImageID in the 'hbmpItem' field of the 'MenuItemInfo' Structure,
;   then call the 'SetMenuItemInfo()' function.
; • But, for some reason, the PureBasic Team chose not to use this option.
;   When you look at the MenuItemInfo Structure in the PureBasic 6.12 Structure
;    viewer, you can see that the 'hbmpItem' field is not even there.
;   There may be a historical reason for this: this field may not have existed
;   when the team registered this structure... and at the time when it was decided
;   to add icons to the menus.
; • The PureBasic way to do this is to store the ImageID of the menu icon in
;   a proprietary Structure, then store the address of that Structure in the
;   'dwItemData' field of 'MenuItemInfo'.
; • This creates a first difficulty: when we want to find or modify the ImageID,
;   we must manage a proprietary structure on which we have no information.
; • There is a second difficulty: since PureBasic has created this proprietary
;   structure, it also stores the text of the menu item in it.
; • But this structure is only created and used when a menu has icons.
;   Otherwise, the classic Windows method is used to store the menu text.
; • Therefore, the way to find the text of an item is not the same depending on
;   whether the menu was created with 'CreateMenu()' or 'CreateImageMenu()'.
; • It's exactly the same for 'CreatePopupMenu()' and CreatePopupImageMenu()'.
; • It seems that the PureBasic team's choice had other consequences since it is
;   impossible to assign an icon to a menu item when this item is an entry for a
;   submenu. For the main menu of a window, it is therefore impossible to assign
;   an icon to its titles, from the moment these titles are provided with subitems.
; • I also noticed a very strange (and very, very peculiar) bug: when you assign
;   the 'OwnerDrawn' style to a menu created with CreateMenu() and try to access
;   the text of one of its items using the GetGadgetItemText() function,
;   it doesn't always work. It doesn't work,in particular, when you are handling
;   the #WM_INITMENU And #WM_INITMENUPOPUP messages of the main window and call
;   GetGadgetItemText() after having set the style to OwnerDrawn.
; _____________________________________________________________________________
;
;          The path chosen to overcome the difficulties encountered
; _____________________________________________________________________________
;
; The approach taken by the functions in this library is to transparently deal with
; both methods of saving information for menu items (Windows and PureBasic)
; depending on the needs and how the menu was created (with CreateMenu/CreatePopupMenu,
; or CreateImageMenu/CreatePopupMenu).
; • When the menu was created with CreateMenu() or CreatePopupMenu(), the icons
;   of the menu items are saved in the Windows way. Because, yes!, you can add icons
;   to a menu that was NOT created with CreateImageMenu().
; • For the main menu of a window (which never has icons when created in PureBasic
;   and has subitems), the Windows data saving method is also used.
; • In all other cases, the PureBasic method is retained. In order to modify the data
;   of the proprietary structure used, I had to observe what it contained in various
;   cases, in order to be able to recreate it and to manipulate it.
;   Fortunately, this structure is very simple.
;
; _____________________________________________________________________________
;
;                      The limitations of this library
; _____________________________________________________________________________
;
; So far, I have identified two small limitations:
; 1- If you create your menu with CreateMenu() or CreatePopupMenu() and assign images
;    to items with the MenuItem(x, "ItemText", ImageID(x)) function, it still won't do
;    anything. It seems that the image ID passed in this case is lost for good.
;    To assign an image with MenuItem(), the menu must have been created with
;    CreateImageMenu(). However, you can still assign ImageIDs with SetMenuItemImageEx(),
;    regardless of how the menu was created.
; 2- If you assign images to items in a menu that was created with CreateMenu()
;    or CreatePopupMenu() using the SetMenuItemMenuEx() function, everything will be fine
;    until you use the SetMenuItemText() function. If you use it, the image previously
;    assigned to the item will be deleted. Therefore, if you want to change the text of
;    an item to which you have already assigned an image, you will have to use
;    SetMenuItemTextEx() instead.
;
; *****************************************************************************
;
;-                 2. SECOND PART: STRUCTURES AND CONSTANTES
;
CompilerIf #PB_Compiler_IsMainFile
  EnableExplicit
CompilerEndIf
;
Structure PBMenuItemData
  ; This structure is intended to correspond to how PureBasic
  ; stores the data of menu items when the menu was created
  ; with the CreateImageMenu() or CreatePopupImageMenu() commands.
  ;
  ; The data in this structure can be accessed through the field
  ; *drawItem\itemData when the #WM_DRAWITEM message is sent
  ; to the main window of the program.
  ;
  ; It can also be accessed with the GetMenuItemInfo_() Windows
  ; API function. See GetMenuItemInfos() as example.
  ;
  ; Due to a lack of documentation on this internal structure
  ; of PureBasic's functionality, the fields have been filled
  ; based on assumptions and observations.
  ;
  *MenuItemTextPntr ; This field contains a pointer
                    ; to the text of the menu item.
  *MenuItemImage    ; This field contains a pointer to the image
                    ; illustrating the menu item.
  Reserved1.i       ; The two following fields 
  Reserved2.i       ;    always contain zero.
  Unknown.i         ; The rest is unknown, even the exact number
                    ;    of fields of the structure.
                    ; But we get enough here for our needs
EndStructure
;
Structure MENUITEMINFO_Fixed Align #PB_Structure_AlignC
  ; The MENUITEMINFO structure described in PureBasic 6.12
  ; is uncomplete.
  ; Great thanks to idle from english PureBasic forum,
  ; for the right form of this structure.
  cbSize.l
  fMask.l
  fType.l
  fState.l
  wID.l
  hSubMenu.i
  hbmpChecked.i
  hbmpUnchecked.i
  dwItemData.i
  *dwTypeData
  cch.l
  hbmpItem.i ; This field is missing in the PureBasic 6.20 description of MENUITEMINFO.
EndStructure

;
; *****************************************************************************
;
;-                     3. THIRD PART: THE LIBRARY ITSELF
;
CompilerIf Not (Defined(GetAnImageForFree, #PB_Procedure))
  Procedure GetAnImageForFree(Start = 1, NbAttempt = 1000, MinSize = 16, PBImage = -1)
    ; By Zapman - Jan 2025.
    ;
    ; Explore the memory an return the first found image from 'Start' address.
    ;
    ; 'NbAttempt' is the limit number of attempts before stopping the exploration.
    ;
    ; 'MinSize' contains the minimum size for the searched image.
    ;
    ; If PBImage contains a valid image created with 'CreateImage()',
    ; the found image will be copied to PBImage.
    ; If PBImage = -1, the memory will only be checked for an existing image
    ; and nothing will be copied nowhere.
    ;
    ; If existing, PBImage can be 24 bits or 32 bits.
    ;
    Protected Result = 0, bitmap.BITMAP, hDestBitmap
    Protected ct, w, h, ImgWidth, ImgHeight, IsAlpha
    Protected x, y, PixelColor, NotEmpty
    Protected EndOfExploration = Start + NbAttempt
    Protected oldDestBitmap, oldSrcBitmap
    Protected blend, *blend.BLENDFUNCTION = @blend
    Protected hdcDest, hdcSrc = CreateCompatibleDC_(#Null) ; Create a memory hDC.
    ;
    If OpenLibrary(0, "Msimg32.dll")
      If hdcSrc
        If IsImage(PBImage) ; Check if the destination given image is valid.
          hDestBitmap = ImageID(PBImage) ; Get the  destination image handle.
          If GetObject_(hDestBitmap, SizeOf(BITMAP), @bitmap) ; get the destination image size.
            ImgWidth = bitmap\bmWidth
            ImgHeight = bitmap\bmHeight
            IsAlpha = bitmap\bmBitsPixel
            If ImgWidth * ImgHeight ; Check if the destination image has not a null size.
              If StartDrawing(ImageOutput(PBImage))
                ; Erase the given image background:
                If isAlpha = 32
                  ; Transparent background
                  DrawingMode(#PB_2DDrawing_AllChannels)
                  Box(0, 0, ImgWidth, ImgHeight, 0)
                Else
                  ; White background
                  Box(0, 0, ImgWidth, ImgHeight, $FFFFFF)
                EndIf
                StopDrawing()
              Else
                Goto GAIFF_CleanAndReturn
              EndIf
              ;
              hdcDest = CreateCompatibleDC_(#Null)    ; Create a memory hDC for the destination image
              oldDestBitmap = SelectObject_(hdcDest, hDestBitmap) ; Associate the destination image with hdcDest.
            Else
              Goto GAIFF_CleanAndReturn
            EndIf
            If oldDestBitmap = 0
              Goto GAIFF_CleanAndReturn
            EndIf
          Else
            Goto GAIFF_CleanAndReturn
          EndIf
        ElseIf PBImage <> -1
          Goto GAIFF_CleanAndReturn
        EndIf
        ;
        For ct = Start To EndOfExploration
          If GetObject_(ct, SizeOf(BITMAP), @bitmap) ; Check is there is an image at this address.
            w = bitmap\bmWidth                       ; If any, get the found image size.
            h = bitmap\bmHeight
            If w >= MinSize And h >= MinSize           ; Ensure that the found image has good dimensions.
              oldSrcBitmap = SelectObject_(hdcSrc, ct) ; Associate the found image with hdcSrc
              If oldSrcBitmap
                Result = ct
                If hDestBitmap
                  ; Copy the found image into PBImage:
                  *blend\BlendOp = #AC_SRC_OVER
                  *blend\BlendFlags = 0
                  *blend\AlphaFormat = #AC_SRC_ALPHA
                  *blend\SourceConstantAlpha = 255
                  CallFunction(0, "AlphaBlend", hdcDest, 0, 0, ImgWidth, ImgHeight, hdcSrc, 0, 0, w, h, blend)
                  ;
                  ; Now, check if the resulting image is empty:
                  ;
                  NotEmpty = #False
                  If StartDrawing(ImageOutput(PBImage))
                    If IsAlpha = 32
                      DrawingMode(#PB_2DDrawing_AllChannels)
                    EndIf
                    ; Check each pixel to see if all are equal to a solid color (e.g., white).
                    For y = 0 To ImgHeight - 1
                      For x = 0 To ImgWidth - 1
                        PixelColor = Point(x, y)
                        If IsAlpha = 24
                          ; Check if the pixel is white:
                          If PixelColor <> RGB(255, 255, 255) ; If a different pixel is found...
                            NotEmpty = #True
                            Break
                          EndIf
                        ElseIf IsAlpha = 32 And Alpha(PixelColor); Check if the pixel is transparent.
                          NotEmpty = #True
                          Break
                        EndIf
                      Next x
                    Next y
                    StopDrawing()
                  Else
                    Result = 0
                  EndIf
                  ;
                  If NotEmpty = #False
                    Result = 0
                  EndIf
                EndIf
                If Result : Break : EndIf
              EndIf
            EndIf
          EndIf
        Next
        ;
        GAIFF_CleanAndReturn: 
        If oldDestBitmap
          SelectObject_(hdcDest, oldDestBitmap)
        EndIf
        If hdcDest : DeleteDC_(hdcDest) : EndIf
        If oldSrcBitmap
          SelectObject_(hdcSrc, oldSrcBitmap)
        EndIf
        If hdcSrc : DeleteDC_(hdcSrc) : EndIf
      EndIf
      CloseLibrary(0)
    EndIf
    ProcedureReturn Result
  EndProcedure
CompilerEndIf  
;
#GMI_StringSearch$ = "GetMenuItemTextExString"
#GMI_SearchFirstImage$ = "GetFirstImageFromMenuItemString"
#GMI_Ignore = -1
;
Procedure.s GetClassicMenuStringFromPosition(hMenu, Position)
  ;
  Protected MenuItemInfo.MENUITEMINFO_Fixed, ItemString$
  ;
  ; Get the size of the string (the number of chars):
  MenuItemInfo\cbSize     = SizeOf(MENUITEMINFO_Fixed)
  MenuItemInfo\fMask      = #MIIM_STRING
  MenuItemInfo\dwTypeData = 0
  GetMenuItemInfo_(hMenu, Position, #MF_BYPOSITION, @MenuItemInfo)
  ;
  If MenuItemInfo\cch
    ; Allocate memory for a unicode string:
    ItemString$ = Space(MenuItemInfo\cch)
    MenuItemInfo\cch + 1 ; Add room for the ending character.
    ; Put the item string buffer address in MenuItemInfo\dwTypeData:
    MenuItemInfo\dwTypeData = @ItemString$
    ; Retreive the string:
    GetMenuItemInfo_(hMenu, Position, #MF_BYPOSITION, @MenuItemInfo)
  EndIf
  ProcedureReturn ItemString$
EndProcedure
;
Procedure GetMenuItemInfos(hMenu, ItemNum = #GMI_Ignore, ByPositionOrCommand = #MF_BYCOMMAND, *StringPointer = #GMI_Ignore)
  ;
  ; Explore hMenu items to find an image and/or a text.
  ; If an image is found, the ImageID (handle) of the image
  ; is returned.
  ; If *StringPointer is a valid pointer to a string, the corresponding
  ; item text is copied into *StringPointer.
  ;
  ; If ItemNum = #GMI_Ignore, the ImageID (handle) of the first found image
  ; is returned (if there is at least one image in the menu items).
  ;
  ; If ItemNum <> #GMI_Ignore, the ImageID of the image of the item designed
  ; by ItemNum is returned.
  ;
  ; If ByPositionOrCommand = #MF_BYPOSITION, ItemNum must contain the position
  ; of the item in the menu. Else, it must contain the item ID.
  ;
  ; If ByPositionOrCommand = #MF_BYCOMMAND, all the submenus of hMenu
  ; will be explored to find 'ItemNum'. Else, only the items of hMenu
  ; will be examined to find the required position.
  ;
  ; If *StringPointer is a valid pointer to a string, the text
  ; of the found item is returned into this string, whatever the
  ; way the menu was created (CreateImageMenu() or CreateMenu()
  ; or CreatePopupMenu() or CreatePopupImageMenu()).
  ;
  ; If ItemNum = #GMI_Ignore and *StringPointer points to a string
  ; equal to #GMI_StringSearch$, the first found item text
  ; is returned in *StringPointer, even if this item has no image.
  ; If ItemNum = #GMI_Ignore and *StringPointer is a valid pointer
  ; but does not point to a string equal to #GMI_StringSearch$,
  ; the item text of the first item with an image is returned 
  ; in *StringPointer. If not any item has an image, the first
  ; found item text is returned in *StringPointer.
  ; If ItemNum <> #GMI_Ignore and *StringPointer is a valid string
  ; pointer, the item text corresponding to ItemNum and
  ; ByPositionOrCommand will be copied in *StringPointer even if
  ; the item has no image. If it has an image, the imageID will
  ; be returned. Else, zero will be returned.
  ;
  Protected Counter, MenuItemInfo.MENUITEMINFO_Fixed, Result = #False
  Protected *PBMenuItemData.PBMenuItemData, bitmap.bitmap
  Protected Cont, SearchFirstImage = 0
  If *StringPointer And *StringPointer <> #GMI_Ignore And PeekS(*StringPointer) = #GMI_SearchFirstImage$
    SearchFirstImage = 1
  EndIf
  ;
  If IsMenu(hMenu)
    hMenu = MenuID(hMenu)
  EndIf
  ;
  For Counter = 0 To GetMenuItemCount_(hMenu) - 1
    Cont = 0
    If ItemNum = #GMI_Ignore
      Cont = 1
    Else
      If ByPositionOrCommand = #MF_BYPOSITION And Counter = ItemNum
        Cont = 1
      ElseIf ByPositionOrCommand = #MF_BYCOMMAND And GetMenuItemID_(hMenu, Counter) = ItemNum
        Cont = 1
      EndIf
    EndIf
    If Cont
      MenuItemInfo\cbSize     = SizeOf(MENUITEMINFO_Fixed)
      MenuItemInfo\fMask      = #MIIM_DATA
      GetMenuItemInfo_(hMenu, Counter, #MF_BYPOSITION, @MenuItemInfo)
      If MenuItemInfo\dwItemData
        ; PureBasic as registered data in MenuItemInfo\dwItemData.
        ; The precise structure of this data is private to PureBasic
        ; and is not documented. But observation permitted to know
        ; that the first field of this structure contains a pointer
        ; to the item string and the second one contains a pointer
        ; to the item image.
        *PBMenuItemData = MenuItemInfo\dwItemData
        Result = *PBMenuItemData\MenuItemImage
        ;
        If *PBMenuItemData\MenuItemTextPntr And *StringPointer And *StringPointer <> #GMI_Ignore
          If PeekS(*StringPointer) = #GMI_StringSearch$
            ; A string is requested and we get one.
            Result = 1
          EndIf
          If SearchFirstImage = 0
            PokeS(*StringPointer, PeekS(*PBMenuItemData\MenuItemTextPntr))
          EndIf
        EndIf
        ;
      Else
        ; PureBasic did not register data in MenuItemInfo\dwItemData.
        ;
        ; Retreive the item image using the classical Windows way:
        MenuItemInfo\fMask      = #MIIM_BITMAP
        GetMenuItemInfo_(hMenu, Counter, #MF_BYPOSITION, @MenuItemInfo)
        Result = MenuItemInfo\hbmpItem
        ;
        ; Retreive the item text using the classical Windows way:
        Protected ItemString$ = GetClassicMenuStringFromPosition(hMenu, Counter)
        If ItemString$ And *StringPointer And *StringPointer <> #GMI_Ignore
          If PeekS(*StringPointer) = #GMI_StringSearch$ And Result = 0
            ; A string is requested and we get one.
            Result = 1
          EndIf
          If SearchFirstImage = 0
            PokeS(*StringPointer, PeekS(@ItemString$))
          EndIf
        EndIf
        ;
      EndIf
      If Result <> 1 And Result <> 0
        ; It seems that we get an image. Check if it is valid:
        If GetObject_(Result, SizeOf(BITMAP), @bitmap.bitmap)
          If SearchFirstImage
            If ByPositionOrCommand = #MF_BYCOMMAND
              PokeS(*StringPointer, Str(GetMenuItemID_(hMenu, Counter)))
            Else
              PokeS(*StringPointer, Str(Counter))
            EndIf
          EndIf
          ; We get an image. Stop searching.
          Break
        Else
          Result = 0
        EndIf
      ElseIf Result = 1
        ; We get a text. Stop searching.
        Break
      EndIf
    EndIf
    If ByPositionOrCommand = #MF_BYCOMMAND
      ; When ItemNum is an ID, look for
      ; a submenu having an item with this ID.
      Protected SubMenu = GetSubMenu_(hMenu, Counter)
      ; Another way of doing the same thing is the following:
;       MenuItemInfo\cbSize = SizeOf(MENUITEMINFO_Fixed)
;       MenuItemInfo\fMask     = #MIIM_SUBMENU
;       MenuItemInfo\fType     = 0
;       GetMenuItemInfo_(hMenu, Counter, #MF_BYPOSITION, @MenuItemInfo)
;       SubMenu = MenuItemInfo\hSubMenu
      ;
      If SubMenu
        ; The item is an entry of a submenu.
        ; Explore the submenu:
        Result = GetMenuItemInfos(SubMenu, ItemNum, ByPositionOrCommand, *StringPointer)
        If Result
          Break
        EndIf
      EndIf
    EndIf
  Next
  ;
  ProcedureReturn Result
  ;
EndProcedure
;
Procedure GetMenuItemImageEx(hMenu, ItemNum)
  ;
  ; This function could be called 'GetMenuItemImage()' but it is possible that
  ; the PureBasic team decides to create seach a function with that name.
  ; So, the name 'GetMenuItemImageEx()' has been choosen.
  ;
  ; It returns the ImageID of the item designed by ItemNum.
  ; ItemNum must contain the item ID.
  ;
  ; All the submenus of hMenu will be explored to find 'ItemNum'.
  ;
  ; When omitting the fourth parameter of GetMenuItemInfos(), image search is preferred.
  ProcedureReturn GetMenuItemInfos(hMenu, ItemNum, #MF_BYCOMMAND)
EndProcedure
;
Procedure GetMenuTitleImageEx(hMenu, ItemNum)
  ;
  ; This function could be called 'GetMenuTitleImage()' but it is possible that
  ; the PureBasic team decides to create seach a function with that name.
  ; So, the name 'GetMenuTitleImageEx()' has been choosen.
  ;
  ; It returns the ImageID of the item designed by ItemNum.
  ; ItemNum must contain the position of the item in the menu.
  ;
  ; When omitting the fourth parameter of GetMenuItemInfos(), image search is preferred.
  ProcedureReturn GetMenuItemInfos(hMenu, ItemNum, #MF_BYPOSITION)
EndProcedure
;
Procedure.s GetMenuItemTextEx(hMenu, ItemNum, ByPositionOrCommand = #MF_BYCOMMAND)
  ; This functions does exactly the same thing As GetMenuItemText()
  ;
  ; BUT : When the menu has been created using CreateMenu() or CreatePopupMenu()
  ; and then set to 'OwnerDrawn' by the program, the PureBasic GetMenuItemText()
  ; doesn't allways work (for an unknown reason).
  ; This function will work in any case.
  Protected *String = AllocateMemory(1000) ; Reserve space for menu item text of any length.
  PokeS(*String, #GMI_StringSearch$) ; Signal a preferred string search.
  GetMenuItemInfos(hMenu, ItemNum, ByPositionOrCommand, *String)
  Protected String$ = PeekS(*String)
  FreeMemory(*String)
  If String$ <> #GMI_StringSearch$
    ProcedureReturn String$
  EndIf
EndProcedure
;
Procedure.s GetMenuTitleTextEx(hMenu, ItemNum)
  ;
  ; This functions does exactly the same thing As GetMenuTitleText()
  ;
  ProcedureReturn GetMenuItemTextEx(hMenu, ItemNum, #MF_BYPOSITION)
EndProcedure
;
Procedure GetFirstMenuItemImage(hMenu, ByPositionOrCommand = #MF_BYCOMMAND)
  ;
  ; Returns the first menu item number that has an image.
  ; If ByPositionOrCommand = #MF_BYCOMMAND, the value returned
  ;    is the ID of the menu item, and submenus will eventually
  ;    be explored to find the image.
  ; Else, the item position will be returned and submenus
  ;    won't be explored.
  Protected String$ = #GMI_SearchFirstImage$
  GetMenuItemInfos(hMenu, #GMI_Ignore, ByPositionOrCommand, @String$)
  If String$ = #GMI_SearchFirstImage$
    ProcedureReturn -1
  Else
    ProcedureReturn Val(String$)
  EndIf
EndProcedure
;
Procedure SetMenuItemInfos(hMenu, ItemNum, *ImageID = #GMI_Ignore, ItemString$ = "", ByPositionOrCommand = #MF_BYCOMMAND)
  ;
  ; If ByPositionOrCommand = #MF_BYPOSITION, ItemNum must contain the position
  ; of the item in the menu. Else, it must contain the item ID.
  ;
  ; If ByPositionOrCommand = #MF_BYCOMMAND, all the submenus of hMenu
  ; will be explored to find 'ItemNum'. Else, only the items of hMenu
  ; will be examined to find the required position of the item in the menu.
  ;
  ; *ImageID can contain the ImageID you want to attribute to the menu item.
  ; ItemString$ can contain the string you want to attribute to the menu item.
  ;
  Protected Counter, MenuItemInfo.MENUITEMINFO_Fixed, Result = #False
  Protected *PBMenuItemData.PBMenuItemData, Cont, hWindow, itemID
  ;
  ; Store ItemString$ in a static list to ensure that the strings
  ; pointers used by the menu will allways point to a valid memory
  ; address.
  Static NewList ListOfString.String()
  Protected Found = 0
  If ItemString$
    ForEach ListOfString()
      If ListOfString()\s = ItemString$
        Found = 1
        Break
      EndIf
    Next
    If Found = 0
      AddElement(ListOfString())
      ListOfString()\s = ItemString$
    EndIf
  EndIf
  ;
  If IsMenu(hMenu)
    hMenu = MenuID(hMenu)
  EndIf
  ;
  If IsImage(*ImageID)
    *ImageID = ImageID(*ImageID)
  EndIf
  ;
  For Counter = 0 To GetMenuItemCount_(hMenu) - 1
    Cont = 0
    If ByPositionOrCommand = #MF_BYPOSITION And Counter = ItemNum
      Cont = 1
    ElseIf ByPositionOrCommand = #MF_BYCOMMAND And GetMenuItemID_(hMenu, Counter) = ItemNum
      Cont = 1
    EndIf
    If Cont
      MenuItemInfo\cbSize     = SizeOf(MENUITEMINFO_Fixed)
      MenuItemInfo\fMask      = #MIIM_DATA
      GetMenuItemInfo_(hMenu, Counter, #MF_BYPOSITION, @MenuItemInfo)
      ;
      If MenuItemInfo\dwItemData = 0
        ; PureBasic has not created a *PBMenuItemData for this menu,
        ; probably because this menu has been created using CreateMenu()
        ; or CreatePopupMenu(), OR because this is the main window menu
        ; (PureBasic does'nt allow to add an image to the items of the
        ;  main menu when they have subitems).
        ;
        If ItemString$
          ; To set the text, use the classical way of doing the thing
          ; using Windows API.
          ; The content of ItemString$ has been saved in a static list
          ; to ensure that the given address always points to a valid
          ; string, whatever the user does whith the original string.
          MenuItemInfo\cbSize     = SizeOf(MENUITEMINFO_Fixed)
          MenuItemInfo\fMask      = #MIIM_STRING
          MenuItemInfo\dwTypeData = @ListOfString()\s
          SetMenuItemInfo_(hMenu, Counter, #MF_BYPOSITION, @MenuItemInfo)
          ;
          ; Now, redraw the menu to update it, if it is the window main menu:
          If IsWindow(EventWindow())
            hWindow = WindowID(EventWindow())
            If hMenu = GetMenu_(hWindow)
              DrawMenuBar_(hWindow)
            EndIf
          EndIf
          Result = 1
        EndIf
        ;
        If *ImageID <> #GMI_Ignore
          ; To set the image, use the classical way of doing the thing
          ; using Windows API.
          ; The ImageID is copied into the hbmpItem field of MenuItemInfo.
          ; Doing that, we can print a menu image EVEN IF the menu was created
          ; using CreateMenu() or CreatePopupMenu(). This also allows to print
          ; an image in a main menu title (even if it has subitems),
          ; while there is no way to do that using native PureBasic functions.
          ;
          MenuItemInfo\fMask      = #MIIM_BITMAP
          MenuItemInfo\hbmpItem   = *ImageID
          SetMenuItemInfo_(hMenu, Counter, #MF_BYPOSITION, @MenuItemInfo)
          ;
          ; Now, redraw the menu to update it, if it is the window main menu:
          If IsWindow(EventWindow())
            hWindow = WindowID(EventWindow())
            If hMenu = GetMenu_(hWindow)
              DrawMenuBar_(hWindow)
            EndIf
          EndIf
          Result = 1
        EndIf
      Else
        ; PureBasic has created a *PBMenuItemData for this menu.
        ; This means that this menu was created using CreateImageMenu()
        ; or CreatePopupImageMenu().
        *PBMenuItemData = MenuItemInfo\dwItemData
        If *ImageID <> #GMI_Ignore
          *PBMenuItemData\MenuItemImage = *ImageID
          Result = 1
        EndIf
        If ItemString$
          ; The content of ItemString$ has been saved in a static list
          ; to ensure that the given address always points to a valid
          ; string, whatever the user does whith the original string.
          *PBMenuItemData\MenuItemTextPntr = @ListOfString()\s
          Result = 1
        EndIf
      EndIf
      Break
    EndIf
    If ByPositionOrCommand = #MF_BYCOMMAND
      ; When ItemNum is an ID, look for
      ; a submenu having an item with this ID.
      Protected SubMenu = GetSubMenu_(hMenu, Counter)
      ; Another way of doing the same thing is the following:
;       MenuItemInfo\cbSize = SizeOf(MENUITEMINFO_Fixed)
;       MenuItemInfo\fMask     = #MIIM_SUBMENU
;       MenuItemInfo\fType     = 0
;       GetMenuItemInfo_(hMenu, Counter, #MF_BYPOSITION, @MenuItemInfo)
;       SubMenu = MenuItemInfo\hSubMenu
      If SubMenu
        ; The item is an entry of a submenu.
        ; Explore the submenu:
        Result = SetMenuItemInfos(SubMenu, ItemNum, *ImageID, ItemString$, ByPositionOrCommand)
        If Result
          Break
        EndIf
      EndIf
    EndIf
  Next
  ProcedureReturn Result
  ;
EndProcedure
;
Procedure SetMenuItemImageEx(hMenu, ItemNum, *ImageID, ByPositionOrCommand = #MF_BYCOMMAND)
  ;
  ; This function could be called 'SetMenuItemImage()' but it is possible that
  ; the PureBasic team decides to create seach a function with that name.
  ; So, the name 'SetMenuItemImageEx()' has been choosen.
  ProcedureReturn SetMenuItemInfos(hMenu, ItemNum, *ImageID, "", ByPositionOrCommand)
EndProcedure
;
Procedure SetMenuTitleImageEx(hMenu, ItemNum, *ImageID)
  ;
  ; This function could be called 'SetMenuTitleImage()' but it is possible that
  ; the PureBasic team decides to create seach a function with that name.
  ; So, the name 'SetMenuTitleImageEx()' has been choosen.
  ProcedureReturn SetMenuItemInfos(hMenu, ItemNum, *ImageID, "", #MF_BYPOSITION)
EndProcedure
;
Procedure SetMenuItemTextEx(hMenu, ItemNum, Text$, ByPositionOrCommand = #MF_BYCOMMAND)
  ;
  ; This function does the same as SetMenuItemText()
  ;
  ; The difference is that when a menu has not been created using CreateImageMenu()
  ; or CreatePopupImageMenu(), and you call the standard PureBasic functions
  ; SetMenuItemText(), PureBasic will erase the image eventually associated With
  ; the corresponding item.
  ;
  ; So, by using SetMenuItemTextEx() instead of using SetMenuItemText()
  ; you preserve the images you've attributed to the items using SetMenuItemImageEx(),
  ; even if the menu has been created by using CreateMenu() or CreatePopupMenu().
  ;
  ProcedureReturn SetMenuItemInfos(hMenu, ItemNum, #GMI_Ignore, Text$, ByPositionOrCommand)
EndProcedure
;
Procedure SetMenuTitleTextEx(hMenu, ItemNum, Text$)
  ;
  ; This function does the same as SetMenuTitleText()
  ;
  ; The difference is that when a menu has not been created using CreateImageMenu()
  ; or CreatePopupImageMenu(), and you call the standard PureBasic functions
  ; SetMenuTitleText(), PureBasic will erase the image eventually associated With
  ; the corresponding item.
  ;
  ; So, by using SetMenuTitleTextEx() instead of using SetMenuTitleText(),
  ; you preserve the images you've attributed to the items using SetMenuItemImageEx(),
  ; specially if you attributed an image to the main window menu main items.
  ;
  ProcedureReturn SetMenuItemInfos(hMenu, ItemNum, #GMI_Ignore, Text$, #MF_BYPOSITION)
EndProcedure
;
;
; *****************************************************************************
;
;-                      4. FORTH PART: A DEMONSTRATION
;
;
CompilerIf #PB_Compiler_IsMainFile
  ;
  ; The following won't run when this file is used as 'Included'.
  ;
  Procedure.s RefreshMsg(MainMenu)
    ;
    Protected ItemPosition, ItemID, ImageID
    Protected Msg$ = ""
    ;
    ItemPosition = GetFirstMenuItemImage(MainMenu, #MF_BYPOSITION)
    If ItemPosition <> -1
      Msg$ + "First found image for menu:" + #CR$
      Msg$ + "   Position = " + Str(ItemPosition) + "  -  ImageID = " + GetMenuTitleImageEx(MainMenu, ItemPosition) + #CR$
      Msg$ + "   Corresponding text is: "  + GetMenuTitleTextEx(MainMenu, ItemPosition) + #CR$
      Msg$ + #CR$
    Else
      ItemID = GetFirstMenuItemImage(MainMenu, #MF_BYCOMMAND)
      If ItemID <> -1
        Msg$ + "First found image for menu:" + #CR$
        Msg$ + "   ItemID   = " + Str(ItemID) + "  -  ImageID = " + GetMenuItemImageEx(MainMenu, ItemID) + #CR$
        Msg$ + "   Corresponding text is: "  + GetMenuItemTextEx(MainMenu, ItemID) + #CR$
        Msg$ + #CR$
      Else
        Msg$ + "There is no image in the menu." + #CR$
        Msg$ + #CR$
      EndIf
    EndIf
    
    ItemID = 3
    ImageID = GetMenuItemImageEx(MainMenu, ItemID)
    If ImageID
      Msg$ + "There is an image for ItemID " + Str(ItemID) + ": " + Str(ImageID) + #CR$
      Msg$ + "   Corresponding text is: "  + GetMenuItemTextEx(MainMenu, ItemID) + #CR$
    Else
      Msg$ + "There is no image for ItemID " + Str(ItemID) + "." + #CR$
      Msg$ + "   Corresponding text is: "  + GetMenuItemTextEx(MainMenu, ItemID) + #CR$
    EndIf
    Msg$ + #CR$
    Msg$ + "Looking by ID, the text having the ID 1 is: "  + GetMenuItemTextEx(MainMenu, 1, #MF_BYCOMMAND) + #CR$
    Msg$ + "Looking by position, the text at position 1 is: "  + GetMenuItemTextEx(MainMenu, 1, #MF_BYPOSITION) + #CR$
    Msg$ + "   because each time you call 'MenuTitle()', PureBasic open a submenu" + #CR$
    Msg$ + "   to register the items of the main menu title. By the way, 'MenuTitle()'" + #CR$
    Msg$ + "   does exactly the same thing as 'OpenSubmenu()', except that it first." + #CR$
    Msg$ + "   call CloseSubMenu() if a precedent submenu (or MenuTitle) has been" + #CR$
    Msg$ + "   opened." + #CR$
    Msg$ + "   So, the main menu has only two positions: 'File' at 0, and  'Edit' at 1." + #CR$
    ;
    Msg$ + #CR$
    Msg$ + #CR$
    Msg$ + "Open the 'File' menu and look at its content." + #CR$
    Msg$ + "Then click on the button below and look again" + #CR$
    Msg$ + "at the 'File' menu content." + #CR$
    ;
    ProcedureReturn Msg$
  EndProcedure
  ;
  ; Create a menu for démonstration
  If OpenWindow(0, 0, 0, 400, 420, "GetMenuInfo Example", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
    CreateImage(1, 24, 24, 32)
    CreateImage(2, 24, 24, 32)
    CreateImage(3, 24, 24, 32)
    Define LastImage = GetAnImageForFree(171, 100, 24, 1)
    LastImage = GetAnImageForFree(LastImage + 4, 100, 24, 2)
    LastImage = GetAnImageForFree(LastImage + 4, 100, 24, 3)
    ;
    Define MainMenu = CreateImageMenu(#PB_Any, WindowID(0))
    
    MenuTitle("File")
    MenuItem(1, "Open")
    MenuItem(2, "Save", ImageID(1))
    MenuBar()
    MenuItem(3, "Exit")
    ;
    MenuTitle("Edit")
    MenuItem(4, "Cut")
    ;
    Define TextGadget = TextGadget(#PB_Any, 10, 10, WindowWidth(0) - 20, 330, RefreshMsg(MainMenu))
    ;
    Define ButtonWidth = 290
    Define ButtonHeight = 25
    Define ButtonX = (WindowWidth(0) - ButtonWidth) / 2
    Define ButtonY = WindowHeight(0) - ButtonHeight - 30
    Define ChangeMenuButton = ButtonGadget(#PB_Any, ButtonX, ButtonY, ButtonWidth, ButtonHeight, "Add an image to Open and change 'Open' to 'Test'")
    ;  
    ;
    Define Event
    Repeat
      Event = WaitWindowEvent()
      If Event And EventGadget() = ChangeMenuButton
        SetMenuItemImageEx(MainMenu, 1, ImageID(2))
        ;
        ; Add an image to a main menu title:
        SetMenuItemImageEx(MainMenu, 0, ImageID(3), #MF_BYPOSITION)
        SetMenuTitleTextEx(MainMenu, 0, "File2")
        ;
        ;
        SetMenuItemTextEx(MainMenu, 1, "Test")
        
        SetGadgetText(TextGadget, RefreshMsg(MainMenu))

      EndIf
    Until Event = #PB_Event_CloseWindow
    
    CloseWindow(0)
  EndIf
  ;
CompilerEndIf
Last edited by Zapman on Sun Feb 02, 2025 12:30 pm, edited 5 times in total.
Sergey
User
User
Posts: 53
Joined: Wed Jan 12, 2022 2:41 pm

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by Sergey »

And we don't dreamed of IMA at line 397 on PB6.20b2 x64 :)
boddhi
Enthusiast
Enthusiast
Posts: 524
Joined: Mon Nov 15, 2010 9:53 pm

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by boddhi »

Hello Zapman.

Thanks for sharing.
But, under Win 10 x64 & PB 6.12 x64, I got an error : 'Line 397 : Invalid memory access (Writing error at address 0)'

Code: Select all

Result = *PBMenuItemData\MenuItemImage
If my English syntax and lexicon are incorrect, please bear with Google translate and DeepL. They rarely agree with each other!
Except on this sentence...
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by Zapman »

Thanks for testing guys.
When testing on my own computer with a x64 version of PureBasic, I've the same error.
It still runs well with x86.
I'll have a look to find what happens and I'll post when I will find.
Thanks again :D
User avatar
idle
Always Here
Always Here
Posts: 5835
Joined: Fri Sep 21, 2007 5:52 am
Location: New Zealand

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by idle »

thanks this works on x64

Code: Select all

Structure MENUITEMINFO_Fixed
  cbSize.l
  fMask.l
  fType.l
  fState.l
  wID.l
  hSubMenu.l
  hbmpChecked.l
  hbmpUnchecked.l
  dwItemData.l
  *dwTypeData
  cch.l
  hbmpItem.i ; This field is missing in the PureBasic native description of MENUITEMINFO.
EndStructure
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by Zapman »

idle wrote: Tue Jan 14, 2025 8:28 am thanks this works on x64
Thank's a lot, idle. Your reasoning is correct and you certainly point the source of the problem, but, even if the memory error doesn't occure with your proposition of structure, the program doesn't work as it should.
I'm still doing tests on both systems (x64 and x86).
User avatar
idle
Always Here
Always Here
Posts: 5835
Joined: Fri Sep 21, 2007 5:52 am
Location: New Zealand

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by idle »

Zapman wrote: Tue Jan 14, 2025 10:35 am
idle wrote: Tue Jan 14, 2025 8:28 am thanks this works on x64
Thank's a lot, idle. Your reasoning is correct and you certainly point the source of the problem, but, even if the memory error doesn't occure with your proposition of structure, the program doesn't work as it should.
I'm still doing tests on both systems (x64 and x86).

Code: Select all

Structure MENUITEMINFO_Fixed Align #PB_Structure_AlignC
  cbSize.l
  fMask.l
  fType.l
  fState.l
  wID.l
  hSubMenu.i
  hbmpChecked.i
  hbmpUnchecked.i
  dwItemData.i
  *dwTypeData
  cch.l
  hbmpItem.i ; This field is missing in the PureBasic native description of MENUITEMINFO.
EndStructure
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by Zapman »

idle wrote: Tue Jan 14, 2025 10:51 am

Code: Select all

Structure MENUITEMINFO_Fixed Align #PB_Structure_AlignC
  cbSize.l
  fMask.l
  fType.l
  fState.l
  wID.l
  hSubMenu.i
  hbmpChecked.i
  hbmpUnchecked.i
  dwItemData.i
  *dwTypeData
  cch.l
  hbmpItem.i ; This field is missing in the PureBasic native description of MENUITEMINFO.
EndStructure
[EDIT] YES! it works!
Sorry for my precedent returns: I had made some changes for testing and those changes were preventing me from getting the right result.
You are a great boss, idle. A very big thank you for your help.
I continue to do tests to make sure everything works as it should and I post a corrected version of the code.
Last edited by Zapman on Tue Jan 14, 2025 11:34 am, edited 2 times in total.
User avatar
idle
Always Here
Always Here
Posts: 5835
Joined: Fri Sep 21, 2007 5:52 am
Location: New Zealand

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by idle »

It was same result on x86 I'll look again in morning
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by Zapman »

idle wrote: Tue Jan 14, 2025 11:22 am It was same result on x86 I'll look again in morning
You were right, sorry! I edited my precedent post.
Thanks a lot again.
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by Zapman »

The bug is fixed.
The corrected and working code on x64 and x86 has replaced the old code in the first message of this post.
boddhi
Enthusiast
Enthusiast
Posts: 524
Joined: Mon Nov 15, 2010 9:53 pm

Re: SetMenuItemImage and GetMenuItemImage [Windows only]

Post by boddhi »

Zapman wrote: Tue Jan 14, 2025 12:04 pm The bug is fixed.
The corrected and working code on x64 and x86 has replaced the old code in the first message of this post.
Hello Zapman,

No problem encountered now under Win10 x64/PB 6.12 x64 :wink:
Thanks again for your code.
If my English syntax and lexicon are incorrect, please bear with Google translate and DeepL. They rarely agree with each other!
Except on this sentence...
Post Reply