Question

Does anyone know of how i can determine when the contextMenu get its placement automatically adjusted due to being too close to the edge of the screen?

My scenario is that i have a contextMenu that has 2 rounded corners and 2 square corners. When the menu opens down i round the bottom 2... and if the menu is opening upwards then i round the top 2. The problem is that I haven't found an event or property to bind to that tells me when the menu gets its direction automatically changed.

Here's some simplified sample code to try out. If you click when the window is at top of screen then menu goes down. If you move window to bottom of screen then the menu will go up.

<Window x:Class="menuRedirection.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="100" Width="200">
  <DockPanel Name="panel" ContextMenuOpening="DockPanel_ContextMenuOpening">
    <DockPanel.ContextMenu>
      <ContextMenu>
        <MenuItem Header="item"/>
        <MenuItem Header="item"/>
        <MenuItem Header="item"/>
        <MenuItem Header="item"/>
      </ContextMenu>
    </DockPanel.ContextMenu>
    <Rectangle DockPanel.Dock="Bottom" Name="menuTarget" Fill="Red" Height="10"/>
    <TextBlock DockPanel.Dock="Top" Text="right click for context menu"/>
  </DockPanel>
</Window>

private void DockPanel_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
  ContextMenuService.SetPlacement(panel, PlacementMode.Bottom);
  ContextMenuService.SetPlacementTarget(panel, menuTarget);
}

Here's what the real application looks like so you can see my problem with needing to know to adjust my rounded corners.

enter image description here

Was it helpful?

Solution 2

I was unable to find a true WPF solution but Justin's comment lead me down the path of experimenting with comparing the menu's location with the PlacementTarget's location.

First step was to subscribe to the contextMenu.Loaded event (this fires after layout has been processed but before it's fully visible on the screen).

<ContextMenu ContextMenu.Loaded="ContextMenu_Loaded">

And then when that fires I can figure out if the menu was internally switched to the alternate placement for my requested placementMode. If it was reversed then I go ahead and adjust my rounded corners accordingly.

NOTE: i initially had used getWindowRect and compared the menu Rect with the target's Rect, but found that the menu Rect was always returning the prior instance's location. To avoid this problem I now get the relevant screen's workingArea and manually see if the menu fits.

NOTE2: be sure your menu's template results in the same window height for both inverted and regular display. Otherwise, your calculation could be off since getWindowRect returns the last menu's size.

void ContextMenu_Loaded(object sender, RoutedEventArgs e)
{
  bool reversed = isMenuDirectionReversed(this.ContextMenu);

  //existing styles are read-only so we have to make a clone to change a property
  if (reversed)
  {//round the top corners if the menu is travelling upward
    Style newStyle = new Style(typeof(ContextMenu), this.ContextMenu.Style);
    newStyle.Setters.Add(new Setter { Property = Border.CornerRadiusProperty, Value = new CornerRadius(10, 10, 0, 0) });
    this.ContextMenu.Style = newStyle;
  }
  else
  { //since we may have overwritten the style in a previous evaluation, 
    //we also need to set the downward corners again    
    Style newStyle = new Style(typeof(ContextMenu), this.ContextMenu.Style);
    newStyle.Setters.Add(new Setter { Property = Border.CornerRadiusProperty, Value = new CornerRadius(0, 0, 10, 10) });
    this.ContextMenu.Style = newStyle;
  }
}

Evaluation method:

private bool isMenuDirectionReversed(ContextMenu menu)
{
  //get the window handles for the popup' placement target
  IntPtr targetHwnd = (HwndSource.FromVisual(menu.PlacementTarget) as HwndSource).Handle;

  //get the relevant screen
  winFormsScreen screen = winFormsScreen.FromHandle(targetHwnd);

  //get the actual point on screen (workingarea not taken into account)
  FrameworkElement targetCtrl = menu.PlacementTarget as FrameworkElement;
  Point targetLoc = targetCtrl.PointToScreen(new Point(0, 0));

  //compute the location for the bottom of the target control
  double targetBottom = targetLoc.Y + targetCtrl.ActualHeight;

  if (menu.Placement != PlacementMode.Bottom)
    throw new NotImplementedException("you need to implement your own logic for other modes");

  return screen.WorkingArea.Bottom < targetBottom + menu.ActualHeight;
}

Final result:

enter image description here

OTHER TIPS

As far as I can tell, this is not possible.

Using JustDecompile, I traced this functionality to the UpdatePosition method in the Popup class. The final location seems to be set here:

    this._positionInfo.X = num4;
    this._positionInfo.Y = num5;
    this._secHelper.SetPopupPos(true, num4, num5, false, 0, 0);

_secHelper is a helper class of type PopupSecurityHelper, and seems to just be an internal helper...And, none of these result in an event or even a public property being changed.

Here is an MSDN article explaining how popup positioning is determined in general (The 'When the popup encounters the edge of a screen' describes your scenario).

However, this article explains how you can use the CustomPopupPlacementCallback to override these behaviors somewhat. However, this still uses a PopupPrimaryAxis, which should flip the menu as necessary, and will result in the same problem.

The only other thing I could think of is that you could look into the PlacementRectangle and maybe poll the size and location similar to how UpdatePosition does things...or just check the popup itself just like UpdatePosition does.

This is a private method, though. So, any logic you try to mimic could change in a future version of the framework.

UPDATE

Also, you could possibly try bastardizing PointToScreen or PointFromScreen, but that would be very convoluted code if it worked...

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top