Real SetForegroundWindow() - Revisited

Share your advanced PureBasic knowledge/code with the community.
Randy Walker
Addict
Addict
Posts: 1064
Joined: Sun Jul 25, 2004 4:21 pm
Location: USoA

Real SetForegroundWindow() - Revisited

Post by Randy Walker »

Original tip submitted by PB: viewtopic.php?t=3751

Later, resubmitted as a tip from Fred: viewtopic.php?f=12&t=7424&hilit=real+Se ... oundWindow

Later argued by others: viewtopic.php?f=3&t=45556
"This function performs a Real SetForegroundWindow() task, but don't ever use it. It is bad!"
I on the other hand would say, the "Real SetForegroundWindow()" function has its place, but it is not for beginners to decide where or how to use it. I'm not a beginner (not a Guru either) and I did find a legitimate place for it. Used "wisely" it can be used "safely". The following is an attempt to illustrate that.

Code: Select all

; The following demo was assembled by Randy Walker from assorted snippets.
; Revised for use in PB ver 4.51
; Test it, alter it or use it in whole or in part however you like.
; Just be warned -- anything "you" break here forward is not my fault :)
; Of course, feel free to comment on this effort any way you like.
If InitMouse() = 0
  End
EndIf
;-  IMPORTANT - READ THIS - IMPORTANT
;/  IMPORTANT - READ THIS - IMPORTANT
; "Window queue management" in general is provided for by the operating system.
; You will encounter undesirable window activity if you do not honor the design
; of the operating system.  Any window you create is "your" business so you can
; pretty much change foreground on "your" windows all you want using the native
; PB commands (OpenWindow, RunProgram, HidwWindow, SetActiveWindow, etc).
;
; Any window you did not create "belongs to someone else" so you MUST HONOR the
; operating system design to avoid stepping on toes.  Best way I have found to
; do that is: Do not change foreground on any external app unless you plan to
; restore it (and thus, the OS window queue) before the user has any say so in
; in the matter.  This demo is designed to illustrate 1 way to do exactly that.
;
; In my "real world" application, I have a problem with Skype chat windows that
; are in the foreground -- a new message will not "bonk".  The chat window will
; "bonk" on a new message if the chat window is not in foreground.  What I want
; to do is make sure I do not have any Skype chat windows in foreground when I
; turn away from my computer, so I can hear the "bonk" on a new message. So the
; objective in my app is basicly the same as illustrated here in this cut down
; demo.  My real world app is a clock, hence the fancy Landing Pad window.
;
; This demo will:
;   1.  Monitor mouse and keyboard for inactivity exceeding a threshold value.
;   2.  Validate "a specific window" is foreground after exceeding the threshold.
;   3.  Switch foreground away ONLY after validating 1 and 2 above.
;   4.  Put everything back as it was the instant the user touches computer.
;
; Critical components in this demo to restore order to the windows queue are:
;   A.  Get the handle of the external window before you steal foreground.
;   B.  Insure you can restore external window before user can intervene.
;   C.  Mouse is placed on my window so I can monitor it and insure item B above.
; Do that and you should have no issues, although you may find other strategies.
;
; Note:  You will notice I hide the mouse when I activate foreground swap.
; The only reason for hiding the mouse is to avoid user disorientation.

; DEMO INSTRUCTIONS:
;   Launch this program, select a delay value from 5 to 99 seconds and press enter.
;   Open the "Windows Calculator" program so that it is in foreground.
;   Watch the Calculator window but do not touch the keyboard or mouse during your
;   specified delay time.
;   After delay and foreground is removed from Calculator, move mouse or press any
;   key to restore the calculator to foreground.

Structure LASTINPUTINFO ; requred by GetLastInputInfo API to determine last activity.
  cbSize.l
  dwTime.l
EndStructure
Global lippi.LASTINPUTINFO           ; Wow! Who understands this stuff??!
lippi\cbSize = SizeOf(LASTINPUTINFO) ; Compensating for "dumb" API I guess?? 

Global mPntr.POINT ; Used to restore mouse position -- << DO NOT REUSE
Global CursorPosition.POINT ; can reuse
Global wSpec.RECT ; can reuse
Global TrumpLimit.l, stolenWndw, dsktp.l ,quit.l, wasMinimized.l
; TrumpLimit.l is used to store user defined inactivity threshold

Procedure ReallySetForegroundWindow(Window)
  
  hWnd = WindowID(Window)
  
  ; If the window is in a minimized state, maximize now
  
  If GetWindowLong_(hWnd, #gwl_style) & #WS_MINIMIZE
    ShowWindow_(hWnd, #SW_MAXIMIZE)
    UpdateWindow_(hWnd)
  EndIf
  
  ; Check To see If we are the foreground thread
  
  foregroundThreadID = GetWindowThreadProcessId_(GetForegroundWindow_(), 0)
  ourThreadID = GetCurrentThreadId_()
  ; If not, attach our thread's 'input' to the foreground thread's
  
  If (foregroundThreadID <> ourThreadID)
    AttachThreadInput_(foregroundThreadID, ourThreadID, #True);
  EndIf
  
  ; Bring our window To the foreground
  SetForegroundWindow_(hWnd)
  
  ; If we attached our thread, detach it now
  If (foregroundThreadID <> ourThreadID)
    AttachThreadInput_(foregroundThreadID, ourThreadID, #False)
  EndIf 
  
  ; Force our window To redraw
  InvalidateRect_(hWnd, #Null, #True)
EndProcedure 

Procedure ActivityCheck() ;/  Providing for steps 1 2 3 as described above in "READ THIS" section
  If stolenWndw = #False ; Don't bother checking foreground window if it was already stolen.
    If TrumpLimit > 0 ; zero means disabled, else check against number of seconds as specified by user setting.
      GetLastInputInfo_(@lippi)
      timeNow.l = ElapsedMilliseconds() ; get current timestamp value for comparison calculations
      If ((timeNow.l - lippi\dwTime)/1000) > TrumpLimit ; << compare inactivity time against user setting. 
        hTopWin = GetForegroundWindow_() ; Will foreground be "the" Windows Calculator???
        classname$ = Space(1024) ; use window classname to verify foreground "is" the calculator window.
        GetClassName_(hTopWin, @classname$, 1024) ; API to capture classname into buffer.
        ;-You can swap the following if statements here if you use Skype and want to demo with that instead.
        ;If classname$="TskMultiChatForm.UnicodeClass" Or classname$="TConversationForm.UnicodeClass"
        If classname$="SciCalc" ; only Windows' "Calculator" window per say uses this "SciCalc" classname!!!
          If GetWindowState(1) = #PB_Window_Minimize  ;/ DO NOT STEAL FOCUS WHILE LANDING PAD IS MINIMIZED!!!!
            SetWindowState(1,#PB_Window_Normal)       ;/ CRITICAL PART OF HONORING WINDOW QUEUE MANAGEMENT!!!!
            wasMinimized = #True                      ;/ CRITICAL PART OF HONORING WINDOW QUEUE MANAGEMENT!!!!
          EndIf                                       ;/ DO NOT STEAL FOCUS WHILE LANDING PAD IS MINIMIZED!!!!
          GetCursorPos_(mPntr.POINT) ; Store mouse position so you can return it to original position.
          stolenWndw = hTopWin ; store the calculator handle so you can restore it to foreground later.
          ReallySetForegroundWindow(1) ; force ONLY the qualifying calculator out of the foreground.
          
          GetWindowRect_(WindowID(1),wSpec) ; Find your "landing pad" window to reposition the mouse.
          SetCursorPos_(wSpec\left+60,wSpec\top+62) ; Place mouse on "landing pad" window to monitor new mouse activity.
          
          ShowCursor_(0) ; hide the mouse so user does not become disoriented seeing the mouse jump around.
        EndIf ;  mouse position and visibility are restored later when user returns with new input activity.
      EndIf
    EndIf
  EndIf
EndProcedure

Procedure InitImage()
  img = CreateImage(1,126,133)
  If img = 0
    MessageRequester("ERROR","Cant create image!",#MB_ICONERROR):End
  EndIf
  LoadFont(1,"Verdana",8,#PB_Font_Italic | #PB_Font_Bold)
  If StartDrawing(ImageOutput(1))
      Box(1,1,119,124,$80BBDF)        ; Pretty gold framing :-0
      Box(2,4,117,117,$A0E4F0)        ; Pretty gold framing :-0
      Box(4,6,113,113,$80BBDF)        ; Pretty gold framing :-0
      Box(6,8,109,109,$A0E4F0)        ; Pretty gold framing :-0
      Box(8,10,105,105,$80BBDF)       ; Pretty gold framing :-0
      Box(10,12,101,101,$A0E4F0)      ; Pretty gold framing :-0
      Box(12,14,97,97,$80BBDF)        ; Pretty gold framing :-0
      Box(14,16,93,93,$A0E4F0)        ; Pretty gold framing :-0
      LineXY( 1,127,121,127, $666666) ; Shadow line across bottom
      ForeGround = GetSysColor_(#COLOR_BTNTEXT)
      FrontColor(RGB(Red(ForeGround),Green(ForeGround),Blue(ForeGround)))
      DrawingMode(1)
      DrawingFont(FontID(1))
      DrawText(20,26,"Landing Pad")
      DrawText(22,46,"here allows")
      DrawText(16,66,"monitoring of")
      DrawText(14,86,"hidden mouse.")
    StopDrawing()
  EndIf
  
  ProcedureReturn img
EndProcedure

Procedure configure()
  Static secflag.l, alarmh$, alarmm$, alarms$, trump$
  ; figure out where to show the configuratin window according to current mouse position.
  SM_CXscreen = GetSystemMetrics_(#SM_CXSCREEN)
  SM_CYscreen = GetSystemMetrics_(#SM_CYSCREEN)
  GetCursorPos_(@CursorPosition.POINT)
  wx.l = CursorPosition\x-40  ; used to determine configuration window placement.
  wy.l = CursorPosition\y-124 ; used to determine configuration window placement.
  ; Don't let creation of configuration window go off edge of screen.
  If wx < 2
    wx = 2
  EndIf
  If wx > SM_CXscreen - 115
    wx = SM_CXscreen - 115
  EndIf
  If wy < 30
    wy = 30
  EndIf
  If wy > SM_CYscreen - 130
    wy = SM_CYscreen - 130
  EndIf
  ;- Create the configuration window (now that we know where).
  trump$ = Right("00"+Str(TrumpLimit),2) ; trump$ is used to receive user defined inactivity threshold
  If OpenWindow(0,wx,wy,105,140," Inactivity Delay",#WS_DLGFRAME)
    If CreateGadgetList(WindowID(0))
      ButtonGadget   (100,   10, 90, 35, 24, "OK")
      StringGadget   (107,   10, 10, 20, 18,trump$, #PB_String_Numeric|#ES_MULTILINE|#ES_AUTOVSCROLL|$10000000)
      ButtonGadget   (108,   50, 90, 48, 24, "ABORT")
      TextGadget     (109,   8, 16, 95, 65,"         seconds of user inactivity before calculator loses focus to the mouse landing pad.")
      SendMessage_(GadgetID(107),#EM_SETLIMITTEXT,2,0)
    EndIf
  EndIf
  SetActiveGadget(100)
  q.l=#False
  Repeat
    oldgadget = GetActiveGadget()
    Select WaitWindowEvent()
      Case #PB_Event_Gadget
        Gadget  = EventGadget()
        Select Gadget
          Case 107  ; Highlight text upon first selection of this gadget
            If oldgadget <> Gadget ; Catch Caret relocation
              PostMessage_(GadgetID(Gadget),#EM_SETSEL,2,0) ; highlight
            EndIf
          Case 100  ; OK
            q = #True
          Case 108  ; Kill
            quit = #True
            Break
        EndSelect
      Case #PB_Event_Timer
        ActivityCheck()
      Case #WM_KEYDOWN
        Select EventwParam()
          Case #VK_RETURN
            s$=GetGadgetText(oldgadget) ;- Stop the stupid CR/LF "DING" on StringGadget input
            k.l=FindString(s$,Chr(13)+Chr(10),1)
            If k.l<>0  ; Was Enter pressed on the StringGadget?
              s$ = ReplaceString(s$,Chr(13)+Chr(10),"")
              SetGadgetText(oldgadget,s$) ; Yes, so remove CR+LF.
              WaitWindowEvent()
              Repeat : _mess = WindowEvent() : Until _mess = 0
            EndIf ;- Stupid CR/LF "ding" has been removed
            Break
        EndSelect
    EndSelect
  Until q.l
  TrumpLimit = Val(GetGadgetText(107))
  If (TrumpLimit > 0) And (TrumpLimit < 5) ; Allow values of zero to disable monitoring.
    TrumpLimit = 5 ; do not accept obsured inactivity setting less than 5 seconds
  EndIf
  trump$=Right("00"+Str(TrumpLimit),2)
  SetGadgetText(107,trump$)
  CloseWindow(0)
EndProcedure

Procedure WinCallback(hWnd, uMsg, wParam, lParam) 
  ; Windows fills the parameter automatically, which we will use in the callback...
  If hWnd = WindowID(1)
    Select uMsg 
      Case #WM_SYSCOMMAND     ; Only effective if window 1 style excludes BorderLess|DLGFRAME.
        If wParam=100         ; 100 is reference to value inside the AppendMenu_ line seen below.
          configure()
        EndIf
      Case #WM_SIZE     ; Only effective if window 1 style excludes BorderLess|DLGFRAME.
        Select wParam
          Case 100 ; from the GetSystemMenu user selection
            configure()
          Case #SIZE_MINIMIZED    ;/ Track Landing Pad window status in case user minimizes manually.
            wasMinimized = #True  ;/ CRITICAL PART OF HONORING WINDOW QUEUE MANAGEMENT!!!!
          Case #SIZE_RESTORED
            wasMinimized = #False ;/ CRITICAL PART OF HONORING WINDOW QUEUE MANAGEMENT!!!!
        EndSelect
    EndSelect 
  EndIf
  ProcedureReturn #PB_ProcessPureBasicEvents 
EndProcedure 

TrumpLimit = 5 ;/ Zero value will disable the "force calculator off foreground" operations
configure() ; allow user to define delay before entering main program  (delay range is 5 to 99 seconds)

;-  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\
;-               ENTERING  MAIN  PROGRAM  LOOP
;-  \/  \/  \/  \/  \/  \/  \/  \/  \/  \/  \/  \/  \/  \/  \/  \/  \/  \/  \/
If OpenWindow(1,3,3,123,129," ...  Demo        Landing Pad Window")
  CreateGadgetList(WindowID(1))
  ImageGadget(1,1,1,124,130,InitImage())
  
  ; Next 3 lines are only useful if window style excludes #PB_Window_BorderLess|#WS_DLGFRAME.
  menubar.l=GetSystemMenu_(WindowID(1),0)
  ModifyMenu_(menubar.l,#SC_CLOSE,#MF_BYCOMMAND,#SC_CLOSE,"CLOSE Inactivity Monitor") ; ,"Close 3amAlarm")
  AppendMenu_(menubar.l,#MF_STRING,100,"Setting...") ; used in main loop under WM_SYSCOMMAND
  SetWindowCallback(@WinCallback())    ; activate the callback to monitor system tray icon
  
  ; no idea how or why the following 4 lines allow window dragging (but I like it :-)
  Frame3DGadget(5, 0, 0, 400, 300, "", #PB_Frame3D_Flat)
  SetWindowLong_(GadgetID(1), #gwl_style, GetWindowLong_(GadgetID(1), #gwl_style) &~#SS_NOTIFY)
  #BS_FLAT=$8000
  SetWindowLong_(GadgetID(1),#gwl_style,GetWindowLong_(GadgetID(1),#gwl_style) |#BS_FLAT)
  
  
  SetTimer_(WindowID(1),0,1000,0) ; Timer message triggers at 1000 milliseconds (1 second interval)
  
  Repeat
    Select WaitWindowEvent()
      Case #PB_Event_CloseWindow
        quit = #True
      Case #WM_LBUTTONDBLCLK      ; Double Click to Minimize landing pad window
        ShowWindow_(WindowID(1),#SW_MINIMIZE)
      Case #WM_RBUTTONDOWN        ; Opens the options window for inacativity delay setting or exit.
        configure()
      Case #WM_LBUTTONDOWN        ; Hold and drag to move landing pad.
        SendMessage_(WindowID(1),#WM_NCLBUTTONDOWN, #HTCAPTION,0)
      Case #PB_Event_CloseWindow  ; Do not allow accidental close -- must use right click option!!!
        ; Quit = #True            ; << Alternative action is possible here, but threatens reliability.
        ShowWindow_(WindowID(1),#SW_MINIMIZE) ; (<< better to just minimize instead)
      Case #PB_Event_Timer        ; test trigger-setting against elapsed user inactivity.
        ActivityCheck()
      Default;/  Providing for steps 4 as described above in "READ THIS" section.
        ;/ Priority monitoring to catch new keyboard or mouse activity when user returns to computer.
        If stolenWndw ; don't bother with restoration if focus was not stolen from Windows Calculator
          GetLastInputInfo_(@lippi) ; Small time difference here means new user input at keyboard or mouse.
          timeNow.l = ElapsedMilliseconds() ; get current timestamp value for comparison calculations.
          If timeNow.l - lippi\dwTime < 4
            SetForegroundWindow_(stolenWndw)
            stolenWndw = #False ; reset our stolen window bookmarker to zero until next qualifying event.
            SetCursorPos_(mPntr\x,mPntr\y) ; return mouse to original position before we stole window focus.
            ShowCursor_(1) ; let mouse be visible in original position.
            If wasMinimized
              SetWindowState(1,#PB_Window_Minimize)
            EndIf
          EndIf
        EndIf ;/ NOTE!!! >>> Landing pad window for mouse simply insures I catch any new mouse activity ! ! !
    EndSelect ;/ Seemingly random forground window transitions will occur if you miss new mouse or key input.
  Until quit
  
  KillTimer_(WindowID(1),0)
  
EndIf
- - - - - - - - - - - - - - - -
Randy
I *never* claimed to be a programmer.