简体   繁体   English

在 WPF 中,如果窗口不在屏幕上,如何将窗口移到屏幕上?

[英]In WPF, how to shift a window onto the screen if it is off the screen?

If I have a window, how can I ensure that the window will never be hidden off the screen?如果我有一个窗口,我如何确保该窗口永远不会隐藏在屏幕之外?

This is important, because sometimes if the user adds or removes a monitor, the window may be permanently hidden off the screen if we have remembered the previous position.这很重要,因为有时如果用户添加或删除监视器,如果我们记得之前的位置,窗口可能会永久隐藏在屏幕之外。

I am using WPF + MVVM .我正在使用WPF + MVVM

This answer has been tested in a large scale real world application.这个答案已经在大规模的现实世界应用程序中进行了测试。

Call this from any attached property to shift the window back onto the visible screen:从任何附加属性调用它以将窗口移回可见屏幕:

public static class ShiftWindowOntoScreenHelper
{
    /// <summary>
    ///     Intent:  
    ///     - Shift the window onto the visible screen.
    ///     - Shift the window away from overlapping the task bar.
    /// </summary>
    public static void ShiftWindowOntoScreen(Window window)
    {
        // Note that "window.BringIntoView()" does not work.                            
        if (window.Top < SystemParameters.VirtualScreenTop)
        {
            window.Top = SystemParameters.VirtualScreenTop;
        }

        if (window.Left < SystemParameters.VirtualScreenLeft)
        {
            window.Left = SystemParameters.VirtualScreenLeft;
        }

        if (window.Left + window.Width > SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth)
        {
            window.Left = SystemParameters.VirtualScreenWidth + SystemParameters.VirtualScreenLeft - window.Width;
        }

        if (window.Top + window.Height > SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight)
        {
            window.Top = SystemParameters.VirtualScreenHeight + SystemParameters.VirtualScreenTop - window.Height;
        }

        // Shift window away from taskbar.
        {
            var taskBarLocation = GetTaskBarLocationPerScreen();

            // If taskbar is set to "auto-hide", then this list will be empty, and we will do nothing.
            foreach (var taskBar in taskBarLocation)
            {
                Rectangle windowRect = new Rectangle((int)window.Left, (int)window.Top, (int)window.Width, (int)window.Height);

                // Keep on shifting the window out of the way.
                int avoidInfiniteLoopCounter = 25;
                while (windowRect.IntersectsWith(taskBar))
                {
                    avoidInfiniteLoopCounter--;
                    if (avoidInfiniteLoopCounter == 0)
                    {
                        break;
                    }

                    // Our window is covering the task bar. Shift it away.
                    var intersection = Rectangle.Intersect(taskBar, windowRect);

                    if (intersection.Width < window.Width
                        // This next one is a rare corner case. Handles situation where taskbar is big enough to
                        // completely contain the status window.
                        || taskBar.Contains(windowRect))
                    {
                        if (taskBar.Left == 0)
                        {
                            // Task bar is on the left. Push away to the right.
                            window.Left = window.Left + intersection.Width;
                        }
                        else
                        {
                            // Task bar is on the right. Push away to the left.
                            window.Left = window.Left - intersection.Width;
                        }
                    }

                    if (intersection.Height < window.Height
                        // This next one is a rare corner case. Handles situation where taskbar is big enough to
                        // completely contain the status window.
                        || taskBar.Contains(windowRect))
                    {
                        if (taskBar.Top == 0)
                        {
                            // Task bar is on the top. Push down.
                            window.Top = window.Top + intersection.Height;
                        }
                        else
                        {
                            // Task bar is on the bottom. Push up.
                            window.Top = window.Top - intersection.Height;
                        }
                    }

                    windowRect = new Rectangle((int)window.Left, (int)window.Top, (int)window.Width, (int)window.Height);
                }
            }
        }
    }

    /// <summary>
    /// Returned location of taskbar on a per-screen basis, as a rectangle. See:
    /// https://stackoverflow.com/questions/1264406/how-do-i-get-the-taskbars-position-and-size/36285367#36285367.
    /// </summary>
    /// <returns>A list of taskbar locations. If this list is empty, then the taskbar is set to "Auto Hide".</returns>
    private static List<Rectangle> GetTaskBarLocationPerScreen()
    {
        List<Rectangle> dockedRects = new List<Rectangle>();
        foreach (var screen in Screen.AllScreens)
        {
            if (screen.Bounds.Equals(screen.WorkingArea) == true)
            {
                // No taskbar on this screen.
                continue;
            }

            Rectangle rect = new Rectangle();

            var leftDockedWidth = Math.Abs((Math.Abs(screen.Bounds.Left) - Math.Abs(screen.WorkingArea.Left)));
            var topDockedHeight = Math.Abs((Math.Abs(screen.Bounds.Top) - Math.Abs(screen.WorkingArea.Top)));
            var rightDockedWidth = ((screen.Bounds.Width - leftDockedWidth) - screen.WorkingArea.Width);
            var bottomDockedHeight = ((screen.Bounds.Height - topDockedHeight) - screen.WorkingArea.Height);
            if ((leftDockedWidth > 0))
            {
                rect.X = screen.Bounds.Left;
                rect.Y = screen.Bounds.Top;
                rect.Width = leftDockedWidth;
                rect.Height = screen.Bounds.Height;
            }
            else if ((rightDockedWidth > 0))
            {
                rect.X = screen.WorkingArea.Right;
                rect.Y = screen.Bounds.Top;
                rect.Width = rightDockedWidth;
                rect.Height = screen.Bounds.Height;
            }
            else if ((topDockedHeight > 0))
            {
                rect.X = screen.WorkingArea.Left;
                rect.Y = screen.Bounds.Top;
                rect.Width = screen.WorkingArea.Width;
                rect.Height = topDockedHeight;
            }
            else if ((bottomDockedHeight > 0))
            {
                rect.X = screen.WorkingArea.Left;
                rect.Y = screen.WorkingArea.Bottom;
                rect.Width = screen.WorkingArea.Width;
                rect.Height = bottomDockedHeight;
            }
            else
            {
                // Nothing found!
            }

            dockedRects.Add(rect);
        }

        if (dockedRects.Count == 0)
        {
            // Taskbar is set to "Auto-Hide".
        }

        return dockedRects;
    }
}

As a bonus, you can implement your own drag'n'drop, and when the drag finishes, the window will be shifted back onto the screen.作为奖励,您可以实现自己的拖放,拖放完成后,窗口将移回屏幕上。

It would be more intuitive from the users point of view if the window slid quickly back into the visible area rather than just skipping back into the visible area, but at least this method gets the right results.如果窗口快速滑回可见区域而不是直接跳回可见区域,从用户的角度来看会更直观,但至少这种方法得到了正确的结果。

/// <summary>
///     Intent: Add this Attached Property to any XAML element, to allow you to click and drag the entire window.
///     Essentially, it searches up the visual tree to find the first parent window, then calls ".DragMove()" on it. Once the drag finishes, it pushes
///     the window back onto the screen if part or all of it wasn't visible.
/// </summary>
public class EnableDragAttachedProperty
{
    public static readonly DependencyProperty EnableDragProperty = DependencyProperty.RegisterAttached(
        "EnableDrag",
        typeof(bool),
        typeof(EnableDragAttachedProperty),
        new PropertyMetadata(default(bool), OnLoaded));

    private static void OnLoaded(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        try
        {
            var uiElement = dependencyObject as UIElement;
            if (uiElement == null || (dependencyPropertyChangedEventArgs.NewValue is bool) == false)
            {
                return;
            }
            if ((bool)dependencyPropertyChangedEventArgs.NewValue == true)
            {
                uiElement.MouseMove += UIElement_OnMouseMove;
            }
            else
            {
                uiElement.MouseMove -= UIElement_OnMouseMove;
            }
        }
        catch (Exception ex)
        {
             // Log exception here.
        }
    }

    /// <summary>
    ///     Intent: Fetches the parent window, so we can call "DragMove()"on it. Caches the results in a dictionary,
    ///     so we can apply this same property to multiple XAML elements.
    /// </summary>
    private static void UIElement_OnMouseMove(object sender, MouseEventArgs mouseEventArgs)
    {
        try
        {
            var uiElement = sender as UIElement;
            if (uiElement != null)
            {
                Window window = GetParentWindow(uiElement);

                if (mouseEventArgs.LeftButton == MouseButtonState.Pressed)
                {
                    // DragMove is a synchronous call: once it completes, the drag is finished and the left mouse
                    // button has been released.
                    window?.DragMove();

                    // See answer in section 'Additional Links' below in the SO answer.
                //HideAndShowWindowHelper.ShiftWindowIntoForeground(window);

                    // When the use has finished the drag and released the mouse button, we shift the window back
                    // onto the screen, it it ended up partially off the screen.
                    ShiftWindowOntoScreenHelper.ShiftWindowOntoScreen(window);
                }
            }
        }
        catch (Exception ex)
        {
            _log.Warn($"Exception in {nameof(UIElement_OnMouseMove)}. " +
                      $"This means that we cannot shift and drag the Toast Notification window. " +
                      $"To fix, correct C# code.", ex);
        }
    }

    public static void SetEnableDrag(DependencyObject element, bool value)
    {
        element.SetValue(EnableDragProperty, value);
    }

    public static bool GetEnableDrag(DependencyObject element)
    {
        return (bool)element.GetValue(EnableDragProperty);
    }

    #region GetParentWindow
    private static readonly Dictionary<UIElement, Window> _parentWindow = new Dictionary<UIElement, Window>();
    private static readonly object _parentWindowLock = new object();

    /// <summary>
    ///     Intent: Given any UIElement, searches up the visual tree to find the parent Window.
    /// </summary>
    private static Window GetParentWindow(UIElement uiElement)
    {
        bool ifAlreadyFound;
        lock (_parentWindowLock)
        {
            ifAlreadyFound = _parentWindow.ContainsKey(uiElement) == true;
        }

        if (ifAlreadyFound == false)
        {
            DependencyObject parent = uiElement;
            int avoidInfiniteLoop = 0;
            // Search up the visual tree to find the first parent window.
            while ((parent is Window) == false)
            {
                parent = VisualTreeHelper.GetParent(parent);
                avoidInfiniteLoop++;
                if (avoidInfiniteLoop == 1000)
                {
                    // Something is wrong - we could not find the parent window.
                    return null;
                }
            }
            lock (_parentWindowLock)
            {
                _parentWindow[uiElement] = parent as Window;
            }
        }
        lock(_parentWindowLock)
        {
            return _parentWindow[uiElement];
        }
    }
    #endregion
}

Additional links附加链接

For tips on how to avoid having a notification window hidden by other windows, see my answer at: Bring a window to the front in WPF .有关如何避免通知窗口被其他窗口隐藏的提示,请参阅我的回答: 将窗口置于 WPF 的前面

The subject is old, but if anyone is looking for another solution, I did this to display a window on a click at the cursor position and ensure the window is always visible according to screen (working area) boudaries :该主题很旧,但如果有人正在寻找其他解决方案,我这样做是为了在光标位置单击时显示一个窗口,并确保根据屏幕(工作区)边界始终可见该窗口:

public static void PositionWindowOnScreen(Window window)
{
    Screen activeScreen = Screen.FromPoint(Control.MousePosition);

    double xPositionToSet = Control.MousePosition.X;
    double yPositionToSet = Control.MousePosition.Y;

    if (xPositionToSet + window.Width > activeScreen.WorkingArea.Width + activeScreen.WorkingArea.X && 
        xPositionToSet - activeScreen.WorkingArea.X - window.Width > 0)
    {
        xPositionToSet = xPositionToSet - window.Width;
    }

    if (yPositionToSet + window.Height > activeScreen.WorkingArea.Height + activeScreen.WorkingArea.Y && 
        yPositionToSet - activeScreen.WorkingArea.Y - window.Height > 0)
    {
        yPositionToSet = yPositionToSet - window.Height;
    }

    window.WindowStartupLocation = WindowStartupLocation.Manual;
    window.Left = xPositionToSet;
    window.Top = yPositionToSet;
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM