Contextual / Popup Menus Appear at Wrong Location

New Project Xojo 2020r2 Windows 10 with dual screens with different scaling eg 100% and 200%.

Add

dim m as new MenuItem()
m.Text = "here"
base.AddMenu(m)
return true

To the window’s ConstructContextualMenu event.
When running the popup appears on screen 1 (100%) while app is on screen 2 (200%)

Same problem when trying to do it yourself

In the mouse down event add

if IsContextualClick then
  dim b as new MenuItem()
  dim m as new MenuItem()
  m.Text = "here"
  b.AddMenu(m)
  call b.PopUp(System.MouseX,System.MouseY)
end if
return true

It also appears on wrong screen.

Same in Xojo 2019r1.1

Guess I will have to look at MBS calls to try adjust to get it to show at correct location

[Screen 2 is on the right of screen1]

This is an old and very well-known problem. If you search the forums, you should find a number of workarounds.

I have searched the forums, with nothing related to contextual menus showing this problem.

Here is, I believe, the most comprehensive result. Found by searching for “windows hidpi position”

You even responded that you’d solved it for your own apps.

This is related to positioning a window, not popup menu’s.

You’re not using PopupMenus, you’re using MenuItems and supplying the X and Y coordinates on the screen.

I said popup menu because I call menuItem.Popup.

That doesn’t make it a PopupMenu. :wink: PopupMenu and MenuItem are entirely separate. You’re supplying the X and Y coordinates to the MenuItem.Popup method. If there’s a bug in positioning on multiple monitors with varying ScaleFactors, you will see this issue.

I would suggest you look through the thread I linked earlier as I believe someone posted a method there to calculate correct coordinates, then open a Feedback case with a project to replicate the issue with MenuItem positioning.

I have tested and cannot see a way to calculate the correct values because as soon as pass in the correct value, times by 2, the MENUITEM appears on the right of the second screen, as if offscreen and windows is bringing it back in bounds.

So screen1 width is 5120, and passing in 5120 * 2 + 1 shows the menu on the right side of screen 2. While 5120 * 2 - 1 shows the menu on the right of screen 1, I cannot see a way to pass in valid values.

Maybe well known to you…

It is a nasty problem I was not aware of… While obviously not an easy fix as windows HiDPI handling has been a moving target, it should not have gone this long WITHOUT a fix! Apple keeps changing things and Xojo jumps right on it… But not for Windows!

I write apps for use on both Macs and windows (That’s why I pay money to use RB/RS/Xojo) …

The Macs I use have multiple screens, but not so the Windows machines, but the apps could get used on Windows machines with Multiple monitors (particularly laptops) and I would never see the issue…

If Xojo says it supports HIDPI… After all this time (and this bug was known years ago) that support should be solid X-Platform (at least for Mac and Windows which matter most to the most customers)…

Again this situation should not IMO have been allowed to remain this way for years!!!

Workarounds are OK for the short term, but long term they are huge time sinks for developer who keep stumbling across these types of issues…

-Karen

2 Likes

I’m still waiting and hoping for a fix on this. From earlier ongoing reading, months ago, it “sounded like” this wouldn’t be difficult for Xojo to fix internally, but the case hasn’t been resolved - and they’ve been busy with what I’d consider higher priority items. I still haven’t found a way to deal with it.

soft declare function TrackPopupMenu lib "user32.dll" (hMenu as integer,uFlags as uint32,x as integer, y as integer, nREs as integer, hWND as integer,prcRect as ptr) as boolean

call TrackPopupMenu(b.Handle(menuitem.HandleType.WindowsParentHMENU),0,5120 + (System.MouseX - 5120) * 2 ,System.MouseY * 2,0,me.Handle,nil)

this is the direction you need to go. It opens the menu at the correct loction on my second screen. Obviously it needs to work off screen sizes etc. Not sure about handling the clicks, maybe addhandler might work in the constructor, or custom menu items

You can use these two methods to get the true position of a window, then calculate your MenuItem.PopUp X and Y coordinates relatively:

Private Function GetTrueLeft(w as Window) as Integer
  #if TargetWindows then
    Declare Sub GetWindowRect Lib "User32" Alias "GetWindowRect" ( w as Integer, r as Ptr )
    
    dim r as new MemoryBlock( 16 )
    GetWindowRect( w.Handle, r )
    
    Return r.Long( 0 )
  #endif
End Function


Private Function GetTrueTop(w as Window) as Integer
  #if TargetWindows then
    Declare Sub GetWindowRect Lib "User32" Alias "GetWindowRect" ( w as Integer, r as Ptr )
    
    dim r as new MemoryBlock( 16 )
    GetWindowRect( w.Handle, r )
    
    Return r.Long( 4 )
  #endif
End Function

I don’t recall if I wrote these methods or they were suggested by someone else (or from WFS, even), but they do work.

dim d as new Dictionary()
dim b as new MenuItem()

dim m as new MenuItem()
m.Text = "here " + str(Slider1.Value)
d.Value(m.Handle(MenuItem.HandleType.WindowsCommandID)) = m
b.AddMenu(m)

soft declare function TrackPopupMenu lib "user32.dll" (hMenu as integer,uFlags as uint32,x as integer, y as integer, nREs as integer, hWND as integer,prcRect as ptr) as integer
dim res as integer = TrackPopupMenu(b.Handle(menuitem.HandleType.WindowsParentHMENU),&h100,5120 + (System.MouseX - 5120) * 2 ,System.MouseY * 2,0,me.Handle,nil)
if res = 0 then 
  return true
end if
if not d.HasKey(res) then 
  return true
end if

m = d.Value(res)
MessageBox(m.Text)

Getting there

Be careful with those magic numbers. It might work on your machine, but likely won’t on any other.

1 Like
Function MouseDown(X As Integer, Y As Integer) Handles MouseDown as Boolean
  if IsContextualClick then
    dim d as new Dictionary()
    dim b as new MenuItem()
    
    dim m as new MenuItem()
    m.Text = "here " + str(Slider1.Value)
    d.Value(m.Handle(MenuItem.HandleType.WindowsCommandID)) = m
    b.AddMenu(m)
    
    x = x * me.ScaleFactor
    y = y * me.ScaleFactor
    
    x = x + getTrueLeft()
    y = y + getTrueTop()
    
    soft declare function TrackPopupMenu lib "user32.dll" (hMenu as integer,uFlags as uint32,x as integer, y as integer, nREs as integer, hWND as integer,prcRect as ptr) as integer
    dim res as integer = TrackPopupMenu(b.Handle(menuitem.HandleType.WindowsParentHMENU),&h100,x,y,0,me.Handle,nil)
    if res = 0 then 
      //selected id
      return true
    end if
    if not d.HasKey(res) then 
      return true
    end if
    
    m = d.Value(res)
    MessageBox(m.Text)
    
  end if
  return true
End Function

Using get true left / top, needs to account for window title bar height though

Take a look at how it’s done in WFS. I believe you’re looking for GetClientRect, then you can do some math on the height that Xojo reports for the window.

if IsContextualClick then
  #if TargetWindows then
    Declare Function GetSystemMetrics Lib "user32" alias "GetSystemMetrics" (nIndex as Integer) as Integer
    Declare function TrackPopupMenu lib "user32.dll" (hMenu as integer,uFlags as uint32,x as integer, y as integer, nREs as integer, hWND as integer,prcRect as ptr) as integer
    Declare Sub GetWindowRect Lib "User32" Alias "GetWindowRect" ( w as Integer, r as Ptr )
    
    dim winMILookup as new Dictionary()
    
    dim winTL,winTT as integer
    
    dim mb as new MemoryBlock( 16 )
    GetWindowRect( me.Handle, mb )
    
    winTL = mb.Long( 0 )
    winTT = mb.Long( 4 )
    
  #endif
  dim b,m,r as menuitem
  
  b = new MenuItem() //Base menu item to popup
  
  //add menu items 
  m = new MenuItem()
  m.Text = "Menu Item 1"
  b.AddMenu(m)
  
  #if TargetWindows then
    //keep a reference to menu for windows
    winMILookup.Value(m.Handle(MenuItem.HandleType.WindowsCommandID)) = m
  #endif
  
  //add more menu items here
  //add menu items 
  m = new MenuItem()
  m.Text = "Menu Item 2"
  b.AddMenu(m)
  
  #if TargetWindows then
    //keep a reference to menu for windows
    winMILookup.Value(m.Handle(MenuItem.HandleType.WindowsCommandID)) = m
  #endif
  
  #if TargetWindows then
    
    //adjust scale for screen we are on
    x = x * me.ScaleFactor 
    y = y * me.ScaleFactor
    
    //adjust for windows true position
    x = x + winTL 
    y = y + winTT
    
    
    //adjust X and y based on how window is setup
    x = x  + GetSystemMetrics(32) * me.ScaleFactor //SM_CXSIZEFRAME
    y = y + GetSystemMetrics(15) * me.ScaleFactor//SM_CYMENU
    y = y + GetSystemMetrics(4) * me.ScaleFactor//SM_CYCAPTION
    
    //track popup and get result windows command ID or 0 for cancelled, flag &h100 sets this
    dim res as integer = TrackPopupMenu(b.Handle(menuitem.HandleType.WindowsParentHMENU),&h100,x,y,0,me.Handle,nil)
    
    //lookup the res in our dictionary
    r = winMILookup.lookup(res,nil)
    
  #else
    //macOS support
    r = b.PopUp(System.MouseX,System.MouseY)
    
  #endif
  
  if r = nil then 
    //canceled
    return true
  else
    
    MessageBox(r.Text)
    
  end if
end if
return true

Self contained mouse down event on a window to handle mac and windows popups

Also I found this site

for looking up ALL windows constants

Just saw this message, Updated code to use window and client bounds to get offsets.

if IsContextualClick then
  #if TargetWindows then
    Declare function TrackPopupMenu lib "user32" (hMenu as integer,uFlags as uint32,x as integer, y as integer, nREs as integer, hWND as integer,prcRect as ptr) as integer
    Declare Sub GetWindowRect Lib "User32" Alias "GetWindowRect" ( w as Integer, r as Ptr )
    declare function GetClientRect lib "user32" (hWnd as integer, r as Ptr ) as boolean
    
    dim winMILookup as new Dictionary()
    
    dim winTL,winTT,winTR,winTB as integer
    
    dim mb as new MemoryBlock( 16 )
    GetWindowRect( me.Handle, mb )
    
    winTL = mb.Long(0)
    winTT = mb.Long(4)
    winTR = mb.Long(8)
    winTB = mb.Long(12)
    
    call GetClientRect(me.Handle,mb)
    
    dim winCL,winCT,winCR,winCB as integer
    
    winCL = mb.Long(0)
    winCT = mb.Long(4)
    winCR = mb.Long(8)
    winCB = mb.Long(12)
    
    //calculate the window and client areas
    dim wh as integer = winTB - winTT
    dim ch as integer = winCB - winCT
    dim ww as integer = winTR - winTL
    dim cw as integer = winCR - winCL
    
    //calculate the border size from width
    dim bw as integer = (ww - cw) / 2
    //calculate the header size frame + any menus
    dim hH as integer = (wh - ch) - 2 * bw
    
  #endif
  dim b,m,r as menuitem
  
  b = new MenuItem() //Base menu item to popup
  
  //add menu items 
  m = new MenuItem()
  m.Text = "Menu Item 1"
  b.AddMenu(m)
  
  #if TargetWindows then
    //keep a reference to menu for windows
    winMILookup.Value(m.Handle(MenuItem.HandleType.WindowsCommandID)) = m
  #endif
  
  //add more menu items here
  //add menu items 
  m = new MenuItem()
  m.Text = "Menu Item 2"
  b.AddMenu(m)
  
  #if TargetWindows then
    //keep a reference to menu for windows
    winMILookup.Value(m.Handle(MenuItem.HandleType.WindowsCommandID)) = m
  #endif
  
  #if TargetWindows then
    
    //adjust scale for screen we are on
    x = x * me.ScaleFactor 
    y = y * me.ScaleFactor
    
    //adjust for windows true position
    x = x + winTL + bW
    y = y + winTT + HH
    
    //track popup and get result windows command ID or 0 for cancelled, flag &h100 sets this
    dim res as integer = TrackPopupMenu(b.Handle(menuitem.HandleType.WindowsParentHMENU),&h100,x,y,0,me.Handle,nil)
    
    //lookup the res in our dictionary
    r = winMILookup.lookup(res,nil)
    
  #else
    //macOS support
    r = b.PopUp(System.MouseX,System.MouseY)
    
  #endif
  
  if r = nil then 
    //canceled
    return true
  else
    
    MessageBox(r.Text)
    
  end if
end if
return true
1 Like

PopupMenu is very similar to combobox except you can’t type on the textbox