Восстановление размера окна / положения с несколькими мониторами - PullRequest
31 голосов
/ 02 июня 2009

Много сообщений о восстановлении позиции и размера WinForm.

Примеры:

Но мне еще предстоит найти код для этого с несколькими мониторами.

То есть, если я закрою свое приложение .NET Winform с окном на мониторе 2, я хочу, чтобы оно сохраняло размер, расположение и состояние окон в настройках приложения, чтобы впоследствии оно могло восстановить монитор 2 при перезапуске. приложение. Было бы неплохо, если бы, как и в примере кода проекта выше, он включал в себя некоторые проверки работоспособности, например, если сохраненное местоположение в основном находится за пределами экрана, оно «исправляет» его. Или, если сохраненное местоположение находится на мониторе, которого больше нет (например, мой ноутбук теперь сам по себе без моего второго монитора), то он корректно перемещает его на монитор 1.

Есть мысли?

Моя среда: C #, .NET 3.5 или ниже, VS2008

Ответы [ 6 ]

36 голосов
/ 02 июня 2009

Попробуйте этот код. Достопримечательности:

  • Проверяет, является ли окно (частично) видимым в любой рабочей области экрана. Например. перетаскивая его за панель задач или полностью переместив его за пределы экрана, вы сбрасываете положение в окно по умолчанию.
  • Сохраняет правильные границы, даже если форма свернута или развернута (общая ошибка)
  • Правильно сохраняет WindowState. Сохранение FormWindowState.Minimized отключено дизайном.

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

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();

        // this is the default
        this.WindowState = FormWindowState.Normal;
        this.StartPosition = FormStartPosition.WindowsDefaultBounds;

        // check if the saved bounds are nonzero and visible on any screen
        if (Settings.Default.WindowPosition != Rectangle.Empty &&
            IsVisibleOnAnyScreen(Settings.Default.WindowPosition))
        {
            // first set the bounds
            this.StartPosition = FormStartPosition.Manual;
            this.DesktopBounds = Settings.Default.WindowPosition;

            // afterwards set the window state to the saved value (which could be Maximized)
            this.WindowState = Settings.Default.WindowState;
        }
        else
        {
            // this resets the upper left corner of the window to windows standards
            this.StartPosition = FormStartPosition.WindowsDefaultLocation;

            // we can still apply the saved size
            this.Size = Settings.Default.WindowPosition.Size;
        }
    }

    private bool IsVisibleOnAnyScreen(Rectangle rect)
    {
        foreach (Screen screen in Screen.AllScreens)
        {
            if (screen.WorkingArea.IntersectsWith(rect))
            {
                return true;
            }
        }

        return false;
    }

    protected override void OnClosed(EventArgs e)
    {
        base.OnClosed(e);

        // only save the WindowState if Normal or Maximized
        switch (this.WindowState)
        {
            case FormWindowState.Normal:
            case FormWindowState.Maximized:
                Settings.Default.WindowState = this.WindowState;
                break;

            default:
                Settings.Default.WindowState = FormWindowState.Normal;
                break;
        }

        // reset window state to normal to get the correct bounds
        // also make the form invisible to prevent distracting the user
        this.Visible = false;
        this.WindowState = FormWindowState.Normal;

        Settings.Default.WindowPosition = this.DesktopBounds;
        Settings.Default.Save();
    }
}

Файл настроек для справки:

<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="ScreenTest" GeneratedClassName="Settings">
    <Profiles />
    <Settings>
        <Setting Name="WindowPosition" Type="System.Drawing.Rectangle" Scope="User">
            <Value Profile="(Default)">0, 0, 0, 0</Value>
        </Setting>
        <Setting Name="WindowState" Type="System.Windows.Forms.FormWindowState" Scope="User">
            <Value Profile="(Default)">Normal</Value>
        </Setting>
    </Settings>
</SettingsFile>
27 голосов
/ 03 июня 2009

Ответ, предоставленный ВВС, очень помог! Однако я обнаружил две незначительные проблемы с этим, поэтому я публикую большую часть его кода с этими ревизиями:

(1) При первом запуске приложения форма открывается в обычном состоянии, но имеет такой размер, что выглядит как строка заголовка. Я добавил условие в конструктор, чтобы исправить это.

(2) Если приложение закрыто, когда свернуто или развернуто, код в OnClosing не запоминает размеры окна в его нормальном состоянии. (3 строки кода - которые я сейчас закомментировал - кажется разумным, но по какой-то причине просто не работают.) К счастью, я ранее решил эту проблему и включил этот код в новый регион в конце кода. отслеживать состояние окна, как оно происходит, а не ждать закрытия.


С этими двумя исправлениями я протестировал:

A. закрытие в нормальном состоянии - восстанавливает в тот же размер / положение и состояние

B. закрытие в свернутом состоянии - восстанавливается до нормального состояния с последним нормальным размером / положением

C. закрытие в максимизированном состоянии - восстанавливается в максимизированном состоянии и запоминает его последний размер / положение, когда один из них впоследствии переходит в нормальное состояние.

D. закрытие на мониторе 2 - восстановление на мониторе 2.

E. закрытие на мониторе 2, затем отсоединение монитора 2 - восстановление в том же положении на мониторе 1

Дэвид: ваш код позволил мне практически без усилий набирать баллы D и E - вы не только предоставили решение для моего вопроса, вы предоставили его в полной программе, поэтому я его запустил и запустил почти через несколько секунд после его вставки в Visual Studio. Так что большое спасибо за это!

public partial class MainForm : Form
{
    bool windowInitialized;

    public MainForm()
    {
        InitializeComponent();

        // this is the default
        this.WindowState = FormWindowState.Normal;
        this.StartPosition = FormStartPosition.WindowsDefaultBounds;

        // check if the saved bounds are nonzero and visible on any screen
        if (Settings.Default.WindowPosition != Rectangle.Empty &&
            IsVisibleOnAnyScreen(Settings.Default.WindowPosition))
        {
            // first set the bounds
            this.StartPosition = FormStartPosition.Manual;
            this.DesktopBounds = Settings.Default.WindowPosition;

            // afterwards set the window state to the saved value (which could be Maximized)
            this.WindowState = Settings.Default.WindowState;
        }
        else
        {
            // this resets the upper left corner of the window to windows standards
            this.StartPosition = FormStartPosition.WindowsDefaultLocation;

            // we can still apply the saved size
            // msorens: added gatekeeper, otherwise first time appears as just a title bar!
            if (Settings.Default.WindowPosition != Rectangle.Empty)
            {
                this.Size = Settings.Default.WindowPosition.Size;
            }
        }
        windowInitialized = true;
    }

    private bool IsVisibleOnAnyScreen(Rectangle rect)
    {
        foreach (Screen screen in Screen.AllScreens)
        {
            if (screen.WorkingArea.IntersectsWith(rect))
            {
                return true;
            }
        }

        return false;
    }

    protected override void OnClosed(EventArgs e)
    {
        base.OnClosed(e);

        // only save the WindowState if Normal or Maximized
        switch (this.WindowState)
        {
            case FormWindowState.Normal:
            case FormWindowState.Maximized:
                Settings.Default.WindowState = this.WindowState;
                break;

            default:
                Settings.Default.WindowState = FormWindowState.Normal;
                break;
        }

        # region msorens: this code does *not* handle minimized/maximized window.

        // reset window state to normal to get the correct bounds
        // also make the form invisible to prevent distracting the user
        //this.Visible = false;
        //this.WindowState = FormWindowState.Normal;
        //Settings.Default.WindowPosition = this.DesktopBounds;

        # endregion

        Settings.Default.Save();
    }

    # region window size/position
    // msorens: Added region to handle closing when window is minimized or maximized.

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        TrackWindowState();
    }

    protected override void OnMove(EventArgs e)
    {
        base.OnMove(e);
        TrackWindowState();
    }

    // On a move or resize in Normal state, record the new values as they occur.
    // This solves the problem of closing the app when minimized or maximized.
    private void TrackWindowState()
    {
        // Don't record the window setup, otherwise we lose the persistent values!
        if (!windowInitialized) { return; }

        if (WindowState == FormWindowState.Normal)
        {
            Settings.Default.WindowPosition = this.DesktopBounds;
        }
    }

    # endregion window size/position
}
8 голосов
/ 19 мая 2011

Большинство других решений здесь основаны на ручном определении текущего положения каждого монитора. Крайние случаи чрезвычайно трудно понять, и очень немногие приложения могут сделать это правильно.

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

Лучший пример, который я видел в C #, - это блог Дэвида Рикарда. Он не только показывает, как использовать SetWindowPlacement, но также показывает, как сериализовать весь результат. http://blogs.msdn.com/b/davidrickard/archive/2010/03/09/saving-window-size-and-location-in-wpf-and-winforms.aspx

3 голосов
/ 24 февраля 2011

Я думаю, что это идеальный вариант, основываясь на ваших ответах и ​​комментариях.


Это решение для сохранения / восстановления размера и положения формы с несколькими мониторами + несколькими документами , несколькими формами или основная форма поддержка. Это не MDI форма, но Microsoft Word как мульти-документ с другим экземпляром основной формы.

Спасибо ВВС, мсоренсу и Яну Голдби. Я объединяю решение из примеров VVS, msorens и MSDN Метод Application.Run (ApplicationContext) , чтобы создать мульти MainForm, но не MDI.

Это исправление включает комментарий Иана Голдби, который использует Form.RestoreBounds для исключения OnResize(), OnMove() и TrackWindowState().

Я также исправляю запоминание монитора, когда форма перемещается на другой монитор и максимизируется перед выходом, потому что я не отслеживал OnResize, OnMove. Благодаря этому исправлению данное решение поддерживает функцию Windows 7 Snap , с помощью которой вы можете перетаскивать заголовок или клавишу Win + Arrow, чтобы привязать окно формы к любому краю монитора или сделать его максимально / нормальным, а также свернутым.

Это решение реализовано в Программе, но не в основной Форме для поддержки многоосновной Формы. Однако вы также можете использовать для одной основной формы.

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using SimpleTestForm.Properties;
using System.Drawing;

namespace SimpleTestForm
{
    static class Program
    {
        static MultiMainFormAppContext appContext;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            appContext = new MultiMainFormAppContext();
            Application.Run(appContext);
        }

        /// <summary>
        /// Create a new MainForm and restore the form size and position if necessary. This method can be called like from Menu File > New click event.
        /// </summary>
        /// <returns></returns>
        public static MainForm createNewMainForm()
        {
            return appContext.createNewMainForm();
        }

        /// <summary>
        /// Get the current active MainForm event if a dialog is opened. Useful to create Dictionary (MainForm, T) to store Form/document dependent field. Please set the Owner of child form to prevent null reference exception.
        /// </summary>
        /// <returns></returns>
        public static MainForm GetCurrentMainFormInstance()
        {
            Form mainForm = Form.ActiveForm;
            while (!(mainForm is MainForm) && mainForm.Owner != null)
                mainForm = mainForm.Owner;
            return mainForm as MainForm;
        }
    }

    class MultiMainFormAppContext : ApplicationContext
    {
        List<MainForm> mainForms = new List<MainForm>();
        Point newRestoredLocation = Point.Empty;

        internal MultiMainFormAppContext()
        {
            createNewMainForm();
        }

        internal MainForm createNewMainForm()
        {
            MainForm mainForm = new MainForm();
            mainForm.FormClosed += new FormClosedEventHandler(mainForm_FormClosed);
            mainForm.LocationChanged += new EventHandler(mainForm_LocationChanged);
            RestoreFormSizeNPosition(mainForm);
            PreventSameLocation(mainForm);
            mainForms.Add(mainForm);
            mainForm.Show();
            return mainForm;
        }

        private void PreventSameLocation(MainForm mainForm)
        {
            const int distance = 20;
            foreach (MainForm otherMainForm in mainForms)
            {
                if (Math.Abs(otherMainForm.Location.X - mainForm.Location.X) < distance &&
                    Math.Abs(otherMainForm.Location.Y - mainForm.Location.Y) < distance)
                    mainForm.Location = new Point(mainForm.Location.X + distance, mainForm.Location.Y + distance);
            }
        }

        /// <summary>
        /// Restore the form size and position with multi monitor support.
        /// </summary>
        private void RestoreFormSizeNPosition(MainForm mainForm)
        {
            // this is the default
            mainForm.WindowState = FormWindowState.Normal;
            mainForm.StartPosition = FormStartPosition.WindowsDefaultBounds;

            // check if the saved bounds are nonzero and visible on any screen
            if (Settings.Default.WindowPosition != Rectangle.Empty &&
                IsVisibleOnAnyScreen(Settings.Default.WindowPosition))
            {
                // first set the bounds
                mainForm.StartPosition = FormStartPosition.Manual;
                mainForm.DesktopBounds = Settings.Default.WindowPosition;

                // afterwards set the window state to the saved value (which could be Maximized)
                mainForm.WindowState = Settings.Default.WindowState;
            }
            else
            {
                // this resets the upper left corner of the window to windows standards
                mainForm.StartPosition = FormStartPosition.WindowsDefaultLocation;

                // we can still apply the saved size if not empty
                if (Settings.Default.WindowPosition != Rectangle.Empty)
                {
                    mainForm.Size = Settings.Default.WindowPosition.Size;
                }
            }
        }

        private void SaveFormSizeNPosition(MainForm mainForm)
        {
            // only save the WindowState as Normal or Maximized
            Settings.Default.WindowState = FormWindowState.Normal;
            if (mainForm.WindowState == FormWindowState.Normal || mainForm.WindowState == FormWindowState.Maximized)
                Settings.Default.WindowState = mainForm.WindowState;

            if (mainForm.WindowState == FormWindowState.Normal)
            {
                Settings.Default.WindowPosition = mainForm.DesktopBounds;
            }
            else
            {
                if (newRestoredLocation == Point.Empty)
                    Settings.Default.WindowPosition = mainForm.RestoreBounds;
                else
                    Settings.Default.WindowPosition = new Rectangle(newRestoredLocation, mainForm.RestoreBounds.Size);
            }

            Settings.Default.Save();
        }

        private bool IsVisibleOnAnyScreen(Rectangle rect)
        {
            foreach (Screen screen in Screen.AllScreens)
            {
                if (screen.WorkingArea.IntersectsWith(rect))
                    return true;
            }
            return false;
        }

        void mainForm_LocationChanged(object sender, EventArgs e)
        {
            MainForm mainForm = sender as MainForm;
            if (mainForm.WindowState == FormWindowState.Maximized)
            {
                // get the center location of the form incase like RibbonForm will be bigger and maximized Location wll be negative value that Screen.FromPoint(mainForm.Location) will going to the other monitor resides on the left or top of primary monitor.
                // Another thing, you might consider the form is in the monitor even if the location (top left corner) is on another monitor because majority area is on the monitor, so center point is the best way.
                Point centerFormMaximized = new Point (mainForm.DesktopBounds.Left + mainForm.DesktopBounds.Width/2, mainForm.DesktopBounds.Top + mainForm.DesktopBounds.Height/2);
                Point centerFormRestored = new Point(mainForm.RestoreBounds.Left + mainForm.RestoreBounds.Width / 2, mainForm.RestoreBounds.Top + mainForm.RestoreBounds.Height / 2);
                Screen screenMaximized = Screen.FromPoint(centerFormMaximized);
                Screen screenRestored = Screen.FromPoint(centerFormRestored);
                // we need to change the Location of mainForm.RestoreBounds to the new screen where the form currently maximized.
                // RestoreBounds does not update the Location if you change the screen but never restore to FormWindowState.Normal
                if (screenMaximized.DeviceName != screenRestored.DeviceName)
                {
                    newRestoredLocation = mainForm.RestoreBounds.Location;
                    int screenOffsetX = screenMaximized.Bounds.Location.X - screenRestored.Bounds.Location.X;
                    int screenOffsetY = screenMaximized.Bounds.Location.Y - screenRestored.Bounds.Location.Y;
                    newRestoredLocation.Offset(screenOffsetX, screenOffsetY);
                    return;
                }
            }
            newRestoredLocation = Point.Empty;
        }

        void mainForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            MainForm mainForm = sender as MainForm;
            SaveFormSizeNPosition(mainForm);
            mainForm.FormClosed -= new FormClosedEventHandler(mainForm_FormClosed);
            mainForm.LocationChanged -= new EventHandler(mainForm_LocationChanged);
            mainForm.Dispose();
            mainForms.Remove(mainForm);
            if (mainForms.Count == 0) ExitThread();
        }
    }
}

Редактировать: добавлен метод PreventSameLocation, чтобы убедиться, что 2-я форма открыта не совсем поверх 1-й формы, и пользователь заметит вновь открытую форму.

1 голос
/ 02 июня 2009

Если у вас несколько мониторов, я считаю, что размеры экранного интерфейса просто больше. Таким образом, обычный подход «1 монитор» хранения и восстановления местоположения будет просто работать. Я не пробовал это, потому что я далеко от моего второго монитора, но это не должно быть трудно проверить. То, как вы задали вопрос, похоже, вы его не проверяли.

Ваше второе требование будет означать, что вам придется проверять максимальные размеры при восстановлении приложения, а затем перемещать при необходимости. Чтобы сделать этот последний бит, я использую этот код:

    private System.Drawing.Rectangle ConstrainToScreen(System.Drawing.Rectangle bounds)
    {
        Screen screen = Screen.FromRectangle(bounds);
        System.Drawing.Rectangle workingArea = screen.WorkingArea;
        int width = Math.Min(bounds.Width, workingArea.Width);
        int height = Math.Min(bounds.Height, workingArea.Height);
        // mmm....minimax            
        int left = Math.Min(workingArea.Right - width, Math.Max(bounds.Left, workingArea.Left));
        int top = Math.Min(workingArea.Bottom - height, Math.Max(bounds.Top, workingArea.Top));
        return new System.Drawing.Rectangle(left, top, width, height);
    }

Я вызываю этот метод при восстановлении формы. Я сохраняю геометрию экрана в реестре при закрытии формы, а затем читаю геометрию при открытии формы. Я получаю границы, но затем ограничиваю восстановленные границы фактическим текущим экраном, используя метод выше.

Сохранить при закрытии:

      // store the size of the form
      int w = 0, h = 0, left = 0, top = 0;
      if (this.Bounds.Width < this.MinimumSize.Width || this.Bounds.Height < this.MinimumSize.Height)
      {
          // The form is currently minimized.  
          // RestoreBounds is the size of the window prior to last minimize action.
          w = this.RestoreBounds.Width;
          h = this.RestoreBounds.Height;
          left = this.RestoreBounds.Location.X;
          top = this.RestoreBounds.Location.Y;
      }
      else
      {
          w = this.Bounds.Width;
          h = this.Bounds.Height;
          left = this.Location.X;
          top = this.Location.Y;
      }
      AppCuKey.SetValue(_rvn_Geometry,
        String.Format("{0},{1},{2},{3},{4}",
              left, top, w, h, (int)this.WindowState));

Восстановление в открытой форме:

    // restore the geometry of the form
    string s = (string)AppCuKey.GetValue(_rvn_Geometry);
    if (!String.IsNullOrEmpty(s))
    {
        int[] p = Array.ConvertAll<string, int>(s.Split(','),
                         new Converter<string, int>((t) => { return Int32.Parse(t); }));
        if (p != null && p.Length == 5)
            this.Bounds = ConstrainToScreen(new System.Drawing.Rectangle(p[0], p[1], p[2], p[3]));
    }
0 голосов
/ 24 июля 2014

Это старый вопрос, но вот версия VB, основанная на предыдущих ответах.

Одна проблема с ответами, предложенными VVS и Michael Sorens, заключается в том, что сохраненная позиция, которая показывает только пару пикселей на экране, считается видимой. Это решение требует пересечения не менее 50х50 пикселей перед восстановлением предыдущего местоположения.

Параметры:

<Settings>
  <Setting Name="WindowState" Type="System.Windows.Forms.FormWindowState" Scope="User">
    <Value Profile="(Default)">Normal</Value>
  </Setting>
  <Setting Name="WindowBounds" Type="System.Drawing.Rectangle" Scope="User">
    <Value Profile="(Default)">10, 10, 800, 600</Value>
  </Setting>
</Settings>

Форма:

Partial Public Class MainForm

    Private loadingComplete As Boolean = False

    Public Sub New()

        InitializeComponent()
        RestoreWindowLocation()

    End Sub

    Private Sub MainForm_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load

        loadingComplete = True

    End Sub

    Private Sub MainForm_Resize(sender As System.Object, e As System.EventArgs) Handles MyBase.Resize

         TrackWindowLocation()

     End Sub

     Private Sub MainForm_Move(sender As System.Object, e As System.EventArgs) Handles MyBase.Move

         TrackWindowLocation()

     End Sub

    Private Sub MainForm_FormClosing(sender As System.Object, e As System.Windows.Forms.FormClosingEventArgs) Handles MyBase.FormClosing

        SaveWindowLocation()

     End Sub


    Private Sub RestoreWindowLocation()

        If IsRectangleVisible(My.Settings.WindowBounds) Then
            Me.StartPosition = FormStartPosition.Manual
            Me.DesktopBounds = My.Settings.WindowBounds
        End If

        If Not My.Settings.WindowState = FormWindowState.Minimized Then
            Me.WindowState = My.Settings.WindowState
        End If

    End Sub

    Private Sub TrackWindowLocation()

        If loadingComplete Then
            If Me.WindowState = FormWindowState.Normal Then
                My.Settings.WindowBounds = Me.DesktopBounds
                My.Settings.WindowState = Me.WindowState
            End If
        End If

    End Sub

    Private Sub SaveWindowLocation()

        If Not Me.WindowState = FormWindowState.Minimized Then
            My.Settings.WindowState = Me.WindowState
        End If

        If Me.WindowState = FormWindowState.Normal Then
            My.Settings.WindowBounds = Me.DesktopBounds
        End If

        My.Settings.Save()

    End Sub

    Private Function IsRectangleVisible(Rectangle As Rectangle) As Boolean

        For Each screen As Screen In screen.AllScreens
            Dim r As Rectangle = Rectangle.Intersect(Rectangle, screen.WorkingArea)
            If Not r.IsEmpty Then
                If r.Width > 50 And r.Height > 50 Then Return True
            End If
        Next

        Return False

    End Function

End Class
...