使用多个监视器恢复窗口大小/位置

时间:2009-06-01 23:28:05

标签: c# winforms multiple-monitors appsettings

关于恢复WinForm位置和大小的许多帖子。

示例:

但是我还没有找到使用多个监视器执行此操作的代码。

也就是说,如果我使用监视器2上的窗口关闭我的.NET Winform应用程序,我希望它将窗口大小,位置和状态保存到应用程序设置,以便以后重新启动时可以恢复到监视器2该应用程序。如果在上面的代码项目示例中它包含一些健全性检查,那将是很好的,因为如果保存的位置大部分是在屏幕外,它“修复”它。或者如果保存的位置在不再存在的显示器上(例如我的笔记本电脑现在没有我的第二台显示器),那么它会正确地将其移动到显示器1。

有什么想法吗?

我的环境:C#,. NET 3.5或更低版本,VS2008

6 个答案:

答案 0 :(得分:36)

试试这段代码。兴趣点:

  • 检查窗口在任何屏幕的工作区域是否(部分)可见。例如。将其拖动到任务栏后面或将其完全移出屏幕会将位置重置为Windows默认值。
  • 即使表格已最小化或最大化(常见错误)
  • ,也会保存正确的边界
  • 正确保存WindowState。保存FormWindowState.Minimized已被设计禁用。

边界和状态存储在appsettings中,并带有相应的类型,因此不需要进行任何字符串解析。让框架做它的序列化魔术。

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>

答案 1 :(得分:27)

VVS提供的答案非常有帮助!我发现它有两个小问题,所以我将大部分代码重新发布到这些修订中:

(1)应用程序第一次运行时,窗体以Normal状态打开,但其大小应使其显示为标题栏。我在构造函数中添加了一个条件来修复它。

(2)如果应用程序在最小化或最大化时关闭,则OnClosing中的代码无法记住处于“正常”状态的窗口的尺寸。 (这三行代码 - 我现在已经注释掉了 - 似乎是合理的,但由于某种原因只是不起作用。)幸运的是,我之前已经解决了这个问题并将代码包含在代码末尾的新区域中跟踪窗口状态,而不是等待关闭。


有了这两个修复程序,我已经测试过了:

一个。在正常状态下关闭 - 恢复到相同的大小/位置和状态

B中。在最小化状态下关闭 - 使用上一个正常大小/位置恢复到正常状态

℃。在最大化状态下关闭 - 恢复到最大化状态并在后者调整到正常状态时记住它的最后大小/位置。

d。关闭监视器2 - 恢复监视2。

电子。关闭监视器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
}

答案 2 :(得分:8)

这里的大多数其他解决方案都依赖于手动计算每个监视器的当前位置。边缘情况非常难以弄清楚,并且很少有应用程序能够正确地完成它们。

Windows中的SetWindowPlacement函数正确处理所有边缘情况 - 如果窗口位于可见屏幕之外,则会相应地调整它。

我在C#中看到的最好的例子是David Rickard的博客。它不仅展示了如何使用SetWindowPlacement,还展示了如何序列化整个结果。 http://blogs.msdn.com/b/davidrickard/archive/2010/03/09/saving-window-size-and-location-in-wpf-and-winforms.aspx

答案 3 :(得分:3)

根据您的回答和评论,我认为这是完美的。


此解决方案是保存/恢复表单大小和位置,使用多显示器 + 多文档多种形式多主要表单支持。它是不是MDI 形式,而是Microsoft Word,就像具有不同主表单实例的多文档一样。

感谢VVS,msorens和Ian Goldby。我合并了来自VVS,msorens和MSDN Application.Run Method (ApplicationContext)示例的解决方案,以制作多主要方式而不是MDI。

此修复程序包含来自Ian Goldby的评论,该评论使用Form.RestoreBounds来消除OnResize()OnMove()TrackWindowState()

我还修复了当Form移动到另一个Monitor并在退出之前获得最大化时记住Monitor,因为我没有跟踪OnResize,OnMove。 通过此修复,此解决方案支持Windows 7 Snap feature,您可以将标题栏或Win +箭头键拖动到任何监视器边缘,或使其最大化/正常以及最小化。

此解决方案在程序中实现,但不在主窗体中实现,以支持多主窗体。但是,您也可以使用单个主表格。

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方法,以确保第二个表单不会完全打开第一个表单,用户会注意到新打开的表单。

答案 4 :(得分:1)

如果您有多台显示器,我相信屏幕UI尺寸会更大。因此,存储和恢复位置的正常“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]));
    }

答案 5 :(得分:0)

这是一个老问题,但这是基于之前答案的VB版本。

VVS和Michael Sorens提出的答案的一个问题是,在屏幕上仅显示几个像素的保存位置计为可见。在恢复之前的位置之前,此解决方案在交叉点中需要至少50x50像素。

设定:

<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