One of my most long standing problems that I have had with RE:VBA is showing a form modally. This in itself is not difficult but as soon as you move away from Raiser’s Edge, bring up another window in front of the modal form and then go back to Raiser’s Edge, the modal form has disappeared. Because the form is modal you are not able to access the underlying window (let’s call this the parent window). This is a pain as the only way to bring it back is to place another window over the parent window and then minimize it. This is not hard to do but it is harder to explain to users and not very intuitive.
Blackbaud do not have an answer for this. The knowledgebase answer BB21890 (How to keep the VBA Modal Form always on top) says that this has been filed as a suggestion and may not be implemented but will be reviewed…
One way around this is to create a form in an ActiveX DLL. This is then called from the VBA code. I have not actually tried this but I am told it does not behave in the same way. There are drawbacks to this though. If the form is going to be used throughout an organization then it means that the DLL will have to be installed on each client too.
I was looking for an integrated solution which would just work without too much of a fix. I posted a question on the Blackbaud forum, not too hopeful that I would get an answer.
I did get an answer though (thanks Mike). It was suggested that I use the Windows API to create a timer, record all the windows that were open as the modal form was created, check the order of them and see which one is on top (this is known as the Z-order). If a Raiser’s Edge window had the lowest Z-Order then the modal form should be on top. In which case use the API to bring it to the top.
This was a really good idea but did present some problems. You would have to find out which of the windows were Raiser’s Edge windows and which were not. This is not always so easy. Some windows would just have the constituent’s name on them and no reference to RE. It would also require some management of the windows too so that if a window was closed the code could handle that.
I adapted this idea to find the window handle of the parent window, the main Raiser’s Edge window and of the modal form. Using the timer if the window that was currently in focus was either Raiser’s Edge window or the parent window the modal form should be brought to the front.
The code for this is spread out over 4 different modules. I am not going to go into detail as to how the API calls work as this is beyond the scope of this blog but I am including the code if anyone wants to see it.
The API timer (of which there are many examples out there) is can be found at http://www.vbforums.com/showthread.php?p=1888414.
The code below shows the procedure where the macro is called from.
Public Sub ModForm(oDataObj As IBBDataObject) Dim oCon As cRecord If TypeOf oDataObj Is cRecord Then Set oCon = oDataObj frmModForm.setParentHwnd modModalForm.getHWndInFocus frmModForm.Show vbModal End If End Sub
Before we show the form we have to register the Windows API handle of the parent form. This can only be done before we open the window. Let’s now look at the code in the modModalForm module.
'declare API: Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _ (ByVal lpClassName As String, ByVal lpWindowName As String) As Long Public Function getHWndInFocus() As Long 'get the topmost window: getHWndInFocus = GetForegroundWindow() End Function
This procedure calls the API function to retrieve the handle of the Window that is currently in focus i.e. our parent window.
The next part of the code shows the window. This in turn calls the activation events of the form as shown below:
Private mlParentHwnd As Long Private mlHwnd As Long Private mlREHwnd As Long Private Sub UserForm_Activate() mlHwnd = modModalForm.getHWndInFocus mlREHwnd = REApplication.SessionContext.MainForm.HWnd Set mAPITimer = New APITimer mAPITimer.StartTimer 500 End Sub
The activate code gets the handle of the current window in focus i.e. the modal form itself. It also gets the handle of the main RE window. This is a little known property of the session context. You are able to retrieve the main RE window and access all its properties (why not try changing the title bar caption!). The HWnd is the Windows handle of the a form.
Now we start the timer so that it fires an event every 500 milliseconds. It is defined “WithEvents” so that its refresh event will be called. We will look at this next.
Private Sub mAPITimer_Refresh() Dim lFocusHwnd As Long lFocusHwnd = modModalForm.getHWndInFocus() If lFocusHwnd = mlParentHwnd Or lFocusHwnd = mlREHwnd Then modModalForm.setFocus mlParentHwnd modModalForm.setFocus mlHwnd End If End Sub
Every half second this procedure is called. First we get the handle to the current window that is in focus. This may or may not be a Raiser’s Edge window. We look to see that either it is the main RE window or the window where we called the macro from. If it is either of these we first put the parent window in focus and then put the modal form in focus. The set focus procedure is shown below:
'declare api function: Private Declare Function BringWindowToTop Lib "user32" _ (ByVal HWnd As Long) As Long Public Sub setFocus(HWnd As Long) BringWindowToTop HWnd End Sub
Again this is a simple API call.
When we close the modal form we want to stop the timer so that we do not try to bring the modal form to the front again. This is done in the form’s deactivate event
Private Sub UserForm_Deactivate() mAPITimer.StopTimer End Sub
I will include the whole source code in the shared file area of the site.
One variation on this theme is that the modal window should open up in front of the constituent window once it has been loaded. At first I thought I would just call this in the before open event of the constituent but of course the modal window will appear on its own before the constituent window is opened. I have solved this now using two timers and a further windows API call to find the window with the specific caption of the constituent once it has opened.