Page 1 of 1

Adding a context menu to a Combo Box in Windows

Posted: Thu Jun 08, 2017 7:47 pm
by CalamityJames
I wanted to add a context menu to a combo box gadget and it proved trickier than I expected. The following code does seem to work and might save someone some time if they want to do the same thing.

Code: Select all

EnableExplicit

Structure ComboBoxInfo
  cbSize.l
  rcItem.RECT
  rcButton.RECT
  stateButton.l
  hwndCombo.i
  hwndEdit.i
  hwndList.i
EndStructure

Global CombBoxOrigProc.i, ComboListOrigProc.i, TheLookUpWord.s, StopComboListBoxWindowUpdate.i
Global EventId.i

Enumeration Menus
  #PopupMenu
  #PopupLookup
  #PopupCancel
EndEnumeration

Enumeration Messages
  #ShowComboBoxListBoxPopupMenu = #PB_Event_FirstCustomValue
  #ShowComboBoxPopupMenu
EndEnumeration

Enumeration Gadgets
  #ComboBox1
  #ComboBox2
EndEnumeration

#Main = 0
#MaxLetters = 16
#LB_ITEMFROMPOINT = $1A9

Procedure ShowPopUpMenuForComboBox(theHwnd)
  Protected Dim WordBuffer.c(#MaxLetters) ; Allows for trailing 0
  If SendMessage_(theHwnd, #WM_GETTEXTLENGTH, 0, 0) <= #MaxLetters ; a check on max length
    If SendMessage_(theHwnd, #WM_GETTEXT, #MaxLetters + 1, @WordBuffer(0)) <> 0 ; a check that there is some text
      TheLookUpWord = LCase(PeekS(@WordBuffer(0)))
      SetMenuItemText(#PopupMenu, #PopupLookup, "Look up " + Chr(34) + TheLookUpWord + Chr(34))
      DisplayPopupMenu(#PopupMenu, WindowID(#Main))
    EndIf
  EndIf
EndProcedure

Procedure ShowPopUpMenuForListBoxSubclasser()
  Protected Dim WordBuffer.c(#MaxLetters) ; Allows for trailing 0
  SetMenuItemText(#PopupMenu, #PopupLookup, "Look up " + Chr(34) + TheLookUpWord + Chr(34))
  DisplayPopupMenu(#PopupMenu, WindowID(#Main))
EndProcedure

Procedure.i ComboBoxListSubclasser(hWnd.i, Msg.i, wParam.i, lParam.i)
  Protected ItemIndex.i
  Protected Dim WordBuffer.c(#MaxLetters) ; Allows for trailing 0
  Protected MousePoint.Point, MouseDownHwnd.i
  
  Select Msg
    Case #WM_RBUTTONDOWN
      ItemIndex = SendMessage_(hwnd, #LB_ITEMFROMPOINT, 0, lParam)
      If ItemIndex & $FFFF0000 = 0 ; the cursor is in the list box window
        If SendMessage_(hwnd, #LB_GETTEXT, ItemIndex, @WordBuffer()) > 0
          TheLookUpWord = PeekS(@WordBuffer())
          SendMessage_(hwnd, #LB_SETCURSEL, ItemIndex, 0)
          PostEvent(#ShowComboBoxListBoxPopupMenu, #Main, 0, #PB_EventType_FirstCustomValue, hwnd)
          ProcedureReturn 0  
        EndIf
      Else
        MousePoint\x = lparam & $0000FFFF
        MousePoint\Y = lparam >>16
        ClientToScreen_(hwnd, @MousePoint)
        MouseDownHwnd = WindowFromPoint_(MousePoint\X + MousePoint\y<<32)
        If MouseDownHwnd = GadgetID(#ComboBox1) Or MouseDownHwnd = GadgetID(#ComboBox2)
          If SendMessage_(MouseDownHwnd, #CB_GETDROPPEDSTATE, 0, 0) ; it's the combo box For this List box 
            SendMessage_(MouseDownHwnd, #CB_SHOWDROPDOWN, #False, 0)
            PostEvent(#ShowComboBoxPopupMenu, #Main, 0, #PB_EventType_FirstCustomValue, MouseDownHwnd)
            ProcedureReturn 0  
          EndIf
        EndIf
      EndIf
    Case #WM_CAPTURECHANGED  
      If StopComboListBoxWindowUpdate
        ProcedureReturn 0
      EndIf  
  EndSelect
  ProcedureReturn CallWindowProc_(ComboListOrigProc, hWnd, Msg, wParam, lParam)
EndProcedure

Procedure.i ComboBoxSubclasser(hWnd.i, Msg.i, wParam.i, lParam.i)
  Select Msg
    Case #WM_RBUTTONDOWN
      PostEvent(#ShowComboBoxPopupMenu, #Main, 0, #PB_EventType_FirstCustomValue, hwnd)
  EndSelect
  ;Call the Default window Procedure
  ProcedureReturn CallWindowProc_(CombBoxOrigProc, hWnd, Msg, wParam, lParam)
EndProcedure

Procedure SetComboBoxSubclassers(ComboBoxGadgetNo)
  Protected ComboInfo.ComboBoxInfo
  ComboInfo\cbSize = SizeOf(ComboBoxInfo)
  If GetComboBoxInfo_(GadgetID(ComboBoxGadgetNo), @ComboInfo)  
    ComboListOrigProc = GetWindowLongPtr_(ComboInfo\hwndList, #GWL_WNDPROC)
    SetWindowLongPtr_(ComboInfo\hwndList, #GWL_WNDPROC, @ComboBoxListSubclasser())
  EndIf
  CombBoxOrigProc = GetWindowLongPtr_(GadgetID(ComboBoxGadgetNo), #GWL_WNDPROC)
  SetWindowLongPtr_(GadgetID(ComboBoxGadgetNo), #GWL_WNDPROC, @ComboBoxSubclasser())
EndProcedure

If OpenWindow(#Main, 0, 0, 270, 180, "Combo Box with context menu", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  If CreatePopupMenu(#PopupMenu)
    MenuItem(#PopupLookup, "Look up this word in a dictionary")
    MenuItem(#PopupCancel, "Cancel")
  EndIf
  
  ComboBoxGadget(#ComboBox1, 10, 10, 250, 21)
  AddGadgetItem(#ComboBox1, -1, "acorn")
  AddGadgetItem(#ComboBox1, -1, "beehive")
  AddGadgetItem(#ComboBox1, -1, "carpenter")
  AddGadgetItem(#ComboBox1, -1, "dancer")
  AddGadgetItem(#ComboBox1, -1, "elephant")
  SetGadgetState(#ComboBox1, 0)
  SetComboBoxSubclassers(#ComboBox1)  
  
  ComboBoxGadget(#ComboBox2, 10, 70, 250, 21)
  AddGadgetItem(#ComboBox2, -1,"factory")
  AddGadgetItem(#ComboBox2, -1,"graphical")
  AddGadgetItem(#ComboBox2, -1,"hotel")
  AddGadgetItem(#ComboBox2, -1,"inveterate")
  AddGadgetItem(#ComboBox2, -1,"jalopy")
  AddGadgetItem(#ComboBox2, -1,"knee")
  SetGadgetState(#ComboBox2, 0)
  SetComboBoxSubclassers(#ComboBox2)  
  
  Repeat
    EventId = WaitWindowEvent()  
    Select EventId
      Case #ShowComboBoxListBoxPopupMenu    
        StopComboListBoxWindowUpdate = #True
        ShowPopUpMenuForListBoxSubclasser()
        SetCapture_(EventData()) ; this is the combo list box hwnd
        StopComboListBoxWindowUpdate = #False
      Case #ShowComboBoxPopupMenu       
        ShowPopUpMenuForComboBox(EventData()) ; EventData has the combo box hwnd)
      Case #PB_Event_Menu
        Select EventMenu()
          Case #PopupLookup
            RunProgram("http://www.collinsdictionary.com/dictionary/english/" + TheLookUpWord) 
        EndSelect
    EndSelect  
  Until EventId = #PB_Event_CloseWindow
EndIf

Re: Adding a context menu to a Combo Box in Windows

Posted: Thu Jun 08, 2017 9:41 pm
by RASHAD
Hi CalamityJames
Sorry mate I do not want to hijack your post
1- First it did not work if the combo is editable
2- It is too much coding it can be more simple than that

Code: Select all

Procedure IsMouseOver(hWnd) 
  GetWindowRect_(hWnd,r.RECT) 
  GetCursorPos_(p.POINT) 
  Result = PtInRect_(r,p\y << 32 + p\x) 
  ProcedureReturn Result 
EndProcedure 

If OpenWindow(0, 0, 0, 270, 180, "Combo Box with context menu", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  If CreatePopupMenu(0)
    MenuItem(0, "Look up this word in a dictionary")
    MenuItem(0, "Cancel")
  EndIf
 
  ComboBoxGadget(0, 10, 10, 250, 21)
  AddGadgetItem(0, -1, "acorn")
  AddGadgetItem(0, -1, "beehive")
  AddGadgetItem(0, -1, "carpenter")
  AddGadgetItem(0, -1, "dancer")
  AddGadgetItem(0, -1, "elephant")
  SetGadgetState(0, 0)
 
  ComboBoxGadget(1, 10, 50, 250, 21,#PB_ComboBox_Editable)
  AddGadgetItem(1, -1,"factory")
  AddGadgetItem(1, -1,"graphical")
  AddGadgetItem(1, -1,"hotel")
  AddGadgetItem(1, -1,"inveterate")
  AddGadgetItem(1, -1,"jalopy")
  AddGadgetItem(1, -1,"knee")
  SetGadgetState(1, 0)
 
  Repeat
    EventId = WaitWindowEvent() 
    Select EventId
      Case #WM_RBUTTONDOWN
        If IsMouseOver(GadgetID(0)) Or IsMouseOver(GadgetID(1))
          DisplayPopupMenu(0, WindowID(0))
        EndIf
 
    EndSelect 
  Until EventId = #PB_Event_CloseWindow
EndIf

Re: Adding a context menu to a Combo Box in Windows

Posted: Fri Jun 09, 2017 12:03 pm
by RichardL
Hi,
Even better would be to have the right click function over the drop down area of the combobox so you could perform a test (whatever) on any word without selecting it... and the return value should be the position of the word in the list +1.
RichardL

Re: Adding a context menu to a Combo Box in Windows

Posted: Fri Jun 09, 2017 5:38 pm
by CalamityJames
In attempting to simplify my code Rashad has spoilt the consistent interface I tried to create and reduced its functionality. I wanted it to be possible to right-click anywhere on the combo box, including the drop-down part, and have the entry returned (in the variable LookupWord) so it could be sent to the dictionary. I also wanted any change from right-clicking the drop-down part to right-clicking the selected word part (if the user changed his mind) to be handled seamlessly. My little program does all of this. Rashad's doesn't. His program does not return the contents of the entry clicked on, and while this is easily done with the selected item (GetGadgetText() does it) it is less simple with the contents of the drop down part. It's true that my program does not handle an edit box part of a combo in the same way but then it is not intended to.

A standard combo box without an edit box does not have any response to a right-click so everything I have done adds functionality without taking anything away. I would hesitate before removing the standard right-click functions of any edit box to replace it with something else. However, if that is what is wanted here it is:

Code: Select all

EnableExplicit

Structure ComboBoxInfo
  cbSize.l
  rcItem.RECT
  rcButton.RECT
  stateButton.l
  hwndCombo.i
  hwndEdit.i
  hwndList.i
EndStructure

Global CombBoxOrigProc.i, ComboListOrigProc.i, ComboEditOrigProc.i, TheLookUpWord.s, StopComboListBoxWindowUpdate.i
Global EventId.i
Global Dim ComboInfo.ComboBoxInfo(1)

Enumeration Menus
  #PopupMenu
  #PopupLookup
  #PopupCancel
EndEnumeration

Enumeration Messages
  #ShowComboBoxListBoxPopupMenu = #PB_Event_FirstCustomValue
  #ShowComboBoxPopupMenu
EndEnumeration

Enumeration Gadgets
  #ComboBox1
  #ComboBox2
EndEnumeration

#Main = 0
#MaxLetters = 16
#LB_ITEMFROMPOINT = $1A9

Procedure ShowPopUpMenuForComboBox(theHwnd)
  Protected Dim WordBuffer.c(#MaxLetters) ; Allows for trailing 0
  If SendMessage_(theHwnd, #WM_GETTEXTLENGTH, 0, 0) <= #MaxLetters ; a check on max length
    If SendMessage_(theHwnd, #WM_GETTEXT, #MaxLetters + 1, @WordBuffer(0)) <> 0 ; a check that there is some text
      TheLookUpWord = LCase(PeekS(@WordBuffer(0)))
      SetMenuItemText(#PopupMenu, #PopupLookup, "Look up " + Chr(34) + TheLookUpWord + Chr(34))
      DisplayPopupMenu(#PopupMenu, WindowID(#Main))
    EndIf
  EndIf
EndProcedure

Procedure ShowPopUpMenuForListBoxSubclasser()
  Protected Dim WordBuffer.c(#MaxLetters) ; Allows for trailing 0
  SetMenuItemText(#PopupMenu, #PopupLookup, "Look up " + Chr(34) + TheLookUpWord + Chr(34))
  DisplayPopupMenu(#PopupMenu, WindowID(#Main))
EndProcedure

Procedure.i ComboBoxListSubclasser(hWnd.i, Msg.i, wParam.i, lParam.i)
  Protected ItemIndex.i
  Protected Dim WordBuffer.c(#MaxLetters) ; Allows for trailing 0
  Protected MousePoint.Point, MouseDownHwnd.i
  
  Select Msg
    Case #WM_RBUTTONDOWN
      ItemIndex = SendMessage_(hwnd, #LB_ITEMFROMPOINT, 0, lParam)
      If ItemIndex & $FFFF0000 = 0 ; the cursor is in the list box window
        If SendMessage_(hwnd, #LB_GETTEXT, ItemIndex, @WordBuffer()) > 0
          TheLookUpWord = PeekS(@WordBuffer())
          SendMessage_(hwnd, #LB_SETCURSEL, ItemIndex, 0)
          PostEvent(#ShowComboBoxListBoxPopupMenu, #Main, 0, #PB_EventType_FirstCustomValue, hwnd)
          ProcedureReturn 0  
        EndIf
      Else
        MousePoint\x = lparam & $0000FFFF
        MousePoint\Y = lparam >>16
        ClientToScreen_(hwnd, @MousePoint)
        MouseDownHwnd = WindowFromPoint_(MousePoint\X + MousePoint\y<<32)
        If MouseDownHwnd = ComboInfo(#ComboBox1)\hwndList Or MouseDownHwnd = ComboInfo(#ComboBox2)\hwndList
          If SendMessage_(MouseDownHwnd, #CB_GETDROPPEDSTATE, 0, 0) ; it's the combo box For this List box 
            SendMessage_(MouseDownHwnd, #CB_SHOWDROPDOWN, #False, 0)
            PostEvent(#ShowComboBoxPopupMenu, #Main, 0, #PB_EventType_FirstCustomValue, MouseDownHwnd)
            ProcedureReturn 0  
          EndIf
        ElseIf MouseDownHwnd = ComboInfo(#ComboBox1)\hwndEdit Or MouseDownHwnd = ComboInfo(#ComboBox2)\hwndEdit  
            PostEvent(#ShowComboBoxPopupMenu, #Main, 0, #PB_EventType_FirstCustomValue, MouseDownHwnd)
            ProcedureReturn 0  
        EndIf
      EndIf
    Case #WM_CAPTURECHANGED  
      If StopComboListBoxWindowUpdate
        ProcedureReturn 0
      EndIf  
  EndSelect
  ProcedureReturn CallWindowProc_(ComboListOrigProc, hWnd, Msg, wParam, lParam)
EndProcedure

Procedure.i ComboBoxSubclasser(hWnd.i, Msg.i, wParam.i, lParam.i)
  Select Msg
    Case #WM_RBUTTONDOWN
      PostEvent(#ShowComboBoxPopupMenu, #Main, 0, #PB_EventType_FirstCustomValue, hwnd)
  EndSelect
  ;Call the Default window Procedure
  ProcedureReturn CallWindowProc_(CombBoxOrigProc, hWnd, Msg, wParam, lParam)
EndProcedure

Procedure.i ComboBoxEditSubclasser(hWnd.i, Msg.i, wParam.i, lParam.i)
  Select Msg
    Case #WM_RBUTTONDOWN
      PostEvent(#ShowComboBoxPopupMenu, #Main, 0, #PB_EventType_FirstCustomValue, hwnd)
  EndSelect
  ;Call the Default window Procedure
  ProcedureReturn CallWindowProc_(ComboEditOrigProc, hWnd, Msg, wParam, lParam)
EndProcedure

Procedure SetComboBoxSubclassers(ComboBoxGadgetNo)
  ComboInfo(ComboBoxGadgetNo)\cbSize = SizeOf(ComboBoxInfo)
  If GetComboBoxInfo_(GadgetID(ComboBoxGadgetNo), @ComboInfo(ComboBoxGadgetNo))  
    ComboListOrigProc = GetWindowLongPtr_(ComboInfo(ComboBoxGadgetNo)\hwndList, #GWL_WNDPROC)
    SetWindowLongPtr_(ComboInfo(ComboBoxGadgetNo)\hwndList, #GWL_WNDPROC, @ComboBoxListSubclasser())
    ComboEditOrigProc = GetWindowLongPtr_(ComboInfo(ComboBoxGadgetNo)\hwndEdit, #GWL_WNDPROC)
    SetWindowLongPtr_(ComboInfo(ComboBoxGadgetNo)\hwndEdit, #GWL_WNDPROC, @ComboBoxEditSubclasser())
  EndIf
  CombBoxOrigProc = GetWindowLongPtr_(GadgetID(ComboBoxGadgetNo), #GWL_WNDPROC)
  SetWindowLongPtr_(GadgetID(ComboBoxGadgetNo), #GWL_WNDPROC, @ComboBoxSubclasser())
EndProcedure

If OpenWindow(#Main, 0, 0, 270, 180, "Combo Box with context menu", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  If CreatePopupMenu(#PopupMenu)
    MenuItem(#PopupLookup, "Look up this word in a dictionary")
    MenuItem(#PopupCancel, "Cancel")
  EndIf
  
  ComboBoxGadget(#ComboBox1, 10, 10, 250, 21)
  AddGadgetItem(#ComboBox1, -1, "acorn")
  AddGadgetItem(#ComboBox1, -1, "beehive")
  AddGadgetItem(#ComboBox1, -1, "carpenter")
  AddGadgetItem(#ComboBox1, -1, "dancer")
  AddGadgetItem(#ComboBox1, -1, "elephant")
  SetGadgetState(#ComboBox1, 0)
  SetComboBoxSubclassers(#ComboBox1)  
  
  ComboBoxGadget(#ComboBox2, 10, 70, 250, 21, #PB_ComboBox_Editable)
  AddGadgetItem(#ComboBox2, -1,"factory")
  AddGadgetItem(#ComboBox2, -1,"graphical")
  AddGadgetItem(#ComboBox2, -1,"hotel")
  AddGadgetItem(#ComboBox2, -1,"inveterate")
  AddGadgetItem(#ComboBox2, -1,"jalopy")
  AddGadgetItem(#ComboBox2, -1,"knee")
  SetGadgetState(#ComboBox2, 0)
  SetComboBoxSubclassers(#ComboBox2)  
  
  Repeat
    EventId = WaitWindowEvent()  
    Select EventId
      Case #ShowComboBoxListBoxPopupMenu    
        StopComboListBoxWindowUpdate = #True
        ShowPopUpMenuForListBoxSubclasser()
        SetCapture_(EventData()) ; this is the combo list box hwnd
        StopComboListBoxWindowUpdate = #False
      Case #ShowComboBoxPopupMenu       
        ShowPopUpMenuForComboBox(EventData()) ; EventData has the combo box hwnd)
      Case #PB_Event_Menu
        Select EventMenu()
          Case #PopupLookup
            RunProgram("http://www.collinsdictionary.com/dictionary/english/" + TheLookUpWord) 
        EndSelect
    EndSelect  
  Until EventId = #PB_Event_CloseWindow
EndIf

Re: Adding a context menu to a Combo Box in Windows

Posted: Sat Jun 10, 2017 8:45 am
by RichardL
@CJ
Very much what I had in mind :D Thanks.
I have put this in my toolbox for later use.
RichardL

Re: Adding a context menu to a Combo Box in Windows

Posted: Tue Jun 13, 2017 4:04 pm
by Kwai chang caine
Very interesting, sure be userful a day :D
Thanks for sharing 8)