В WPF, как сдвинуть окно на экран, если оно отключено от экрана?

Если у меня есть окно, как я могу гарантировать, что окно никогда не будет скрыто от экрана?

Это важно, потому что иногда, если пользователь добавляет или удаляет монитор, окно может быть навсегда скрыто от экрана, если мы вспомнили предыдущую позицию.

Я использую WPF + MVVM.

Ответ 1

Этот ответ был протестирован в широкомасштабном реальном мире.

Вызовите это из любого прикрепленного свойства, чтобы переместить окно обратно на видимый экран:

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/info/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;
    }
}

В качестве бонуса вы можете реализовать свой собственный drag'n'drop, и когда перетаскивание закончится, окно будет перенесено обратно на экран.

Было бы более интуитивно понятным с точки зрения пользователей, если бы окно быстро вернулось в видимую область, а не просто пропустило обратно в видимую область, но по крайней мере этот метод получил правильные результаты.

/// <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
}

Дополнительные ссылки

За подсказками о том, как избежать окна с уведомлением, скрытого другими окнами, см. мой ответ по адресу: Принесите окно в WPF.