35

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

例子:

但是我还没有找到代码来使用多个监视器来做到这一点。

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

有什么想法吗?

我的环境:C#,.NET 3.5 或以下,VS2008

4

6 回答 6

39

试试这个代码。兴趣点:

  • 检查窗口是否(部分)在任何屏幕的工作区域上可见。例如,将其拖到任务栏后面或将其完全移出屏幕会将位置重置为 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>
于 2009-06-02T15:44:04.753 回答
29

VVS提供的答案很有帮助!不过,我发现了两个小问题,所以我用这些修订版重新发布了他的大部分代码:

(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
}
于 2009-06-02T21:48:15.340 回答
9

这里的大多数其他解决方案都依赖于手动确定每台显示器的当前位置。边缘情况非常难以弄清楚,很少有应用程序可以自行解决。

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

于 2011-05-19T17:09:37.457 回答
3

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


该解决方案是通过多显示器+多文档多表单或多主表单支持来保存/恢复表单大小和位置。不是 MDI表单,而是 Microsoft Word 之类的具有不同主表单实例的多文档。

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

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

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

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

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

于 2011-02-24T11:25:10.357 回答
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]));
    }
于 2009-06-02T00:39:05.603 回答
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
于 2014-07-24T01:32:33.687 回答