To help demonstrate how to use dialogs, I'll present one final example-an object inspector. The object inspector accepts an optional argument that identifies the object to inspect. If the object is a UNO object, then the object contains debug properties that list the supported properties, methods , services, and interfaces. The primary purpose of this dialog is to inspect an object to determine what you can do with it.
In this final example, a dialog is created without using the Basic IDE. The inspection information is stored in a multi-line text edit control. I chose to use a text edit control rather than a list box, because the text control allows me to copy the contained text to the clipboard and the list box control does not. I frequently inspect an object and then copy the supported properties, methods, and interfaces to the clipboard so that I can use them as a reference.
Radio buttons dictate what is displayed in the text control. The text control can display the supported properties, methods, interfaces and services, and general inspection information such as the object's data type (see Figure 8 ).
Numerous utility methods are required to generically inspect an object in OOo. The utility methods do not control, access, or update the dialog or any controls so I have separated them from the common functionality. The techniques and methods used here should be somewhat familiar, so I haven't discussed them in depth. All of the routines are contained in the Inspector module in this chapter's source code.
A text character that appears as open space is called a "white space character." For example, in this book there is a single space between each word. The new-line character can create white space between lines. While inspecting an object, the resulting strings frequently contain leading and trailing white space of different types that must be removed. The functions in Listing 18 identify the different types of white space and remove them from a string.
'************************************************************************* '** Is the specified character white space? The answer is true if the '** character is a tab, CR, LF, space, or a non-breaking space character! '** These correspond to the ASCII values 9, 10, 13, 32, and 160 '************************************************************************* Function IsWhiteSpace(iChar As Integer) As Boolean Select Case iChar Case 9, 10, 13, 32, 160 IsWhiteSpace = True Case Else IsWhiteSpace = False End Select End Function '************************************************************************* '** Find the first character starting at location i% that is not white space. '** If there are none, then the return value is greater than the '** length of the string. ************************************************************************* Function FirstNonWhiteSpace(ByVal i%, s$) As Integer If i <= Len(s) Then Do While IsWhiteSpace(Asc(Mid$(s, i, 1))) i = i + 1 If i > Len(s) Then Exit Do End If Loop End If FirstNonWhiteSpace = i End Function '************************************************************************* '** Remove white space text from both the front and the end of a string. '** This modifies the argument string AND returns the modified string. '** This removes all types of white space, not just a regular space. '************************************************************************* Function TrimWhite(s As String) As String s = Trim(s) Do While Len(s) > 0 If Not IsWhiteSpace(ASC(s)) Then Exit Do s = Right(s, Len(s) - 1) Loop Do While Len(s) > 0 If Not IsWhiteSpace(ASC(Right(s, 1))) Then Exit Do s = Left(s, Len(s) - 1) Loop TrimWhite = s End Function
The CStr() method converts most standard data types to a string. Not all variables are easily converted to a string using CStr(), so the ObjToString() method attempts to make an intelligent conversion. The most troublesome variable types are Object and Variant, which can contain the value Empty or Null-these two cases are identified and printed in Listing 19 . Variables of type Object cannot be trivially converted to a String, so they are not. The standard types are converted to a String by Listing 19. The ObjToString() method truncates the string at 100 characters .
Function ObjToString(oInsObj) As String Dim s As String Select Case VarType(oInsObj) Case 0 s = "Empty" Case 1 s = "Null" Case 2, 3, 4, 5, 7, 8, 11 s = CStr(oInsObj) Case Else s = "[Cannot convert " & TypeName(oInsObj) & " to a string]" End Select If Len(s) > 100 Then s = Left(s, 100) & "...." ObjToString = s End Function
The ObjInfoString() method (shown in Listing 20 ) creates a string that describes an object. This string includes detailed information about the object type. If the object is an array, the array dimensions are listed. If possible, even the UNO implementation name is obtained. No other UNO- related information-such as supported properties or methods-is obtained.
Function ObjInfoString(oInsObj) As String Dim s As String REM We can always get the type name and variable type. s = "TypeName = " & TypeName(oInsObj) & CHR$(10) &_ "VarType = " & VarType(oInsObj) & CHR$(10) s = s & "Value = " & ObjToString(oInsObj) & CHR$(10) REM Check for NULL and EMPTY If IsNull(oInsObj) Then s = s & "IsNull = True" ElseIf IsEmpty(oInsObj) Then s = s & "IsEmpty = True" Else If IsObject(oInsObj) Then On Local Error GoTo DebugNoSet s = s & "Implementation Name = " Dim oTmpObj oTmpObj = oInsObj s = s & oTmpObj.getImplementationName() DebugNoSet: On Local Error Goto 0 s = s & CHR$(10) & "IsObject = True" & CHR$(10) End If If IsUnoStruct(oInsObj) Then s = s & "IsUnoStruct = True" & CHR$(10) If IsDate(oInsObj) Then s = s & "IsDate = True" & CHR$(10) If IsNumeric(oInsObj) Then s = s & "IsNumeric = True" & CHR$(10) If IsArray(oInsObj) Then On Local Error Goto DebugBoundsError: Dim i%, sTemp$ s = s & "IsArray = True" & CHR$(10) & "range = (" Do While (i% >= 0) i% = i% + 1 sTemp$ = LBound(oInsObj, i%) & " To " & UBound(oInsObj, i%) If i% > 1 Then s = s & ", " s = s & sTemp$ Loop DebugBoundsError: On Local Error Goto 0 s = s & ")" & CHR$(10) End If End If ObjInfoString = s End Function
An interesting extension to the ObjInfoString() method would be to allow it to recognize an array and to display array contents as well. This is left as an exercise for the reader.
Many objects contain so many properties and implement so many methods, that when they are displayed in the text box, it is difficult to find a specific entry. Sorting the output makes it easier to find individual entries.
Assume that I have two arrays. The first array contains a list of names and the second array contains a list of ages. The third item in the age array contains the age of the third item in the name array. In computer science, these are referred to as "parallel arrays." Parallel arrays are used in the object browser. When dealing with properties, the property type is stored in the first array, and the property name is stored in the second array. The same thing is done while dealing with the supported methods.
While creating the data for inspection, parallel arrays are created and maintained . I want to sort the information, but I can't sort one array unless I sort all of the arrays. A typical solution to this problem is to create an index array that contains integers, which are used to index into the array. The index array is sorted rather than the actual data. For example, sort the array oItems()=("B", "C", "A"). The sorted index array is (2, 0, 1), implying that oltems(2) is the first item, oItems(0) is the second item, and oItems(1) is the last item. The function SortMyArray() in Listing 21 sorts an array by using an index array.
Sub SortMyArray(oItems(), iIdx() As Integer) Dim i As Integer 'Outer index variable Dim j As Integer 'Inner index variable Dim temp As Integer 'Temporary variable to swap two values. Dim bChanged As Boolean 'Becomes True when something changes For i = LBound(oItems()) To UBound(oItems()) - 1 bChanged = False For j = UBound(oItems()) To i+1 Step -1 If oItems(iIdx(j)) < oItems(iIdx(j-1)) Then temp = iIdx(j) iIdx(j) = iIdx(j-1) iIdx(j-1) = temp bChanged = True End If Next If Not bChanged Then Exit For Next End Sub
The Object Inspector uses Private variables for the dialog and frequently referenced objects. The dialog contains a progress bar that is updated while information is retrieved to fill the text control. The inspected object is also stored as a global variable because it is used in event handlers that have no other way to obtain the object. See Listing 22 .
Option Explicit Private oDlg 'Displayed dialog Private oProgress 'Progress control model Private oTextEdit 'Text edit control model that displays information Private oObj 'Currently inspected object
Unfortunately, dialogs created in the IDE can only be created and run from within Basic. It is possible, however, to create and use a dialog at run time in any programming language that can use the OOo UNO API. The dialog functionality is scheduled to be improved in OOo 2.0 so that you can design dialogs in an IDE and display them using languages other than Basic. The Inspect() method in Listing 23 creates a dialog and all of its contained controls without using the IDE. Follow these steps to create and display the dialog:
Use CreateUnoService("com.sun.star.awt.UnoControlDialogModel") to create a dialog model and then set the model's properties.
Use the createInstance(name) method in the dialog model to create a model for each control that is inserted into the dialog model. Set the properties on the control's model and then use the insertByName(name, oModel) method-defined by the dialog model-to insert the control's model into the dialog's model. This step is important! To insert a control into a dialog, you must use the dialog model to create a control model, which is then inserted into the dialog model. You don't create or manipulate controls, only models.
Use CreateUnoService("com.sun.star.awt.UnoControlDialog") to create a dialog.
Use the setModel() method on the dialog to set the dialog's model.
Use CreateUnoListener(name) to create any desired listeners. Add the listener to the correct object; the correct object is usually a control.
Use CreateUnoService("com.sun.star.awt.Toolkit") to create a window toolkit object.
Use the createPeer(oWindow, null) method on the dialog to tell the dialog to create an appropriate window to use.
Execute the dialog.
Sub Inspect(Optional oInsObj) Dim oDlgModel 'The dialog's model Dim oModel 'Model for a control Dim oListener 'A created listener object Dim oControl 'References a control Dim iTabIndex As Integer 'The current tab index while creating controls Dim iDlgHeight As Long 'The dialog's height Dim iDlgWidth As Long 'The dialog's width REM If no object is passed in, then use ThisComponent. If IsMissing(oInsObj) Then oObj = ThisComponent Else oObj = oInsObj End If iDlgHeight = 300 iDlgWidth = 350 REM Create the dialog's model oDlgModel = CreateUnoService("com.sun.star.awt.UnoControlDialogModel") setProperties(oDlgModel, Array("PositionX", 50, "PositionY", 50,_ "Width", iDlgWidth, "Height", iDlgHeight, "Title", "Object Inspector")) createInsertControl(oDlgModel, iTabIndex, "PropButton",_ "com.sun.star.awt.UnoControlRadioButtonModel",_ Array("PositionX", 10, "PositionY", 10, "Width", 50, "Height", 15,_ "Label", "Properties")) createInsertControl(oDlgModel, iTabIndex, "MethodButton",_ "com.sun.star.awt.UnoControlRadioButtonModel",_ Array("PositionX", 65, "PositionY", 10, "Width", 50, "Height", 15,_ "Label", "Methods")) createInsertControl(oDlgModel, iTabIndex, "ServiceButton",_ "com.sun.star.awt.UnoControlRadioButtonModel",_ Array("PositionX", 120, "PositionY", 10, "Width", 50, "Height", 15,_ "Label", "Services")) createInsertControl(oDlgModel, iTabIndex, "ObjectButton",_ "com.sun.star.awt.UnoControlRadioButtonModel",_ Array("PositionX", 175, "PositionY", 10, "Width", 50, "Height", 15,_ "Label", "Object")) createInsertControl(oDlgModel, iTabIndex, "EditControl",_ "com.sun.star.awt.UnoControlEditModel",_ Array("PositionX", 10, "PositionY", 25, "Width", iDlgWidth - 20,_ "Height", (iDlgHeight - 75), "HScroll", True, "VScroll", True,_ "MultiLine", True, "HardLineBreaks", True)) REM Store the edit control's model in a global variable. oTextEdit = oDlgModel.getByName("EditControl") createInsertControl(oDlgModel, iTabIndex, "Progress",_ "com.sun.star.awt.UnoControlProgressBarModel",_ Array("PositionX", 10, "PositionY", (iDlgHeight - 45) ,_ "Width", iDlgWidth - 20, "Height", 15, "ProgressValueMin", 0,_ "ProgressValueMax", 100)) REM Store a reference to the progress bar oProgress = oDlgModel.getByName("Progress") REM Notice that I set the type to OK so I do not require an action REM listener to close the dialog. createInsertControl(oDlgModel, iTabIndex, "OKButton",_ "com.sun.star.awt.UnoControlButtonModel",_ Array("PositionX", Clng(iDlgWidth / 2 - 25), "PositionY", iDlgHeight-20,_ "Width", 50, "Height", 15, "Label", "OK",_ "PushButtonType", com.sun.star.awt.PushButtonType.OK)) REM Create the dialog and set the model oDlg = CreateUnoService("com.sun.star.awt.UnoControlDialog") oDlg.setModel(oDlgModel) REM The item listener for all of the radio buttons oListener = CreateUnoListener("radio_", "com.sun.star.awt.XItemListener") oControl = oDlg.getControl("PropButton") ocontrol.addItemListener(oListener) oControl = oDlg.getControl("MethodButton") ocontrol.addItemListener(oListener) oControl = oDlg.getControl("ServiceButton") ocontrol.addItemListener(oListener) oControl = oDlg.getControl("ObjectButton") ocontrol.addItemListener(oListener) oControl.getModel().State = 1 REM Now, set the dialog to contain the standard dialog information DisplayNewObject() REM Create a window and then tell the dialog to use the created window Dim oWindow oWindow = CreateUnoService("com.sun.star.awt.Toolkit") oDlg.createPeer(oWindow, null) REM Finally, execute the dialog oDlg.execute() End Sub
To shorten the code in Listing 23, try using two utility methods to set properties, create a control model, and insert the control model into the dialog model. Notice that the control itself is never created-only the model is created. Listing 24 contains the methods to create the control models and to set the properties. Although properties are set using the setPropertyValue method, in Basic you can set the properties directly if desired.
Sub createInsertControl(oDlgModel, index%, sName$, sType$, props()) Dim oModel oModel = oDlgModel.createInstance(sType$) setProperties(oModel, props()) setProperties(oModel, Array("Name", sName$, "TabIndex", index%)) oDlgModel.insertByName(sName$, oModel) REM This changes the value because it is not passed by value. index% = index% + 1 End Sub REM Generically set properties based on an array of name/value pairs. Sub setProperties(oModel, props()) Dim i As Integer For i=LBound(props()) To UBound(props()) Step 2 oModel.setPropertyValue(props(i), props(i+1)) Next End sub
The only required listener is for the radio buttons. Each time a new radio button is selected, the method radio_itemStateChanged() is called (the listener is set up in Listing 23). The event handler calls the DisplayNewObject() subroutine, which handles the actual work when a new property type is requested . See Listing 25 . I wrote a separate routine so that it can be used apart from the event handler. For example, it calls DisplayNewObject() before the dialog is displayed so that the dialog contains useful information when it first appears.
Sub radio_itemStateChanged(oItemEvent) DisplayNewObject() End Sub Sub DisplayNewObject() REM Reset the progress bar! oProgress.ProgressValue = 0 oTextEdit.Text = "" On Local Error GoTo IgnoreError: If IsNull(oObj) OR IsEmpty(oObj) Then oTextEdit.Text = ObjInfoString(oObj) ElseIf oDlg.getModel().getByName("PropButton").State = 1 Then processStateChange(oObj, "p") ElseIf oDlg.getModel().getByName("MethodButton").State = 1 Then processStateChange(oObj, "m") ElseIf oDlg.getModel().getByName("ServiceButton").State = 1 Then processStateChange(oObj, "s") Else oTextEdit.Text = ObjInfoString(oObj) End If oProgress.ProgressValue = 100 IgnoreError: End Sub
In Listing 25, notice that if the properties, methods, or services are requested, then the processStateChange() subroutine (see Listing 26 ) is called. If properties, methods, or services are not requested, simple object information is added to the text control.
Sub processStateChange(oInsObj, sPropType$) Dim oItems() BuildItemArray(oInsObj, sPropType$, oItems()) If sPropType$ = "s" Then Dim s As String On Local Error Resume Next s = "** INTERFACES **" & CHR$(10) & Join(oItems, CHR$(10)) s = s & CHR$(10) & CHR$(10) & "** SERVICES **" & CHR$(10) & _ Join(oInsObj.getSupportedServiceNames(), CHR$(10)) oTextEdit.Text = s Else oTextEdit.Text = Join(oItems, CHR$(10)) End If End Sub
Again, the processStateChange() subroutine contains very little logic. The primary purpose of Listing 26 is to add the supported services to the end of the text control after the interfaces are obtained from the dbg_SupportedInterfaces property.
The BuildItemArray() subroutine contains the majority of the important code for this dialog. First, the code inspects the dbg_ properties to obtain the list of properties, methods, or interfaces as a single string. It then separates the individual items from the single string and sorts them. If properties are displayed, the property set information object is used to obtain the value of each supported property. This allows you to see both the property name and its value. Although the property set information object does not support all of the properties returned by the property dbg_properties, it supports most of them. The individual data is returned as an array of strings in the oItems() array. See Listing 27 for an example.
Sub BuildItemArray(oInsObj, sType$, oItems()) On Error Goto BadErrorHere Dim s As String 'Primary list to parse Dim sSep As String 'Separates the items in the string Dim iCount% ' Dim iPos% ' Dim sNew$ ' Dim i% ' Dim j% ' Dim sFront() As String 'When each piece is parsed, this is the front Dim sMid() As String 'When each piece is parsed, this is the middle Dim iIdx() As Integer 'Used to sort parallel arrays. Dim nFrontMax As Integer 'Maximum length of front section nFrontMax = 0 REM First, see what should be inspected. If sType$ = "s" Then REM Dbg_SupportedInterfaces returns interfaces and REM getSupportedServiceNames() returns services. s = oInsObj.Dbg_SupportedInterfaces 's = s & Join(oInsObj.getSupportedServiceNames(), CHR$(10)) sSep = CHR$(10) ElseIf sType$ = "m" Then s = oInsObj.DBG_Methods sSep = ";" ElseIf sType$ = "p" Then s = oInsObj.DBG_Properties sSep = ";" Else s = "" sSep = "" End If REM The dbg_ variables have some introductory information that REM we do not want, so remove it. REM We only care about what is after the colon. iPos = InStr(1, s, ":") + 1 If iPos > 0 Then s = TrimWhite(Right(s, Len(s) - iPos)) REM All data types are prefixed with the text Sbx. REM Remove all of the "Sbx" charcters s = Join(Split(s, "Sbx"), "") REM If the separator is NOT CHR$(10), then remove REM all instances of CHR$(10). If ASC(sSep) <> 10 Then s = Join(Split(s, CHR$(10)), "") REM Split on the separator character and update the progress bar. oItems() = Split(s, sSep) oProgress.ProgressValue = 20 Rem Create arrays to hold the different portions of the text. Rem The string usually contains text similar to "SbxString getName()" Rem sFront() holds the data type if it exists and "" if it does not. Rem sMid() holds the rest. ReDim sFront(UBound(oItems)) As String ReDim sMid(UBound(oItems)) As String ReDim iIdx(UBound(oItems)) As Integer REM Initialize the index array and remove leading and trailing REM spaces from each string. For i=LBound(oItems()) To UBound(oItems()) oItems(i) = Trim(oItems(i)) iIdx(i) = i j = InStr(1, oItems(i), " ") If (j > 0) Then REM If the string contains more than one word, the first word is stored REM in sFront() and the rest of the string is stored in sMid(). sFront(i) = Mid$(oItems(i), 1, j) sMid(i) = Mid$(oItems(i), j+1) If j > nFrontMax Then nFrontMax = j Else REM If the string contains only one word, sFront() is empty REM and the string is stored in sMid(). sFront(i) = "" sMid(i) = oItems(i) End If Next oProgress.ProgressValue = 40 Rem Sort the primary names. The array is left unchanged, but the Rem iIdx() array contains index values that allow a sorted traversal. SortMyArray(sMid(), iIdx()) oProgress.ProgressValue = 50 REM Dealing with properties so attempt to find the value REM of each property. If sType$ = "p" Then Dim oPropInfo 'PropertySetInfo object Dim oProps 'Array of properties Dim oProp 'com.sun.star.beans.Property Dim v 'Value of a single property Dim ss As String On Error Goto NoPropertySetInfo oPropInfo = oInsObj.getPropertySetInfo() For i=LBound(sMid()) To UBound(sMid()) If oPropInfo.hasPropertyByName(sMid(i)) Then v = oInsObj.getPropertyValue(sMid(i)) sMid(i) = sMid(i) & " = " & ObjToString(v) End If Next NoPropertySetInfo: End If oProgress.ProgressValue = 60 nFrontMax = nFrontMax + 1 iCount = LBound(oItems()) REM Now build the array of the items in sorted order. REM Sometimes, a service is listed more than once. REM This routine removes multiple instances of the same service. For i = LBound(oItems()) To UBound(oItems()) sNew = sFront(iIdx(i)) & " " & sMid(iIdx(i)) 'Uncomment these lines if you want to add uniform space 'between front and mid. This is only useful if the font is fixed in width. 'sNew = sFront(iIdx(i)) 'sNew = sNew & Space(nFrontMax - Len(sNew)) & sMid(iIdx(i)) If i = LBound(oItems()) Then oItems(iCount) = sNew ElseIf oItems(iCount) <> sNew Then iCount = iCount + 1 oItems(iCount) = sNew End If Next oProgress.ProgressValue = 75 ReDim Preserve oItems(iCount) Exit Sub BadErrorHere: MsgBox "Error " & err & ": " & error$ + chr(13) + "In line : " + Erl End Sub
You can use the object inspector to investigate objects when you aren't certain exactly what you can do with them. You might want to copy this to an application-level library so that you can access the macro from all of your other macros.