January 1, 2015

Anchor WPF Popup exactly where you need it with PrecisePopup

Popup in WPF of course allows you to select one of the predefined locations relative to placement target. The trouble begins when your placement target is close to the edge of the screen. If the popup doesn't fit in the screen when positioned as specified, Popup control will automatically move it to ensure the whole popup is visible. This is okay for usual rectangular popups, but it's a headache for authors of "eared" or balloon popups that have tabpage-like ear or pointed anchor. PrecisePopup, part of my opensource JungleControls library, is designed to handle these scenarios.

Below you can see PrecisePopup in action. The window was moved so that default bottom-right placement would be obscured by taskbar. Instead of sliding the popup to the left like the built-in Popup does, PrecisePopup selected another placement (bottom-left) from its discrete list of allowed placements.



Below you can see how the PrecisePopup was configured to yield the above behavior. Some details have been omitted for brevity.

<jc:PrecisePopup Name="MyPopup"
                 PlacementTarget="{Binding ElementName=MyButton}"
                 IsOpen="{Binding IsOpen}"
                 Background="#a0ffffff">
    <jc:PrecisePopup.Placements>
        <jc:PrecisePopupPlacement
            Tag="BottomRight" />
        <jc:PrecisePopupPlacement
            Tag="BottomLeft"
            HorizontalTargetAlignment="Right"
            HorizontalPopupAlignment="Right" />
        <jc:PrecisePopupPlacement
            Tag="TopRight"
            VerticalTargetAlignment="Bottom"
            VerticalPopupAlignment="Bottom" />
        <jc:PrecisePopupPlacement
            Tag="TopLeft"
            HorizontalTargetAlignment="Right"
            HorizontalPopupAlignment="Right" />
    </jc:PrecisePopup.Placements>
    <Border Width="200"
            Height="200"
            BorderBrush="Gray"
            BorderThickness="2">
        <TextBlock
            Text="{Binding SelectedPlacement.Tag,
                           ElementName=MyPopup}"
            HorizontalAlignment="Center"
            VerticalAlignment="Center" />
    </Border>
</jc:PrecisePopup>

PrecisePopup is configured with one or more PrecisePopupPlacement instances. Each PrecisePopupPlacement specifies location of popup relative to placement target. PrecisePopup never adjusts position specified in PrecisePopupPlacement. If the popup happens to fall off the screen, so be it.

The only thing that PrecisePopup does is to select one PrecisePopupPlacement from the list of possible alternatives. It will select the first PrecisePopupPlacement that fits on screen. If no one fits fully, PrecisePopupPlacement will select the one that has least area clipped behind screen edges. This allows you to define several layouts for your popup depending on where the popup is opened.

PrecisePopup auto-sizes to fit its contents. It is possible to limit popup size to screen bounds without moving the popup. Notice that the popup always grows in some direction to accommodate its content. For example, setting HorizontalPopupAlignment to Left causes the popup to grow to the right. Center alignment causes it to grow equally in both directions. If you set ClipToScreen on PrecisePopupPlacement, PrecisePopup will stop growing when it reaches screen boundary. For Center alignment, popup will stop growing when it hits screen boundary that is closer to popup center.

PrecisePopup properties:
  • Content - What is to be displayed in the popup.
  • ContentTemplate, ContentTemplateSelector, ContentStringFormat - How the content should be displayed (inherited from ContentControl).
  • Placements - List of PrecisePopupPlacement instances specifying possible locations of the popup.
  • IsOpen - Whether the popup is open at the moment. This property is automatically reset when user clicks outside of the popup.
  • PlacementTarget - Element where the popup is anchored.
  • SuppressTarget - Clicks to this element will be ignored when the popup is being closed. Usually the same element as PlacementTarget. Setting SuppressTarget resolves issues with popup reopening when associated ToggleButton is clicked.
  • SelectedPlacement (read only) - PrecisePopupPlacement that is currently in use. This property can be used with triggers to change content layout depending on current popup placement. You can use Tag property on the placement to make placements easier to identify.
  • AllowsTransparency - Whether the popup window should have transparency enabled.
  • Background - Background for the popup window.
PrecisePopupPlacement properties:
  • HorizontalTargetAlignment, VerticalTargetAlignment - Alignment point on target. Popup will be positioned so that its alignment point is over target's alignment point. Stretch is treated the same as Center.
  • HorizontalPopupAlignment, VerticalPopupAlignment - Alignment point on the popup. Popup will be positioned so that this point is over target's alignment point. Stretch is treated the same as Center.
  • HorizontalOffset, VerticalOffset - These properties cause the popup to be displaced by the specified amount from the place where it would otherwise appear.
  • Tag - Arbitrary attached data, usually the name of this PrecisePopupPlacement. It can be used to identify PrecisePopupPlacement returned from SelectedPlacement property above.
  • ClipToScreen - Limits popup growth to screen boundaries. See above for explanation.
You can get PrecisePopup from NuGet as part of my opensource JungleControls library.

3 comments:

  1. Nice control.
    Any chance for a version with a dependency property that sets a margin to extend the screen nudge coordinates?

    The reason is because I have a popup that has a rendered shadow, therefore it needs to extend the popup over its edges (using a negative margin of -32) to be able to render its shadow, but then the screen nudging takes this margin into account and nudges the shadow edge instead of the actual content.

    So I think a popup property of Thickness OffsetScreenClip should be useful in this case (which I'd set to the same as the margin via binding).

    Also, any chance for a StaysOpen?

    ReplyDelete
  2. Also I noticed it cannot be used as popup for menu styles, I just tried and it doesn't show unlike the regular popup :(

    ReplyDelete
  3. xavi, I no longer actively develop the library since I shifted my attention to web stack. You are however free to fork it and introduce the properties you need.

    ReplyDelete