Overcome OS Imposed Windows Form Minimum Size Limit
Asked Answered
R

8

9

In an application I am developing, I need to be able to make a windows form smaller than the minimum height limit imposed by the operating system (36 px in Vista). I have tried intercepting WM_GETMINMAXINFO and providing my own information to override the OS limits, but this only works for the user. From code I can set the height to a value smaller than the limit, but my change only works until WM_WINDOWPOSCHANGED is posted to the message queue (which happens just after I change the height).

Runofthemine answered 14/6, 2009 at 7:18 Comment(0)
R
17

After much experimentation and trial-and-error, I have discovered a solution. I was overriding OnResize and conforming the size of the form to the ListBox in it (see my comment on John Saunders answer).

As I mentioned in my question, I noticed that the size of the form regresses after WM_WINDOWPOSCHANGED is sent. Further investigation revealed that the size regression actually begins when WM_WINDOWPOSCHANGING is sent.

WM_WINDOWPOSCHANGING is the sister message of WM_WINDOWPOSCHANGED which occurs before the window size actually changes. I don't know why, but for some reason WM_WINDOWPOSCHANGING blindly conforms the size of the form to the OS specified limits (apparently it does not query the window with WM_GETMINMAXINFO). Thus, I needed to intercept WM_WINDOWPOSCHANGING and override it with the size I really wanted.

This means that I am no longer conforming the size of the form using OnResize, but instead I am conforming the form size when I receive WM_WINDOWPOSCHANGING. This is even better than OnResize, because there is no associated flicker which occurs when the size is changed and then changed again when the size is conformed during OnResize.

Also, it is necessary to intercept and override WM_GETMINMAXINFO, otherwise, even intercepting WM_WINDOWPOSCHANGING will do you no good.

using System.Runtime.InteropServices;

private const int WM_WINDOWPOSCHANGING = 0x0046;
private const int WM_GETMINMAXINFO = 0x0024;

protected override void WndProc(ref Message m)
{
    if (m.Msg == WM_WINDOWPOSCHANGING)
    {
        WindowPos windowPos = (WindowPos)m.GetLParam(typeof(WindowPos));

        // Make changes to windowPos

        // Then marshal the changes back to the message
        Marshal.StructureToPtr(windowPos, m.LParam, true);
    }

    base.WndProc(ref m);

    // Make changes to WM_GETMINMAXINFO after it has been handled by the underlying
    // WndProc, so we only need to repopulate the minimum size constraints
    if (m.Msg == WM_GETMINMAXINFO)
    {
        MinMaxInfo minMaxInfo = (MinMaxInfo)m.GetLParam(typeof(MinMaxInfo));
        minMaxInfo.ptMinTrackSize.x = this.MinimumSize.Width;
        minMaxInfo.ptMinTrackSize.y = this.MinimumSize.Height;
        Marshal.StructureToPtr(minMaxInfo, m.LParam, true);
   }
}

struct WindowPos
{
     public IntPtr hwnd;
     public IntPtr hwndInsertAfter;
     public int x;
     public int y;
     public int width;
     public int height;
     public uint flags;
}

struct POINT
{
    public int x;
    public int y;
}

struct MinMaxInfo
{
    public POINT ptReserved;
    public POINT ptMaxSize;
    public POINT ptMaxPosition;
    public POINT ptMinTrackSize;
    public POINT ptMaxTrackSize;
}
Runofthemine answered 15/6, 2009 at 1:10 Comment(5)
Zach, your approach works like a charm and it helps me a lot. Thanks!Zachery
Excellent, I owe you a copy of WindowTabs if your interested!Interpleader
@MauriceFlanagan Sure, send me a DM on twitter (I'm @zachoverflow) or send me an email at zach at zachjohnson.net.Runofthemine
In order for the code to work fully, in the first if where it says "Make changes to windowPos", the following should be added: windowPos.cx = this.Width; windowPos.cy = this.Height; and it seems this would be sufficient. Thanks Zach!Fanjet
Works like a charm, thank you very much! I did have to add a SecuritySafeCritical attribute to not get a MethodAccessException at Marshal.StructureToPtr though (.NET 4.0): [SecuritySafeCritical] // Will otherwise get a MethodAccessException.Fahrenheit
S
4

Alexey was so close!

    protected override void SetBoundsCore(int x,int y,int width, int height,BoundsSpecified specified)
    {
        base.SetBoundsCore(x, y, this.MinimumSize.Width, this.MinimumSize.Height, specified);
    }

Did the trick for me. I set the Form's Minimum Size to whatever I want the Form's actual Size to be.

In my project that's all I have to do to get the Form to be tiny, that might be because setting the Minimum size triggers SetBoundsCore or maybe I'm doing something else that triggers it; in which case I guess you have to somehow trigger SetBoundsCore yourself.

Subphylum answered 30/11, 2012 at 1:12 Comment(0)
F
2

When playing with minimum form size, I noticed, that minimum form size is restricted to system minimum form size in Form.SetBoundsCore(...). When I look into IL disassemly, I found, that this .Net method always corrects what you give it (width and height) to SystemInformation.MinimumWindowSize if they are smaller and the form don't have a parent and its FormBorderStyle is FixedSingle, Fixed3D, FixedDialog or Sizable.

The easiest solution to this problem is not handling WM_WINDOWPOSCHANGING, but simply setting FormBorderStyle = System.Windows.Forms.FormBorderStyle.None in the form constructor.

Freiman answered 11/11, 2011 at 4:6 Comment(1)
That will work well if you don't want a border on your form, but if you want to keep the border but have a slightly smaller window (think intellisense suggestion window with only 1 item in it), then another route will be necessary.Runofthemine
D
1

I wish I could give more than +1 to Zach for that, it's great and saved my bacon. For future readers, here's the VB translation of Zach's code:

Imports System.Runtime.InteropServices
Imports System.Windows.Forms
Imports System.Drawing

Public Class MyForm

    ' Ghastly hack to allow the form to be narrower than the widows-imposed limit (about 132 in WIndows 7)
    ' Thanks to https://mcmap.net/q/1130013/-overcome-os-imposed-windows-form-minimum-size-limit

    Private Const WM_WINDOWPOSCHANGING As Integer = &H46
    Private Const WM_GETMINMAXINFO As Integer = &H24
    Protected Overrides Sub WndProc(ByRef m As Message)
        If m.Msg = WM_WINDOWPOSCHANGING Then
            Dim windowPos As WindowPos = CType(m.GetLParam(GetType(WindowPos)), WindowPos)

            ' Make changes to windowPos

            ' Then marshal the changes back to the message
            Marshal.StructureToPtr(windowPos, m.LParam, True)
        End If

        MyBase.WndProc(m)

        ' Make changes to WM_GETMINMAXINFO after it has been handled by the underlying
        ' WndProc, so we only need to repopulate the minimum size constraints
        If m.Msg = WM_GETMINMAXINFO Then
            Dim minMaxInfo As MINMAXINFO = DirectCast(m.GetLParam(GetType(MINMAXINFO)), MINMAXINFO)
            minMaxInfo.ptMinTrackSize.X = Me.MinimumSize.Width
            minMaxInfo.ptMinTrackSize.Y = Me.MinimumSize.Height
            Marshal.StructureToPtr(minMaxInfo, m.LParam, True)
        End If
    End Sub

    Private Structure WindowPos
        Public hwnd As IntPtr
        Public hwndInsertAfter As IntPtr
        Public x As Integer
        Public y As Integer
        Public width As Integer
        Public height As Integer
        Public flags As UInteger
    End Structure
    <StructLayout(LayoutKind.Sequential)> _
    Private Structure MINMAXINFO
        Dim ptReserved As Point
        Dim ptMaxSize As Point
        Dim ptMaxPosition As Point
        Dim ptMinTrackSize As Point
        Dim ptMaxTrackSize As Point
    End Structure

    .... rest of the form

End Class
Doublehung answered 23/7, 2011 at 15:19 Comment(0)
K
0

You mean, aside from using a different OS?

How about "Don't use a form"? How big is this thing you need to display? A pixel? Does it need full Windows Forms functionality?

Now, I don't exactly know how to do the above, but it might be a start for you - think outside of the (bounding) box.

Koressa answered 14/6, 2009 at 23:39 Comment(1)
My form contains a ListBox (which contains one or more items) and I am trying to make the form conform to the ListBox's size. When the ListBox contains only one item, the height of the form only needs to be 16 px plus the height of the borders (which varies). In Vista, the total height usually comes out at about 32 px, and so Vista's 36 px minimum height leaves extra whitespace at the bottom of the form (which looks ugly). In XP the situation is the same.Runofthemine
A
0

I followed Zach's answer and it almost solved my problem. However, in a dual monitor setup, the form disappeared when it was maximized on the second screen. For some reason Windows positioned the form outside the visible region. Adding a test for the primary screen solved this problem for me:

if (m.Msg == (int)CWinApi.Messages.WM_GETMINMAXINFO)
{
    if (this.FormBorderStyle == System.Windows.Forms.FormBorderStyle.None)
    {
        Screen screen = Screen.FromControl(this);

        if (screen.Primary)
        {
            CWinApi.MINMAXINFO minMaxInfo = (CWinApi.MINMAXINFO)m.GetLParam(typeof(CWinApi.MINMAXINFO));

            minMaxInfo.ptMaxSize.x = screen.WorkingArea.Size.Width;
            minMaxInfo.ptMaxSize.y = screen.WorkingArea.Size.Height;
            minMaxInfo.ptMaxPosition.x = screen.WorkingArea.X;
            minMaxInfo.ptMaxPosition.y = screen.WorkingArea.Y;

            System.Runtime.InteropServices.Marshal.StructureToPtr(minMaxInfo, m.LParam, true);
        }
    }
}
Aegis answered 23/8, 2016 at 8:49 Comment(0)
Q
0

Has does anyone have a WPF version of this? I could not get it to work on my window, there doesnt seems a way to call the

base.WndProc(ref m)

function when adding a hook to the WndProc function.

Quijano answered 15/3, 2021 at 15:35 Comment(0)
B
0

I'm using C# / WinForms and I hit this limitation today.

After some test, I found that setting MinimumSize = new Size(1, 1); will make user able to shrink the window beyound the system limit, so it's not necessary to handle WM_GETMINMAXINFO message - in fact, after setting MinimumSize, WM_GETMINMAXINFO will return the set value in ptMinTrackSize, it's just that by default MinimumSize is set to (0, 0) which result in returning system default values.

Even though setting MinimumSize to (1, 1) allows the user to shrink the window further (even with FormBorderStyle set to Sizable (default)), when I try to set the size in code it's still constrained by system default values. I quote this part from Alexey's answer:

When I look into IL disassemly, I found, that this .Net method always corrects what you give it (width and height) to SystemInformation.MinimumWindowSize if they are smaller and the form don't have a parent and its FormBorderStyle is FixedSingle, Fixed3D, FixedDialog or Sizable.

So... it seems trying to remove this unwanted constrain will be very hard. However, my program already have a routine for handling WM_WINDOWPOSCHANGING, so I came up with a simple workaround:

  • (Important) first set MinimumSize = new Size(1, 1); to make WM_GETMINMAXINFO return (1,1) instead of system defaults, this only needs to be done once for the form.

  • When receiving a WM_WINDOWPOSCHANGING message with a specific size (magic number), set the size to the desired size. This "magic size" shouldn't be too large or too small, I'm using SystemInformation.MinimumWindowSize.

    private Size DesiredSize { get; set; } = Size.Empty;

    private void SetSizeBelowSystemMinimum(Size size)
    {
        DesiredSize = size;
        Size = SystemInformation.MinimumWindowSize;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct WINDOWPOS
    {
        public IntPtr hwnd;
        public IntPtr hwndInsertAfter;
        public int x, y, cx, cy;
        public uint flags;
    }

    protected override void WndProc(ref Message m)
    {
        const int WM_WINDOWPOSCHANGING = 0x0046;

        switch (m.Msg) {
            case WM_WINDOWPOSCHANGING:
                var w = (WINDOWPOS)Marshal.PtrToStructure(m.LParam, typeof(WINDOWPOS));
                //Console.WriteLine($"WM_WINDOWPOSCHANGING: x{w.x} y{w.y} w{w.cx} h{w.cy}");

                if (DesiredSize != Size.Empty && w.cx == SystemInformation.MinimumWindowSize.Width && w.cy == SystemInformation.MinimumWindowSize.Height) {
                    w.cx = DesiredSize.Width;
                    w.cy = DesiredSize.Height;
                    Marshal.StructureToPtr(w, m.LParam, true);
                    DesiredSize = Size.Empty;
                }
                break;
        }

        base.WndProc(ref m);
    }
Buntline answered 28/3, 2023 at 1:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.