Using f90VB to access Visual Basic procedures from Fortran
One of the more frequently asked questions posted in Fortran discussion groups is how to call Visual Basic procedures from Fortran. The most common answer you will see is that you can’t. This is not completely true, but the amount of work and knowledge involved in doing this has always been so much that, in most cases, it defeats the reasons for trying to do it; to take advantage of the GUI design tools of Visual Basic that allow easy and fast development. With f90VB, however, accessing Visual Basic procedures from Fortran is a snap. The simple trick you need to know, however; is that you must expose your Visual Basic procedures as methods of an ActiveX object. In this section you will learn how to do this.
Example 6.4
Let’s say you have developed the ultimate Fortran program to compute logarithms. The program accepts an initial value, a final value and a step increment, and then prints a table showing the computed logarithms. This ultimate logarithm program that you have developed may well look like this:
program UltimateLogs
implicit none
real(8)::InitValue, FinalValue, IncStep
real(8)::vLog,Value
print *,'The Ultimate logarithms program'
print *,'Enter Initial Value, Final Value and Step Increment:'
read(*,*) InitValue, FinalValue, IncStep
if ((InitValue.le.0).or.(FinalValue.le.0).or.(IncStep.le.0)) then
print *,'I ain''t falling for this trick'
stop
endif
print *,''
write(*,'(2a15)') 'Value','Log(Value)'
Value=InitValue
do while (Value.le.FinalValue)
write(*,'(2f15.5)') Value,log(value)
Value=Value+IncStep
enddo
stop
end
All you need now to become the next software tycoon is to develop a nice graphic user interface (GUI) for your program. Because your specialty is advanced mathematics and not GUI programming, you decide to contract Canaima Software to create one for you. After an extensive negotiation that finally settles for several million, we provide you with one ActiveX object called UlitmateLogsIO. This object has three properties (InitValue, EndValue and StepIncrement) and two methods (GetInputData and ShowLogTable). The content of the three properties is obvious.
Method GetInputData shows a form on the screen that allows you to modify the properties InitValue, EndValue and StepIncrement (Figure 6.3).
In addition, method GetInputData also accepts three optional arguments. If these arguments are used, then the method uses them to return the values entered by the user [40].
Method ShowLogTable displays the results of the computations performed by your program (Figure 6.4). The method receives one argument, a Safe Array of double precision values, containing two columns and as many lines as necessary.
Now, we are going to show you how to do it. To create the UlitmateLogsIO object you start by opening a new Visual Basic project. Select the ActiveX DLL project type (Figure 6.5).
Visual Basic automatically adds a new class named Class1 to the project. Click on the name of the class, and using the Properties Inspector change the name to UltimateLogsIO (Figure 6.6).
Add the following code to the new class module:
'*******************************************
'******* CLASS UltimateLogsIO **************
'*******************************************
Option Explicit
'local variable(s) to hold property value(s)
Private mvarInitValue As Double 'local copy
Private mvarEndValue As Double 'local copy
Private mvarStepIncrement As Double 'local copy
Private mvarInputForm As frmUltimateLogsInput
Private mvarOutputForm As frmUltimateLogsOutput
Public Sub ShowLogTable(TableArray() As Double)
'Show mvarOutputForm with text control filled out
'with the values passed in TableArray()
Dim i As Long
Dim TmpStr As String
TmpStr = ""
For i = LBound(TableArray, 1) To UBound(TableArray, 1)
TmpStr = TmpStr & TableArray(i, 1) & " " & TableArray(i, 2) & vbCrLf
Next i
mvarOutputForm.Text1 = TmpStr
mvarOutputForm.Show vbModal
End Sub
Public Function GetInputData(Optional InitValue As Double = -1, _
Optional EndValue As Double = -1, _
Optional StepIncrement As Double = -1) _
As Boolean
'Get input data from the user
On Error GoTo ErrHndl
'Set the values in the form
mvarInputForm.Text1 = Me.InitValue
mvarInputForm.Text2 = Me.EndValue
mvarInputForm.Text3 = Me.StepIncrement
'Display the form allowing user to change the values
mvarInputForm.Show vbModal
'Set the object properties from user input
Me.InitValue = mvarInputForm.Text1
Me.EndValue = mvarInputForm.Text2
Me.StepIncrement = mvarInputForm.Text3
'Check that InitValue is lower than EndValue
If InitValue > EndValue Then
Err.Raise (20000 Or vbObjectError)
End If
'Return true to indicate success
GetInputData = True
'If user passed optional arguments, return property values
If (InitValue <> -1) Then InitValue = Me.InitValue
If (EndValue <> -1) Then EndValue = Me.EndValue
If (StepIncrement <> -1) Then StepIncrement = Me.StepIncrement
Exit Function
ErrHndl:
GetInputData = False
If Err.Number = (20000 Or vbObjectError) Then
Err.Raise (20001 Or vbObjectError), _
"UltimateLogsIOServer.UltimateLogsIO", _
"Invalid input value/s"
Else
Err.Raise (20002 Or vbObjectError), _
"UltimateLogsIOServer.UltimateLogsIO", _
"Unexpected error"
End If
End Function
Public Property Let StepIncrement(ByVal vData As Double)
If vData > 0 Then
mvarStepIncrement = vData
Else
Err.Raise (20000 Or vbObjectError), _
"UltimateLogsIOServer.UltimateLogsIO", _
"Invalid property value"
End If
End Property
Public Property Get StepIncrement() As Double
StepIncrement = mvarStepIncrement
End Property
Public Property Let EndValue(ByVal vData As Double)
If vData > 0 Then
mvarEndValue = vData
Else
Err.Raise (20000 Or vbObjectError), _
"UltimateLogsIOServer.UltimateLogsIO", _
"Invalid property value"
End If
End Property
Public Property Get EndValue() As Double
EndValue = mvarEndValue
End Property
Public Property Let InitValue(ByVal vData As Double)
If vData > 0 Then
mvarInitValue = vData
Else
Err.Raise (20000 Or vbObjectError), _
"UltimateLogsIOServer.UltimateLogsIO", _
"Invalid property value"
End If
End Property
Public Property Get InitValue() As Double
InitValue = mvarInitValue
End Property
Private Sub Class_Initialize()
'Set default values for properties
mvarInitValue = 1
mvarEndValue = 10
mvarStepIncrement = 1
Set mvarInputForm = New frmUltimateLogsInput
Set mvarOutputForm = New frmUltimateLogsOutput
End Sub
Private Sub Class_Terminate()
'Remove forms from memory
Set mvarInputForm = Nothing
Set mvarOutputForm = Nothing
End Sub
There is not much to explain in this code. Subroutine Class_Initialize is run when the class is instantiated, it sets the default values for the class properties and creates two forms frmUltimateLogsInput and frmUltimateLogsOutput. These are the forms shown to the user when you call methods GetInputData and ShowLogTable, repectively. Class_Terminate is the procedure called just before the object is unloaded from memory. In this case all it does is to ensure that the two forms used by the object are unloaded (we achieve this by setting the variables that hold the forms to Nothing). The functions to set and get the properties of the object are very standard, they check that you pass acceptable values to set the properties and raise an exception if you don’t..
Now you need to create the two forms used by the object. Add a new form to the project (click on Project/Add Form on the main menu). Rename the form to frmUltimateLogsInput, and add three text controls, one command button and four label controls as shown in Figure 6.7.
Add the following code to the form:
Private Sub Command1_Click()
Me.Visible = False
End Sub
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
'Only unload the form if requested programmatically
If UnloadMode = vbFormControlMenu Then
Me.Visible = False
Cancel = True
End If
End Sub
Command1_Click is the code executed when the user clicks the Command1 button, which will hide the form. Form_QueryUnload uses a little trick. This procedure is executed before the form is unloaded from memory. If argument Cancel is set to True, the unloading is canceled and the form stays in memory. This is done by checking if the user has clicked the Close Window icon (that little X icon on the top right corner of the form), in which case the form becomes invisible rather than unloading. So the only way to unload the form from memory is programmatically or when forced by the operating system.
Add another form and name it frmUltimateLogsOutput. Add two label controls, one text control and one command button control as shown in Figure 6.8.
Open the code window for form frmUltimateLogsOutput and add exactly the same code you used in form frmUltimateLogsInput.
Ok, you are almost there. Open the Project Properties Window (click on Project/Properties in the main menu bar) and set the name of the project to UltimateLogsIOServer (Figure 6.9).
Save everything and compile and link your project (click on File/Make UltimateLogsIOServer dll ). You have now produced an in-process server (UltimateLogsIOServer.dll). You now need to register the server, so programs that use automation can find it. To do this, open a command window in the same directory where UltimateLogsIOServer.dll is located and issue the following command:
Regsvr32 UltimateLogsIOServer.dll
You should see a message indicating that the server was registered successfully. Ok, you are done with the Visual Basic part. Let’s see the new code in your Fortran program now:
Example 6.4A
program UltimateLogsA
use f90VBDefs
use f90VBBStrings
use f90VBSafeArrays
use f90VBVariants
use f90VBAutomation
implicit none
type(VARIANT)::UltLogsIO,TmpVar,TmpLogTable
real(8),pointer::MapArray(:,:)
integer(HRESULT_KIND)::iRet, i
type(EXCEPINFO)::EInfo
character(len=255)::TmpStr
integer(SAFEARRAY_KIND)::LogTable
real(8)::InitValue, FinalValue, IncStep
real(8)::Value
iRet = OleInitialize()
!create an instance of UltimateLogsIO object
UltLogsIO = CreateOleObject('UltimateLogsIOServer.UltimateLogsIO',iRet)
if (iRet.ne.S_OK) goto 1000
!Show input screen and get range for the table
TmpVar = ExecMethod(UltLogsIO,'GetInputData',iRet=iRet,EInfo=EInfo)
if (iRet.ne.S_OK) goto 1000
!Get input values
InitValue = VariantToDouble(PropertyGet(UltLogsIO,'InitValue'))
FinalValue = VariantToDouble(PropertyGet(UltLogsIO,'EndValue'))
IncStep = VariantToDouble(PropertyGet(UltLogsIO,'StepIncrement'))
!Compute size of array and allocate an appropriate Safe Array
call SafeArrayCreate(LogTable,VT_R8, &
1,int((FinalValue - InitValue) / IncStep + 1), &
1,2)
!Map Fortran array to Safe Array for fast access
call SafeArrayAccessData(LogTable,MapArray)
!compute the table of logs
Value = InitValue
do i=1,ubound(MapArray,1)
MapArray(i,1)=Value
MapArray(i,2) =dlog(Value)
Value = Value+IncStep
enddo
!we don't need the mapped array anymore, so unmap it
call SafeArrayUnAccessData(LogTable)
!hand-build a variant containing LogTable + VT_BYREF
TmpLogTable%varVal%vt = VT_BYREF+VT_R8+VT_ARRAY
TmpLogTable%varVal%byref = loc(LogTable)
!show the logarithm table
TmpVar = ExecMethod(UltLogsIO,'ShowLogTable',TmpLogTable,iRet=iRet, &
EInfo=EInfo)
if (iRet.ne.S_OK) goto 1000
goto 2000
1000 continue
!Print error messages
print *, 'The following Errors were detected:'
print *, 'HRESULT:',iRet
call StrCopy(EInfo%bstrDescription,TmpStr)
print *,TmpStr
2000 continue
!release all variants, safe arrays, etc.
call Release(UltLogsIO)
call ExceptionClear(EInfo)
call VariantClear(TmpVar)
call SafeArrayDestroy(LogTable)
call VariantClear(TmpLogTable)
call OleUnInitialize()
stop
end
Ok, this definitely looks much more complicated that the original version of The Ultimate Logarithms Program. So let’s check the details of how it works.
By now, you should be used to the first portion of code. It initializes OLE and creates an instance of the UltimateLogsIO object. If an error occurs, the program goes to label 1000 which prints the error code.
iRet = OleInitialize()
!create an instance of UltimateLogsIO object
UltLogsIO = CreateOleObject('UltimateLogsIOServer.UltimateLogsIO',iRet)
if (iRet.ne.S_OK) goto 1000
Next, we call method GetInputData, which opens the input form on the screen and allows the user to enter the range and increment for the table of logarithms the program will generate:
!Show input screen and get range for the table
TmpVar = ExecMethod(UltLogsIO,'GetInputData',iRet=iRet,EInfo=EInfo)
if (iRet.ne.S_OK) goto 1000
Note that we did not have to pass any of the optional arguments to the method. Again, if an error is reported, the program flows to label 1000, where the error is printed. Method GetInputData automatically updates the properties in the object using the values entered by the user, so we can get those values by querying the object for these properties:
!Get input values
InitValue = VariantToDouble(PropertyGet(UltLogsIO,'InitValue'))
FinalValue = VariantToDouble(PropertyGet(UltLogsIO,'EndValue'))
IncStep = VariantToDouble(PropertyGet(UltLogsIO,'StepIncrement'))
Function VariantToDouble converts the return value of function PropertyGet into a double precision real.
Now the program has all it needs to compute the resulting table. Method ShowLogTable, that shows the results, expects a Safe Array, so we use one to store the results to avoid delays and wasting of resources. The technique used (i.e. mapping a Fortran array into the Safe Array memory) is described and exemplified in Chapter 2 (Safe Array Fundamentals), so we won’t explain it here again. This is the loop that performs the actual logarithm calculations:
!compute the table of logs
Value = InitValue
do i=1,ubound(MapArray,1)
MapArray(i,1)=Value
MapArray(i,2) =dlog(Value)
Value = Value+IncStep
enddo
Now, here is another little trick that improves speed and memory usage. As you already know, all arguments to an object’s method must be passed as Variant values. We really don’t want to create a Variant containing just another copy of the Safe Array we have already computed, so we handcraft a Variant-by-reference. As explained in chapter 3, a Variant with the flag VT_BYREF contains a pointer to the value, rather than the value itself. So we create a Variant containing a pointer to the Safe Array handle. Note how we assign loc(LogTable) rather than the handle itself. f90VB takes care of doing any de-referencing of Variants that needs to be done:
!hand-build a variant containing LogTable + VT_BYREF
TmpLogTable%varVal%vt = VT_BYREF+VT_R8+VT_ARRAY
TmpLogTable%varVal%byref = loc(LogTable)
Finally, the program executes the method, which presents the results on the screen:
!show the logarithm table
TmpVar = ExecMethod(UltLogsIO,'ShowLogTable',TmpLogTable,iRet=iRet, &
EInfo=EInfo)
if (iRet.ne.S_OK) goto 1000
The rest of the program just performs the necessary clean up, releasing references to objects, destroying safe arrays, etc.
Handling arguments by reference