News:

Masm32 SDK description, downloads and other helpful links
Message to All Guests
NB: Posting URL's See here: Posted URL Change

Main Menu

GetWindowPos function

Started by guga, April 23, 2025, 11:10:57 PM

Previous topic - Next topic

guga

Hi Guys, i made a specialized function to retrieve the windows position (x, y, width and height) outputting the value as Logical pixels.

General Info
;;
    GetWindowPos v 1.2
        Retrieves the position and size of a window, adjusted for DPI scaling, in physical pixels.
        Returns coordinates relative to the parent's client area (for child windows) or screen/owner
        coordinates (for non-child windows), with border adjustments for child windows. Compatible
        with Windows XP, 7, and later, using system DPI for scaling.

  Arguments:
    hWnd (in): Handle to the window (child, top-level, dialog, control, etc.).
    PosStruct (out): Pointer to a WINPOS structure to receive the position and size.
                     The output values of WINPOS (cx, cy, width, height) are in logical pixels.

  Return Value:
    For child windows: Returns the parent window handle (hParent) on success.
    For non-child windows: Returns TRUE (1) on success.
    Returns 0 on failure (invalid hWnd, PosStruct, or system errors).

  WINPOS Structure:
    [WINPOS:
     WINPOS.cx: D$ 0      ; x-coordinate of the window's upper-left corner (pixels). The value can also be negative. See Remarks
     WINPOS.cy: D$ 0      ; y-coordinate of the window's upper-left corner (pixels). The value can also be negative. See Remarks
     WINPOS.width: D$ 0   ; Width of the window (pixels).
     WINPOS.height: D$ 0]  ; Height of the window (pixels).
    - Child windows: cx, cy are relative to the parent's client area, adjusted for borders;
      width, height are client dimensions.
    - Non-child windows: cx, cy are in screen coordinates (hParent = NULL) or owner's
      client coordinates (hParent = owner), no border adjustment; width, height include borders.

  Remarks:
    - Enhanced version of GetWindowPos to handle child and non-child windows with DPI awareness.
    - Uses GetClientRect for child windows, GetWindowRect for non-child windows.
    - Converts coordinates to parent's client area (child) or screen/owner (non-child) using MapWindowPoints.
    - Adjusts child window coordinates for borders using WINDOWINFO.cxWindowBorders, cyWindowBorders.
    - All coordinates and borders are in logical pixels before SSE2 scaling.
    - Scales coordinates and sizes to physical pixels using system DPI from GetDeviceCaps,
      ensuring compatibility with Windows XP and 7 (no GetDpiForWindow or manifest required).
    - Uses SSE2 to vectorize DPI scaling (ratio = Dpi / 96) for performance, with rounding to nearest.
    - Borders are subtracted in logical pixels before scaling; SSE2 scaling (mulps) applies DPI ratio
      to adjusted coordinates (e.g., cx = 10 - 2 = 8, then 8 * 1.5 = 12), correctly accounting for
      border's effect in physical pixels (e.g., 2 * 1.5 = 3).
    - No separate border scaling is needed, as WINPOS and borders are both in logical pixels.
    - Assumes System DPI Awareness (default without manifest), using primary monitor's DPI.
    - Does not support per-monitor DPI (unavailable on XP/7); all windows use system DPI.
    - Uses LOGPIXELSX for DPI scaling, as X and Y DPI are equal in standard Windows configurations.
    - Color depth experiments (e.g., (16M >> 8) & 511 = 36) are unrelated to DPI; GetDeviceCaps provides accurate DPI.
    - Validates window handles, thread desktop, and default desktop for robustness.
    - Preserves ECX and EDX registers as per calling convention.
    - Uses PosStruct directly for rectangle operations, eliminating temporary buffers.
    - Returns physical pixels for cx, cy, width, height, suitable for precise rendering or positioning.
    - Fallback to no scaling (Dpi = 96) if DPI retrieval fails.

  Usage Example:
    [WINPOS:
     WINPOS.cx: D$ 0
     WINPOS.cy: D$ 0
     WINPOS.width: D$ 0
     WINPOS.height: D$ 0]
    call 'USER32.GetDlgItem' D@hDlg, 156  ; Get control handle
    call GetWindowPosEx eax, WINPOS
    If eax = 0
        ; Handle failure
    Else_If eax = 1
        ; Non-child window, use WINPOS
    Else
        ; Child window, eax = hParent, use WINPOS and parent
    End_If

    Notes:
            Negative values for cx and cy - Why and When cx and cy Can Be Negative ?

            The cx and cy values in the WINPOS structure come from the left and top fields of the temporary Rect (a WINPOS structure)
            after MapWindowPoints transforms coordinates. Negative values arise when the window's top-left corner is positioned
            to the left or above the origin (0,0) of the target coordinate system. The target coordinate system depends on the window type:
                * Child windows: Parent's client coordinates (origin at the top-left of the parent's client area).
                " Non-child windows: Screen coordinates (hParent = NULL) or owner's client coordinates (hParent = owner).
               
                Here are some specific situations for each case, including whether dragging a window off the desktop contributes.

                1. Child Windows
                    * How MapWindowPoints Works:
                        * GetClientRect(hWnd, @Rect) sets @Rect to {left: 0, top: 0, right: client_width, bottom: client_height}
                          (client coordinates of hWnd).
                        * MapWindowPoints(hWnd, hParent, @Rect, 2) converts the points (left, top) and (right, bottom) from
                            hWnd's client coordinates to hParent's client coordinates.
                        * The parent's client area has its origin (0,0) at the top-left of its client area (excluding the parent's
                          borders, title bar, etc.).
                        * If hWnd's client area is positioned left or above the parent's client area origin, left (cx) or top (cy) become negative.
                    * Situations:
                        * Negative Positioning in Dialogs: In dialog-based applications, a child control (e.g., an edit box) can
                          be placed at a negative position relative to the dialog's client area. This is common in dialog templates
                          or dynamic layouts where controls are offset for visual effects or alignment.
                        * Example: A dialog template (in a .rc file) defines an edit control at
                                   CONTROL "Edit", IDC_EDIT, "EDIT", WS_CHILD, -10, -5, 100, 20.
                        * The edit control's client area top-left is at (-10, -5) in the dialog's client coordinates.
                        * After MapWindowPoints, @Rect.left = -10, @Rect.top = -5, so cx = -10, cy = -5.
                        * Border adjustment (e.g., cxWindowBorders = 2) makes it more negative: cx = -10 - 2 = -12.
                     * Dynamic Positioning:
                          If code programmatically moves a child window using SetWindowPos(hWnd, NULL, -10, -5, width, height, SWP_NOZORDER),
                          the child's position becomes negative relative to the parent's client area.
                        * Parent Scrolling: In a scrollable parent (e.g., a dialog with a scrollable client area), a child control's
                          position may appear negative when the parent is scrolled, shifting the child's relative position above or
                          left of the visible client area origin.
                     * Dragging Off Desktop?:
                        * Not Applicable:
                            Child windows (e.g., controls in a dialog) cannot be dragged by the user, as they are
                            fixed within the parent's client area. Negative cx, cy for child windows result from deliberate positioning
                            (via dialog templates or SetWindowPos), not user interaction like dragging.
                     * Visualization:
                        * Imagine a dialog's client area as a canvas starting at (0,0). If an edit control is placed at (-10, -5),
                          its top-left corner is outside the dialog's client area (to the left and above). This is valid in Windows
                          and results in negative cx, cy.
                2. Non-Child Windows
                    " How MapWindowPoints Works:
                        * GetWindowRect(hWnd, @Rect) sets @Rect to {left, top, right, bottom} in screen coordinates (entire window, including borders).
                        * MapWindowPoints(0, hParent, @Rect, 2) converts these screen coordinates to:
                            " Screen coordinates (if hParent = NULL): No change, but negative values are possible from GetWindowRect.
                            " Owner's client coordinates (if hParent is an owner window).
        Conclusion
            * Can cx, cy Be Negative?: Yes, for both child and non-child windows.
            * Situations:
            * Child Windows: Negative positions in dialog templates, programmatic SetWindowPos to negative coordinates,
                             or scrollable parent offsets. Not from dragging, as child windows are fixed.
            * Non-Child Windows: Multi-monitor setups (secondary monitor with negative coordinates), popups above/left of an
                                 owner's client area, or dragging/programmatic positioning off the primary monitor's left/top edge.
                                 Dragging off desktop is a valid case for top-level windows.



  Author: Gustavo Trigueiros (aka: Beyond2000!/Guga)
  Build Date: May 2012 (v1.0), Updated April 2025 (v1.4)
;;

Function

[DpiAdjust: F$ (1/96)]

Proc GetWindowPos::
    Arguments @hWnd, @PosStruct
    Local @hParent, @IsChild, @Dpi, @hDC, @AdjustForDpi, @Return
    Structure @WINDOWINFO 96,  @WINDOWINFO.cbSizeDis 0,  @WINDOWINFO.rcWindow_leftDis 4,  @WINDOWINFO.rcWindow_topDis 8,
                               @WINDOWINFO.rcWindow_rightDis 12,  @WINDOWINFO.rcWindow_bottomDis 16,  @WINDOWINFO.rcClient_leftDis 20,
                               @WINDOWINFO.rcClient_topDis 24,  @WINDOWINFO.rcClient_rightDis 28,  @WINDOWINFO.rcClient_bottomDis 32,
                               @WINDOWINFO.dwStyleDis 36,  @WINDOWINFO.dwExStyleDis 40,  @WINDOWINFO.dwWindowStatusDis 44,  @WINDOWINFO.cxWindowBordersDis 48,
                               @WINDOWINFO.cyWindowBordersDis 52,  @WINDOWINFO.atomWindowTypeDis 56,  @WINDOWINFO.wCreatorVersionDis 58,
                               @XMMReg0Dis 62, @XMMReg1Dis 78
    Uses ecx, edx

    ; Preserve XMM registers efficiently
    movdqu X@XMMReg0Dis XMM0
    movdqu X@XMMReg1Dis XMM1

    xor eax eax
    On D@hWnd = 0, ExitP
    On D@PosStruct = 0, ExitP

    ; Validate thread's desktop
    Call 'KERNEL32.GetCurrentThreadId'
    Call 'USER32.GetThreadDesktop' eax
    On eax = 0, ExitP

    ; Validate default desktop window
    Call 'USER32.GetDesktopWindow'
    On eax = 0, ExitP

    ; Validate window handle
    Call 'USER32.IsWindow' D@hWnd
    On eax = 0, ExitP

    ; Get parent or owner window
    Call 'USER32.GetAncestor' D@hWnd, &GA_PARENT
    mov D@hParent eax

    ; Validate parent handle (NULL is valid for top-level windows)
    If D@hParent <> 0
        Call 'USER32.IsWindow' D@hParent
        On eax = 0, ExitP
    End_If

    ; Get system DPI for scaling
    ; Use GetDeviceCaps on primary monitor's DC (hDC = NULL) to get LOGPIXELSX
    ; Default to 96 (100% scaling) if retrieval fails
    mov D@Dpi 96
    Call 'user32.GetDC' &NULL
    mov D@hDC eax
    If eax <> 0
        Call 'GDI32.GetDeviceCaps' D@hDC, &LOGPIXELSX ; No need to calculate for LOGPIXELSY, since they are basically the same
        If eax <> 0
            mov D@Dpi eax
        End_If
        call 'user32.ReleaseDC' &NULL, D@hDC
    End_If

    ; get a ratio for be fast. x*dpi/96 = x*Ratio, where ratio = Dpi/96
    mov D@AdjustForDpi &FALSE
    If D@dpi <> 96 ; If dpi is not 96, then we need to compute a ratio
        movss xmm0 X$DpiAdjust
        cvtsi2ss xmm1 D@Dpi ; converts a signed integer to double
        mulss xmm1 xmm0 | pshufd xmm1 xmm1 SSE_FILL_DWORDS ; our ratio is now in the 4 dwords of the xmm1 register
        mov D@AdjustForDpi &TRUE
    End_If

    ; Check if this is a child window
    call IsChildForWindowPos D@hParent, D@hWnd
    mov D@IsChild eax
    If eax = 0
        ; Non-child window: Get window rect and convert to parent/owner coords. Since we use the the same structure we need to correct the size here
        call 'USER32.GetWindowRect' D@hWnd, D@PosStruct
        mov ecx D@PosStruct
        mov eax D$ecx+WINPOS.widthDis | sub eax D$ecx+WINPOS.cxDis | mov D$ecx+WINPOS.widthDis eax  ; width = right - left
        mov eax D$ecx+WINPOS.heightDis | sub eax D$ecx+WINPOS.cyDis | mov D$ecx+WINPOS.heightDis eax  ; height = bottom - top
        ; and now we an pass to MapWindowPoints get cx and cy - which can be negative
        call 'USER32.MapWindowPoints' 0, D@hParent, D@PosStruct, 2 ; values will be converted to logical pixels
    Else
        ; Child window: Get client rect and convert to parent coords
        call 'USER32.GetClientRect' D@hWnd, D@PosStruct
        mov ecx D@PosStruct
        mov eax D$ecx+WINPOS.widthDis | sub eax D$ecx+WINPOS.cxDis | mov D$ecx+WINPOS.widthDis eax  ; width = right - left
        mov eax D$ecx+WINPOS.heightDis | sub eax D$ecx+WINPOS.cyDis | mov D$ecx+WINPOS.heightDis eax  ; height = bottom - top
        ; and now we an pass to MapWindowPoints get cx and cy - which can be negative. values will be converted to logical pixels
        call 'USER32.MapWindowPoints' D@hWnd, D@hParent, D@PosStruct, 2
    End_If

    ; Adjust for borders only for child windows
    If D@IsChild = &TRUE
        call 'RosMem.FastZeroMem' D@WINDOWINFO, 64 ; Size_of_WINDOWINFO is, in fact 64 bytes nd not 60
        mov D@WINDOWINFO.cbSizeDis 64
        call 'USER32.GetWindowInfo' D@hWnd, D@WINDOWINFO ; the information retrieved is also in logical pixels
        mov ecx D@PosStruct
        mov eax D@WINDOWINFO.cxWindowBordersDis | sub D$ecx+WINPOS.cxDis eax  ; adjust X for borders
        mov eax D@WINDOWINFO.cyWindowBordersDis | sub D$ecx+WINPOS.cyDis eax  ; adjust Y for borders
        mov eax D@hParent ; Also sucess, but we return the hParent, instead of &TRUE. So you can kow that hwnd is a child windows
    Else
        mov eax &TRUE ; On sucess, and if hWnd is a window, returns &TRUE
    End_If
    mov D@Return eax

    If D@AdjustForDpi = &TRUE
        mov ecx D@PosStruct
        movdqu xmm0 X$ecx | SSE_CONV_4INT_TO_4FLOAT xmm0 | mulps xmm0 xmm1 | SSE_CONV_4FLOAT_TO_4INT_ROUND xmm0 | movdqu X$ecx xmm0
    End_If

    mov eax D@Return

    ; Restore XMM registers
    movdqu XMM0 X@XMMReg0Dis
    movdqu XMM1 X@XMMReg1Dis

EndP


Function helper

;;
  IsChildForWindowPos
  Specialized version of IsChild for use with GetWindowPosEx. Determines whether a window
  is a child or descendant of a specified parent window, assuming input validation is done
  by the caller.
 
  Arguments:
    hWndParent (in): Handle to the potential parent window (may be NULL for top-level windows).
    hWnd (in): Handle to the window to test (assumed valid).
 
  Return Value:
    Returns TRUE (1) if hWnd is a child or descendant of hWndParent.
    Returns FALSE (0) if:
      - hWndParent is NULL or invalid.
      - hWnd is not a child window (lacks WS_CHILD style or has WS_POPUP).
      - The parent chain ends without reaching hWndParent.
 
  Remarks:
    - Optimized for GetWindowPosEx, which validates hWnd, hWndParent, and desktop context.
    - Skips redundant IsWindow, GetThreadDesktop, and GetDesktopWindow checks.
    - Traverses the parent chain using GetParent until hWndParent is found or a non-child
      window is encountered.
    - Preserves ECX and EDX registers.
    - Assumes hWnd is valid (checked by GetWindowPosEx) and hWndParent is either valid,
      NULL, or invalid (checked before traversal).
 
  Author: Gustavo Trigueiros (aka: Beyond2000!)
  Build Date: April 2025 (v1.0)
;;

Proc IsChildForWindowPos:
    Arguments @hWndParent, @hWnd
    Local @CurrentWnd, @Style
    Uses ecx, edx

    ; Initialize return value
    xor eax eax
    On D@hWndParent = 0, ExitP

    ; Initialize CurrentWnd = hWnd
    mov eax D@hWnd | mov D@CurrentWnd eax
    ; Traverse parent chain
    .While D@CurrentWnd <> 0
        ; Get window style
        call 'USER32.GetWindowLongA' D@CurrentWnd, &GWL_STYLE

        ; Check if window is a child: (style & (WS_POPUP | WS_CHILD)) == WS_CHILD
        and eax (&WS_POPUP+&WS_CHILD)   ; Bitwise OR for mask
        .If eax = &WS_CHILD
             ;  Get parent window
            Call 'USER32.GetParent' D@CurrentWnd
            mov D@CurrentWnd eax
            ; Check if parent is NULL
            On eax = 0, ExitP
            ; Check if parent matches hWndParent
            If eax = D@hWndParent
                mov eax &TRUE
                ExitP
            End_If
        .Else
            ; Not a child window, stop traversal
            xor eax eax
            ExitP
        .End_If

    .End_While

    ; Default return FALSE
    xor eax eax

EndP


Macros used:

[SSE_FILL_DWORDS 0]         ; Fill all 4 Dwords in a xmm register with the value of the 1st Dword. So, we are copying the 1st Dword to all others 3
                            ; Ex: mov al 053 | imul eax eax 01010101 | movd xmm1 ax | pshufd xmm1 xmm1 SSE_FILL_DWORDS ; the contents of each one of all 8 words in xmm1 will be 053


[pshufd | pshufd #1 #2 #3]

[SSE_ABS_REAL4 | pslld #1 1 | psrld #1 1] ; pslld xmm1 1 | psrld xmm1 1
[SSE_CONV_4INT_TO_4FLOAT | cvtdq2ps #1 #1] ; pslld xmm1 1 | psrld xmm1 1 cvtdq2ps xmm1 xmm1
[SSE_FIND_REAL4_MAX | pshufd xmm7 #1 SSE_SWAP_DWORDS | maxps #1 xmm7 | pshufd xmm7 #1 SSE_INVERT_DWORDS | maxps #1 xmm7]
[SSE_CONV_4FLOAT_TO_4INT | cvttps2dq #1 #1]; Convert four floats to integers (truncate)
[SSE_CONV_4FLOAT_TO_4INT_ROUND | cvtps2dq #1 #1] ; Convert four floats to integers (round to nearest)

Coding in Assembly requires a mix of:
80% of brain, passion, intuition, creativity
10% of programming skills
10% of alcoholic levels in your blood.

My Code Sites:
http://rosasm.freeforums.org
http://winasm.tripod.com