wiredfool

Python and Com Components

There is some info out there on how to use com and activex components from python, but most of the walk throughs and howtos focus on using com to communicate with processes that reside in their own address space and don’t need to have an event loop. A good clue that you’ve got this sort of control is if you get a catastrophic failure exception when calling a method in the dll the first time.

One control that I’ve had to deal with needs to be instantiated into some sort of container that has an event loop. In the .NET world, that means inheriting from an AxHost and instantiating the control on a windows form. In the python world, that means either wxPython or WinPython. Since I’m integrating into an existing wxPython app, that’s my choice.

The first hurdle is figuring out the CLSID and actual name of the control. In WinPython, the tools menu has a makepy.py command. It will pop up a browse list of the human readable names of the COM objects that are installed on the system. that script will generate glue in the site-packages/win32com/gen-py directory, with the filename being the CLSID and version of the COM object. Open that file, in the top page there will be a CLSID formatted correctly, and near the bottom there will be a line that contains the Name of the control. In this case, it’s RANGER.RangerCtrl.1.

With that information, the minimal wxApp that worked with my control was this:

\#!/usr/bin/python

import win32com.client
from wxPython import wx 
import wx.activex
import wx.py

class evt:
  def OnTransportNewState(self, newState=None, oldState=None):
    print "Changing State, %s to %s" %(oldState, newState)
  # ...
class mainwindow(wx.Frame):
  def __init__(self):
    wx.Frame.__init__(self, None , wx.ID_ANY, "Test")

    # turns out that this isn't necessary   
    #self.Show()

    ctrl = wx.activex.ActiveXWindow(frame,
                   clsId=wx.activex.CLSID('{1C5C9095-05A8-11D4-9AF9-00104B23E2B1}'),
                   id=-1,
                   name="somename"
                   )

    # the dispatch to send messages to the control
    self.d = win32com.client.Dispatch("RANGER.RangerCtrl.1")
    # Event object with call backs to receive events
    self.e = win32com.client.WithEvents(self.d, evt)

    # this lets you poke around
    self.shell = wx.py.shell.ShellFrame()
    self.shell.Show()
    # and this shouldn't fail badly, at least for this control
    print self.d.GetVersion()

if __name__== '__main__':
  app = wx.PySimpleApp()

  frame = mainwindow()
  app.MainLoop()    

The frame doesn’t need to be shown on screen, but it does need to be created to receive events. This control uses call backs to intercept events, so I’m using an eventbuilder dispatch class to catch those. Both the dispatch object and the ActiveX control are necessary, as it doesn’t appear that the activex control actually gets the methods that are supposed to be included. I’m not sure if this is actually common over controls of this sort, or if it’s a peculiarity of this particular control and it’s structure.

The handlers in the event class can be determined by looking in the makepy.py generated class. In this case, there was a list of about 20, of which I’ve intercepted about half.

The final wrinkle in this excercise is packaging using py2exe. Win32com needs to be added as a package in the setup.py, the MSVCP71.dll C++ runtime needs to be included in the base application directory, and wx.activex is really picky about the working directory when it’s imported. Even when that runtime dll is in the same directory as the C runtime, there are import errors unless I’ve chdir’d into that directory. Since this happend on import, it’s something that needs to happen really early in the application.

In the setup.py script for py2exe, I’ve subclassed the build_exe class following the lead of this page. I’m including the following method:

def plat_finalize(self, modules, py_files, extensions, dlls):
  BuildExe.plat_finalize(self, modules, py_files, extensions, dlls)
  # from http://www.py2exe.org/index.cgi/TixSetup
  cpdll = os.path.join(sys.prefix, 'MSVCP71.dll')
  dlls.add(cpdll)
  self.dlls_in_exedir.append( 'msvcp71.dll')

And finally, before I import wx.activex,

if hasattr(sys, 'frozen'):
  ### when running from py2exe and we're run from a shortcut, then
  ### when we import the wx.activex, we need to be in the same directory
  ### as the msvcp71.dll
  if os.path.dirname(sys.executable) != os.path.abspath(os.path.curdir):
    os.chdir(os.path.dirname(sys.executable))
No comments

No comments yet. Be the first.

Leave a reply

You must be logged in to post a comment.