Search code examples
wpfcontextmenu

WPF ContextMenu placement adjusted event


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


Solution

  • 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