WPF Handedness with Popups

2019-01-08 21:36发布

问题:

I just moved my PC to Windows 8 from Windows 7 and while running our WPF application I noticed that our WPF popups and/or tool tips are now in the lower-left by default instead of the normal lower right. Has anyone noticed this? I know you can specify their location on each tooltip in the xaml, but we have a lot of tool tips and popups. I want to know if there is a way to specify the default location globally in a WPF app. Google hasn't yielded many results on this subject. We have a reason to keep them in the same original default position (some popups have content relative to their start up position).

Windows 8: (Lower left)

Windows 7: (Lower right)

Same code! Standard "tooltip" xaml attribute.

Any ideas?

RESOLVED AND I POSTED THE COMMENTS


Ok, I have found the issue. It has to do with Tablet PCs/Touchscreens. (left handed.. right handed preference) This other link provided a reason. I am working on a solution to resolve this now. Ill post up the details soon!

windows 8 popup location

回答1:

Thanks @TravisWhidden for the solution. Just implemented an improved version of it that listens to the StaticPropertyChanged event, I'll paste it in here because it seems less of a "hack".

    private static readonly FieldInfo _menuDropAlignmentField;
    static MainWindow()
    {
        _menuDropAlignmentField = typeof(SystemParameters).GetField("_menuDropAlignment", BindingFlags.NonPublic | BindingFlags.Static);
        System.Diagnostics.Debug.Assert(_menuDropAlignmentField != null);

        EnsureStandardPopupAlignment();
        SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
    }

    private static void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        EnsureStandardPopupAlignment();
    }

    private static void EnsureStandardPopupAlignment()
    {
        if (SystemParameters.MenuDropAlignment && _menuDropAlignmentField != null)
        {
            _menuDropAlignmentField.SetValue(null, false);
        }
    }


回答2:

Ok, for those that do not want this to occur at all in their app (which was our desire), We have created a nice little hack for WPF. This works well for us.

First:

This code will be what runs which fixes the issue:

public static void SetAlignment()
{
    var ifLeft = SystemParameters.MenuDropAlignment;

    if (ifLeft)
    {
        // change to false
        var t = typeof(SystemParameters);
        var field = t.GetField("_menuDropAlignment", BindingFlags.NonPublic | BindingFlags.Static);
        field.SetValue(null, false);

        ifLeft = SystemParameters.MenuDropAlignment;
    }
}

However, the environment can de-validate microsofts internal cache of these values, so we have to hook into WinProc to get this. I wont post the WinProc code, just the messages that are needed:

These are the Win32 messages that will de-validate the internal cache:

private const int WM_WININICHANGE = 0x001A;
private const int WM_DEVICECHANGE = 0x219;
private const int WM_DISPLAYCHANGE = 0x7E;
private const int WM_THEMECHANGED = 0x031A;
private const int WM_SYSCOLORCHANGE = 0x15;

And the quick snippit that will set your preference back. Because we are hooked into WinProc, you will want to change this value after WinProc is finished with the message on other handlers. We have a delay to re-set the preference value back to what we want.

if (msg == WM_WININICHANGE || msg == WM_DEVICECHANGE || msg == WM_DISPLAYCHANGE || msg == WM_THEMECHANGED || msg == WM_SYSCOLORCHANGE)
{
    Timer timer = null;
    timer = new Timer((x) =>
        {
            WpfHelperHacks.SetAlignment();
            timer.Dispose();
        },null, TimeSpan.FromMilliseconds(2), TimeSpan.FromMilliseconds(-1));
}

And just like that its complete. I hope this helps someone else!



回答3:

If you cannot use solutions that alter this behavior system-wide, here's how I do it for a single popup:

public enum HorizontalPlacement { Left, Right, Center };

public enum VerticalPlacement { Top, Bottom, Center };

/// <summary>
/// In WPF, PopUps pop up in different places on different machines (due to different "handedness" on touch-enabled screens.  This fixes it.
/// See Also: http://social.msdn.microsoft.com/Forums/vstudio/en-US/19ef3d33-01e5-45c5-a845-d64f9231001c/popup-positioningalignments?forum=wpf
/// </summary>
public static class PopupPlacement
{
    /// <summary>
    /// Usage: In XAML, add the following to your tooltip: 
    ///     Placement="Custom" CustomPopupPlacementCallback="CustomPopupPlacementCallback" 
    /// and call this method from the CustomPopupPlacementCallback.
    /// </summary>
    public static CustomPopupPlacement[] PlacePopup(Size popupSize, Size targetSize, Point offset, VerticalPlacement verticalPlacement, HorizontalPlacement horizontalPlacement)
    {
        Point p = new Point
        {
            X = GetHorizontalOffset(popupSize, targetSize, horizontalPlacement),
            Y = GetVerticalOffset(popupSize, targetSize, verticalPlacement)
        };

        return new[]
        {
            new CustomPopupPlacement(p, PopupPrimaryAxis.Horizontal)
        };
    }

    private static double GetVerticalOffset(Size popupSize, Size targetSize, VerticalPlacement verticalPlacement)
    {
        switch (verticalPlacement)
        {
            case VerticalPlacement.Top:
                return -popupSize.Height;
            case VerticalPlacement.Bottom:
                return targetSize.Height;
            case VerticalPlacement.Center:
                return -(popupSize.Height/ 2) + targetSize.Height / 2;
        }

        throw new ArgumentOutOfRangeException("verticalPlacement");
    }

    private static double GetHorizontalOffset(Size popupSize, Size targetSize, HorizontalPlacement horizontalPlacement)
    {
        switch (horizontalPlacement)
        {
            case HorizontalPlacement.Left:
                return -popupSize.Width;
            case HorizontalPlacement.Right:
                return 0;
            case HorizontalPlacement.Center:
                return -(popupSize.Width / 2) + targetSize.Width / 2;
        }
        throw new ArgumentOutOfRangeException("horizontalPlacement");
    }
}