Ugh, finally managed to find a solution to the Web Browser Control HTML Editing focus problems that I’ve been having. It turns out the solution was a lot easier although tricky than I had original thought.
The issue is as follows: I’m using the Web Browser Control and use HTML Editing by setting designMode in the document to .t.. Once in edit mode context switches to another application – then back to Help Builder – caused the Editor control to loose its keyboard input focus. The control activated but there’d be no cursor and you couldn’t type into the control. This using Visual FoxPro by the way but this edit focus problem also manifests in other ActiveX container environment so the solution here might work with other environments as well.
The first problem in FoxPro at least is that FoxPro doesn’t fire any events for application context switches. It does fire LostFocus() and GotFocus() events for window switches of the same application (ie. multiple top level or MDI windows), but not if you switch off to another application or return back from another application. This likely is also the source of the ActiveX activation problem.
Luckily in VFP 9, FoxPro introduced event binding to Windows Messages, so you can now easily hook Windows Messages to any FoxPro form. The code below hooks the WM_ACTIVATEAPP event which is fired both when focus leaves the current application and gets focus back from another application:
SET PROCEDURE TO WinEvents ADDITIVE
#INCLUDE WINEvents.h
loEventHandler = CREATEOBJECT("wwHelpWinEvents")
BINDEVENT(goHelp.hWnd, WM_ACTIVATEAPP,loEventHandler, "WM_ACTIVATE_Handler")
BINDEVENT works by hooking the Hwnd of a form or control (ActiveX controls often have Hwnds) and specifying which message to handle and to which object and method to fire it to. I’ve used the above handler quite extensively in several of my applications – this has been a pretty big missing feature in VFP that we can now work around with Windows Event binding.
To handle this event I implemented a class and a method within it.
#INCLUDE wwHelp.h
#include WinEvents.H
*************************************************************
DEFINE CLASS wwHelpWinEvents AS Custom
*************************************************************
PROTECTED nOldProc, lHtmlEditOnLostFocus
nOldProc = 0
lHtmlEditOnLostFoucs = .f.
************************************************************************
* WinEvents :: Init
****************************************
FUNCTION Init(lnHwnd)
IF EMPTY(lnHwnd)
lnHwnd = _SCREEN.hWnd
ENDIF
*** Need to keep track of the original WinProc
declare integer GetWindowLong in Win32API ;
integer hWnd, integer nIndex
This.nOldProc = GetWindowLong(lnHwnd, GWL_WNDPROC)
ENDFUNC
************************************************************************
* WinEvents :: WM_ACTIVATE_Handler
****************************************
*** Function: Handler for App activation from external Apps
*** Assume:
*** Pass:
*** Return:
************************************************************************
FUNCTION WM_ACTIVATE_Handler(hWnd, Msg, wParam, lParam)
IF VARTYPE(goHelp) = "O"
loException = null
TRY
IF wParam = 0 && DeActivation set flag
*** Set flag if HTML Editor is active
IF TYPE("goHelp.oViewer.pages[goHelp.oViewer.ActivePage].ActiveControl") = "O" AND ;
LOWER(goHelp.oViewer.PAGES[goHelp.oViewer.ActivePage].ACTIVECONTROL.CLASS)="wwbrowser_editor"
this.lHtmlEditOnLostFocus = .t.
ENDIF
ELSE
*** Only handle if HTML Editor was active on lost focus
IF this.lhtmleditonlostfocus
this.lHtmlEditOnLostFocus = .f.
*goHelp.oViewer.pgBody.tmrHtmlEditActivator.Interval = 50
*** Fire off a timer and call SetEditFocus in 50ms
goHelp.oViewer.pgBody.oHtmlEdit.SetEditFocus()
*** IMPORTANT:
*** Focus another control so the form activates properly
*** and the deactivation fires on the Web Browser
goHelp.oViewer.pgBody.txtTopic.SetFocus()
ENDIF
ENDIF
CATCH TO loException
MESSAGEBOX("Non fatal error handling Window reactivation" + CHR(13) + CHR(13) +;
loException.Message,48,WWHELP_APPNAME)
ENDTRY
ENDIF
*** Redeclare here to make 100% sure it's here!
DECLARE INTEGER CallWindowProc in Win32API ;
integer lpPrevWndFunc, integer hWnd, integer Msg, integer wParam, ;
integer lParam
*** Must call back original WinProc
RETURN CallWindowProc(THIS.nOldProc, hWnd, Msg, wParam, lParam)
ENDFUNC
ENDDEFINE
This code sets up an event handler method that is called in response to the WM_ACTIVATEAPP request. The Init finds the original WinProc so that we can call it back when we handle our windows message event. If we don’t call back the event gets eaten and that would be bad since the app wouldn’t activate/deactivate.
The actual handler is implemented in the WM_ACTIVATE_Handler method. The WM_ACTIVATEAPP method is fired to our application whenever focus is lost (wParam = 0) or when focus returns to us from another window (wParam = the other window’s hWnd ie. non-zero). I need to capture events because I need to make sure that a flag gets set only when the HTML Edit control is active. Otherwise we don’t need any of this code and simply want to forward directly to the original WinProc.
In Help Builder the Editor is buried in a PageFrame Page, so the check for making sure that the control is active is a bit lengthy. Once the flag is set the next activation of Help Builder will cause this method to fire again, so this is the hook needed to set the focus.
After all the pain I went through originally to try and activate the Html Editor (see previous entry), the final solution was pretty simple. The problem with all the previous approaches was the simple fact that this method fires before the form is actually activated by VFP’s internal event handling. IOW, VFP doesn’t know the form is alive and any call to SetFocus() at this point has really no effect. Even using various Windows events doesn’t work because VFP reinitializes the form when it finally gets control and do it’s normal window processing.
So the solution here is to not send the SetFocus() call directly, but rather delay it with a timer. This gives VFP a chance to get ready. In addition the code fires a SetFocus() call on another control to force VFP deactivate the Web Browser control first. Then the timer kicks in after 50 milliseconds and changes the focus back to the Web Browser’s Edit interface. And that actually works!
The Web Browser Editor class implements the SetEditFocus method that creates a custom timer object, so that that this functionality is somewhat encapsulated.
FUNCTION SetEditFocus()
IF TYPE("THIS.PARENT.oHtmlEditTimer") # "O"
THIS.Parent.AddObject("oHtmlEditTimer","HTMLEditTimer")
ENDIF
THIS.parent.oHtmlEditTimer.Interval = 50
The timer is a custom timer, and the timer method doesn’t doesn’t do anything fancy:
*** This event acts as a delayed SetFocus to the
*** HtmlEditControl
THIS.Interval=0
THIS.Parent.oHtmlEdit.SetFocus()
So, the solution is convoluted and requires a bit of logic scattered over a few objects unfortunately and some app specific logic in these components. It will be interesting to see whether I can remember how this works in a few months down the road .
But heck that’s what this BLOG entry is for.
Thanks to Doug Hennig and Calvin Hsia for a few helpful hints...
Other Posts you might also like