CBTProc problem

Just starting out? Need help? Post your questions and find answers here.
Everything
Enthusiast
Enthusiast
Posts: 224
Joined: Sat Jul 07, 2018 6:50 pm

CBTProc problem

Post by Everything »

I set a hook on create a open file dialog box to change the coordinates of its appearance.
Faced a problem - the hook is triggered and the window appears at the desired coordinates, but after a split second it moves to the last position (apparently after window creating in the code there is a call to SetWindowPos)

Code: Select all

Global Hook

Procedure CBTProc(nCode, wParam, lParam)
  Protected *CBT.CBT_CREATEWND
  Protected *PCS.CREATESTRUCT
  Protected ClassName.s{#MAX_PATH}
  
  Select nCode
    Case #HCBT_CREATEWND
      *CBT = lParam : *PCS = *CBT\lpcs
      If *PCS\lpszClass = 32770 ; simpler than > If GetClassName_(wParam, @ClassName, #MAX_PATH-1) : If ClassName = "#32770"
          Debug "Found!"
          *PCS\x = 100
          *PCS\y = 100
      EndIf
  EndSelect
   
  ; If nCode is less than zero, the hook procedure must pass the message to the CallNextHookEx function without further processing and should return the value returned by CallNextHookEx
  If nCode < 0
    ProcedureReturn CallNextHookEx_(Hook, nCode, wParam, lParam)
  Else
  ; For HCBT_CREATEWND return value must be 0 to allow the operation, or 1 to prevent it
    ProcedureReturn #False
  EndIf   
EndProcedure


Hook = SetWindowsHookEx_ (#WH_CBT, @CBTProc() , 0, GetCurrentThreadId_())
OpenFileRequester( "" , "" , "" , 0 )
If Hook : UnhookWindowsHookEx_( Hook ) : EndIf 
Run the example, move the window to the lower right corner, press "cancel" and run it again.
#HCBT_MOVESIZE doesn't work here.
Tested on Win7x64.

Is there a way to fix it without install another hook for messages (I consider this an overcomplication) or it's the only way?
I will be glad to any working examples.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8433
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Re: CBTProc problem

Post by netmaestro »

Couple of points here:
MSDN wrote:At the time of the HCBT_CREATEWND notification, the window has been created, but its final size and position may not have been determined and its parent window may not have been established.
You might consider the HCBT_ACTIVATE trigger instead. Any messages you send to reposition should stick from here.

Also, I noticed you saved a few keystrokes by using the lpszClass member. This is not recommended by the doc:
MSDN wrote:Because the lpszClass member can contain a pointer to a local (and thus inaccessable) atom, do not obtain the class name by using this member. Use the GetClassName function instead.
BERESHEIT
Everything
Enthusiast
Enthusiast
Posts: 224
Joined: Sat Jul 07, 2018 6:50 pm

Re: CBTProc problem

Post by Everything »

netmaestro wrote:Also, I noticed you saved a few keystrokes by using the lpszClass member. This is not recommended by the doc:
MSDN wrote:Because the lpszClass member can contain a pointer to a local (and thus inaccessable) atom, do not obtain the class name by using this member. Use the GetClassName function instead.
But I don't obtain the class name by using this member I just read member itself.
It's integer which can be a pointer to a class string or directly the class number (as in our case and GetClassName just converts it to a string and adds a '#' character to the beginning)
So I still think it's a safe way.
netmaestro wrote:You might consider the HCBT_ACTIVATE trigger instead
Forgot to mention why #HCBT_CREATEWND
I know about #HCBT_ACTIVATE and #WH_FOREGROUNDIDLE+#HC_ACTION methods and they work great but there is one problem with them - initially the window still appears at the default coordinates and only then these methods are moved dialogbox to the desired location.
I just wanted to get rid of these window jumps, that’s why I’m trying to intercept the very beginning of window manipulation.
Also, in case the user disable saving the window position option in OS, I did not want to deal with the registry (HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\Bags\...) and make it more universal way.
Everything
Enthusiast
Enthusiast
Posts: 224
Joined: Sat Jul 07, 2018 6:50 pm

Re: CBTProc problem

Post by Everything »

Everything wrote:apparently after window creating in the code there is a call to SetWindowPos
I was right.
After the window of our dialogbox is created and shown, this call moves it to the last coordinates from the registry:
uxtheme.dll+BA03 | FF15 173D0300 | call qword ptr ds:[<&ZwUserSetWindowPos>]
Of course, we can set a hook on ZwUserSetWindowPos and block it once when window handle is our handle, but I think that this is too complicated way and there are easier options.
Everything
Enthusiast
Enthusiast
Posts: 224
Joined: Sat Jul 07, 2018 6:50 pm

Re: CBTProc problem

Post by Everything »

At the moment, this is the simplest working solution that I found

Code: Select all

Global Hook
Global DefaultCallback
Global MsgCounter

Procedure DlgCallback(hWnd, Msg, wParam, lParam)
  
  If MsgCounter < 3
    Select Msg
      Case #WM_WINDOWPOSCHANGING
        MsgCounter+1
        Debug MsgCounter
        Protected *WP.WINDOWPOS = lParam
        *WP\x = 100
        *WP\y = 100
        *WP\cx = 1000
        *WP\cy = 500
        ProcedureReturn #False
    EndSelect  
  EndIf 
  
  ProcedureReturn CallFunctionFast(DefaultCallback, hWnd, Msg, wParam, lParam)
EndProcedure

Procedure CBTProc(nCode, wParam, lParam)
  Protected *CBT.CBT_CREATEWND
  Protected *PCS.CREATESTRUCT
  ;Protected ClassName.s{#MAX_PATH}
 
  Select nCode
    Case #HCBT_CREATEWND
      *CBT = lParam : *PCS = *CBT\lpcs
      If *PCS\lpszClass = 32770 ; simpler than > If GetClassName_(wParam, @ClassName, #MAX_PATH-1) : If ClassName = "#32770"
        Debug "Found!"
        ; first create dialog out of view
        *PCS\y = -7500 ; must be changed to -(winH+current monitor H)        
        DefaultCallback = GetWindowLongPtr_(wParam, #GWL_WNDPROC) : Debug Hex(DefaultCallback)
        ; and set own callback
        If Not SetWindowLong_(wParam, #GWL_WNDPROC, @DlgCallback()) : Debug "SetWindowLong fail" : ProcedureReturn #False : EndIf : Debug Hex( GetWindowLongPtr_(hWnd, #GWL_WNDPROC) )
      EndIf
  EndSelect
   
  ; If nCode is less than zero, the hook procedure must pass the message to the CallNextHookEx function without further processing and should return the value returned by CallNextHookEx
  If nCode < 0
    ProcedureReturn CallNextHookEx_(Hook, nCode, wParam, lParam)
  Else
  ; For HCBT_CREATEWND return value must be 0 to allow the operation, or 1 to prevent it
    ProcedureReturn #False
  EndIf   
EndProcedure


Hook = SetWindowsHookEx_ (#WH_CBT, @CBTProc() , 0, GetCurrentThreadId_())
OpenFileRequester( "" , "" , "" , 0 )
If Hook : UnhookWindowsHookEx_( Hook ) : EndIf
MsgCounter = 0
P.S.
It is more reliable to use #WM_NCPAINT instead of the message counter (called for the first time immediately after the final setting of the dialog box position)
Also note that on OS < Vista PB use GetOpenFileName instead of IFileOpenDialog and we don't need to use DlgCallback
RASHAD
PureBasic Expert
PureBasic Expert
Posts: 4662
Joined: Sun Apr 12, 2009 6:27 am

Re: CBTProc problem

Post by RASHAD »

Hi
I did the next snippet long time back
- Very simple
- Works with all Requesters
- Tested with windows 7,8,8.1 & 10

But it has a non noticeable drawback

Code: Select all

Procedure Requster_Pos(Parameter)
  Repeat
    RUN + 1
    hwnd = FindWindow_("#32770", 0)
    w = 800;r\right - r\left
    h = 600;r\bottom - r\top
    x = 100;(DesktopWidth(0) - w)/2
    y = 100;(DesktopHeight(0) - h)/2
    MoveWindow_(hwnd,x,y,w,h,1)     
    Delay(1)        
  Until RUN = 100  
EndProcedure

Thread = CreateThread(@Requster_Pos(), 0)

File$ = OpenFileRequester("Info","Please choose file To load", "", 0)
If IsThread(Thread)
  KillThread(thread)
EndIf
; Delay(10)
; File$ = SaveFileRequester("Please choose file to save", "", "", 0)
; Delay(10)
; Result = FontRequester("", 0, #PB_FontRequester_Effects)
; Delay(10)
; result = ColorRequester()
; Delay(10)
; MessageRequester("Problem:", "What will all the lassies dae when Willie gaes a wa?")
; Delay(10)
Egypt my love
Everything
Enthusiast
Enthusiast
Posts: 224
Joined: Sat Jul 07, 2018 6:50 pm

Re: CBTProc problem

Post by Everything »

RASHAD wrote:Hi
I did the next snippet long time back
Hi, RASHAD!
Over the years spent on the forum, I had time to get to know you as a specialist offering beautiful and elegant solutions.
But this snippet is something beyond good and evil :shock:
I will not even list all the shortcomings of this approach...

I did not reverse the PB code but I'm pretty sure that the pseudo code for the OpenFileRequester is something like:

Code: Select all

If WinVer >= Vista
  Use IFileOpenDialog
Else
  Use GetOpenFileName
Endif
And we know that IFileOpenDialog will send ~3 #WM_WINDOWPOSCHANGING messages with #SWP_SHOWWINDOW at the last one (positioning should be done for all of them) and than we will get #WM_NCPAINT that tells us we done with message handling.
At least my research says so.
So far, my (improved) version of the code has been tested on all OS above XP (processing is disabled for it) and everything works quickly and clearly.

I assume that there are options to make it easier, but so far I have not found them.
RASHAD
PureBasic Expert
PureBasic Expert
Posts: 4662
Joined: Sun Apr 12, 2009 6:27 am

Re: CBTProc problem

Post by RASHAD »

Egypt my love
Everything
Enthusiast
Enthusiast
Posts: 224
Joined: Sat Jul 07, 2018 6:50 pm

Re: CBTProc problem

Post by Everything »

RASHAD wrote:Hi
Tested with PB 5.72 x86 - Windows 10 2004 x64

Code: Select all

Global hWnd,oldProc,x,y,w,h

Procedure ReqProc(hWnd, uMsg, wParam, lParam)
  Select uMsg      
    Case #WM_SIZE,#WM_MOVE,#WM_PAINT
      MoveWindow_(hWnd,x,y,w,h,1)      
      
    Case #WM_NCLBUTTONDOWN
      SetWindowLongPtr_(hWnd, #GWL_WNDPROC,0)    
      
  EndSelect
  ProcedureReturn CallWindowProc_(oldProc, hWnd, uMsg, wParam, lParam)
EndProcedure

Procedure HookCB ( uMsg , wParam , lParam)   
  Select uMsg
    Case #HCBT_ACTIVATE
      hWnd = wParam 
      oldProc = SetWindowLongPtr_(hWnd, #GWL_WNDPROC, @ReqProc())
      UnhookWindowsHookEx_ (Hook)    
        
   EndSelect
   
   ProcedureReturn Result
EndProcedure

x = 100
y = 100
w = 800
h = 600
Hook = SetWindowsHookEx_ ( #WH_CBT, @ HookCB () , 0, GetCurrentThreadId_ ())

Pattern$ = "Text (*.txt)|*.txt;*.bat|PureBasic (*.pb)|*.pb|All files (*.*)|*.*"
Pattern = 0 
File$ = OpenFileRequester("Please choose file to load", StandardFile$, Pattern$, Pattern)

Thx!
Your code gave me the idea that with own callback we can start with #HCBT_ACTIVATE and skip #HCBT_CREATEWND part

BTW You just forgot to declare global variables (in your case):
Global oldProc, x, y, w, h, Hook, hWnd* (last one must have some other name because of same arg proc name and adding some validations would be nice)
And also 'Result' must be '#False' or CallNextHookEx*

But it's all the little things :)
RASHAD
PureBasic Expert
PureBasic Expert
Posts: 4662
Joined: Sun Apr 12, 2009 6:27 am

Re: CBTProc problem

Post by RASHAD »

Hi Everything
I am too busy with Danilo and his Raylib project :)
You are free to add and post your suggestions with appreciation
Egypt my love
Post Reply