Automating ChemDraw

I’ve been working on a project in which I needed to generate logP values using ChemDraw 12, for thousands of molecules. Since I didn’t have access to the ChemScript module, I needed a way to automate this procedure. After fiddling around with Visual Studio and various debuggers, I came across the Windows Application Testing Using Python (WATSUP). This is a set of Python methods, built on top of the win32api package that allows one to interact with a Windows GUI programmatically. Thus one can identify a top level window, get a specific control (button, combo box etc) and click on buttons, menu items and so on.

After a little digging around (and liberal use of the dumpWindow() function in WATSUP), I was able to put together a simple bit of code that would load an SDF (containing a single structure) and save it as a CDXML file. For this to work, I make sure that the ChemDraw application is running and the “Chemical Properties” window is visible. On loading an SDF, the chemical properties (stuff like logP, MR, melting point etc) get computed automatically. We then “click” the paste button and then save to CDXML format. In the resultant CDXML file, the chemical property values are included – which can then be easily extracted using a regex. Here’s the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import win32com.client
import win32api
import sys, os, time, datetime, glob, pprint
from watsup.winGuiAuto import *
from watsup.launcher import launchApp,terminateApp

def getButtonControl(toplevel, buttonText):
    elems = dumpWindow(toplevel)
    openButtonHwnd = None
    for elem in elems:
        if len(elem) != 3: continue
        hwnd, name, klass = elem
        if name == buttonText and klass == 'Button':
            openButtonHwnd = hwnd
            break
    openButton = findControls(toplevel, wantedClass="Button", wantedText=buttonText)
    openButton = openButton[openButton.index(openButtonHwnd)]
    return openButton

def setFileName(toplevel, filename):
    fnameCombo = findControl(toplevel, wantedClass='ComboBoxEx32')
    fnameComboEdit = findControl(fnameCombo, wantedClass = "Edit")
    setEditText(fnameComboEdit, filename)
       
filename = "c:\\work\\test.sdf"

form=findTopWindow(wantedText='ChemBioDraw Ultra')
mainMenu = getTopMenu(form)

# click the File->Open menu option
activateMenuItem(form, ('file', 'open'))
time.sleep(0.5)

## OK, we get the file selection window
openWindow = findTopWindow(wantedText='Open')

## get the file type combo box and select the SDF format
ftypeCombo = findControls(openWindow, wantedClass = "ComboBox")    
if len(getComboboxItems(ftypeCombo[0])) > 10: ftypeCombo = ftypeCombo[0]
elif len(getComboboxItems(ftypeCombo[1])) > 10: ftypeCombo = ftypeCombo[1]
else: ftypeCombo = ftypeCombo[2]
selectComboboxItem(ftypeCombo, 14)

## get the filename combo box and set it
setFileName(openWindow, filename)

## get the open button, click it and get the file
openButton = getButtonControl(openWindow, "&Open")
clickButton(openButton)
time.sleep(1)

## get the properties window and then paste in the
## chemical properties that are autocalculated
propWindow = findTopWindow(wantedText = "Chemical Properties", retryInterval = 0.1, maxWait = 5)
pasteButton = findControl(propWindow, wantedClass="Button", wantedText="Paste")
clickButton(pasteButton)

## now save the file as a cdxml
newfilename = getOutputfileName(filename)
activateMenuItem(form, ("file", "save as"))
time.sleep(0.5)
saveWindow = findTopWindow(wantedText='Save As')
setFileName(saveWindow, newfilename)

## set file type
ftypeCombo = findControls(saveWindow, wantedClass = "ComboBox")
if len(getComboboxItems(ftypeCombo[0])) > 10: ftypeCombo = ftypeCombo[0]
elif len(getComboboxItems(ftypeCombo[1])) > 10: ftypeCombo = ftypeCombo[1]
else: ftypeCombo = ftypeCombo[2]
selectComboboxItem(ftypeCombo, 1)

## get the save button and click it to save the file
saveButton = getButtonControl(saveWindow, "&Save")
clickButton(saveButton)
time.sleep(1)

## looks like we have to do a save and then we close
activateMenuItem(form, ['file', 'save'])
activateMenuItem(form, ['file', 'close'])

Obviously this approach is an inelegant hack and is dog slow – approximately 2.5 sec per structure. But in the absence of anything else, it gets the job done.

While implementing this solution there were a few quirks. For example, the widgets contained with in window, represent a hierarchy. The findControls() method traverses this hierarchy, but does not return the controls of the specific type in the same order on consecutive runs. So to find the appropriate combo box (say for the file type) I need to do some extra work (rather than just going with the first of three combo boxes that are located on a Open dialog). One contributing factor to the slowness is that I needed to insert a few sleep statements here and there, to ensure that the proper windows showed up before I started setting values in the various widgets. Finally, for some reason I had to do a “Save As” followed by a “Save” to get the final CDXML file with all the computed properties.

Leave a Reply

Your email address will not be published. Required fields are marked *