Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: How to set minimum window size (Desktop) #2945

Closed
dynamiquel opened this issue Jul 17, 2020 · 17 comments
Closed

Question: How to set minimum window size (Desktop) #2945

dynamiquel opened this issue Jul 17, 2020 · 17 comments
Assignees
Labels
appModel-win32 Exclusive to WinUI 3 Win32 Desktop apps area-AppWindow needs-triage Issue needs to be triaged by the area owners product-winui3 WinUI 3 issues question

Comments

@dynamiquel
Copy link

WinUI 3 Preview 2 Desktop C#

Sorry, but how do I set the minimum window size? Currently the app can be shrunk almost entirely.

@msft-github-bot msft-github-bot added the needs-triage Issue needs to be triaged by the area owners label Jul 17, 2020
@StephenLPeters
Copy link
Contributor

does this do iApplicationView.GetForCurrentView().SetPreferredMinSize()? It seems like the default window size for the desktop skew might be strang @MikeHillberg do you know if we have the ability to set a more reasonable default?

@StephenLPeters StephenLPeters added area-AppWindow appModel-win32 Exclusive to WinUI 3 Win32 Desktop apps product-winui3 WinUI 3 issues and removed needs-triage Issue needs to be triaged by the area owners labels Jul 17, 2020
@ranjeshj
Copy link
Contributor

@marb2000 for the window APIs.

@marb2000
Copy link
Contributor

Desktop WinUI 3 don't have MinSize or MaxSize properties yet on a Window class, and it's not trivial to do this for a developer. The way to do it in Win32 is listening the message WM_GETMINMAXINFO, but you need first to get access to the procedure that get the messages in Windows (the WindowProc). Unfortunately, WinUI 3 Desktop doesn't expose it, so the solution is Subclassing. Subclassing allows to intercept and process messages sent to window before the WindowProc.

I wrote this code snippet that could help you to understand what to do:

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

using WinRT;

  public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
            SubClassing();
        }

        private delegate IntPtr WinProc(IntPtr hWnd, PInvoke.User32.WindowMessage Msg, IntPtr wParam, IntPtr lParam);
        private WinProc newWndProc = null;
        private IntPtr oldWndProc = IntPtr.Zero;
        [DllImport("user32")]
        private static extern IntPtr SetWindowLong(IntPtr hWnd, PInvoke.User32.WindowLongIndexFlags nIndex, WinProc newProc);
        [DllImport("user32.dll")]
        static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, PInvoke.User32.WindowMessage Msg, IntPtr wParam, IntPtr lParam);

        private void SubClassing()
        {
            //Get the Window's HWND
            var hwnd = this.As<IWindowNative>().WindowHandle;

            newWndProc = new WinProc(NewWindowProc);
            oldWndProc = SetWindowLong(hwnd, PInvoke.User32.WindowLongIndexFlags.GWL_WNDPROC, newWndProc);
        }

        int MinWidth = 800;
        int MinHeight = 600;

        [StructLayout(LayoutKind.Sequential)]
        struct MINMAXINFO
        {
            public PInvoke.POINT ptReserved;
            public PInvoke.POINT ptMaxSize;
            public PInvoke.POINT ptMaxPosition;
            public PInvoke.POINT ptMinTrackSize;
            public PInvoke.POINT ptMaxTrackSize;
        }

        private IntPtr NewWindowProc(IntPtr hWnd, PInvoke.User32.WindowMessage Msg, IntPtr wParam, IntPtr lParam)
        {
            switch (Msg)
            {
                case PInvoke.User32.WindowMessage.WM_GETMINMAXINFO:
                    var dpi = PInvoke.User32.GetDpiForWindow(hWnd);
                    float scalingFactor = (float)dpi / 96;

                    MINMAXINFO minMaxInfo = Marshal.PtrToStructure<MINMAXINFO>(lParam);
                    minMaxInfo.ptMinTrackSize.x = (int)(MinWidth * scalingFactor);
                    minMaxInfo.ptMinTrackSize.y = (int)(MinHeight * scalingFactor); 
                    Marshal.StructureToPtr(minMaxInfo, lParam, true);
                    break;
                    
            }
            return CallWindowProc(oldWndProc, hWnd, Msg, wParam, lParam);
        }


        [ComImport]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        [Guid("EECDBF0E-BAE9-4CB6-A68E-9598E1CB57BB")]
        internal interface IWindowNative
        {
            IntPtr WindowHandle { get; }
        }
   }

@marb2000 marb2000 self-assigned this Feb 10, 2021
@sylveon
Copy link
Contributor

sylveon commented Feb 11, 2021

SetWindowLong only accepts 32 bits values which can result in unexpected truncations (therefore a crash due to a bad function pointer), SetWindowLongPtr should be used.

Subclassing via SetWindowLongPtr is also not recommended by the Microsoft docs due to various disadvantages, but the alternative subclassing APIs only work with common controls 6: https://docs.microsoft.com/en-us/windows/win32/controls/subclassing-overview#disadvantages-of-the-old-subclassing-approach

Why not add a protected overridable function to the Window class which is similar to WinForms' WndProc (with a respective Message struct which mirrors the native MSG struct)? It would be much simpler for everyone involved.

@rcohn
Copy link

rcohn commented Mar 19, 2022

Hello -
Is using PInvoke.User32.WindowMessage.WM_GETMINMAXINFO, as described above, really the only way to set the minimum size of a C# WinUI3 desktop app?

I'm using Windows App SDK 1.0. Is there no property that can be set, either at the App or Window level?

Thank you.

@rcohn
Copy link

rcohn commented Mar 19, 2022

Hello @marb2000 -

I tried the PInvoke.User32.WindowMessage.WM_GETMINMAXINFO as described above, and NewWindowProc is never receiving any messages. Again, I am running a C# WinUI3 desktop app.

Do you have any advice at this point?

Thank you.

@dotMorten
Copy link
Contributor

dotMorten commented Apr 28, 2022

@rcohn use WinUIEx and its WindowEx Window class for now. It adds the needed min sizes as well as the window messaging stuff https://dotmorten.github.io/WinUIEx/concepts/WindowEx.html

@castorix
Copy link

Just use SetWindowSubclass

@rcohn
Copy link

rcohn commented Apr 28, 2022

Hi -
I ended up using the SetWindowSubclass approach so that I could hook WM_GETMINMAXINFO and set the min size. That seems to work fine. Here's a snippet that shows how I did this:

image

Thanks very much to all who replied. It was really helpful.
PS - sorry about the code image. The code editing quotes do not pick up all the code.

@crramirez
Copy link
Contributor

Hi - I ended up using the SetWindowSubclass approach so that I could hook WM_GETMINMAXINFO and set the min size. That seems to work fine. Here's a snippet that shows how I did this:

image

Thanks very much to all who replied. It was really helpful. PS - sorry about the code image. The code editing quotes do not pick up all the code.

This reminds me Turbo Pascal 6.0

@stefansjfw
Copy link

Is there a plan to add minWidth/minHeight/min window size any time soon?

@dotMorten
Copy link
Contributor

@bhuvana-01m this is the approach the WinUIEx helper extensions take. I’d suggest you look at using that which will allow you to set min size in a couple lines of code , until WinUI itself has this implemented

@castorix
Copy link

castorix commented Apr 6, 2023

I have tried this ,but it seems not working !! Also i can able to resize the window size still!!

What does not work ?
It sets a minimum size.
(and, as posted and quoted in MSDN, it is better to use SetWindowSubclass)

@rcohn
Copy link

rcohn commented Apr 6, 2023

Hi -

I used SetWindowSubclass and that works fine (see my code snippet far above). It's unfortunate that we are still having to go through so many hoops with WinUI 3 (and I'm still stumbling through those hoops), but it's fortunate that we have all you experts that read and advise on this forum.

Many, many thanks for your help.

Best regards.

@dgellow
Copy link

dgellow commented May 19, 2023

@sylveon Thank you so much for pointing out the issue with SetWindowLong, and mentioning SetWindowLongPtr. I couldn't figure out why my WNDPROC was IntPtr.Zero after the call to SetWindowLong() (and no error code via Marshal.GetLastWin32Error()...).

With this change @marb2000 code works correctly for me.

-    [DllImport("user32")]
-    private static extern IntPtr SetWindowLong(
-        IntPtr hWnd,
-        PInvoke.User32.WindowLongIndexFlags nIndex,
-        WinProc? newProc
-    );
+    [DllImport("user32")]
+    private static extern IntPtr SetWindowLongPtr(
+        IntPtr hWnd,
+        PInvoke.User32.WindowLongIndexFlags nIndex,
+        WinProc? newProc
+    );

@dgellow
Copy link

dgellow commented May 19, 2023

To follow-up my previous comment, the solution of using SetWindowLongPtr only works on 64-bit systems.

In the Win32 world, SetWindowLongPtr is a macro that nicely takes care of platform differences for us. On 32-bit platforms, it actually is defined as SetWindowLong and on 64-bit platforms, it keeps its original form.

// winuser.h
#ifdef _WIN64
LONG_PTR WINAPI SetWindowLongPtrA(HWND,int,LONG_PTR);
LONG_PTR WINAPI SetWindowLongPtrW(HWND,int,LONG_PTR);
#else 
#define SetWindowLongPtrA SetWindowLongA
#define SetWindowLongPtrW SetWindowLongW
#endif

But in our C# world, we can't rely on macros, and PInvoke doesn't automatically handle the mapping for us.

So, if we call SetWindowLongPtr directly on a 32-bit process, we're in for a disappointment because that function just doesn't exist in user32.dll on those platforms. We end up needing to do the function selection ourselves, explicitly picking SetWindowLong or SetWindowLongPtr based on the architecture of our process.

My solution here is a small NativeMethods class that does exactly that.

It basically mimics the behavior of the SetWindowLongPtr macro in the Windows headers, but in a way we can use in C#/.NET.

Here's the code snippet:

// Note: this DllImport can now be commented or removed.
//
//[DllImport("user32")]
//private static extern IntPtr SetWindowLongPtr(
//    IntPtr hWnd,
//    PInvoke.User32.WindowLongIndexFlags nIndex,
//    WinProc? newProc
//);

private void SubClassing()
{
    var hwnd = this.As<IWindowNative>().WindowHandle;
    if (hwnd == IntPtr.Zero)
    {
        int error = Marshal.GetLastWin32Error();
        throw new InvalidOperationException($"Failed to get window handler: error code {error}");
    }

    newWndProc = new(NewWindowProc);
    
    // Here we use the NativeMethods class 👇
    oldWndProc = NativeMethods.SetWindowLong(hwnd, PInvoke.User32.WindowLongIndexFlags.GWL_WNDPROC, newWndProc);
    if (oldWndProc == IntPtr.Zero)
    {
        int error = Marshal.GetLastWin32Error();
        throw new InvalidOperationException($"Failed to set GWL_WNDPROC: error code {error}");
    }
}

public static class NativeMethods
{
    // We have to handle the 32-bit and 64-bit functions separately.
    // 'SetWindowLongPtr' is the 64-bit version of 'SetWindowLong', and isn't available in user32.dll for 32-bit processes.
    [DllImport("user32.dll", EntryPoint = "SetWindowLong")]
    private static extern IntPtr SetWindowLong32(IntPtr hWnd, PInvoke.User32.WindowLongIndexFlags nIndex, WinProc newProc);

    [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
    private static extern IntPtr SetWindowLong64(IntPtr hWnd, PInvoke.User32.WindowLongIndexFlags nIndex, WinProc newProc);

    // This does the selection for us, based on the process architecture.
    public static IntPtr SetWindowLong(IntPtr hWnd, PInvoke.User32.WindowLongIndexFlags nIndex, WinProc newProc)
    {
        if (IntPtr.Size == 4) // 32-bit process
        {
            return SetWindowLong32(hWnd, nIndex, newProc);
        }
        else // 64-bit process
        {
            return SetWindowLong64(hWnd, nIndex, newProc);
        }
    }
}

// ... the rest is similar to what @marb2000 suggested in his solution

That should provide support for for 32 and 64-bit systems.

@AVDAIN
Copy link

AVDAIN commented Mar 12, 2024

Hey everyone, I've already submitted a proposal so that a workaround no longer needs to be developed.

Issue: #7296

@microsoft-github-policy-service microsoft-github-policy-service bot added the needs-triage Issue needs to be triaged by the area owners label Mar 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
appModel-win32 Exclusive to WinUI 3 Win32 Desktop apps area-AppWindow needs-triage Issue needs to be triaged by the area owners product-winui3 WinUI 3 issues question
Projects
None yet
Development

No branches or pull requests