克服操作系统强制的Windows窗体最小尺寸限制

9

在我正在开发的应用程序中,我需要能够使 Windows 窗体小于操作系统强制规定的最小高度限制(在 Vista 中为 36 像素)。我尝试拦截 WM_GETMINMAXINFO 并提供自己的信息来覆盖操作系统的限制,但这只适用于用户。从代码中,我可以将高度设置为小于限制的值,但我的更改仅在 WM_WINDOWPOSCHANGED 被发送到消息队列之前生效(这发生在我更改高度后不久)。

8个回答

17
经过多次尝试和试错,我发现了一个解决方案。我重写了OnResize方法,并将窗体大小与其中的ListBox相协调(请参见我在John Saunders答案中的评论)。
正如我在问题中提到的,我注意到在发送WM_WINDOWPOSCHANGED之后,窗体的大小会回归。进一步的调查揭示了,在发送WM_WINDOWPOSCHANGING时,大小回归实际上已经开始了。
WM_WINDOWPOSCHANGING是WM_WINDOWPOSCHANGED的姐妹消息,它出现在窗口大小实际改变之前。我不知道为什么,但由于某种原因,WM_WINDOWPOSCHANGING盲目地将窗体大小调整为操作系统指定的限制(显然它没有通过WM_GETMINMAXINFO查询窗口)。因此,我需要拦截WM_WINDOWPOSCHANGING并使用我真正想要的大小来覆盖它。
这意味着我不再使用OnResize来协调窗体大小,而是在接收到WM_WINDOWPOSCHANGING时协调窗体大小。这比OnResize甚至更好,因为当在OnResize期间更改大小并在大小在OnResize期间被确认时再次更改大小时,会出现相关的闪烁。
此外,有必要拦截和覆盖WM_GETMINMAXINFO,否则即使拦截WM_WINDOWPOSCHANGING也没有用。
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;
}

Zach,你的方法非常有效,对我帮助很大。谢谢! - wenqiang
太好了,如果你有兴趣,我欠你一份WindowTabs的副本! - Maurice Flanagan
@MauriceFlanagan 当然,可以在推特上私信我(我的账号是@zachoverflow),或者发邮件到zach at zachjohnson.net。 - Zach Johnson
1
为了让代码完全正常工作,在第一个“if”语句中,当它说“对windowPos进行更改”时,应添加以下内容:windowPos.cx = this.Width; windowPos.cy = this.Height; 看起来这就足够了。谢谢 Zach! - interDist
非常好用,非常感谢!不过我必须添加一个SecuritySafeCritical属性,以免在Marshal.StructureToPtr时出现MethodAccessException (.NET 4.0): [SecuritySafeCritical] //否则会出现MethodAccessException。 - Wout

4

Alexey离成功很近了!

    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);
    }

对我很有用。 我把表单的最小尺寸设置为实际大小。

在我的项目中,这就是我所需做的,这可能是因为设置最小尺寸会触发SetBoundsCore,或者我正在执行其他触发它的操作;如果是这种情况,我猜你必须以某种方式自己触发SetBoundsCore。


2

当我尝试最小化窗体大小时,我注意到,Form.SetBoundsCore(...) 中的最小窗体大小被限制为系统最小窗体大小。通过查看IL反汇编,我发现,如果宽度和高度比系统最小窗口大小要小,并且该窗体没有父级,并且其FormBorderStyle为FixedSingle、Fixed3D、FixedDialog或Sizable,那么这个.Net方法会自动将它们更正为SystemInformation.MinimumWindowSize。

解决此问题的最简单方法不是处理 WM_WINDOWPOSCHANGING,而是在窗体构造函数中设置 FormBorderStyle = System.Windows.Forms.FormBorderStyle.None


2
如果您不想在窗体上使用边框,那么这个方法很有效,但是如果您想保留边框但又想缩小窗口的大小(例如只显示一个 IntelliSense 建议窗口),则需要采用其他方法。 - Zach Johnson

1

我真希望能给Zach更多的+1,他的代码非常棒,救了我的命。

对于未来的读者,这是Zach代码的VB翻译:

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://dev59.com/Y0fSa4cB1Zd3GeqPBv_-

    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

0

我按照Zach的答案,几乎解决了我的问题。然而,在双显示器设置中,当窗体在第二个屏幕上最大化时,它会消失。由于某种原因,Windows将窗体定位在可见区域之外。为主屏幕添加一个测试可以解决这个问题:

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);
        }
    }
}

0

我正在使用C# / WinForms,今天遇到了这个限制。

经过一些测试,我发现设置MinimumSize = new Size(1, 1);将使用户能够缩小窗口超出系统限制,因此不必处理WM_GETMINMAXINFO消息 - 实际上,在设置MinimumSize之后,WM_GETMINMAXINFO将在ptMinTrackSize中返回设置的值,只是默认情况下MinimumSize设置为(0, 0),导致返回系统默认值。

即使将MinimumSize设置为(1, 1)允许用户进一步缩小窗口(即使将FormBorderStyle设置为Sizable(默认)),但当我尝试在代码中设置大小时,仍受到系统默认值的限制。我引用了Alexey的答案中的这部分内容:

当我查看IL反汇编时,我发现,如果它们更小并且表单没有父级,并且其FormBorderStyle为FixedSingle、Fixed3D、FixedDialog或Sizable,这个.Net方法总是将您提供的内容(宽度和高度)更正为SystemInformation.MinimumWindowSize。
所以...似乎尝试去除此不必要的约束将非常困难。然而,我的程序已经有一个处理WM_WINDOWPOSCHANGING的例程,因此我想出了一个简单的解决方法:
- 首先设置MinimumSize = new Size(1, 1);以使WM_GETMINMAXINFO返回(1,1)而不是系统默认值,这只需要对表单执行一次。 - 当接收到具有特定大小(魔术数字)的WM_WINDOWPOSCHANGING消息时,将大小设置为所需大小。这个“魔法大小”不应该太大或太小,我正在使用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);
    }

0
有没有人有这个的WPF版本? 我无法在我的窗口上使它工作,似乎没有办法调用。
base.WndProc(ref m)

在向 WndProc 函数添加钩子时使用的函数。


0

你的意思是,除了使用不同的操作系统之外?

那么“不使用表单”怎么样?你需要显示多大的东西?一个像素吗?它需要完整的Windows Forms功能吗?

现在,我不确定如何实现上述内容,但这可能是你的一个起点——跳出(边界)框架。


1
我的表单包含一个ListBox(其中包含一个或多个项),我正在尝试使表单符合ListBox的大小。当ListBox仅包含一个项目时,表单的高度只需要是16像素加上边框的高度(变化的)。在Vista中,总高度通常为约32像素,因此Vista的36像素最小高度会在表单底部留下额外的空白(看起来很丑)。在XP中情况也是一样的。 - Zach Johnson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接