Win API: Add a control to an unowned application

Share your advanced PureBasic knowledge/code with the community.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Win API: Add a control to an unowned application

Post by netmaestro »

Just a little timewaster for a Monday afternoon. You can't directly add a PureBasic gadget to an application not owned by your thread, however using API it can be managed within certain limits. The main limitation is that you can't get the events of the application's existing controls, but in a lot of cases you don't really need them. Here's a small example that adds a PI button to the Windows Calculator. It resizes the CLEAR button smaller to make room first. A PI button is already on the Scientific view, so the one added here is an extra one if you're using that view.

Note that if the window name of your calculator is not "Calculator" you will have to modify this code slightly to reflect the name yours is using.

Also, it'll look best if you check "XP Skin Support" in the Compiler Options.

Code: Select all

;========================================================================== 
; Program:          Add a functional button to the Windows Calculator 
; Author:           netmaestro 
; Date:             June 11, 2007 
; Target Compiler:  PureBasic 4.0 and later 
; Target OS:        Microsoft Windows XP Only
;========================================================================== 

Global oldproc, quit=0, cebutton, clearbutton, one, two, three 
Global four, five, six, seven, eight, nine, zero, point 

Procedure ButtonProc(hwnd, msg, wParam, lParam) 
  Select msg 
    Case #WM_NCDESTROY 
      End 
    Case #WM_LBUTTONUP 
      SendMessage_(cebutton, #BM_CLICK, 0,0) 
      SendMessage_(three,    #BM_CLICK, 0,0) 
      SendMessage_(point,    #BM_CLICK, 0,0) 
      SendMessage_(one,      #BM_CLICK, 0,0) 
      SendMessage_(four,     #BM_CLICK, 0,0) 
      SendMessage_(one,      #BM_CLICK, 0,0) 
      SendMessage_(five,     #BM_CLICK, 0,0) 
  EndSelect 
  ProcedureReturn CallWindowProc_(oldproc, hwnd, msg, wParam, lParam) 
EndProcedure 

Procedure GetClearButtons(hwnd, param) 
  wt.s = Space(10) 
  SendMessage_(hwnd, #WM_GETTEXT, 10, @wt)
  If wt = "C" 
    clearbutton = hwnd 
  ElseIf wt = "CE" 
    cebutton = hwnd 
  EndIf 
  ProcedureReturn 1 
EndProcedure 

Procedure GetNumbers(hwnd, param) 
  wt.s = Space(10) 
  SendMessage_(hwnd, #WM_GETTEXT, 10, @wt)
  Select wt 
    Case "1" : one = hwnd 
    Case "2" : two = hwnd 
    Case "3" : three = hwnd 
    Case "4" : four = hwnd 
    Case "5" : five = hwnd 
    Case "6" : six = hwnd 
    Case "7" : seven = hwnd 
    Case "8" : eight = hwnd 
    Case "9" : nine = hwnd 
    Case "0" : zero = hwnd 
    Case "." : point = hwnd 
    EndSelect 
    ProcedureReturn 1 
EndProcedure 

RunProgram("calc.exe") 
start = ElapsedMilliseconds() 
Repeat 
  c = FindWindow_(0, "Calculator") 
  Delay(1) 
Until c Or ElapsedMilliseconds()-start > 500 

If c 
  EnumChildWindows_(c, @GetClearButtons(), 0) 
  EnumChildWindows_(c, @GetNumbers(), 0) 
  SetWindowPos_(clearbutton, 0,0,0,28,29,#SWP_NOZORDER|#SWP_NOMOVE|#SWP_FRAMECHANGED) 
  InitCommonControls_() 
  GetWindowRect_(c, @cr.RECT) 
  w = cr\right-cr\left 
  If w > 270 ; Scientific 
    button = CreateWindowEx_(0,"Button", "PI", #WS_CHILD|#WS_VISIBLE,432,62,28,29,c,0,GetModuleHandle_(0),0) 
  Else ; Standard 
    button = CreateWindowEx_(0,"Button", "PI", #WS_CHILD|#WS_VISIBLE,216,37,28,29,c,0,GetModuleHandle_(0),0) 
  EndIf 
Else 
  MessageRequester("OOPS!", "Can't find the Calculator!", #MB_ICONERROR) 
  End 
EndIf 

oldproc = SetWindowLong_(button, #GWL_WNDPROC, @ButtonProc()) 

result = GetMessage_(@msg.MSG, #Null, #Null, #Null) 
While result <> 0 
  If result = -1 ; GetMessage failed 
    MessageRequester("OOPS!", "Failure in GetMessage Loop", #MB_ICONERROR) 
    End 
  EndIf 
  TranslateMessage_(msg) 
  DispatchMessage_(msg) 
  result = GetMessage_(@msg.MSG, #Null, #Null, #Null) 
Wend 
Last edited by netmaestro on Wed Jun 13, 2007 10:03 pm, edited 4 times in total.
BERESHEIT
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Post by Trond »

I tried that with WaitWindowEvent() instead and it just hung. Good job of figuring it out.
FreeThought
User
User
Posts: 54
Joined: Mon Jul 18, 2005 10:28 am

Post by FreeThought »

works nicely on 4.10 , Thanks for the code. :)
User avatar
Shardik
Addict
Addict
Posts: 2058
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Post by Shardik »

@netmaestro,

thank you for your nice code example. But one small improvement: you correctly state that you have to modify the window name if it is not "Calculator" (for example in German it is "Rechner"). You can circumvent this problem by using the option #PB_Program_Open with RunProgram() which returns a program-ID. With this program-ID you can evaluate the process ID. And eddy and klonk have already demonstrated how to obtain a window handle from a process ID:
http://www.purebasic.fr/english/viewtopic.php?t=6753

Code: Select all

CalcProgramID = RunProgram("Calc.Exe", "", "", #PB_Program_Open)

If CalcProgramID
  CalcProcessID = ProgramID(CalcProgramID)

  Count = 0 
  Repeat 
    Delay(100)          ; Avoid hanging of windows during start of application while searching for the window 
    Count = Count + 1   ; Security counter to avoid hanging 
    WinHandle = FindWindow_(0,0) 
    While WinHandle <> 0 
      GetWindowThreadProcessId_(WinHandle, @ProcessID) 
      If ProcessID = CalcProcessID 
        c = WinHandle 
        Break 
      EndIf 
      WinHandle = GetWindow_(WinHandle, #GW_HWNDNEXT) 
    Wend 
  Until c Or (Count=50) ; Wait up to 5 seconds for window to occur 
EndIf
Another problem: your code doesn't run under WinNT 4 SP6 because GetWindowText() does only return the text from the calculator input window ("0," in the German version of calc) but not the text for the buttons. With WinXP SP2 it runs quite fine. Do you have an explanation for it? I am wondering about its functioning under WinXP at all because the MSDN states (http://msdn2.microsoft.com/en-us/library/ms633520.aspx):
MSDN wrote: However, GetWindowText cannot retrieve the text of a control in another application.
Did this restriction change in WinXP?

Update:
The MSDN gives the following hint:
MSDN wrote: To retrieve the text of a control in another process, send a WM_GETTEXT message directly instead of calling GetWindowText.
But even if you change

Code: Select all

GetWindowText_(hwnd, @wt, 10)
against

Code: Select all

SendMessage_(hwnd, #WM_GETTEXT, 10, @wt)
this still only works with WinXP but not with WinNT 4...

Update 2:
netmaestro's code doesn't run under Win98SE neither, after closing the calculator it even crashes. Tested with the unchanged GetWindowText_(hwnd, @wt, 10) and SendMessage_(hwnd, #WM_GETTEXT, 10, @wt) in PB 4.02... The same result as in WinNT: only "0," is returned, the other buttons always return 0 as result from calling GetWindowText() and SendMessage() with #WM_GETTEXT...
Last edited by Shardik on Wed Jun 13, 2007 12:30 pm, edited 2 times in total.
FreeThought
User
User
Posts: 54
Joined: Mon Jul 18, 2005 10:28 am

Post by FreeThought »

Hi shardik
in the code you presented if i want to "debug winhandle", it only works on 4.10 with threadsafe option is on , but works fine on 4.02. am I missing anything.
User avatar
Michael Vogel
Addict
Addict
Posts: 2797
Joined: Thu Feb 09, 2006 11:27 pm
Contact:

Post by Michael Vogel »

Shardik wrote:@netmaestro,

[...]netmaestro's code doesn't run under Win98SE neither [...] The same result as in WinNT: only "0," is returned [...]
Same in Windows 2000 :cry:
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post by netmaestro »

I replaced GetWindowText with #WM_GETTEXT as recommended in MSDN, does that make it work in more OS's? Specifically, w2000 or 2003 server?
BERESHEIT
User avatar
Shardik
Addict
Addict
Posts: 2058
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Post by Shardik »

I have taken a quick look into the calc.exe versions of WinNT and WinXP with Resource Hacker. They are different. In WinXP the dialog resource contains in nearly each button control definition the name of that button. In WinNT the dialog resource contains only very few button names in the control definitions. But in both versions exists a resource "Accelerators" which links the dialog control id of a button with its name. Reading this accelerators table resource would provide a way to link the button name to its control-ID which is obtainable by GetDlgCtrlID(hControl)...
User avatar
Shardik
Addict
Addict
Posts: 2058
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Post by Shardik »

This modified example works with WinXP and Win98SE (although is still crashes in Win98SE after closing the calculator :roll:):

Code: Select all

;========================================================================== 
; Program:          Add a functional button to the Windows Calculator 
; Author:           netmaestro 
;========================================================================== 

Global oldproc, quit=0, cebutton, clearbutton, one, two, three 
Global four, five, six, seven, eight, nine, zero, point 

Procedure ButtonProc(hwnd, msg, wParam, lParam) 
  Select msg 
    Case #WM_NCDESTROY 
      End 
    Case #WM_LBUTTONUP 
      SendMessage_(cebutton, #BM_CLICK, 0,0) 
      SendMessage_(three,    #BM_CLICK, 0,0) 
      SendMessage_(point,    #BM_CLICK, 0,0) 
      SendMessage_(one,      #BM_CLICK, 0,0) 
      SendMessage_(four,     #BM_CLICK, 0,0) 
      SendMessage_(one,      #BM_CLICK, 0,0) 
      SendMessage_(five,     #BM_CLICK, 0,0) 
  EndSelect 
  ProcedureReturn CallWindowProc_(oldproc, hwnd, msg, wParam, lParam) 
EndProcedure 

Procedure GetClearButtons(hwnd, param)
  CtrlID = GetDlgCtrlID_(hwnd)
 
  If CtrlID = 81 
    clearbutton = hwnd 
  ElseIf CtrlID = 82
    cebutton = hwnd 
  EndIf 
  ProcedureReturn 1 
EndProcedure 

Procedure GetNumbers(hwnd, param)
  Select GetDlgCtrlID_(hwnd)
    Case 124 : zero = hwnd 
    Case 125 : one = hwnd 
    Case 126 : two = hwnd 
    Case 127 : three = hwnd 
    Case 128 : four = hwnd 
    Case 129 : five = hwnd 
    Case 130 : six = hwnd 
    Case 131 : seven = hwnd 
    Case 132 : eight = hwnd 
    Case 133 : nine = hwnd 
    Case 85 : point = hwnd 
    EndSelect 
    ProcedureReturn 1 
EndProcedure 


CalcProgramID = RunProgram("Calc.Exe", "", "", #PB_Program_Open)

If CalcProgramID
  CalcProcessID = ProgramID(CalcProgramID)
  Count = 0 
  Repeat 
    Delay(100)          ; Avoid hanging of windows during start of application while searching for the window 
    Count = Count + 1   ; Security counter to avoid hanging 
    WinHandle = FindWindow_(0,0) 
    While WinHandle <> 0 
      GetWindowThreadProcessId_(WinHandle, @ProcessID) 
      If ProcessID = CalcProcessID 
        c = WinHandle 
        Break 
      EndIf 
      WinHandle = GetWindow_(WinHandle, #GW_HWNDNEXT) 
    Wend 
  Until c Or (Count=50) ; Wait up to 5 seconds for window to occur 
EndIf

If c 
  EnumChildWindows_(c, @GetClearButtons(), 0) 
  EnumChildWindows_(c, @GetNumbers(), 0) 
  SetWindowPos_(clearbutton, 0,0,0,28,29,#SWP_NOZORDER|#SWP_NOMOVE|#SWP_FRAMECHANGED) 
  InitCommonControls_() 
  GetWindowRect_(c, @cr.RECT) 
  w = cr\right-cr\left 
  If w > 270 ; Scientific 
    button = CreateWindowEx_(0,"Button", "PI", #WS_CHILD|#WS_VISIBLE,432,62,28,29,c,0,GetModuleHandle_(0),0) 
  Else ; Standard 
    button = CreateWindowEx_(0,"Button", "PI", #WS_CHILD|#WS_VISIBLE,216,37,28,29,c,0,GetModuleHandle_(0),0) 
  EndIf 
Else 
  MessageRequester("OOPS!", "Can't find the Calculator!", #MB_ICONERROR) 
  End 
EndIf 

oldproc = SetWindowLong_(button, #GWL_WNDPROC, @ButtonProc()) 

result = GetMessage_(@msg.MSG, #Null, #Null, #Null) 
While result <> 0 
  If result = -1 ; GetMessage failed 
    MessageRequester("OOPS!", "Failure in GetMessage Loop", #MB_ICONERROR) 
    End 
  EndIf 
  TranslateMessage_(msg) 
  DispatchMessage_(msg) 
  result = GetMessage_(@msg.MSG, #Null, #Null, #Null) 
Wend
I also will test it tomorrow in my office with Win2K Server and WinNT 4. This is a quick hack without reading out the "Accelerator" resource because the dialog control IDs of the buttons in Win98SE and WinXP are identical. If they should be different in Win2K or WinNT, I will try to adapt the example.
codemaniac
Enthusiast
Enthusiast
Posts: 289
Joined: Mon Apr 02, 2007 7:22 am
Location: Finland

Post by codemaniac »

None of them works in Vista :roll:
Cute?
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post by netmaestro »

; Target OS: Microsoft Windows XP Only
Fixed. :twisted:
BERESHEIT
codemaniac
Enthusiast
Enthusiast
Posts: 289
Joined: Mon Apr 02, 2007 7:22 am
Location: Finland

Post by codemaniac »

netmaestro wrote:
; Target OS: Microsoft Windows XP Only
Fixed. :twisted:
Windows XP? Isn't that the bug in your code? Better fix it soon :D
Cute?
User avatar
Shardik
Addict
Addict
Posts: 2058
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Post by Shardik »

I have modified my last example code that improves on netmaestro's code, to run with WinNT 4, Win2000 Server and WinXP. Win98SE should also run, but probably still produces a crash after closing the calculator (I can't try to fix it now, because I have no access to Win98 in my office). Sorry, but I currently don't have the opportunity to test it with Vista...

Code: Select all

Global oldproc, quit=0, cebutton, clearbutton, one, two, three 
Global four, five, six, seven, eight, nine, zero, point 

Procedure ButtonProc(hwnd, msg, wParam, lParam) 
  Select msg 
    Case #WM_NCDESTROY 
      End 
    Case #WM_LBUTTONUP 
      SendMessage_(cebutton, #BM_CLICK, 0,0) 
      SendMessage_(three,    #BM_CLICK, 0,0) 
      SendMessage_(point,    #BM_CLICK, 0,0) 
      SendMessage_(one,      #BM_CLICK, 0,0) 
      SendMessage_(four,     #BM_CLICK, 0,0) 
      SendMessage_(one,      #BM_CLICK, 0,0) 
      SendMessage_(five,     #BM_CLICK, 0,0) 
  EndSelect 
  ProcedureReturn CallWindowProc_(oldproc, hwnd, msg, wParam, lParam) 
EndProcedure 

Procedure GetClearButtons(hwnd, param) 
  CtrlID = GetDlgCtrlID_(hwnd) 
  
  If CtrlID = 81 
    clearbutton = hwnd 
  ElseIf CtrlID = 82 
    cebutton = hwnd 
  EndIf 
  ProcedureReturn 1 
EndProcedure 

Procedure GetNumbers(hwnd, param) 
  Select GetDlgCtrlID_(hwnd) 
    Case 124 : zero = hwnd 
    Case 125 : one = hwnd 
    Case 126 : two = hwnd 
    Case 127 : three = hwnd 
    Case 128 : four = hwnd 
    Case 129 : five = hwnd 
    Case 130 : six = hwnd 
    Case 131 : seven = hwnd 
    Case 132 : eight = hwnd 
    Case 133 : nine = hwnd 
    Case 85 : point = hwnd 
    EndSelect 
    ProcedureReturn 1 
EndProcedure 


CalcProgramID = RunProgram("Calc.Exe", "", "", #PB_Program_Open) 

If CalcProgramID 
  CalcProcessID = ProgramID(CalcProgramID) 
  Count = 0 
  Repeat 
    Delay(100)          ; Avoid hanging of windows during start of application while searching for the window 
    Count = Count + 1   ; Security counter to avoid hanging 
    WinHandle = FindWindow_(0,0) 
    While WinHandle <> 0 
      GetWindowThreadProcessId_(WinHandle, @ProcessID) 
      If ProcessID = CalcProcessID
        c = WinHandle 
        Break 
      EndIf 
      WinHandle = GetWindow_(WinHandle, #GW_HWNDNEXT) 
    Wend 
  Until c Or (Count=50) ; Wait up to 5 seconds for window to occur 
EndIf 

If c 
  EnumChildWindows_(c, @GetClearButtons(), 0) 
  EnumChildWindows_(c, @GetNumbers(), 0) 
  SetWindowPos_(clearbutton, 0,0,0,28,29,#SWP_NOZORDER|#SWP_NOMOVE|#SWP_FRAMECHANGED) 
  InitCommonControls_() 
  GetWindowRect_(c, @cr.RECT) 
  w = cr\right-cr\left 
  If w < 270 ; Standard
    If OSVersion() = #PB_OS_Windows_NT_4
      yPosPIButton = 46
    Else
      yPosPIButton = 37
    EndIf      
    button = CreateWindowEx_(0,"Button", "PI", #WS_CHILD|#WS_VISIBLE,216,yPosPIButton,28,29,c,0,GetModuleHandle_(0),0)
  EndIf
  RedrawWindow_(button, 0, 0, #RDW_UPDATENOW)
Else 
  MessageRequester("OOPS!", "Can't find the Calculator!", #MB_ICONERROR) 
  End 
EndIf 

oldproc = SetWindowLong_(button, #GWL_WNDPROC, @ButtonProc()) 

result = GetMessage_(@msg.MSG, #Null, #Null, #Null) 
While result <> 0 
  If result = -1 ; GetMessage failed 
    MessageRequester("OOPS!", "Failure in GetMessage Loop", #MB_ICONERROR) 
    End 
  EndIf 
  TranslateMessage_(msg) 
  DispatchMessage_(msg) 
  result = GetMessage_(@msg.MSG, #Null, #Null, #Null) 
Wend
The following changes were made:

- The inclusion of the PI button in the scientific mode was removed because in scientific mode a PI button already exists (with 31 digits after the decimal point/comma)

- For WinNT the y-position of the PI button has to be adjusted:

Code: Select all

  If w < 270 ; Standard
    If OSVersion() = #PB_OS_Windows_NT_4
      yPosPIButton = 46
    Else
      yPosPIButton = 37
    EndIf      
    button = CreateWindowEx_(0,"Button", "PI", #WS_CHILD|#WS_VISIBLE,216,yPosPIButton,28,29,c,0,GetModuleHandle_(0),0)
  EndIf
- For WinNT and Win2K Server a redraw of the newly added PI button was necessary because without the redraw the PI button wouldn't be visible (only after clicking into the empty space):

Code: Select all

  RedrawWindow_(button, 0, 0, #RDW_UPDATENOW)
codemaniac
Enthusiast
Enthusiast
Posts: 289
Joined: Mon Apr 02, 2007 7:22 am
Location: Finland

Post by codemaniac »

Shardik, I tried your code using PureBasic 4.10 Beta 2 on Windows Vista and all I get is the normal stinky Calculator without the PI button (the Calculator is in Normal Mode not Scientific).
Cute?
User avatar
Shardik
Addict
Addict
Posts: 2058
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Post by Shardik »

codemaniac wrote: Shardik, I tried your code using PureBasic 4.10 Beta 2 on Windows Vista and all I get is the normal stinky Calculator without the PI button (the Calculator is in Normal Mode not Scientific).
Sorry, but I stated that I don't have the opportunity to currently do any tests with Vista. :? All my tests were done using PB 4.02...
Post Reply