This only applies in a specific scenario: You want to restrict either the form's width or height, not both, to a maximum that is less than the screen's dimensions. In other words, the form can be maximized, but when it is, only the width or height is maximized, not both.
Edited: Disabling ability to maximize the form would prevent having to adjust; otherwise...
In such scenarios, you are likely subclassing to ensure professional-looking size restrictions vs. readjusting size in the Form_Resize event. The latter is reactive and sloppy vs. the former being proactive. But this may be useful either way.
When subclassing, you are handling the WM_GetMinMaxInfo message. Doing so, you are filling out the applicable MINMAXINFO structure's members, i.e., ptMaxTrackSize, ptMinTrackSize, and ptMaxSize as needed. So, let's say the scenario is that the form can be maximized vertically but not horizontally. Do you leave ptMaxSize.y alone, using the default value passed to you by Windows? If you do, on modern systems, the height may be larger than you expected and larger than fits on the screen or too big and falls behind the taskbar if you are taking that into account also. Why? Because the O/S may be including space needed to show shadows and other fluff. That extra space is included in the ptMaxSize default values passed to you.
For example, on my Win10 machine where the actual screen height is 1050 pixels, ptMaxSize.y is passed as 1066. If I don't account for those added pixels, I won't be setting the size correctly. The solution, in this scenario is provided below:
Code:
' APIs used. Note I changed some parameters, for my convenience, to AS ANY
Private Declare Sub RtlMoveMemory Lib "kernel32.dll" (ByRef Destination As Any, ByRef Source As Any, ByVal length As Long)
Private Declare Function SystemParametersInfo Lib "user32.dll" Alias "SystemParametersInfoW" (ByVal uAction As Long, ByVal uParam As Long, ByRef lpvParam As Any, ByVal fuWinIni As Long) As Long
Private Declare Function GetSystemMetrics Lib "user32.dll" (ByVal nIndex As Long) As Long
Private Type POINTAPI
x As Long
y As Long
End Type
Private Type MINMAXINFO
ptReserved As POINTAPI
ptMaxSize As POINTAPI
ptMaxPosition As POINTAPI
ptMinTrackSize As POINTAPI
ptMaxTrackSize As POINTAPI
End Type
Private Const WM_GETMINMAXINFO As Long = &H24
Private Const SPI_GETWORKAREA As Long = &H30
Private Const SM_CYSCREEN As Long = &H1
In the subclass procedure, the applicable part looks like this. Comments added for clarity
Code:
Private Function pvWndProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long, _
ByVal uIdSubclass As Long, ByVal dwRefData As Long) As Long
...
If uMsg = WM_GETMINMAXINFO Then
Dim mmi As MINMAXINFO, lRect(0 To 3) As Long ' faux RECT structure
' Get screen real estate size excluding any taskbars
SystemParametersInfo SPI_GETWORKAREA, 0&, lRect(0), 0&
' Get the MinMaxInfo structure passed by the O/S
RtlMoveMemory mmi, ByVal lParam, LenB(mmi)
' Set the min/max window tracking size as needed: mmi.ptMinTrackSize & mmi.ptMaxTrackSize
...
' adjust the max dimensions for any system-added border fluff, i.e., shadows
' in this case, we allow maximized height, adjusted in next line, but keep width restricted
mmi.ptMaxSize.y = lRect(3) - lRect(1) + (mmi.ptMaxSize.y - GetSystemMetrics(SM_CYSCREEN))
mmi.ptMaxSize.x = mmi.ptMaxTrackSize.x
' update the adjusted structure & done
RtlMoveMemory ByVal lParam, mmi, LenB(mmi)
Exit Function ' must return 0 if we handled this message
End If
...
End Function
Even though the available height on my desktop, minus the taskbar height, is 1020 pixels, when maximized, the form will be 1036 pixels. Why an extra 16 pixels? Shadows likely are using 8 pixels for both top & bottom of window. The form is 1036 pixels and offset -8 pixels in both the left & top edge of the screen. In other words, form Left is -8 and form Top is -8 when maximized. If using the actual available size of 1020, then the form is displayed slightly shorter than it should since 1036 is the correct value in this scenario. Height of 1036 is also reported by VB for any typical maximized form.
Visually... The top half of the image below (yellow) shows the bottom border of a textbox. The textbox is at the bottom of the form. This is what is expected, the form's bottom edge rests on the top of the taskbar. In the bottom half of the image (white), you can't see that border because the form's bottom edge is below & behind the taskbar. If adjustment not made and Windows default value is left as is, that is the result.
Last edited by LaVolpe; Apr 16th, 2017 at 12:38 PM.
Insomnia is just a byproduct of, "It can't be done"
I took a stab at this and found a few weird things. The main one being strange secondary monitor maximize behavior. There are also some nudges and tweaks that probably don't apply on a downlevel OS but are needed in Windows 10.
This demo probably requires Windows 10 because of the tweaks. You can choose to limit width or height. Then you can either drag-resize or click the Maximize button.
It seems to work well on the primary monitor but maximize is a little off on a secondary monitor... in the same way maximize is off without any of this code.
Your solution, unless anyone finds any issues, is better in that it handles multiple monitors. I don't have that system setup so I couldn't see the issue you were describing, but can visualize it (either bottom hidden by taskbar or bottom sits a bit high above taskbar). With the code I posted above, it is tied to the primary monitor by using SystemParametersInfo.
Out of curiosity, both monitors have same DPI? Just wondering if virtualization is in effect or per-monitor awareness applies
I saw your notes regarding repositioning on the secondary monitor. That aside, maybe in cases where you only want 1 axis of the form adjustable, simply disable maximizing. That wouldn't be a horrible workaround but would force users to manually resize larger when needed vs. simply clicking the max button for a quick fix.
Here's the solution by Raymond Chen and it works perfectly on the primary monitor. You or someone else would need to test in a dual-monitor system to see if works also. This does require calling a lot of APIs for the WM_GetMinMaxInfo message.
Code:
' APIs first
Private Declare Sub RtlMoveMemory Lib "kernel32.dll" (ByRef Destination As Any, ByRef Source As Any, ByVal length As Long)
Private Declare Function AdjustWindowRectEx Lib "user32.dll" (ByRef lpRect As Any, ByVal dsStyle As Long, ByVal bMenu As Long, ByVal dwEsStyle As Long) As Long
Private Declare Function GetWindowLong Lib "user32.dll" Alias "GetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long) As Long
Private Declare Function MonitorFromWindow Lib "user32" (ByVal hWnd As Long, ByVal dwFlags As Long) As Long
Private Declare Function GetMonitorInfo Lib "user32" Alias "GetMonitorInfoW" (ByVal hMonitor As Long, ByVal MONITORINFOEX As Long) As Long
Private Const GWL_EXSTYLE As Long = -20
Private Const GWL_STYLE As Long = -16
Private Const MONITOR_DEFAULTTOPRIMARY As Long = &H1
Private Const CCHDEVICENAME As Long = 32
Private Type POINTAPI
x As Long
y As Long
End Type
Private Type MINMAXINFO
ptReserved As POINTAPI
ptMaxSize As POINTAPI
ptMaxPosition As POINTAPI
ptMinTrackSize As POINTAPI
ptMaxTrackSize As POINTAPI
End Type
Private Type MONITORINFOEX
cbSize As Long
rcMonitor(0 To 3) As Long ' faux RECT
rcWork(0 To 3) As Long ' faux RECT
dwFlags As Long
szDevice As String * CCHDEVICENAME
End Type
Private Const WM_GETMINMAXINFO As Long = &H24
Now the adjustment for max height
Code:
' subclass procedure that handles WM_GetMinMaxInfo
Dim mmi As MINMAXINFO, lRect(0 To 3) As Long ' faux RECT structure
Dim mi As MONITORINFOEX
RtlMoveMemory mmi, ByVal lParam, LenB(mmi)
lRect(2) = 500: mi.cbSize = LenB(mi)
AdjustWindowRectEx lRect(0), GetWindowLong(hWnd, GWL_STYLE), 0&, GetWindowLong(hWnd, GWL_EXSTYLE)
GetMonitorInfo MonitorFromWindow(hWnd, MONITOR_DEFAULTTOPRIMARY), VarPtr(mi)
mmi.ptMaxTrackSize.y = mi.rcWork(3) - mi.rcWork(1) - mmi.ptMaxPosition.y + lRect(3)
' set fixed width to mmi.ptMaxTrackSize.x & mmi.ptMinTrackSize.x
....
mmi.ptMaxSize = mmi.ptMaxTrackSize
RtlMoveMemory ByVal lParam, mmi, LenB(mmi)
Edited: Tested it in Vista and even Win2K and it worked on the primary, as is. Surprisingly, common controls subclassing (by ordinals) exists on my Win2K VM build.
Last edited by LaVolpe; Apr 16th, 2017 at 07:23 PM.
Reason: forgot the POINTAPI declaration
Insomnia is just a byproduct of, "It can't be done"
I wasn't trying to offer a "solution" but just wanted to show you what I had cobbled together to try to understand the problem.
As far as I know Windows 10 can be a little bit "special" because of the invisible thick borders on sizable top-level windows. If you do this in a VB6 program:
Code:
Private Sub cmdMove00_Click()
Move 0, 0
End Sub
This moves the top to the screen top, but the left to a slight inset from the screen left by 8 pixels or so (depending on your theme settings). That seems to be true whether DPI-Aware or not.
This does the same thing:
Code:
Private Sub cmdMove00_Click()
Dim MyMonitor As Monitor
Dim RECT As RECT
With Monitors
.Refresh
Set MyMonitor = .Item(CStr(.HMonitorOfForm(Me)))
GetWindowRect hWnd, RECT
MoveWindow hWnd, _
MyMonitor.WorkLeft, _
MyMonitor.WorkTop, _
RECT.Right - RECT.Left, _
RECT.Bottom - RECT.Top, WIN32_TRUE
End With
End Sub
The second version works relative to the current monitor. Trying the same thing on a secondary monitor (or even just a plain old normal maximize) seems to put the top of the Form above the screen top by 8 px or so as well as inset from the left.
As far as I know that's all that is "special" about Windows 10. Well that and Per-Monitor DPI-Awareness, but I wasn't even trying to be DPI-Aware at all and I ran my tests at 96 DPI.
I'll try the Raymond Chen approach and see what I find there.
Ok, I stripped my demo down and substituted the Raymond Chen suggestions.
My bulky multimonitor code was something I had just slopped in from another project that needed a lot more info. Now the code to get just what is needed here is inline within Form1.frm and those MonitorsXXX modules have been yanked.
Works fine on secondary monitors as well as the primary, aside from having the same "maximize glitch" on secondary monitors.
The "Move 0, 0" issue is only slightly related, so I pulled that code as well. I was only doing that to help me diagnose my code anyway.
Oh, and I still haven't tried marking this as DPI-Aware or Per-Monitor DPI-Aware and testing at various DPI settings. Should be fine though... at least for the System-DPI-Aware only case. Everything is being done in device pixels.
Sorry for the coding style. I've grown fond of whitespace as the years have passed, as you can probably tell.
@dilettante: I'm right there with you. As we get older, Readability > Code density
also you might want to forward WM_NCDESTROY to DefSubclassProc in your SubclassProxy() https://code.msdn.microsoft.com/wind...ssing-2ef7ee53
What the default minimum is varies by version of LINK.EXE, but most of programs are defaulting to 4.0 (Win9x and Win NT 4.0). I went back to using the version of LINK.EXE from VB6 installation and Service Packs. The subsystem is of course Windows by default.
If you have more information on what specifying different minversion values might do it might be interesting. There is very little that I have found that even mentions it.
As far as I can tell specifying 5.0 as the minimum Windows subsystem is saying "Windows 2000 or later is required by this program." I doubt it has any impact on calls to GDI, User32, etc. but perhaps you have evidence to the contrary?
edit: it seems the community content on the GetWindowRect MSDN page has gone missing.
The "fixes" are applied in Windows Vista/7/8 when you specify winver/subsystem 6.0 or greater. (Vista)
And then it seems the behavior of GetWindowRect etc is changed again in windows 10... *sigh*
Last edited by DEXWERX; Apr 17th, 2017 at 11:01 AM.
Good catch. I'm not sure that it is critical but it does make sense:
Code:
Private Function SubclassProxy( _
ByVal hWnd As Long, _
ByVal uMsg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long, _
ByVal uIdSubclass As Object, _
ByVal dwRefData As Object) As Long
If uMsg = WM_NCDESTROY Or uMsg = WM_UAHDESTROYWINDOW Then
'Just in case the client fails to clean up.
Unsubclass hWnd, uIdSubclass
SubclassProxy = DefSubclassProc(hWnd, uMsg, wParam, lParam)
Else
SubclassProxy = uIdSubclass.SubclassProc(hWnd, uMsg, wParam, lParam, dwRefData)
End If
End Function
Hello guys, is there any reason why you are removing the subclass in the WM_NCDESTROY message and not WM_DESTROY?
no. It's just one of the last possible places to remove the subclass before the window is destroyed.
That particular practice is just a safeguard in case the forwarded SubclassProc forgets...
Dilettante's SubclassProxy() routine is just a static forward (hence the name proxy)
It also allows all the other messages that might happen after WM_DESTROY through, but really the forwarded subclassproc could remove itself on WM_DESTROY.
OK, and what about the WM_UAHDESTROYWINDOW? I could not find much information about it.
Could it be sent instead of the WM_DESTROY under some conditions?
WM_UAHDESTROYWINDOW is a mostly undocumented Aero message. I have stumbled over that one in the past and not handling it caused a crash.
Again, I'm just using it to unhook my subclassing if the client code failed to do so as it should. It is part of a safety net.
OK, but this safety is important, mostly when running in the IDE (when the stop button is pressed -or similar functions-).
I don't figure a situation when this precaution code can be triggered in a compiled exe, unless the program really forgets to unsubclass before leaving.
I think I'll add this message to my subclassing code, because now I'm automatically unsubclassing only upon receiving the WM_DESTROY.
OK, but this safety is important, mostly when running in the IDE (when the stop button is pressed -or similar functions-).
Nothing short of hijacking the IAT of VB6's Project Reset is going to save you from the stop button. Even then - it's not consistent, and if you have a bug in your SubclassProc you're still going to crash the IDE the second you try and change the code or hit Stop.
Last edited by DEXWERX; Apr 17th, 2017 at 02:17 PM.
Nothing short of hijacking the IAT of VB6's Project Reset is going to save you from the stop button.
I don't understand what you mean.
When using the subclasser compiled in a dll (the one that I use), I can press the Stop button or put an End statement anywhere in the code and it doesn't crash.
Originally Posted by DEXWERX
Even then - it's not consistent, and if you have a bug in your SubclassProc you're still going to crash the IDE the second you try and change the code or hit Stop.
I can have bugs. I just performed a test and in the WM_MOVE message I've put a line: Debug.Print 1/0 and it raised the error (yellow back color line), then I stepped to the next line and it continued running fine.
I don't understand what you mean.
When using the subclasser compiled in a dll (the one that I use), I can press the Stop button or put an End statement anywhere in the code and it doesn't crash.
Well you didn't say compiled in a dll I've got one of those too.
edit:and if you're not using mine, where did you get the component from - and should I just open source mine?
Last edited by DEXWERX; Apr 17th, 2017 at 03:03 PM.
It also allows all the other messages that might happen after WM_DESTROY through, but really the forwarded subclassproc could remove itself on WM_DESTROY.
Basically just reiterates what DEXWERX has described, but with a few additional samples and explanations. NCDESTROY is generally considered best practice.
On Win 10 something to note is that the helper "DwmGetWindowAttribute" API returns different values depending on window visibility. When a window is already visible, it will return extra "shadow frame" padding just like GetWindowRect. If the target window is invisible, however, padding won't be returned. See also these comments on StackOverflow.
This is primarily a problem when trying to do things like set window position prior to showing it, because you don't know how much padding is used by the current theme until your window is actually visible. For resizing an already visible window, however, it's less of a problem.
One other FYI: "Snap Points" (or whatever the hell they're called) will show a proper estimate of final window size, when WM_GetMinMaxInfo is used. For example on Windows 10, drag dilettante's "Limit Resize 2.zip" app title bar to a monitor corner or edge, and notice how any "max size" values are respected in the glowing snap preview border.
Note also that the glowing "preview border" assumes you want natural padding around the window, so the preview position vs the actual position (when you release the mouse) will be different. Not sure if this bothers anyone.
It seems like this discussion is turning in a different direction. It seems like this should be in it's own thread, but it grew out of this one. I'd be inclined to split the discussion of WM_DESTROY out to a new thread, but since it didn't start as a clean break, it wouldn't be quite so easy.
@Shaggy... Personally, I don't mind. The original question is more or less answered. There are at least 3 different ways of producing the same result on the primary monitor. The secondary monitor appears to be iffy regardless of method used and should someone post a fool-proof solution -- great. Regarding the subclassing discussion; just let it go -- I've already picked up a nugget or two that I feel is worth pursuing.
If you do split this, I'll resolve the thread. If you don't split it, I don't care. Thanx for the 'moderating'
Insomnia is just a byproduct of, "It can't be done"
>This is primarily a problem when trying to do things like set window position prior to showing it, because you don't know how much padding is used by the current theme until your window is actually visible. For resizing an already visible window, however, it's less of a problem.
This can be overcome if the 'Aero' borders for all types of Forms are retrieved/ stored and reapplied to a subject Type of Form before it is made visible.
Ref. my http://www.vbforums.com/showthread.p...g-Aero-Borders, already referenced above by Dex.
@Shaggy... Personally, I don't mind. The original question is more or less answered. There are at least 3 different ways of producing the same result on the primary monitor. The secondary monitor appears to be iffy regardless of method used and should someone post a fool-proof solution -- great. Regarding the subclassing discussion; just let it go -- I've already picked up a nugget or two that I feel is worth pursuing.
If you do split this, I'll resolve the thread. If you don't split it, I don't care. Thanx for the 'moderating'