7—
Building a GUI with COM

In the last chapter we built a number of Python classes that let us do some useful work in the financial domain, assuming we were happy to work at a command prompt and write our own scripts. Now we'll embed this in a traditional GUI by exposing the Python classes as COM servers that provide an engine.

You can imagine a whole family of applications based on our classes, each specialized for a specific task such as viewing and editing data, comparing BookSets, generating forecasts and laying out reports. What you need first is a browser for your sets of accounts that can show the same types of views as the command-line version: lists of accounts, details of any account, and a data-ordered ''journal.'' It should also allow you to edit existing transactions and add new ones.

Our example browser is written in Visual Basic (VB) 6. We cover only a small selection of the features and code, but the full application is available with the code examples at http://starship.python.net/crew/mhammond/ppw32/. We have also built a cut-down version of the browser in Delphi; a small section at the end of the chapter explains the differences when using Delphi as a client language. Most mainstream development environments support COM, so you should be able to easily adapt what follows to your favorite development environment.

Designing COM Servers

We want to expose the Transaction and BookSet classes as COM servers. In Chapter 5, Introduction to COM, we saw an example of how a few simple additions can turn any Python class into a COM server. Now that our classes are larger and more complex, this isn't always the best solution, and it's worth thinking about alternatives. The basic problem is that COM-exposed methods sometimes need to handle their arguments in a different way than ordinary Python methods.

For example, if a COM client such as Visual Basic calls the save method of our BookSet, it passes in a Unicode string, which needs to be converted to a Python string:

# our ordinary save method for use from Python
def save(self, filename):
    f = open(filename, 'wb')
    cPickle.dump(self.__journal,f)
    f.close()

# what we would need for use from COM
def save(self, unicode_filename):
    # convert it to a python string:
    python_filename = str(unicode_filename)

    f = open(python_filename, 'wb')
    cPickle.dump(self.__journal, f)
    f.close()

Furthermore, the whole philosophy of COM is about defining a fixed interface and sticking to it. This strongly suggests building a separate class for the COM interface and hooking it to our native Python classes, which can be far more dynamic. Here are several design patterns to do this:

COM base class, pure Python subclass
Here you define a base class and expose it as a COM server, initially doing nothing with the arguments to the methods, which defines your COM interface neatly in one place. Then implement a subclass (which is much longer) to do the actual work. This pattern is most appropriate when designing a class whose main function is to be used from COM and not from Python.

Pure Python base class, COM subclass
Here you inherit from the existing Python BookSet class and rewrite the relevant methods to handle string and object arguments differently.

COM interface, Python delegate
Here you define a COM class to define your interface. Rather than using inheritance, this has an internal variable pointing to a pure Python counterpart, the delegate. Its methods translate their arguments, return values as needed, and forward them to the delegate.

Since we designed our Python classes first and want to be able to use them independently from COM, we'll go for the delegate pattern.

We've made a separate Python source file within the Doubletalk package called comservers.py. You'll add to this later, but let's start off with an absolutely trivial COM server:

# comservers.py - to be expanded
class COMBookSet:

    _reg_clsid_ = '{38CB8241-D698-11D2-B806-0060974AB8A9}'
    _reg_progid_ = 'Doubletalk.BookServer'
    _public_methods_ = ['double']

    def __init__(self):
        self.__BookSet = doubletalk.bookset.BookSet()

    def double(self, arg):
        # trivial test function to check it's alive
       -return arg * 2

if __name__ == '__main__':
    win32com.server.register.UseCommandLine(COMBookSet)

When this is created, it creates a pure Python BookSet and stores it in self.__BookSet. For now we've just exposed a single public method that doubles a number. The module needs to be registered, which can be done from File ® Run in PythonWin or a double-click in Explorer. It would be a good idea at this point to register it in debug mode, which provides extra information for developers; this is covered later on in Chapter 12, Advanced Python and COM.

A VB Client

Next, we've created a VB application with a multiple document interface. You can do this by hand and add the forms and modules you need, or use VB's Application Wizard and strip out the stuff you don't need. We've defined a public variable called BookServer and three public methods in the form, InitCOMServer, CloseCOMServer, and TestCOMServer, and hooked them up so that InitCOMServer is called when the form loads:

Public BookServer As Object

Private Sub MDIForm_Load()
    InitCOMServer
    frmJournal.Show
End Sub

Private Sub MDIForm_Unload(Cancel As Integer)
    CloseCOMServer
End Sub

Sub InitCOMServer()
    'called when the program starts
    On Error GoTo InitCOMServer_error
    Set BookServer = CreateObject("Doubletalk.BookServer")
    Exit Sub

InitCOMServer_error:
    Dim msg As String
    msg = "There was an error trying to initialize the BookServer." + _

            "Please check that it is properly registered and try the Python " + _
            "test functions first. The program will now abort."
    MsgBox msg
    End
End Sub

Sub CloseCOMServer()
    Set BookServer = Nothing
End Sub -

Sub TestCOMServer()
    'Just to check it is alive
    Dim hopefully_four As Integer
    hopefully_four = BookServer.Double(2)
    MsgBox "2 x 2 = " & hopefully_four & ", so your server is alive"
End Sub

Private Sub mnuToolsTestServer_Click()
    'this helps establish if the COM server is alive
    'using a minimal diagnostic function in the modMain module
    TestCOMServer
End Sub

That the COM server isn't registered is a common error, so you need to handle this error and close the program immediately if it occurs.

It's not necessary to explicitly unload the BookServer; it's freed automatically when the VB program closes down. However, it's good programming practice to free variables explicitly to show that you know when it's happening, and it provides the right hook in case you want to do some extra processing at this point (such as committing changes to a file or database).

We've also added a menu item on the Tools menu to call our test function. Hit Run, and the application should start up. If you get as far as the main screen, it should have a live Python COM server inside it. To prove it, click on Tools ® Test Server, and you should see Figure 7-1.

0103-01.gif
Figure 7-1.
Not much of a GUI yet!

Now we've got this running, we can start to expand the capabilities of the COM server and add the code to call it on the VB side. We've added a number of pub-

lic methods, declaring them in the _public_methods_ attribute as we implement them.

Building the First View

Adding the following methods to your COMBookSet allows you to load a data file, see how many transactions the BookSet contains, and then build a view that displays a Journal, a listing of all the transactions in date order:

# more methods for COMBookSet - must be named in _public_methods_
    def load(self, filename):
        self.__BookSet.load(str(filename))

    def count(self):
        # return number of transactions
        return len(self.__BookSet)

    def getTransactionString(self, index):
        return self.__BookSet[index].asString()

In load, you perform a str(filename) operation to convert the Unicode filename from COM into an ordinary Python string. Then pass the request to the delegate Python BookSet instance. Most methods follow this pattern, doing any necessary transformations of arguments on the way in and out and passing the real work onto the delegate.

Now you need to open files. This is the VB code for the File ® Open menu option (generated by one of VB's Wizards and so is somewhat verbose):

Private Sub mnuFileOpen_Click()
    Dim sFile As String
    With dlgCommonDialog
        .DialogTitle = "Open"
        .CancelError = False
        'ToDo: set the flags and attributes of the common dialog control
        .Filter = "Doubletalk Journal Files (*.dtj)|*.dtj"
        .ShowOpen
        If Len(.FileName) = 0 Then
            Exit Sub
        End If
        sFile = .FileName
    End With
    BookServer.Load sFile

    'display something helpful in the Journal caption
    frmJournal.Caption = sFile & ", " & BookServer.count & " Transactions"
End Sub

The only line of interest here is in BookServer.Load sFile, where Python is asked to do the work. Then, clicking on File ® Open lets you open a BookSet

stored on disk. The Doubletalk package includes one file created by the demodata1.py script with a thousand transactions; it takes about one second to load on a Pentium 120. The various other loading and saving methods in your COMBookSet class can be hooked up to menu items in a similar manner.

Next, build a Journal view, an MDI child window to display a list of transactions in date order. This has a list box filling its client area and some code to resize it smoothly. It also has a method called UpdateView as follows:

Public Sub UpdateView()
    'make a list with a string describing each transaction

    Dim count, i As Integer
    Dim trantext As String
    Dim tran As Object

    Screen.MousePointer = vbHourglass
    1stJournal.Clear

    For i = 0 To frmMain.BookServer.count -1
        trantext = frmMain.BookServer.getOneLineDescription(i)
        1stJournal.AddItem trantext
    Next i

    Screen.MousePointer = vbDefault
    Caption = "Journal view - " & 1stJournal.ListCount & "transactions"

End Sub

The implementation of this UpdateView method is simple: first ask the BookServer for the number of transactions. then for the text representation of each one in turn. These are added to the list one at a time. When run, you see a view like Figure 7-2.

0105-01.gif
Figure 7-2.
The first view

We'll define a range of views in the next few pages, all with an UpdateView public method. You can then ask them all to redraw themselves whenever the data changes. Later on we will see some other techniques that remove the need to do the looping in VB; the Python COM framework can return a list of strings or allow VB to iterate over the BookSet directly.

At this point, it's worth it to pause and reflect. We have built the engine of an application in Python, but delivered it inside an industry-standard, 100% Windows user interface of the kind users expect.

More about Transactions

You'll want to add and edit individual transactions. This leads to more fundamental design considerations. You need to expose Transaction class as a COM server. You also need to think about how to create these transactions and manage their lifecycles and about the right choice of patterns. One option is to make a COMTransaction class the same way we did for COMBookset.

The overall goal is to make sure that code that works with transactions in VB is as similar as possible to the corresponding code in Python. The VB code to create and add a new transaction looks like this:

Dim newtran As Object

Set newtran = CreateObject("Doubletalk.Transaction")
newtran.setDateString "31/12/99"
newtran.setComment "Python on Windows Royalty Cheque"
newtran.addLine "MyCo.Assets.NCA.CurAss.Cash", 5000
newtran.addLastLine "MyCo.Capital.PL.Income.Writing"

BookServer.Add newtran

There is another choice, however. Adding a factory method to BookServer gives a new transaction (outside of the BookSet but otherwise fully formed). In this case there is no need to register the class at all; it's never created directly from the registry. Microsoft Office uses this model a lot. Most of the Excel objects are obtained from the application object's factory methods and are not directly creatable. If you go this way, your VB code to add a transaction looks like this:

Dim newtran As Object

Set newtran = BookServer.CreateTransaction
newtran.setDateString "31/3/2000"
newtran.setComment "Even more royalties"
newtran.addLine "MyCo.Assets.NCA.CurAss.Cash", 5000
newtran.addLastLine "Myco.Capital.PL.Income.Writing"

BookServer.Add newtran

The benefit is that the COMTransaction class can be much smaller.

We've added a class called COMTransaction to comservers.py that delegates to a pure Python transaction. This provides all the methods needed to edit and create transactions in your client code. Here's an example:

class COMTransaction:
    # we don't need all the _reg_ stuff, as we provide our own
    # API for creating these and don't use the registry.
   _public_methods_ = [
                'asString',
                'getDateString'
                'setDateString'
                'setCOMDate',
                'getCOMDate',
                'getComment',
                'setComment',
                'getLineCount',
                'getAccount',
                'getAmount',
                'addLine',
                'addLastLine',
                'getOneLineDescription'
                ]

    def __init__(self, tran=None):
        if tran is None:
            self._tran = doubletalk.transac.Transaction()
        else:
            self._tran = tran

    def asString(self):
        return self._tran.asString()

    def getDateString(self):
        return self._tran.getDateString()

    def setDateString(self, aDateString):
        self._tran.setDateString(str(aDateString)

    def setCOMDate(self, comdate):
        self._tran.date = (comdate - 25569.0) * 86400.0

    def getCOMDate(self):
        return (self._tran.date / 86400.0) + 25569.0

    def getComment(self):
        return self._tran.comment

    def setComment(self, aComment):
        self._tran.comment = str(aComment)

    def getOneLineDescription(self):
        return '%-15s %s %10.2f' % (

            self._tran.getDateString(),
            self._tran.comment,
            self._tran.magnitude()
            )

    def getLineCount(self):
        return len(self._tran.lines)

    def getAccount(self, index):
        return self._tran.lines[index][0]

    def getAmount(self, index):
        return self._tran.lines[index][1]

    def addLine(self, account, amount):
        self._tran.addLine(str(account), amount)

    def addLastLine(self, account):

        self._tran.addLastLine(str(account))

This example has a number of interesting features:

� Like COMBookSet, this has an instance variable called self._tran pointing to a pure Python transaction instance. You can pass in an existing transaction on initialization to wrap it up, or it creates one if none is provided.

� There's no need to register it at all, since you will create instances from a factory method provided in COMBookSet. The only special attribute needed is the list of public methods.

� Since Python dates use a different representation than Microsoft, we have provided setCOMDate and getCOMDate methods to handle the conversion. Python's date system is that of Unix, counting seconds since 1/1/1970; COM uses the Excel format of days since 31-Dec-1899. We also offer setDateString and getDateString methods to sidestep the need for a conversion routine.

� Rather than expose comment as a simple attribute, we have implemented setComment and getComment methods. These let you convert the Unicode string. As a general principle, we are not directly exposing any attributes (although the Python COM framework does allow it).

� We provided some extra methods to get the number of lines and to retrieve a specific account name or amount using an index. These let you write a VB loop to get all the lines. Alternative ways of exposing the detail rows would be to return arrays (which are covered later in this chapter) and enumerators (which appear in Chapter 12).

Now we need to look at how to get hold of these and store them in our BookServer. At this point you need to do a little more conversion and under-

stand a little more about COM. In the previous VB example to create and add a transaction, there were actually three different Transaction objects involved. The raw one used by VB is an IDispatch object. IDispatch is a standard COM interface that lets you attempt to call any method or property and finds out later whether they work; the Python COM framework creates these around Python objects. Hiding within this is an instance of our COMTransaction class, written in Python. And because you are using a delegation mechanism, you have your own pure Python Transaction class as an attribute of the COMTransaction. When you create new transactions to use with COM or try to add ones to the BookSet, you have a little more conversion to do.

This method in COMBookSet creates a new transaction for Visual Basic to use:

def createTransaction(self):
        comTran = COMTransaction()
        idTran = win32com.server.util.wrap(comTran)
        return idTran

Before handing this out to a client language such as VB, call the wrap utility. This creates a PyIDispatch wrapper around the Python object, which is what VB actually works with. More information on wrapping Python objects for COM can be found in Chapter 12.

When you want to add a new transaction to the BookSet, you need to unwrap it to get at the Python object inside:

def add(self, idTran):
        comTran = win32com.server.util.unwrap(idTran)
        pyTran = comTran._tran
        self.__BookSet.add(pyTran)

Throughout this code we have prefixed the PyIDispatch object with id, the Python COM classes in comservers.py with com, and the pure Python classes in bookset.py and transac.py with py.

If you design your application purely to work with COM and not for general Python use, you can take another tack and store the PyIDispatch objects in the list.

Adding and Editing Transactions

Now we are ready to add and edit transactions. The natural thing to do is to allow a double-click on a transaction to lead to a special dialog for editing. This should perform the necessary validation to ensure that the transaction balances before the dialog is closed, and, if there is a change, it should update all of the current views. We've built a pair of dialogs in VB: one to edit transactions and the other (called from it) to edit individual line items. Figure 7-3 shows the transaction dialog.

0110-01.gif
Figure 7-3.
Transaction editing dialog

The code to edit the transaction is exposed as a method of the form as follows:

Public Function Edit (index As Integer) As Boolean
    'returns true is the transaction was successfully edited
    Dim i As Integer
    Dim linecount As Integer

    Set tran = frmMain.BookServer.getTransaction(index)

    'display the transaction details
    lblTranIndex.Caption = Str(index)
    txtDate.text = FormatDateTime(tran.getCOMDate)
    txtComment.text = tran.getComment

    linecount = tran.getLineCount
    grdItems.rows = linecount + 1
    For i = 0 To linecount - 1
        grdItems.TextMatrix(i + 1, 0) = tran.GetAccount(i)
        grdItems.TextMatrix(i + 1, 1) = tran.getAmount(i)
    Next i
    Set tran = Nothing
    UpdateFormState

    'display and allow editing
    Me.Show vbModal

    If FormExit = vbOK Then
       Set tran = frmMain.BookServer.CreateTransaction
       tran.setComment txtComment.text
       If IsDate(txtDate.text) Then tran.setCOMDate
                                                       CDbl (CDate(txtDate.text))

        For i = 1 To grdItems.rows - 1
            tran.AddLine grdItems.TextMatrix(i, 0), -
                                    CDbl (grdItems.TextMatrix(i, 1))

        Next i
        frmMain.BookServer.Edit index, tran
        Set tran = Nothing
        Edit = True
    Else
        Edit = False
    End If

End Function

This example is one of many ways to organize the code. Fetch a transaction from the BookSet and display its attributes in the user interface before discarding it. Then display the dialog modally: the user must finish using the dialog before the Edit method continues. During this period, quite a bit of validation code is running; the OK button is enabled only if the transaction is valid and in balance. If OK is clicked, the transaction is valid. Now create a new COMTransaction to hold the data, copy the data from the user interface into it, and tell the BookSet to accept this as a replacement for the its existing data in that position. An alternative might be to work with the same transaction throughout.

The dialog also exposes a method to add a new transaction; this displays an empty dialog, but behaves identically after displaying the form.

To summarize, you can now add and edit transactions from Visual Basic. The techniques used would be the same for any kind of nested or master-detail object. The VB code to do this looks extremely natural and straightforward, pretty much the same as the Python code for creating transactions. On the server side, you need to distinguish between the PyIDispatch objects COM is using and Python's internal representations, but this needs to be done only once per class.

Building Views with Arrays

So far we've seen examples of passing numbers, strings, and Python objects back and forth between Python and VB. Now we'll develop some more kinds of views and look at passing around arrays of data at a time. This is a real time saver and can simplify the preceding code in several places.

Many financial reports take the form of a 2D grid or spreadsheet. What's needed is a generic solution for getting a Python list of lists into such a grid. In the last chapter we showed a number of BookSet methods for getting a list of accounts, the details of an account and so on. For example, getAccountDetails returns a list of tuples of (transaction number, date, comment, amount, runningTotal). This is effectively a matrix with five columns but an unknown number of rows. You need to expose this the usual way in COMBookSet, but then access the entire matrix from VB. The Python COM framework automatically converts lists (or lists of lists, or lists of tuples) into arrays that can be accessed in VB.

We've created a new form, frmAccountView, which can be given the name of an account as a string. This has a Microsoft FlexGrid control called grdTable. Here's the UpdateView method that fetches and displays the data:

Public Sub UpdateView()
    Dim table As Variant
    Dim rows As Integer, cols As Integer
    Dim row As Integer, col As Integer

    table = frmMain.BookServer.getAccountDetails(AccountName)

    rows = UBound(table, 1) - LBound(table, 1) + 1
    cols = UBound(table, 2) - LBound(table, 2) + 1 'should be 5

    grdTable.rows = rows + 1  'leave room for titles
    For row = 0 To rows - 1
        For col = 0 To cols - 1
            grdTable.TextMatrix(row + 1, col) = table(row, col)
        Next col
    Next row
End Sub

Figure 7-4 displays the new view in action.

0112-01.gif
Figure 7-4.
A 2D grid view

Although we've hardcoded a view, it would be easy to write a completely generic form to display any data and push the management of views back into Python itself. We'll look at this in Chapter 8, Adding a Macro Language.

Graphics and Callbacks

It would be convenient to look at a graphical view of an account, so let's create one. This allows you to illustrate another level of integration between Python and VB: callbacks. Within the framework you've just created, the VB COM client has been accessing the properties and methods of the Python COM server. But it doesn't have to be that way; the Python code can access the properties and methods of any VB object, whether built-in or user-defined.

You need to pass a VB form to Python, which then draws directly on it. You could also instead write another view that grabs the same array of data as the last one and uses VB graphics code to do the drawing. That is a better design, since it keeps a strict separation between the engine and interface; once you add drawing code on the Python side, you presume that a VB form with a certain API is provided. But this example is fun and highly instructive.

Let's start by creating a new form in our application called frmAccountChart, similar to the other views so far. Add a public method to the COMBookSet server called drawAccountChart, which expects a VB form as an argument. We'll start with a simple test: our drawing routine sets the caption of the form with a timestamp. On the VB side, the UpdateView method for frmAccountChart asks Python to do the work:

'Method of frmAccountChart
Public Sub UpdateView()
    'ask Python to scribble on me
    frmMain. BookServer.drawAccountChart Me
End Sub

The form passes itself (using the keyword Me, which is VB's equivalent of self) to the BookServer. On the Python side, here is a method that does something visible to the form:

def drawAccountChart(self, vbForm):
    # Make a Dispatch wrapper around the VB Form object so we can call
    # any of its methods.
    idForm = win32com.client.Dispatch(vbForm)

    # access a property of the VB form
    idForm.Caption = "Python Drew this chart at " + time.ctime(time.time())

The first line is critical. The actual object passed to Python in the variable vbForm is a raw IDispatch object, which is difficult to use directly. However, by creating a dynamic Dispatch wrapper around it, you can access any of its methods and properties as a normal COM object. Figure 7-5 shows the proof.

0114-01.gif
Figure 7-5.
Python accessing VB objects

The meaning of this wrapping is discussed in greater detail in Chapter 12. For now, just remember to do it for any Visual Basic objects you want to manipulate.

Python can access the public methods and properties of any VB object, not just user-interface widgets such as forms and list boxes, but also classes you define yourself.

Exposing a Graphics API to Python, a More Complex Example

Now let's extend what we just did and let Python do some real work. VB provides a picture control that allows drawing. However, some of the drawing methods use an esoteric syntax with hyphens, seen nowhere else in VB, and which no other language could reasonably be expected to call. You should therefore provide your own easy-to-use graphics API in VB at the form level. Add a single picture control called picChart to frmAccountChart, along with code to fill the entire form-when the form is resized. Then add a few methods to the Visual Basic form to make it easier to do the graphics. Python can then call these methods. Here is the minimal graphics API defined for frmAccountChart:

'Visual Basic code to provide a minimal graphics API
Public Sub DrawLine(x1 As Integer, y1 As Integer, x2 As Integer, _
                    y2 As Integer, color As Long)
    picChart.FillStyle = vbTransparent

    picChart.Line (x1, y1)-(x2, y2), color
    ' see what we mean about the funny syntax?

End Sub
Public Sub DrawBox(x1 As Integer, y1 As Integer, x2 As Integer, _
                   y2 As Integer, lineColor As Long, _
                   isSolid As Boolean, fillColor As Long)
    If isSolid Then
        picChart.FillStyle = vbSolid
    Else
        picChart.FillStyle = vbTransparent
    End If
    picChart.fillColor = fillColor
    picChart.Line (x1, y1)-(x2, y2), lineColor, B
End Sub

Public Sub DrawText(x As Integer, y As Integer, size As Integer, _
                   text As String)
    picChart.CurrentX = x
    picChart.CurrentY = y
    picChart.FontSize = size
    picChart.Print text
End Sub

Public Function GetClientArea() As Variant
    'return a two-element variant array
    GetClientArea = Array(picChart.Width, picChart.Height)
End Function

This code gives one method to clear the chart, and three methods to draw lines, boxes (filled or otherwise), and place text with a choice of font size. There's also a method that says which account the form is tracking and another method to return the dimensions of the form, so you can figure out how to scale the chart.

The drawAccountChart method in our Python COM server now needs to do three things: first, it queries the form to find out which account to draw; then it queries it to determine its size; finally it uses the graphics methods to do some drawing.

We won't repeat all the chart code but here are a few lines:

def drawAccountChart(self, vbForm):
    # Make a Dispatch wrapper around the vb Form object so we can call
    # any of its methods.
    print 'Drawing chart�'

Note the print statement. In normal use this won't go anywhere. However, the Python COM framework provides a debugging mode for COM servers and a tool for collecting these traces. It took a few tries to get all of the scaling right, and we used numerous print statements to examine the data.

Debugging COM Servers

At this point, we need to briefly explain how to debug a COM server. The theory behind this is covered in Chapter 12. It's easy to use: just run the COM server script (comservers.py in our case) with the argument --debug: note the double hyphen. The output of any print statements in the server code can then be seen in the Trace Collector window on the PythonWin Tools menu.

Now it's time to work with the form passed in from Visual Basic:

idForm = win32com.client.Dispatch(vbForm)

As before, you wrap the VB object using Dispatch:

# call a method we defined on the VB form
# arrays are converted automatically to Python tuples we can access

(width, height) = idForm.GetClientArea()
account = idForm.GetAccount()

The previous line is interesting for two reasons. First, you called a public method of the VB form you defined. This works in exactly the same way as calling methods of built-in VB objects. Second, you received back an array. Earlier we saw that a Python list or tuple can be passed to VB, where it's interpreted as a variant array. The GetClientArea() method of our VB form returned a two-element variant array, which is received as a straightforward tuple. You can pass arrays easily in both directions. (Bear in mind that if VB returns an array containing strings, they are Unicode strings that must be tidied with the str function).

# access a built-in property of the VB form
idForm.Caption = "Account " + account

#############################################################
# now for the chart drawing - calling our own VB methods�
#############################################################

idForm.ClearChart()    #clear the form

# if the area is too small to do anything with, exit
if width < 1440:
    return
if height < 1440:
    return

#work out the inner drawing rectangle
plotrect = (720,720, width-720, height - 720)

# draw a blue bounding rectangle
idForm.DrawBox(plotrect[0], plotrect[1], plotrect[2], plotrect[3], 0xFF0000,
     0xFFFFFF)

We've omitted the rest of the charting code, but as you can see in Figure 7-6, it works rather nicely.

Writing a Delphi User Interface

Little of what we have seen is specific to Visual Basic. You can call Python COM servers from any language that supports COM, and we know of substantial programs using Visual Basic, Visual Basic for Applications, Visual C++, Delphi and PowerBuilder. We have also implemented a cut-down BookSet browser in Delphi, which is included in the examples for this chapter. We'll show a few edited highlights.

The authors believe that Delphi is a superior language to Visual Basic. However, in a corporate environment the choice is usually made for you. We will try to restrain our comparisons to how each language interacts with our Python COM server.

0117-01.gif
Figure 7-6.
Python drawing charts on VB forms

Instantiating the Server

We declared a variable of type Variant in the main form to hold the COM server. Delphi supports Variants purely in order to support COM, although they go somewhat against the grain in Delphi programming where the type of every variable is specified. The Delphi code to instantiate the server is as follows:

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  try
    BookServer := CreateOleObject('Doubletalk.BookServer');
    StatusBar1.SimpleText := 'BookServer running';
  except
    MessageDlg(
      'An instance of the "Doubletalk.BookServer" COM class ' +
      'could not be created. Make sure that the BookServer application ' +
      'has been registered using a command line. If you have modified ' +
      'the source of the server, make very sure all public methods and ' +
      'attributes are spelled correctly',
      mtError, [mbOk], 0);
    Application.Terminate;
  end;
end;

As with Visual Basic, one line does the work.

Calling Simple Methods

Delphi does exactly as good a job as VB as far as passing simple arguments is concerned. The following example, behind the File ® Open menu item, shows how to call methods of the BookSet with integer and string arguments:

procedure TfrmMain.Open1Click(Sender: TObject);
{prompt for a filename and ask BookSet to load it}
var trancount: integer;
    filename: string;
begin
    if OpenDialog1.Execute then
        begin
        filename := OpenDialog1.FileName;
        BookServer.load(OpenDialog1.FileName);
        trancount := BookServer.Count;
        StatusBar1.SimpleText := Format('Loaded file %s, %d transactions',
                [filename, trancount]
                );
        UpdateAllViews;
        end;
end;

It's legal to declare trancount as an integer and assign the return value of BookServer.Count to it, even though the Delphi compiler can't know the type of the return value; the compiler knows that BookServer is a COM object and decides that type checking is somebody else's problem.

Unpacking Variant Arrays

Delphi's handling of variant arrays is somewhat closer to that of C or C++ than Visual Basic. It provides three functions to find the number of dimensions in a variant array and the high and low bounds of a given dimension. The first of these, VarArrayDimCount, is extremely useful and something VB lacks; the easiest way to find this from VB is to ask for the bounds of higher and higher dimensions until an error occurs. VarArrayHighBound (array, dimension) and VarArrayHighBound (array, dimension) are the equivalents of UBound() and LBound(). Arrays returned from Python always have a lower bound of zero.

If you want to iterate over a 1D array, you can't use a for each loop as in VB; instead you need to find the upper bound of the array. Here's the code to update a list box of accounts:

procedure TfrmMain.UpdateAccountList;
var AccountList: Variant;
    i: integer;
begin
    lstAllAccounts.Items.Clear;

    AccountList := BookServer.GetAccountList;
    for i := 0 to VarArrayHighBound(AccountList, 1) do
        lstAllAccounts.Items.Add(AccountList[i]);
end;

The array has one dimension, so ask for the upper bound of dimension 1.

The expression AccountList[i] returns a Variant, which Delphi coerces to a string when adding to the list box.

Delphi also offers more functions for constructing Variant arrays of given dimensions, which can efficiently pass data to Python.

Callbacks, or the Lack Thereof

Unfortunately this isn't so easy as with Visual Basic. In VB, every user interface object and class module supports automation; that is, all their properties and methods can be accessed from Python with the Dispatch wrapper. Delphi lets you create automation objects that can be accessed in the same way, but it isn't done by default; the compiler just won't let you do the following:

procedure TfrmMain.doCallbackDemo;
begin
    {this just does not work:
    BookServer.doDelphiCallbackDemo(Self);
    }
end;

However, Delphi does provide tools to create automation objects and a wizard to make it easier. With a bit more work, you could explicitly provide a Delphi API for your Python server to call.

As previously noted, we think the use of callbacks is generally a poor design principle; the server is much more useful if not tied to a particular GUI implementation, so this isn't much of a limitation in practice.

Conclusion

In this chapter we have learned how to build a functioning Python COM server, and a graphical client application that uses it. This approach uses each language where it's strongest: Python excels at data manipulation and object-oriented development, whereas Visual Basic allows you to build rich, commercial-quality user interfaces. From the user's viewpoint, there's no way to tell she is using Python.

In addition to building the core of an application in Python, this technique offers an easy way to add a small amount of Python functionality to existing applications. For example, if you have a large VB application and want to use a specific Python tool or library, you could wrap it with a COM server and call it from VB.

We have seen examples of the various data types that can be passed back and forth between the two languages: numbers, strings, and arrays. The ability to pass multidimensional arrays allows you to move large amounts of data between the two languages without writing a lot of conversion code. The exact rules on parameter passing are covered later in Chapter 12, but in general it all works as expected with little effort on your part.

Although we focused on VB, the client can be built in many different development tools. We've given an example in Delphi, and we know of people working in other environments such as PowerBuilder.

Although the application so far is technically interesting, it could have been written (with a bit more work) in VB without any of Python's powerful language features. In the next chapter we develop our application further to offer exciting capabilities only Python can provide.


Back