Interface Oriented Programming

An interface is a group of properties, events and methods that belong to a class. Every class has at least one interface, which includes all of the methods and properties declared in that class. The default interface is usually referred to by the name of the class, though its formal name is the class name preceded by an underscore character.

Using interfaces in MicroStation VBA

The MicroStationDGN object model includes several different interfaces that are designed to make certain programming tasks easier to implement. In order to understand why, a brief discussion of how to use an interface in VBA is necessary.

Implements Statement

Since an interface is really a collection of prototypes for the methods and properties of a class, it is necessary for a class that “implements” the interface to duplicate all of the Public methods declared as part of the interface. A missing member of an interface will cause an error in compilation. The Implements statement, used to declare that a class is making use of an interface, is really a contract that states the class is going to provide code for each of the methods of the interface.

The Implements statement to inform MicroStation that a class is going to provide code for the IPrimitiveCommandEvents interface looks like this:

Implements IPrimitiveCommandEvents

The class then must provide code for each of the six methods in the PrimitiveCommand interface. These are:

IPrimitiveCommandEvents_DataPoint
IPrimitiveCommandEvents_Dynamics
IPrimitiveCommandEvents_Keyin
IPrimitiveCommandEvents_Reset
IPrimitiveCommandEvents_Start
IPrimitiveCommandEvents_Cleanup

These methods allow your VBA program to respond to each of the events necessary to create and place a design element in the active model. MicroStation calls each of these methods by name when the actions of the user dictate. For an example of implementing this interface, see the Element Creation Command example.

To conclude, the use of an interface tells MicroStation that the class has methods to call in certain circumstances, and that the class behaves in a particular way. The IPrimitiveCommandEvents interface says the class behaves like a MicroStation tool such as Place Arc or Place Circle (both “primitive” command tools because they place primitive elements). When the class implementing this interface is activated, the IPrimitiveCommandEvents_Start method is called first, which usually places a prompt message in the status bar area. Other methods are called at similar times. See the IPrimitiveCommandEvents description for more details.

Utilizing interfaces

There are a several interfaces that you can utilize in your VBA programs to receive notification when certain things happen in MicroStation. For example, using the IAttachmentEvents interface, your program can receive notification immediately before and after a reference is attached to the active DGN file.

Listed below are some of the interfaces that your program can use to receive notification of such events in MicroStation, and a general description of the kinds of events your program can receive with each.

  • IAttachmentEvents — Receives notification before and after a reference is attached to the active DGN file, when a reference attachment is modified, and before and after a reference is detached.
  • IChangeTrackEvents — Receives notification before and after MicroStation executes an Undo or Redo command at the user's request. Also receives notification when an element in the active model changes. The type of the change is specified by MicroStation.
  • IModalDialogEvents — Receives notification when a modal dialog is opened in MicroStation, and when a modal dialog is closed.

Registering to receive notification

Creating an object that implements an interface is not sufficient to receive the messages that your program requires. For each of the interfaces listed above, your program must “register” the object that implements the interface. This tells MicroStation that your object is interested in receiving the notification messages. This ClassModule example shows a class that implements the IChangeTrackEvents interface, and a program that “registers” an object of this class with MicroStation.

Implements IChangeTrackEvents

Private Sub IChangeTrackEvents_BeginUndoRedo _
(ByVal AfterUndoRedo As Element, _
ByVal Action As MsdChangeTrackAction, _
ByVal IsUndo As Boolean)
   If IsUndo = True Then
      MsgBox "Undo was selected"
   Else
      MsgBox "Redo was selected"
   End If
End Sub

Private Sub IChangeTrackEvents_ElementChanged _
(ByVal AfterChange As Element, _
ByVal BeforeChange As Element, _
ByVal Action As MsdChangeTrackAction, _
CantBeUndone As Boolean)

End Sub

Private Sub IChangeTrackEvents_FinishUndoRedo _
(ByVal IsUndo As Boolean)

End Sub

Private Sub IChangeTrackEvents_Mark()

End Sub

Sub UndoNotify()
   Dim undoNotifier As New myCTEventsHandler
   AddChangeTrackEventsHandler undoNotifier
End Sub

To run this example, create a ClassModule named myCTEventsHandler in your MVBA project and copy the code above (except the UndoNotify subroutine) to this class module. Then in the module for your MVBA project, copy the UndoNotify subroutine lines above. Run the UndoNotify subroutine. In your design file, each time you select Undo or Redo, a message box will appear telling you which command was selected.

The IChangeTrackEvents_ElementChanged event can be used to receive notification of a wide variety of events in your design file including changes to elements and models. See IChangeTrackEvents for more information. Note: It is important to test the action that caused the event, otherwise your program may try to handle a situation that did not occur.

Interacting with MDL

This section describes ways a VBA program can interact with an MDL application. The section Calling DLL functions from VBA describes how a VBA program can using MDL built-ins and other functions in DLL's.

The mechanism through which a VBA program interacts with an MDL application is the same as it is for MicroStation Basic macros. By publishing variables to make them accessible to a Visual Basic program, an MDL application can create a way to pass values between the two different types of applications. This is the primary means of passing data back and forth between MDL and VBA programs.

Commands can be sent from a Visual Basic program to an MDL program through the SendMessageToApplication method of the CadInputQueue object.

An MDL application can execute VBA code a variety of ways, including using the mdlInput functions to queue VBA RUN commands; using mdlVBA_runMacro or mdlVBA_runProcedure to run a macro or procedure; or using mdlVBA_executeLine to execute a line of VBA code. For example,

 // Executes method Test in project Default. It is declared as // Sub Test(i As Long, j As Long, str As String) mdlVBA_executeLine ("Default", "Test 1, 2, \"Hello World\""); // Executes CadInputQueue.SendCommand in the VBA context. That synchronously runs // the macro MyMacro mdlVBA_executeLine (NULL, "CadInputQueue.SendCommand \"vba run MyMacro\"");

Accessing published variables

An MDL program must first create and publish a variable before it can be accessed by a VBA application. Once this is done, a VBA program can use the GetCExpressionValue method of the Application object to retrieve the value of the variable. The Visual Basic programmer must know at design time the name of the published variable, though its type is not necessary. A variant is used to receive the value of the named variable.

To set the value of a published variable, a similar method of the Application object is used, SetCExpressionValue .

For an example of how these functions can be used, see the Arcs By Length Example.

MDL Structure Equivalents

To utilize data in MDL structures, a binary equivalent UDT (User Defined Type) may be used. The term “binary compatible” means that the structure layouts are identical, byte for byte. Using information in the MDL header (.h) files these data types can be created for the structures you need to access. Each field in the MDL structure will translate to one field in the VBA UDT, utilizing the following conversion information:

MDL Type VBA Type Notes
BoolInt Long A value of 0 represents False, and a value of 1 represents True. (Usually, any non-zero value is treated as True.)
byte, unsigned char Byte  
char (not char*) String * 1 In VBA, “String * n” defines a string whose length is always n.
double Double  
Int16, short Integer  
Int32, long Long  
int64 DLong DLong is a MicroStationDGN-specific type. For more information, see DLong.
MSWChar Integer  
UInt32, ULong, unsigned long Long VBA does not have an unsigned long type. Therefore, values greater than 2,147,483,647 (hex 7FFFFFFF) will appear as negative values.
UInt16, UShort Integer VBA does not have an unsigned integer type. Therefore, values greater than 32,767 (hex 7FFF) will appear as negative values.

Bit Fields

The Visual Basic language does not directly support bit fields. If you're translating an MDL structure that includes bit fields, you'll need to combine the bit fields into an appropriately sized integer field, then use Visual Basic's “And” and “Or” operators to parse out the bits.

For example, this MDL Structure:

typedef struct applyViewGroupOptions
{
   UInt16   doNotApply:1;
   UInt16   applyActiveParamsSameModel:1;
   UInt16   optionPadding:14;
   UInt16   optionPadding2:16;
} ApplyViewGroupOptions;

Would have the following equivalent VBA UDT:

Type ApplyViewGroupOptions
   BitFields As Long
End Type

In this example, the field name “BitFields” is arbitrary. The VBA Long type comprises 32 bits, the total number of bits in the MDL structure.

Nested Structures

Nested structures, one structure included or defined within the body of another, are not uncommon in MDL. To defined the equivalent UDT in Visual Basic you must first define the sub-structures separately. For example, the MDL structure TextSizeParam comprises a field whose type is that of another structure, MSTextSize:

typedef struct mstextsize
{
   double   width;
   double   height;
} MSTextSize;
  
typedef struct textSizeParam
{
   int         mode;
   MSTextSize  size;
   double      aspectRatio;
} TextSizeParam;

The equivalent VBA UDT for these structures is coded like this:

Type MSTextSize
   width As Double;
   height As Double;
End Type
  
Type TextSizeParam
   mode As Long
   size As MSTextSize
   aspectRatio As Double
End Type

Note: For some of the commonly used MDL structure types, the VBA UDTs are already defined.

MDL Structure Type Equivalent VBA UDT (already defined)
DPoint2d, Dpoint2d Point2d
DPoint3d, Dpoint3d Point3d
DVector3d Range3d
RotMatrix Matrix3d
Transform Transform3d

These pre-defined types can be used to define UDTs. For example, this MDL structure:

typedef struct dimCircleGeom
{
   double        radius;
   Dpoint3d      origin;
   RotMatrix     rMatrix;
} DimCircleGeom;

requires both Dpoint3d, and RotMatrix types. Since these are defined in VBA already, we can write this structure as follows:

Type DimCircleGeom
   radius As Double
   origin As Point3d
   rMatrix As Matrix3d
End Type

Arrays

Array equivalents are defined using the Visual Basic syntax for arrays. For the MDL definition field[n] the equivalent VBA definition is field (0 to n-1). If the array size n is defined using a constant in MDL, you'll need to define the equivalent constant in Visual Basic. For example, this MDL structure:

typedef struct dwgHatchDefLine
{
   double     angle;
   DPoint2d   through;
   DPoint2d   offset;
   short      nDashes;
   double     dashes[MAX_DWG_HATCH_LINE_DASHES];
} DwgHatchDefLine;

Would have the following equivalent definition in Visual Basic:

Const MAX_DWG_HATCH_LINE_DASHES = 20 'From mdl.h
  
Type DwgHatchDefLine
   angle As Double
   through As Point2d
   offset As Point2d
   nDashes As Integer
   dashes(0 to MAX_DWG_HATCH_LINE_DASHES - 1) As Double
End Type

Pointers

A pointer to anything including a char* and a void*) should be defined as a Long in Visual Basic. For example, this structure in MDL:

typedef struct dimLStringGeom
{
   byte      closed;
   long      numVerts;
   Dpoint3d  *points;
} DimLStringGeom;

Would have the following equivalent definition in Visual Basic:

Type DimLStringGeom
   closed As Byte
   numVerts As Long
   points As Long   'pointer to Point3d
End Type

Use VBA's VarPtr function to assign values to Long fields that represent pointers. You should also use VarPtr to pass the addresses of UDTs to MDL functions.

Note: The VarPtr function is an undocumented feature of Visual Basic. For more information, visit the Microsoft Developer Network library website (http://msdn.microsoft.com/library), and search for the Knowledge Base article Q199824 in “All Knowledge Base.”

Calling DLL functions from VBA

Visual Basic and VBA programs can use functions that are defined in native code DLLs. This capability lets VBA programs use the Windows API. It also lets VBA programs running in MicroStation use MDL built-in functions. Most VBA programs and most VBA programmers will not use this capability. New VBA programmers should skip this section concentrating on pure VBA instead.

Note: MicroStation’s built-in functions have to execute as part of MicroStation; therefore, a standalone VB program or a VBA program running under the control of a host outside of MicroStation, such as a Microsoft Office application, will get unpredictable results if it attempts to use the MDL built-ins.

Many functions in DLLs can execute in any process. That is not true of MicroStation’s built-in functions. Programs that need to call MicroStation’s built-in functions from another process must use GetCExpressionValue to do so.

When a Visual Basic program calls a MicroStation built-in function, it is actually using a wrapper function instead of directly using the MicroStation built-in function. The wrapper functions are in the DLLs:

  • stdbspline.dll
  • stdcons.dll
  • stdimage.dll
  • stdkisolid.dll
  • stdmdlaccessor.dll
  • stdmdlbltin.dll
  • stdraster.dll
  • stdrdbms.dll
  • stdrender.dll

VB programs cannot use the MDL built-ins directly because:

  • VB programs can only call DLL functions that are declared as __stdcall . MicroStation built-in functions are declared as __cdecl
  • The VB dispatcher does not set the current MDL descriptor. If VB called MDL functions directly, they would be calling functions with a random MDL descriptor

The typical wrapper function is declared the same as the function that it wraps, except it is declared as __stdcall. The wrapper function sets the MDL descriptor to one reserved for all of VBA, calls the wrapped function, and then restores the MDL descriptor.

Note, the MDL descriptor reserved for VBA has the current transform set to master units.

Declaration of a function

A function must be declared within the body of a VBA program before it can be called. The declaration states that the function is in a DLL and specifies the function’s parameters and return type.

Declare statement

There are two forms of the declare statement:

[Public | Private] Declare Sub name Lib "libname" [Alias "aliasname"] [([arglist])]

[Public | Private] Declare Function name Lib "libname" [Alias "aliasname"] [([arglist])] [As type]

The Visual Basic Reference describes the declare statement well, but does not give all of the information needed to decide how to declare the arguments and use return values.

The syntax for an argument is:

[Optional] [ByVal | ByRef] [ParamArray] varname[()] [As type]

This is the same as an argument declaration for a VB procedure. The syntax for declaring the function return type is also the same as the syntax for a standard VBA return type. It is important to understand how the ByVal, ByRef, and the data types correspond to C concepts. The following sections address these topics.

Simple Arguments

First, consider how the base types match. The table below shows C and Visual Basic base type equivalents:

C base type Visual Basic base type
char Byte
short Integer
int Long
long Long
float Single
double Double

VB’s user-defined types are compatible with C structures, although it may be necessary in some cases to add padding to a UDT to make it line up with the MDL struct. In VB, Long and Double fields always start on a 4-byte boundary. With MicroStation’s C structures, long fields always start on a 4-byte boundary but double fields always start on an 8-byte boundary.

Here is an example of a UDT that requires some padding to make it line up with the MicroStation structure:

' The field pad is needed to force width to line up with the C structure

Type TextSize
     mode As Long
     pad As Long
     width As Double
     height As Double
     aspectRatio As Double
End Type

MicroStation’s object model provides some of the common simple structures from MicroStation. A C DPoint3d is a VB Point3d; a C RotMatrix is a VB Matrix3d; a C Transform is a Transform3d.

Declaring an argument ByRef means that it is a reference to the memory that holds the argument. C provides a similar capability via pointers. A VB declaration ByRef dlbArg as Double is equivalent to the C declaration double *dblArg. ByVal means only the value of the location is passed to the function. Since the called function does not get the address of the argument, it cannot modify the original value.

MDL built-ins do not use structures as arguments. If a MDL built-in requires a structure, it declares an argument that is a pointer to a structure. For a C declaration of a pointer to a structure, the corresponding VBA declaration is a ByRef variable of the corresponding UDT type. For example, given the C declaration: DPoint3d *arg use the VB declaration: ByRef arg as Point3d.

Pointer Arguments

Many MicroStation built-ins take arguments that are pointers. Here are some of the uses of pointer arguments. Some of these are discussed in depth later.

  • Dynamically Allocated Types: These are types such as DgnModelRefP and DialogBox*
  • Function pointers. MicroStation does not work well with function pointers from VBA.
  • Arrays
  • Structures. When a MicroStation built-in requires an argument that is a structure, it uses a pointer type. For example, DPoint3d *
  • Optional arguments: If an argument is optional, it normally is declared as a pointer. The caller can pass in NULL or pass a pointer to the actual argument value
  • Variable argument types: For some MicroStation built-in functions, the data type of one argument depends on the data type of another argument. To allow this, the argument that has different types is declared as void *. For example, if the second argument to mdlParams_setActive is ACTIVEPARAM_COLOR_BY_NAME then the void * represents a char *. If the second argument is ACTIVEPARAM_TEXTWIDTH then void * argument represents a double *.
  • ASCII Strings: These are declared as char *.
  • Unicode Strings: These are declared as MSWChar *.

Pointers for Dynamically Allocated Types

For many types, MicroStation allocates and frees the memory. Some examples are MdlDesc *, DialogBox *, and DgnModelRefP *. Since VB does not have a pointer type, VB programs have to use Long to represent a pointer type.

For some of these types, MicroStation’s object model provides hidden methods for converting between the pointer and the corresponding object. The hidden methods in Application are MdlGetDesignFileFromModelRefP, MdlGetModelReferenceFromModelRefP , and MdlCreateElementFromElementDescrP. DesignFile has the hidden method MdlFileObjP and MdlModelRefP. Element and all of the specific element types have the methods MdlElementDescrP and MdlSetElementDescrP . ElementScanCriteria has the method MdlScanCriteriaP.

This example shows how to use a DgnModelRefP. The C declaration declares that the input argument is a DgnModelRefP, but the VB program has to treat this a long. It uses the hidden method MdlModelRefP to get the pointer, and passes that into mdlModelRef_isTransient. Here is the C declaration:

BoolInt mdlModelRef_isTransient
(
DgnModelRefP modelRef
);

The VB code equivalent is:

Declare Function mdlModelRef_isTransient _
     Lib "stdmdlbltin.dll" _
     (ByVal modelRef As Long) As Long

Function IsTransient(oModelRef As ModelReference) _
     As Boolean
    Dim lDgnModelRefP As Long
    Dim result As Long
    lDgnModelRefP = oModelRef.MdlModelRefP
    result = mdlModelRef_isTransient(lDgnModelRefP)
    IsTransient = CBool(result)
End Function

Arrays

Visual Basic arrays and C arrays are very different. A Visual Basic array is a data structure that has a description of the array bounds and a pointer to the data. A C array just has the data. Since the C function wants a pointer to the actual data, a VB program should never call an MDL built-in passing the address of the array. In most cases, it should pass in the address of the first element of the array. The normal technique for passing in the address is to declare the argument as ByRef. This causes the VB compiler to generate the code to pass the address of the first argument. This example using mdlRefFile_setClip illustrates this. The C declaration of mdlRefFile_setClip is:

int mdlRefFile_setClip
(
DgnModelRefP modelRef,
DPoint2d *pts,
int      nverts
);

The VB declaration for mdlRefFile_setClip is:

Declare Function mdlRefFile_setClip _
    Lib "stdmdlbltin.dll" _
    (ByVal modelRef As Long, _
    ByRef pts As Point2d, _
    ByVal nverts As Long) As Long

Given a VB array declared as clipbound(0 to 6)As Point2d , a call to this function would look like:

     mdlRefFile_setClip modelRefP, clipbound(0), 7

This passes the address of clipbound(0) to mdlRefFile_setClip.

Optional Arguments

Many MicroStation built-in functions support variable arguments by using a pointer for the argument. A C program specifies NULL if it is not supplying a value for the argument, or the address of a variable containing the value otherwise. In Visual Basic, the standard way of passing a pointer to an argument is to declare the argument as ByRef. In general, this is the best approach for passing pointer arguments but it does not work well with optional arguments because there is no way to pass NULL for a typed ByRef argument if the argument’s type is a UDT such as Point3d or Matrix3d.

It is possible to pass a NULL value if the argument is declared as a ByRef numeric type or declared as a ByRef Any. The following example shows 2 possible declarations for mdlAccudraw_optimizeForContext that take advantage of this.

This example is based on the C declaration for mdlAccudraw_optimizeForContext:

Public int mdlAccudraw_optimizeForContext
(
     long flags,        /* => flags from accudraw.h */
     DPoint3d *originP, /* => NULL okay (for all 5) */
     DPoint3d *deltaP,  /* => */
     double *distanceP, /* => */
     double *angleP,    /* => */
     void *orientationP /* => RotMatrix or DPoint3d (unit vector) */
)

All of the arguments except flags are pointers, so the initial inclination is to declare them as ByRef. This results in the following declaration:

Declare Function mdlAccudraw_optimizeForContext _
     Lib "stdmdlbltin.dll" (ByVal flags As Long, _
     ByRef originP As Point3d, _
     ByRef deltaP As Point3d, _
     ByRef distanceP As Double, _
     ByRef angleP As Double, _
     ByRef orientationP As Any) As Long

That will force the compiler to check that the base type is correct (is it a Double, Point3d, etc.) and will also force the compiler to generate the code to pass a pointer to the value. This is safe and easy to code, but it requires the programmer to provide a reasonable value for each of the arguments. That may be a nuisance to the programmer. A solution is to change the declaration to:

Declare Function mdlAccudraw_optimizeForContext1 _
     Lib "stdmdlbltin.dll" (ByVal flags As Long, _
     ByRef originP As Any, _
     ByRef deltaP As Any, _
     ByRef distanceP As Double, _
     ByRef angleP As Double, _
     ByRef orientationP As Any) As Long

Using this technique, a VBA program can pass NULL by specifying ByVal 0. For example:

mdlAccudraw_optimizeForContext flags, ByVal 0, _
     ByVal 0, ByVal 0, ByVal 0, 0

The disadvantage of using this declaration is that the compiler catchers fewer coding errors. With this declaration, the compiler no longer checks the data type of the arguments passed to originP and deltaP.

Variable Argument Types

The last section ignored the function’s final argument. Depending on the value passed in the flags argument, the final argument may be a pointer to an RMatrix (Matrix3d) or to a DPoint3d (Point3d). The VB program can handle this by declaring the argument as ByRef orientation as Any and passing the proper kind of argument, or the VB program can handle this by supplying multiple declarations with one declaring the last argument as ByRef orientation as Matrix3d and the second declaring it as ByRef orientation as Point3d. Once again, the approach using multiple declarations is safer.

Strings

Visual Basic stores Strings as Unicode strings. Many MicroStation functions require ASCII strings. These arguments are declared as char *. Other MicroStation functions require Unicode strings. These arguments are normally declared as MSWChar *.

Visual Basic has some automatic processing for handling a ByVal String being passed to a native code function. It converts the Unicode String to an ASCII string before calling the function, and converts the string back when the called function returns. Therefore, a VB program can pass a ByVal String to a function expecting an ASCII string.

There is no automatic conversion of a String to an MSWChar *. Actually, no conversion is required but Visual Basic’s automatic conversion of ByVal String’s gets in the way. No conversion is necessary, but VB converts the string. The VB program can circumvent this by declaring the argument as ByVal Long, and using StrPtr to get a pointer to the data in the string. For example, given the C declaration:

void mdlOutput_statusUW (MSWChar *pMessage);

use the VB declaration:

Declare Sub mdlOutput_statusUW Lib "stdmdlbltin.dll" _
(ByVal pMessage As Long)

Then call this function with:

mdlOutput_statusUW StrPtr (“Display in status area”)

Of course, the simplest approach to write to the status area is to use the object model directly: ShowStatus “Display in status area”.

So far, this section has discussed the handling of char * and MSWChar * for input. There are functions that use these types for output as well. For example, in C, mdlFile_find is declared as:

int mdlFile_find
(
    char      *outname,
    const char *inname,
    const char *envvar,
    const char *iext
);

The corresponding Visual Basic declaration for this is:

Declare Function mdlFile_find _
     Lib "stdmdlbltin.dll" _
     (ByVal outname As String, _
     ByVal inname As String, _
     ByVal envvar As String, _
     ByVal iext As String) As Long

The function mdlFile_find uses the base name, environment variable, and extension to find the file. It stores the result in the string that outname points to. It returns 0 if it successfully finds the file.

The String to hold the result must be large enough to hold any string that mdlFile_find returns. The function mdlFile_find will place a vbNullChar at the end of the C string that it places into the buffer, so the VB program can use the location of the vbNullChar to decide how to truncate the buffer. The typical sequence of steps to handle data returned in an ASCII string is:

  1.      Initialize a String with a large buffer. Make sure the buffer is big enough. You will never notice if the String is way too large, but it will be disastrous if it is too small.
  2.      Call the function.
  3.      Truncate the String so that everything up to the vbNullChar is preserved.

Here is an example using this approach:

Sub FindFile()
     Dim strFullName As String
     'Allocate the buffer
     strFullName = Space(512)

     'Call the function
     If mdlFile_find(strFullName, "accudraw", "MS_MDL", ".ma") = 0 Then
          'Truncate at C’s end-of-string
          strFullName = Left$(strFullName, _
            InStr(1, strFullName, _
            vbNullChar) - 1)
          Debug.Print "The full path is " & strFullName
     End If
End Sub

Handling an output buffer declared as MSWChar* buffer is similar, except the VB program has to declare the argument as ByVal Long and it must use StrPtr to get a pointer to a data area. The following example using mdlModelRef_getDisplayName illustrates this.

The C declaration is:

StatusInt mdlModelRef_getDisplayName
(
     DgnModelRefP modelRef,
     MSWChar *displayName,
     unsigned int maxChars,
     MSWChar *separator
);

The VB declaration is:

Declare Function mdlModelRef_getDisplayName _
     Lib "stdmdlbltin.dll" _
     (ByVal modelRef As Long, _
     ByVal displayName As Long, _
     ByVal maxChars As Long, _
     ByVal separator As Long) As Long

This code passes a buffer to displayName. Then it truncates the string mdlModelRef_getDisplayName stores in that buffer.

Function GetDisplayName(modelRef As ModelReference) As String
     Dim length As Long
     Dim dgnModelRefP As Long

     ' A subsequent section will discuss the method MdlModelRefP
     dgnModelRefP = modelRef.MdlModelRefP

     ' Allocate the buffer
     length = 2000
     GetDisplayName = Space(length)

     mdlModelRef_getDisplayName dgnModelRefP, _
          StrPtr(GetDisplayName), length, 0

     ' Truncate at C's end-of-string
     GetDisplayName = Left$(GetDisplayName, InStr(1, _
          GetDisplayName, vbNullChar) - 1)
End Function

A Visual Basic program can pass NULL for a String simply by passing vbNullString, any variable that has been set to vbNullString, or an uninitialized String variable. For example, to use mdlFile_find without specifying an extension, call mdlFile_find strFullName, "accudraw", "MS_MDL", vbNullString. Note, vbNullString is not the same as the zero-length string “”. Passing “” passes a pointer to a zero-length buffer. Passing vbNullString passes a NULL pointer.

Int64 Arguments

Visual Basic does not support 64 bit integers, but a Visual Basic program can use DLong to simulate 64 bit integers. For example, the C declaration of mdlSystem_getFreeDiskSpace is:

int mdlSystem_getFreeDiskSpace
(
     int64 *pTotalBytes,
     int64 *pFreeBytes,
     char *fileSys /* => fileSys letter */
);

The Visual Basic declaration is:

Declare Function mdlSystem_getFreeDiskSpace _
     Lib "stdmdlbltin.dll" _
     (ByRef pTotalBytes As DLong, _
     ByRef pFreeBytes As DLong, _
     ByVal fileSys As String) As Long

Here is an example using it:

Sub PrintFreeDiskSpace()
     Dim freeSpace As DLong
     Dim totalSpace As DLong

     mdlSystem_getFreeDiskSpace totalSpace, freeSpace, "D:"
     Debug.Print "D: Total space " & DLongToString(totalSpace)
     Debug.Print ", Free Space: " & DLongToString(freeSpace)
End Sub

Pointer Return Values

All pointer return values must be handled as Longs.

If the return value is one of the dynamically allocated types, then the Visual Basic program can either leave it as a Long if the program will use it as an argument to another function, or the program can use one of the hidden Mdl… methods to get the corresponding VBA object.

If the pointer is a char *, then the VBA program can convert it. A simple approach is to use the function SysAllocString. Visual Basic itself uses SysAllocString to allocate strings. Here is an example that uses the char * value that mdlSystem_getCurrTaskID returns.

Private Declare Function mdlSystem_getCurrTaskID _
          Lib "stdmdlbltin.dll" () As Long ' Returns a char*
Private Declare Function SysAllocString Lib "oleaut32" _
          (ByVal CharPtr As Long) As String
Sub PrintName()
     Dim lTaskId As Long
     Dim strTaskId As String
     lTaskId = mdlSystem_getCurrTaskID()
     strTaskId = SysAllocString(lTaskId)
     Debug.Print "The current MDL task is: " & strTaskId
End Sub

Copying Memory

Visual Basic programs that use native code API’s sometimes need to copy memory. VBA programs can use the function RtlMoveMemory to copy memory. As with all uses of pointers, the programmer has to decide whether to declare the variables as Long forcing the programmer to generate the pointer, or as ByRef letting the compiler generate the address. Typically, the program will be copying to or from an address provided by MDL. In those cases, the VB program has to work with a pointer represented as long. Here are 2 good declarations for that case.

Declare Sub CopyMemoryToVBA Lib "kernel32" _
     Alias "RtlMoveMemory" _
     (ByRef VBALocation As Any, _
     ByVal SourceLoc As Long, _
     ByVal length As Long)

Declare Sub CopyMemoryFromVBA Lib "kernel32" _
     Alias "RtlMoveMemory" _
     (ByVal Destination As Long, _
     ByRef VBALocation As Any, _
     ByVal length As Long)

If both addresses are Longs, then both the source and destination have to be declared as ByVal long. If so, then the program can use the declaration:

Declare Sub MoveMemory Lib "kernel32" _
     Alias "RtlMoveMemory" _
     (ByVal strDest As Any, _
     ByVal lpSource As Any,
     ByVal length As Long)

The same program may have all 3 of these declarations.

Examples

The project MdlExample has examples illustrating many of these concepts.

The module modACS has a procedure that returns information regarding the current Auxiliary Coordinate System. It shows how to use a function that returns strings into MSWChar * buffers.

The module modSystem has a procedure ListMdlApps that dumps a list of loaded MDL applications. This example shows how handle a char * return value. The function mdlSystem_getTaskID returns a pointer to an MDL application’s task ID. ListMdlApps converts that to a VB String.

The module modFileOpenDialog shows how a VBA program can use the standard MicroStation file open dialogs. A VB program can also use the Windows Common Dialog file open dialog boxes. See either of the previously mentioned references for more information on using the Windows Common Dialogs.

The module modMline shows how a VBA program can pass an array to a function in a DLL. It also shows how a get a pointer to the MSElement associated with an Element object.

The MdlExamples project has a reference to the NativeCodeUtilities project. The module modRepresentations of NativeCodeUtilities shows how to:

  • Get an MSElementDescr * from an Element object
  • Get an ElementRef from an Element object
  • Get an MSElement * from an Element object
  • Get an Element object from an MSElementDescr *, giving control of the MSElementDescr * to the Element object
  • Get an Element object from an MSElementDescr* without giving control of the MSElementDescr * to the Element object
  • Get an Element object from an ElementRef and a DgnModelRefP

The module modNativeTypes has the methods needed for converting strings, and for copying memory.

The module modViewConversions has methods for converting between a View object and a MicroStation window handle.

Units and coordinate systems

In MicroStation V8, unit definitions and the global origin position are stored as properties of a ModelReference . To determine distances in meaningful units other than master units, your VBA program must get the unit definitions and relationships from the ActiveModelReference object.

Distances

Nearly all of the functions in VBA in MicroStation that calculate and return a distance value give the value in master units. The same is true of most functions that return a position as a Point2d or Point3d, the coordinates are in master units of the active model. As an example, the following lines calculate the distance between two points, pnt1 and pnt2.

Dim pnt1 As Point3d
Dim pnt2 As Point3d
Dim dist As Double

dist = Point3dDistance (pnt1, pnt2)

If the value in dist were to be displayed to the user in sub units rather than master units, we would need to convert it to sub units using the SubUnitsPerMasterUnit property of the ActiveModelReference in a calculation like the following:

dist = dist * ActiveModelReference.SubUnitsPerMasterUnit

The value in dist is now in sub units. The same type of conversion can be made to get the value in terms of positional units, storage units or units of resolution if necessary.

Global origin

A ModelReference object has a GlobalOrigin property that gives the location of the global origin for the model as a Point3d.

Element rotation and Matrix3d

The rotation of an element in space is usually represented by a 3x3 matrix of values. In Visual Basic this matrix is in the form of the Matrix3d data type. The functions necessary to manage and apply these data types are part of the Application object.

The values in a Matrix3d should not be accessed directly, but should be handled through the Matrix3dGetComponentByRowAndColumn and Matrix3dSetComponentByRowAndColumn global functions.

The orientation of a View is also given by a Matrix3d. You can get and set the orientation of a View using the Rotation property.

Applying rotation using a Matrix3d

Below is a short example of using a Matrix3d to rotate a text element by 15 degrees about the Z axis at the origin of the element.


Public Sub rotateText ()
   Dim rMatrix As Matrix3d
   Dim tTransform As Transform3d
   Dim dAngle As Double
   Dim zAxis as Point3d
   Dim oEl As TextElement
   Dim oElEnum as ElementEnumerator
 

   dAngle = 15 * Pi /180   'Change the angle to radians
   zAxis.x = 0   'Set the zAxis to (0,0,1)
   zAxis.y = 0
   zAxis.z = 1
 
   'Now we can get the Matrix3d for rotation
   rMatrix = Matrix3dFromVectorAndRotationAngle (zAxis, dAngle)
 
   'Find the text elements in the selection set, skip others
   Set oElEnum = ActiveModelReference.GetSelectedElements
   oElEnum.Reset   'reset oElEnum to the first element
   While oElEnum.MoveNext
      If (oElEnum.Current.Type = msdElementTypeText) Then
         Set oEl = oElEnum.Current
         tTransform = Transform3dFromMatrix3dAndFixedPoint3d (rMatrix, oEl.Origin)
         oEl.Transform tTransform
         ActiveModelReference.ReplaceElement oElEnum.Current, oEl
         oEl.Redraw
      End If
   Wend
   RedrawAllViews

End Sub

Explanation of the example

The example above uses hardcoded values for the rotation angle and axis of rotation for the sake of simplicity. The angle is converted to radians since all rotation calculations in MicroStation occur in radians. The axis of rotation is set to the Z axis (0,0,1). The Matrix3d is then calculated from the rotation angle and the axis of rotation (a vector).

The set of elements in the selection set is then collected in oElEnum. While there are elements in this ElementEnumerator they are tested one at a time, and only the text elements are rotated. The Matrix3d cannot be applied to an element by itself, so a Transform3d is formed from the origin of the text element and the Matrix3d created earlier. The element is then transformed using this Transfom3d, then the original element is replaced with the modified element, and the element is redrawn.

Note: Before you run this example, place a text element into the current design then select it with the Element Selection tool. You can have multiple elements in the selection set, though only text elements will be affected.

The Transform3d data type, usually used for scaling and translation of elements, is discussed in further detail below.

Transform3ds

A Transform3d is a data type similar to a Matrix3d, but it represents a 3x4 matrix used to store and apply values for scaling, rotation and translation operations. Like the Matrix3d, the functions to manage and apply a Transform3d are global functions that belong to the Application object. Also like the Matrix3d, you should not try to access the values within the matrix directly, but use functions instead. For the Transform3d type, the functions are Transform3dGetMatrixComponentByRowAndColumn and Transform3dSetMatrixComponentByRowAndColumn. Additionally, the data within the Transform3d can be retrieved as Point3d data types using the Transform3dGetPointComponent function, and set using the Transform3dSetPointComponent function.

Using Transform3d to apply rotation and scaling

The lines below can be added to the code example above to create an additional Matrix3d used to scale the text element by a factor of 1.5. This transformation is applied at the same time as the rotation.

   Dim sMatrix As Matrix3d
   sMatrix = Matrix3dFromScale (1.5)
   tTransform = Transform3dFromMatrix3dTimesTransform3d (sMatrix, tTransform)

To add the scaling operation to the example above copy these lines and paste them just before the line that reads:

         oEl.Transform tTransform

After creating the sMatrix and setting it to a scale factor of 1.5 this Matrix3d is added to the tTransform Transform3d so that it applies both the rotation and scaling at the same time.

Events in MicroStation

The VBA environment in MicroStation V8 does not have the capability of creating new event types. There are some specific events in MicroStation that your VBA program can listen for and respond to. The Application and UndoBuffer objects both fire events of interest to VBA programs.

Receiving events

The first challenge in handling events is telling MicroStation that your class wants to receive the event messages. This is done by creating a reference to the type of object that is generating the events using the WithEvents keyword. To listen for events from the Application object, the reference is created like this:

Dim WithEvents oApplication As Application

Remember, this isn't creating an Application object, it's just creating a reference to an Application object. When the class is actually ready to receive the event messages, this reference is set to the object itself. In the case of the Application object, the code would be:

Set oApplication = Application

From this point on, the class receives the events coming from the Application object. It is up to you to decide which events to handle.

Note: The WithEvents keyword is only valid in a class module. The sub procedures you create to respond to the events are part of the class, and are called “event handlers”.

Application events

The Application object in MicroStation only has two events that your VBA program can receive. These are the OnDesignFileOpened and OnDesignFileClosed events. These are discussed in detail in the section titled Working with MicroStation events , so they are not discussed further here.

Handling Design Elements

Sooner or later your Visual Basic programs in MicroStation will have to create or manipulate elements within a DGN file. Understanding how to create an element and add it to the design file is important to writing good VBA programs for MicroStation.

Creating elements

Creating an element in a VBA program is actually rather easy. A line, for example, can be created with the LineElement class as shown by the following code:

Dim oEl as LineElement
Set oEl = CreateLineElement2 (Nothing, startPnt, endPnt)

Assuming you have valid Point3d objects for startPnt and endPnt, the line oEl is created when this code is run. If you run it though, nothing happens on the screen. No line shows up. The reason is that drawing a line is an additional step. To make the line element appear on the screen, we must tell it to redraw itself. The Redraw method is a member of the Element class, so the code to do this is:

oEl.Redraw

Now the line appears on the screen, but when you refresh the screen, the line is gone. Even though we created the line object, and made it redraw on the screen, we did not add it to the contents of the DGN file. This task requires telling the ActiveModelReference to add the LineElement to its list of elements. The code for this is:

ActiveModelReference.AddElement oEl

Now, when the program is run the line is drawn on the screen and added to the DGN file. The process of adding the element to the ActiveModelReference can easily be overlooked.

Getting existing elements

Getting existing elements is another task that has hidden complexities to it. MicroStation has more than one way for the user to select elements on which to perform an operation, so your VBA program that manipulates existing elements may have to recognize more than one way as well. The two primary ways of getting elements that have previously been selected by the user are via the selection set, and from a fence. A third way to find elements is by scanning the DGN file for the desired elements. A fourth way is to prompt the user to enter a data point, but that requires writing methods for the ILocateCommandEvents interface. That topic is covered elsewhere.

Getting the selection set

The selection set is made of the currently selected elements. These could be chosen with a single data point using the Element Selection tool, or by Power Selector or another means, it really doesn't matter to your VBA program. The selection set is already made up when your program runs. So you must get the selection set and find out whether it has any elements of interest to your program.

The selection set is obtained from the ActiveModelReference by calling the GetSelectedElements method. This creates an ElementEnumerator which allows you to process the elements it holds one at a time. This process sounds complicated, but the code is simple. It looks like this:

Public Sub getSelectionSet ()
   Dim oElEnum As ElementEnumerator
   Dim oEl As Element
   Set oElEnum = ActiveModelReference.GetSelectedElements
   oElEnum.Reset
   While oElEnum.MoveNext
      Set oEl = oElEnum.Current
      'Change the color of the element
      oEl.Color = 3
      oEl.Redraw msdDrawingModeNormal
      oEl.Rewrite
   Wend
End Sub

This example changes the color of each element to red then redraws it on the screen. To make the change permanent, the element must be replaced in the ActiveModelReference after the change is made. The line that does this is:

      oEl.Rewrite

The MoveNext method of the ElementEnumerator returns True if there are more elements in the selection set, and False if there aren't any more. This makes it very convenient to process the elements just once.

Note: When you get the ElementEnumerator from the GetSelectedElements method, always remember to call Reset to make sure that you are processing the ElementEnumerator from the beginning.

Processing elements in a fence

Processing the set of elements in a fence is very similar to using a selection set. The initial setup requires a few more lines since there is not always an active fence and your program must test for it before processing. By contrast, the ActiveModelReference will always return an ElementEnumerator when you call the GetSelectedElements method to handle a selection set, it just will not have any elements if nothing is selected.

The code to see if a fence is active uses the Fence object and looks like this:

Dim oFence As Fence
Set oFence = ActiveDesignFile.Fence
If oFence.IsDefined Then
   'we can process the elements here
End If

To get and process the elements in the fence, we use an ElementEnumerator just like we did for the selection set in the example above. So the code for the same example, only this time getting the elements from the fence, looks like this:

Public Sub getFenceElements()
   Dim oFence As Fence
   Dim oElEnum As ElementEnumerator
   Dim oEl As Element
   Set oFence = ActiveDesignFile.Fence
   If oFence.IsDefined Then
      Set oElEnum = oFence.GetContents
      oElEnum.Reset
      While oElEnum.MoveNext
         Set oEl = oElEnum.Current
         'Change the color of the element
         oEl.Color = 3
         oEl.Redraw msdDrawingModeNormal
         oEl.Rewrite
      Wend
   End If
End Sub

A really flexible program is able to take its input from either a fence or a selection set. This is probably most easily handled by testing for the fence, and if it is not active, getting the selection set. Either way, we get an ElementEnumerator containing the elements. The code to handle both would be written this way:

Public Sub getElementsFromFenceOrSelectionSet()
   Dim oFence As Fence
   Dim oElEnum As ElementEnumerator
   Dim oEl As Element
   Set oFence = ActiveDesignFile.Fence
   If oFence.IsDefined = True Then
      Set oElEnum = oFence.GetContents
   Else
      Set oElEnum = ActiveModelReference.GetSelectedElements
   End If
   oElEnum.Reset
   While oElEnum.MoveNext
      Set oEl = oElEnum.Current
      'Change the color of the element
      oEl.Color = 3
      oEl.Redraw msdDrawingModeNormal
      oEl.Rewrite
   Wend

End Sub

Another example, Selection Set Processing is also provided.

Scanning the design file for elements

The ModelReference class has a Scan function that can be used with or without an ElementScanCriteria object. The difference is that when you use it without the ElementScanCriteria, you get the set of all elements within the model and you have to sort out which ones you want to process. The ElementScanCriteria lets you filter out the elements you do not want to process so the Scan function returns only those elements that meet your criteria. Here again, we use the ElementEnumerator to receive the objects from the Scan function.

This example finds only the elements with color 3. Once found, these elements are changed to color 2, redrawn and rewritten to the design file.

Public Sub changeColor()
   Dim oElScan As ElementScanCriteria
   Dim oElEnum As ElementEnumerator
   Dim oEl As Element
   Set oElScan = New ElementScanCriteria
   oElScan.ExcludeAllColors
   oElScan.IncludeColor 3
   Set oElEnum = ActiveModelReference.Scan (oElScan)
   oElEnum.Reset
   While oElEnum.MoveNext
      Set oEl = oElEnum.Current
      'Change the color of the element
      oEl.Color = 2
      oEl.Redraw msdDrawingModeNormal
      oEl.Rewrite
   Wend

End Sub

The code in this example looks pretty much the same as the previous examples from the point at which the ElementEnumerator is filled with elements to the end of the sub procedure.

Note: The ElementScanCriteria initially will accept all elements, so before calling the IncludeColor (3) method, we must set it to exclude all of the other colors, otherwise it will accept them too. This is the true for all of the different criteria types that the ElementScanCriteria can be set to filter.

Other examples of using the Scan method to retrieve elements are shown in the Change Element Level Examples.

Organization of a MicroStation Command

There are two types of MicroStation-style commands that you can implement in VBA using provided interfaces. These tools behave like other MicroStation tools written in MDL, but there are certain guidelines that should be followed when writing one of these tools. The Primitive Command tool, and the Locate Command tool are discussed in detail here.

Primitive Command tools

A tool that implements the IPrimitiveCommandEvents interface is called a “primitive” command tool because its purpose is to place primitive drawing elements into the active model. The sub procedures your program must implement when using this interface are organized so that MicroStation can call each at a specific time during the use of the tool.

To write a Primitive Command tool, you must create a class module in your VBA project, since an interface can only be implemented by an object created from a class. To implement the interface your class must begin with the following line:

Implements IPrimitiveCommandEvents

In the class module you'll include code for each of the six events in the IPrimitiveCommandEvents interface. Each of the methods is discussed below in general terms of what should be accomplished by that method. They are presented in the usual order they are called, though the actual order depends on the user's actions.

Start method

The Start method is called by MicroStation when the command is first started. This happens when the program is initiated. Usually, the Start sub procedure initializes any objects or variables necessary for the command, and writes the name of the command to the command area of the status bar in MicroStation's main window. The ShowCommand method is used for this purpose. A user prompt is usually displayed in the prompt area of the status bar as well. The ShowPrompt method is used for this purpose.

DataPoint method

The DataPoint method is called by MicroStation when the user enters a data point while your command is active. This method receives the location of the data point and the view in which the data point was entered.

Depending on the nature of your command this method might be called more than one time before the element is drawn and added to the active model, so you may need to save data point locations. The Element Creation Command Example demonstrates a DataPoint method that saves data points in an array of Point3d data so that a line element can be created.

The DataPoint method may call the StartDynamics method of the CommandState object so that you can dynamically draw the element your command is creating. When you start the dynamics MicroStation calls your Dynamics method, which is discussed next.

Note: MicroStation automatically turns off dynamics when a command is started, so it is not necessary to call StopDynamics unless you have a specific reason to do so.

When your DataPoint sub procedure receives the last point necessary to create the element, you should draw the element being created using the msdDrawingModeNormal drawing mode, and you should add the element to the active model. Typically these tasks are done close together at the end of the DataPoint method, but this is not a requirement. Use the Redraw method of the Element class, followed by the AddElement method of the ActiveModelReference to add the element to the active model.

Dynamics method

The Dynamics method is pretty much the same as the DataPoint point method, with two exceptions. First, MicroStation is not providing the location of a data point entered by the user, but the location of the mouse pointer instead. Second, an additional parameter is passed to the Dynamics sub procedure: the drawing mode. Using the pointer location to draw the element (it is supplied in a Point3d data type) means the element is “attached” to the pointer. The Dynamics method is called every time the pointer location changes, so doing lots of calculations in this method may affect the performance of the command.

As you draw the element on the screen during the Dynamics method, use the drawing mode that MicroStation specifies in the DrawMode parameter. The code to do this is:

   oEl.Redraw DrawMode

See the Element Creation Command Example for an example of using this.

Reset method

The Reset method is called by MicroStation when the user presses the reset button on the mouse while the command is active. This generally signals the user has completed the command, or wants to interrupt the command, so resetting variables is performed here. Typically, the Reset method also calls the StartPrimitive method of the CommandState object to restart the command again. The code for this uses the Me keyword to refer to the current object:

   CommandState.StartPrimitive Me

Cleanup method

The Cleanup method gives your program a chance to reset any object variables it may have created and do any other necessary tasks before the command exits.

Keyin method

The Keyin method is only called by MicroStation if the user enters a keyin, and the command was initiated with a call to StartPrimitive with WantKeyins set to True. In this case, MicroStation calls the Keyin sub procedure to allow your command to respond to the key-in command. This may happen at any time while your command is active.

Running a Primitive Command tool

Since the IPrimitiveCommandEvents interface is implemented by a class, there must be some additional code written to run it. VBA cannot execute the code within a class by itself, so a standard module (also called a code module) is created with a sub procedure that will create an instance object of the class, and tell MicroStation to run it. This code looks like this:

Sub Procedure runPrimitiveTool ()
   CommandState.StartPrimitive New PlaceLineCommand
End Sub

Locate Command tools

A tool that implements the ILocateCommandEvents interface is generally one that is used to modify elements that already exist in the DGN file. Like the Primitive Command tools discussed above, Locate Commands must implement a group of sub procedures that MicroStation expects to call at various times throughout the execution of the command.

To write a VBA program that is a Locate Command, you must create a class that implements the ILocateCommandEvents interface. To implement the interface, your class must begin with the following line:

   Implements ILocateCommandEvents

In the class module, you must include code for each of the seven events in the interface. These are each explained below in general terms of what is accomplished in each. The methods are listed in the order they are usually called by MicroStation, but in actual use the order will be determined by the actions of the user.

Start method

The Start method is the same for the Locate command as for the Primitive command type. Here the program should initalize variables if they are needed, and display prompt messages to the user, usually by means of the ShowCommand and ShowPrompt methods.

LocateFilter method

The LocateFilter method is called by MicroStation when the user selects an element while the command is active. The parameters passed to this method include the element chosen by the user, the data point location that was used to select the element and a Boolean variable called Accepted, which determines whether the element is acceptable to your command. If you set Accepted to False, the element is rejected and MicroStation will search for another element using the same datapoint. If you do not set Accepted to False within the LocateFilter method, the element is accepted.

If you reject an element, MicroStation will call LocateFilter again if it finds another element at the same location, and your program must accept or reject that element. This process continues until no more elements are found at the location of the data point. If this happens, MicroStation will call the LocateFailed sub procedure.

Depending on the nature of the command, you may choose to call the StartDynamics method of the CommandState object.

Note: MicroStation automatically turns off dynamics when a command is started, so it is not necessary to call StopDynamics unless you have a specific reason to do so.

Dynamics method

Called when the user moves the pointer on the screen, the Dynamics sub procedure gives your program the opportunity to draw elements dynamically on the screen to show the user what is going on. This is helpful to the user especially if your program is moving or modifying elements using specific locations.

LocateFailed method

When all elements at the entered data point have been rejected either by your LocateFilter method, or by the user, MicroStation calls the LocateFailed method. In this method your program should reset any variables it needs for another use of the command, then call StartLocate on the CommandState object to begin the command again.

Accept method

After the LocateFilter has accepted the element passed to it, the user has the opportunity to accept or reject the element. The Accept method is called if the user enters a data point to accept the selected element. This is where your program will do the necessary work of modifying or processing the selected element according to the purpose of your command.

LocateReset method

The LocateReset method is called by MicroStation when the user presses the reset button on the mouse while the command is active. This generally signals the user has completed the command, or wants to interrupt the command, so resetting variables is performed here. Typically, the LocateReset method also calls the StartLocate method of the CommandState object to restart the command again. The code for this uses the Me keyword to refer to the current object:

   CommandState.StartLocate Me

Cleanup method

The Cleanup method gives your program a chance to reset any object variables it may have created and do any other necessary tasks before the command exits.

Calling a Locate Command

Typically, a locate command is activated from a procedure in a standard code module. The code to start a Locate Command tool would look like this:

Sub Procedure runLocateTool ()
   CommandState.StartLocate New fixElementCommand
End Sub

For an example of a complete Locate Command, see the Copy Element Command Example.

Accessing data and other applications

Part of the reason that Visual Basic has become so popular is that it works well with many applications. Each program in the Microsoft Office suite has some implementation of Visual Basic for applications. A typical use for VBA programs then is to automate processes within an application or to automate the process of sharing information between applications. Here, we briefly describe the process of copying data from a MicroStation DGN file to another application.

Accessing another application

To access another application via Visual Basic, you create an object of that application then send commands to it. With the programs in Microsoft Office you can do more than just send commands, but for the sake of brevity we will limit this discussion to basics.

In the following example the list of level names is obtained and written to a spreadsheet in Microsoft Excel, putting the names in the cells in column 1. A short explanation of this example follows after the code.

Note: To run this example, you must have Microsoft Excel installed on your computer, otherwise an error will occur.

Sub sendLevelNamesToExcel()
   Dim oXL As Object
   Dim oBook As Object
   Dim oSheet As Object
   Dim i As Integer, lvlCount As Long
   Dim lvlName As String
   Dim oLevels As Levels
   Dim oLvl As Level
   
   Set oXL = CreateObject ("Excel.Application")
   oXL.Visible = True
   Set oBook = oXL.Workbooks.Add
   Set oSheet = oBook.Sheets(1)
 
   Set oLevels = ActiveDesignFile.Levels
   lvlCount = oLevels.Count
   For i = 1 to lvlCount
      Set oLvl = oLevels.Item(i)
      lvlName = oLvl.Name
      oSheet.Cells(i,1).Value = lvlName
   Next i
End Sub

This program begins by creating objects for the Excel application, a workbook, and a worksheet. These items are all declared as type Object. Using methods documented in the help files for Excel, a workbook is created and a worksheet is created within the workbook.

Next, this program gets a Levels object containing the set of levels from the ActiveDesignFile object, and from this obtains the Count of levels. This number is used for the limit of the loop.

In turn, the program gets each Level from the Levels object, gets its Name and sends it to the Excel spreadsheet specifying the cell row and column to place the name in.

Accessing MicroStation from another application

The Automation library for MicroStation V8 can be accessed from other applications that also include Microsoft Visual Basic for Applications, such as Microsoft Excel.

The Bentley MicroStation DGN 8.0 Object Library

To access the same Automation library that you see in MicroStation's Visual Basic Editor from another application, you need to add a “reference” to the MicroStation library. This requires that you have MicroStation installed on the computer where you are writing your code and also on each computer that will run the VBA program you write.

To create a reference to the DGN 8.0 library:

  1. Open the Microsoft Visual Basic environment window in the application you are working in. This can usually be done with the <Alt-F11> keyboard shortcut.
  2. Select the Tools menu, and choose References.
    The Reference dialog box appears displaying a list of the libraries that may be available for use with VBA on your computer.
  3. In this list, scroll down until you see “Bentley MicroStation DGN 8.0 Object Library”.
  4. Click the small check box next to the MicroStation DGN 8.0 Library and then click the OK button in the References dialog.
    The MicroStationDGN library will appear in the Object Browser window.

Common VBA Mistakes

This section describes some common mistakes encountered in Visual Basic programs, highlighting several problems that are difficult to diagnose because the symptoms often do not reflect the programming error. Included in this section are explanations for the following problem statements:

  • oTextElement.Origin.X = 4
  • ActiveModelReference.AddElement (ele)
  • oModel = ActiveModelReference

User Defined Types as Property Values

User defined types (UDTs) are data types that store specific values. A variable declared as a UDT type contains all of the data for that UDT. A variable declared as an object type contains a reference to an object that contains the data. When a program assigns a UDT from one variable to another, all of the data is copied. After the assignment, the program has one more copy of the data. When a program assigns an object reference from one variable to another, only the reference is copied. After the assignment, the program has one more reference to the data.

'pt.X is equal to zero immediately.
Dim pt As Point3d

'Copies the entire Point3d into pt variable.
pt = ActiveModelReference.GlobalOrigin

Conversely, when a variable is declared with an object type, it creates only a reference to an object.

'oElement contains no data.
Dim oElement As Element

'Saves a reference to the element.
'The element is NOT copied.
Set oElement = oElementEnumerator.Current

This difference often confuses programmers when using an object property that is a user defined type. Accessing the property copies the entire UDT into or out of the object. Changing a field in the UDT does not change anything in the object until the program assigns the UDT to the object property. In Visual Basic it is possible to use the syntax object-reference.property-name.field-reference = value to copy a UDT out of an object into a temporary copy and then to change one field of the temporary copy. Because of this, it is possible to create code that does nothing even though many programmers expect it to work. For example, the origin of a text element can be changed with this code:

Dim pt as Point3d
pt = oTextElement.Origin
pt.x = pt.x + 1
oTextElement.Origin = pt

The same action cannot be accomplished with this code:

Dim xVal As Double
xVal = oTextElement.Origin.x
oTextElement.Origin.x = xVal + 1

This only creates a temporary copy of the origin point on the stack, then modifies the X member of this temporary copy. The origin of the text element is not modified.

Parentheses in function calls

Parentheses are only interpreted as part of a function call if the return value is used in an expression or assignment statement. For example, the parentheses are required and interpreted as part of this function call:

Set oElEnum = ActiveModelReference.Scan(oScanCrit)

Because the return value of the function is assigned to a variable, the call to the function must include parentheses. When using a function in an expression the parentheses are also required, as in this example:

If oElement.HasAnyXData("WidgetMaker") Then
      ...
End If

When calling a Sub routine, parentheses are only needed when using the Call syntax to invoke a sub-routine that requires arguments. To add an element to the active model, you might use this:

Call ActiveModelReference.AddElement (oElement)

Without the Call keyword, the parentheses will cause an error. This same line is more often written without the parentheses, and the optional keyword, as:

ActiveModelReference.AddElement oElement

If the parentheses were included in this line without the Call keyword, it would be interpreted as “take the default property of oElement and pass that to AddElement”. Since MicroStation objects don't have default properties, an error message is displayed: “Object doesn’t support this property or method.”

Set Syntax

The Set keyword is required when an object reference is assigned to a variable of that object's type. For the Set statement to be valid, the type of the variable must be consistent with the object assigned to it. The Dim statement and other declaration keywords only create a variable of the specified type. No actual object is assigned to the variable until the Set statement is used.

'Create a variable of type ModelReference
Dim oModel As ModelReference

'Incorrect way to assign it.
oModel = ActiveModelReference

'Correct way to assign it.
Set oModel = ActiveModelReference

An assignment statement with an object and without the Set keyword means “assign to the default property of the object.” In the previous example, the incorrect assignment statement would be interpreted as “assign the default property value of ActiveModelReference to the default property of oModel.” Since oModel is equal to Nothing when this statement would be executed, the error message “Object variable or With block variable not set” is displayed.

Exceptions in Event Handlers

If the Visual Basic Error Trapping option is set to “Break on Unhandled Errors” (see Tools > Options > General > Error Trapping), then errors in class modules will not cause a break. Instead, code execution will stop without any indication that an error occurred. The most useful setting for this option is “Break in Class Module” which will cause a break whenever an error occurs, whether it occurs in a module or a class.

Option Explicit

The Option Explicit statement in a class or module can save unnecessary time debugging code because of mistyped variable names. Without this optional statement in the General Declarations section, every mistyped variable name becomes a new variable with an implied variant type. When the Option Explicit statement is used, every undefined variable produces an error.

To set the editor to automatically include the Option Explicit statement, enable the checkbox labeled “Require Variable Declaration” in Tools > Options > Editor.