Saving window size and location in WPF and WinForms

A common user interface tweak is to save the size and location of a window and restore that state when the program starts up again. That way, the users don’t need to re-do their work of resizing and moving the windows to where they want them. However I’ve found that there isn’t really a convenient way to do with in C#. Many solutions focus on recording the window width, height, x and y coordinates and current state (maximized, restored, etc), then applying all those values again on startup. But then you have to deal with edge cases: What if the user has changed resolutions since your program last ran? What if it was present on a monitor that’s now disconnected?

Fortunately, there are a couple of native Windows functions that can do all this work for us: SetWindowPlacement and GetWindowPlacement. MSDN has a sample demonstrating how to PInvoke to these functions in a WPF project, but it’s got a few issues with it. For one, it’s trying to store the placement as a custom type in settings, which can cause strange errors every time you open your Settings file. Here’s my take on it:

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace WindowPlacementExample
{
    // RECT structure required by WINDOWPLACEMENT structure
    [Serializable]
    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;

        public RECT(int left, int top, int right, int bottom)
        {
            this.Left = left;
            this.Top = top;
            this.Right = right;
            this.Bottom = bottom;
        }
    }

    // POINT structure required by WINDOWPLACEMENT structure
    [Serializable]
    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int X;
        public int Y;

        public POINT(int x, int y)
        {
            this.X = x;
            this.Y = y;
        }
    }

    // WINDOWPLACEMENT stores the position, size, and state of a window
    [Serializable]
    [StructLayout(LayoutKind.Sequential)]
    public struct WINDOWPLACEMENT
    {
        public int length;
        public int flags;
        public int showCmd;
        public POINT minPosition;
        public POINT maxPosition;
        public RECT normalPosition;
    }

    public static class WindowPlacement
    {
        private static Encoding encoding = new UTF8Encoding();
        private static XmlSerializer serializer = new XmlSerializer(typeof(WINDOWPLACEMENT));

        [DllImport("user32.dll")]
        private static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WINDOWPLACEMENT lpwndpl);

        [DllImport("user32.dll")]
        private static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl);

        private const int SW_SHOWNORMAL = 1;
        private const int SW_SHOWMINIMIZED = 2;

        public static void SetPlacement(IntPtr windowHandle, string placementXml)
        {
            if (string.IsNullOrEmpty(placementXml))
            {
                return;
            }

            WINDOWPLACEMENT placement;
            byte[] xmlBytes = encoding.GetBytes(placementXml);

            try
            {
                using (MemoryStream memoryStream = new MemoryStream(xmlBytes))
                {
                    placement = (WINDOWPLACEMENT)serializer.Deserialize(memoryStream);
                }

                placement.length = Marshal.SizeOf(typeof(WINDOWPLACEMENT));
                placement.flags = 0;
                placement.showCmd = (placement.showCmd == SW_SHOWMINIMIZED ? SW_SHOWNORMAL : placement.showCmd);
                SetWindowPlacement(windowHandle, ref placement);
            }
            catch (InvalidOperationException)
            {
                // Parsing placement XML failed. Fail silently.
            }
        }

        public static string GetPlacement(IntPtr windowHandle)
        {
            WINDOWPLACEMENT placement = new WINDOWPLACEMENT();
            GetWindowPlacement(windowHandle, out placement);

            using (MemoryStream memoryStream = new MemoryStream())
            {
                using (XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8))
                {
                    serializer.Serialize(xmlTextWriter, placement);
                    byte[] xmlBytes = memoryStream.ToArray();
                    return encoding.GetString(xmlBytes);
                }
            }
        }
    }
}

To get the placement of an existing window, you give it the native handle for the window, and get back a string of XML that represents the placement for the window. You save this string somewhere, then when the program starts back up again, apply it with SetPlacement. The above code works for both WinForms and WPF. And in both cases you can open your Properties/Settings.settings file and add a new setting to store the string:

image

This will store the window state locally, per-user. Of course you could also store the string wherever you wanted to.

What I like about this approach is that the WindowPlacement class does the XML serialization for you and gives you a nice, tidy string containing all the placement data.

But where in the program code should we hook in to save and re-apply the window placements? The answer is a bit different between WinForms and WPF.

WPF:

You might benefit by throwing a couple of extension methods on the WindowPlacement class to make things a bit easier:

public static void SetPlacement(this Window window, string placementXml)
{
    WindowPlacement.SetPlacement(new WindowInteropHelper(window).Handle, placementXml);
}

public static string GetPlacement(this Window window)
{
    return WindowPlacement.GetPlacement(new WindowInteropHelper(window).Handle);
}

To save the window placement, just handle the Closing event on the Window:

private void Window_Closing(object sender, CancelEventArgs e)
{
    Settings.Default.MainWindowPlacement = this.GetPlacement();
    Settings.Default.Save();
}

To restore, you’ll need to hook in on SourceInitialized:

protected override void OnSourceInitialized(EventArgs e)
{
    base.OnSourceInitialized(e);
    this.SetPlacement(Settings.Default.MainWindowPlacement);
}

WinForms:

This one is a bit more straightforward. Save on the FormClosing event and restore the window state in the Load event handler. The Form’s native handle is accessible from its Handle property.

Now we have a robust and relatively painless way of persisting window sizes and positions, and we only need to keep track of one string per window.

21 Replies to “Saving window size and location in WPF and WinForms”

  1. Get an error on this line:

    placement = (WINDOWPLACEMENT)serializer.Deserialize(memoryStream);

    Data at the root level is invalid. Line 1 Position 1

    I'm saving the string to a database field, then reading it in.  Any ideas?

  2. I ended up NOT using UTF-8 encoding (human readability wasn't a factor for me).  UTF-8 has a BOM (Byte Order Marker) that wasn't being encoded/decoded correctly when I saved my string out to the db.  I ended up using Convert.ToBase64String and Convert.FromBase64String.

    Otherwise it works great!  Thanks!

  3. Thanks for the Article – It made the task easy.

    I was also saving to a DB (SQL 2K – uggh) and I just changed the encoding to ASCII since I still wanted to be able to read the saved xml.

    Changed lines:

    private static Encoding encoding = new ASCIIEncoding();

    using (XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.ASCII))

  4. Yeah they changed the blog formatting on me. I just switched up the layout so you should be able to see everything now. (though you can always copy/paste and get everything anyway)

  5. BindingFailure was detect in 🙁

    private static XmlSerializer serializer = new XmlSerializer(typeof(WINDOWPLACEMENT));

  6. @Lordman: It's usually added by default in GUI projects. If you don't have it, you can Add New Item -> Visual C# -> General -> Settings File.

  7. Here is my translation to VB.NET:

    Namespace Extensions

       Public Module WindowExtensions

    #Region "Properties"

           Private Property Encoding As System.Text.Encoding = New System.Text.UTF8Encoding()

           Private Property Serializer As New System.Xml.Serialization.XmlSerializer(GetType(NativeMethods.WINDOWPLACEMENT))

    #End Region

    #Region "Extension Methods"

           <System.Runtime.CompilerServices.Extension> _

           Public Sub SetPlacement(window As Window, placementXml As String)

               If Not String.IsNullOrEmpty(placementXml) Then

                   SetPlacement(New System.Windows.Interop.WindowInteropHelper(window).Handle, placementXml)

               End If

           End Sub

           <System.Runtime.CompilerServices.Extension> _

           Public Sub SetPlacement(form As System.Windows.Forms.Form, placementXml As String)

               If Not String.IsNullOrEmpty(placementXml) Then

                   SetPlacement(form.Handle, placementXml)

               End If

           End Sub

           <System.Runtime.CompilerServices.Extension> _

           Public Function GetPlacement(window As Window) As String

               Return GetPlacement(New System.Windows.Interop.WindowInteropHelper(window).Handle)

           End Function

           <System.Runtime.CompilerServices.Extension> _

           Public Function GetPlacement(form As System.Windows.Forms.Form) As String

               Return GetPlacement(form.Handle)

           End Function

    #End Region

    #Region "Supporting Methods"

           Private Sub SetPlacement(windowHandle As IntPtr, placementXml As String)

               If String.IsNullOrEmpty(placementXml) Then

                   Return

               End If

               Dim placement As NativeMethods.WINDOWPLACEMENT

               Dim xmlBytes As Byte() = Encoding.GetBytes(placementXml)

               Try

                   Using memoryStream As New System.IO.MemoryStream(xmlBytes)

                       placement = DirectCast(Serializer.Deserialize(memoryStream), NativeMethods.WINDOWPLACEMENT)

                   End Using

                   placement.length = System.Runtime.InteropServices.Marshal.SizeOf(GetType(NativeMethods.WINDOWPLACEMENT))

                   placement.flags = 0

                   placement.showCmd = (If(placement.showCmd = NativeMethods.ShowWindowCommands.ShowMinimized, NativeMethods.ShowWindowCommands.Normal, placement.showCmd))

                   NativeMethods.SetWindowPlacement(windowHandle, placement)

                   ' Parsing placement XML failed. Fail silently.

               Catch generatedExceptionName As InvalidOperationException

               End Try

           End Sub

           Private Function GetPlacement(windowHandle As IntPtr) As String

               Dim placement As New NativeMethods.WINDOWPLACEMENT()

               NativeMethods.GetWindowPlacement(windowHandle, placement)

               Using memoryStream As New System.IO.MemoryStream

                   Using xmlTextWriter As New System.Xml.XmlTextWriter(memoryStream, System.Text.Encoding.UTF8)

                       Serializer.Serialize(xmlTextWriter, placement)

                       Dim xmlBytes As Byte() = memoryStream.ToArray()

                       Return Encoding.GetString(xmlBytes)

                   End Using

               End Using

           End Function

    #End Region

       End Module

    End Namespace

    Namespace Extensions

       Public Class NativeMethods

           <System.Runtime.InteropServices.DllImport("user32.dll")> _

           Public Shared Function SetWindowPlacement(ByVal hWnd As IntPtr, ByRef lpwndpl As WINDOWPLACEMENT) As Boolean

           End Function

           <System.Runtime.InteropServices.DllImport("user32.dll")> _

           Public Shared Function GetWindowPlacement(ByVal hWnd As IntPtr, ByRef lpwndpl As WINDOWPLACEMENT) As Boolean

           End Function

           ' RECT structure required by WINDOWPLACEMENT structure

           <Serializable>

           <System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)>

           Public Structure RECT

               Public Left As Integer

               Public Top As Integer

               Public Right As Integer

               Public Bottom As Integer

               Public Sub New(left As Integer, top As Integer, right As Integer, bottom As Integer)

                   Me.Left = left

                   Me.Top = top

                   Me.Right = right

                   Me.Bottom = bottom

               End Sub

           End Structure

           ' POINT structure required by WINDOWPLACEMENT structure

           <Serializable>

           <System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)>

           Public Structure POINT

               Public X As Integer

               Public Y As Integer

               Public Sub New(x As Integer, y As Integer)

                   Me.X = x

                   Me.Y = y

               End Sub

           End Structure

           ' WINDOWPLACEMENT stores the position, size, and state of a window

           <Serializable>

           <System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)>

           Public Structure WINDOWPLACEMENT

               Public length As Integer

               Public flags As Integer

               Public showCmd As ShowWindowCommands

               Public minPosition As POINT

               Public maxPosition As POINT

               Public normalPosition As RECT

           End Structure

           Public Enum ShowWindowCommands As Integer

               Hide = 0

               Normal = 1

               ShowMinimized = 2

               Maximize = 3

               ShowMaximized = 3

               ShowNoActivate = 4

               Show = 5

               Minimize = 6

               ShowMinNoActive = 7

               ShowNA = 8

               Restore = 9

               ShowDefault = 10

               ForceMinimize = 11

           End Enum

       End Class

    End Namespace

  8. Thanks for the great example, I am implementing it on a c# WPF application.

    I found a couple of missing items:  In order to keep my code as clean as possible, I put all of this in a new cs file in my application namespace.  I needed to add the following at the top:

    using System.Windows;//for the Window class

    using System.WIndows.Interop;//for the Interop helper

    both of which are for the small extension methods added to the WindowPlacement class just before the code to call the methods in the Window_Close and OnSourceInitialized methods in my main window class.

    The commands to Set and Get placement refer to "Settings.Default.MainWindowPlacement", which would require a "using Properties;" directive at the top.  I just changed them to

    "Properties.Settings.Default.MainWindowPlacement"

  9. If my window is maximised on the second monitor then close it, when I restart the app the window gets maximised on the primary monitor. Any ideas why?

  10. @Andy&Zollsa

    https://www.experts-exchange.com/questions/28962118/Window-placement.html

    “I did come up with a work around – If the window is maximized, I trap the form closing, cancel it, make the window normal, then close. I save a Boolean that indicates this situation. Now when I start up, I maximize if necessary based on the save Boolean value.”

    Adding two segments to GetPlacement should fix the thing.

    First add:
    private const int SW_SHOWMAXIMIZED = 3;

    Then:
    public static string GetPlacement(IntPtr windowHandle)
    {
    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    bool overrideToMaximized = false;
    HwndSource hwndSource = HwndSource.FromHwnd(windowHandle);
    Window wnd = hwndSource.RootVisual as Window;
    if (wnd != null)
    {
    if (wnd.WindowState == WindowState.Maximized)
    {
    wnd.Hide(); // first hide so that putting to normal state can’t be seen
    wnd.WindowState = WindowState.Normal;
    overrideToMaximized = true;
    }
    }
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    if (overrideToMaximized) placement.showCmd = SW_SHOWMAXIMIZED;
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

    using (MemoryStream memoryStream = new MemoryStream())
    {
    using (XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8))
    {
    serializer.Serialize(xmlTextWriter, placement);
    byte[] xmlBytes = memoryStream.ToArray();
    return encoding.GetString(xmlBytes);
    }
    }
    }

  11. Sorry, above code does not work.
    Here is the code that is a bit of dirty but it works this time…

    private const int SW_SHOWMAXIMIZED = 3;

    public static void SetPlacement(IntPtr windowHandle, string placementXml)
    {
    if (string.IsNullOrEmpty(placementXml))
    {
    return;
    }

    WINDOWPLACEMENT placement;
    byte[] xmlBytes = encoding.GetBytes(placementXml);

    try
    {
    using (MemoryStream memoryStream = new MemoryStream(xmlBytes))
    {
    placement = (WINDOWPLACEMENT)serializer.Deserialize(memoryStream);
    }

    placement.length = Marshal.SizeOf(typeof(WINDOWPLACEMENT));

    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    bool overrideToMaximized = false;
    if (placement.flags == 1) overrideToMaximized = true;
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    bool overrideToMaximized = false;
    HwndSource hwndSource = HwndSource.FromHwnd(windowHandle);
    Window wnd = hwndSource.RootVisual as Window;
    if (wnd != null)
    {
    if (wnd.WindowState == WindowState.Maximized)
    {
    wnd.Hide(); // first hide so that putting to normal state can’t be seen
    wnd.WindowState = WindowState.Normal;
    overrideToMaximized = true;
    }
    }
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

    WINDOWPLACEMENT placement = new WINDOWPLACEMENT();
    GetWindowPlacement(windowHandle, out placement);

    #region MaximizedOnSecundaryMonitorFix
    if (overrideToMaximized) placement.flags = 1; // to pass info to SetPlacement to maximize
    #endregion

    using (MemoryStream memoryStream = new MemoryStream())
    {
    using (XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8))
    {
    serializer.Serialize(xmlTextWriter, placement);
    byte[] xmlBytes = memoryStream.ToArray();
    return encoding.GetString(xmlBytes);
    }
    }
    }

Leave a Reply

Your email address will not be published. Required fields are marked *