tcom

tcom , maintained by Chin Huang provides both client and server COM programmability.

News

PO 2012-11-17: See my Work-In_Progress presentation at the EuroTcl 2012. The proposed Cawt package is available at https://sourceforge.net/projects/cawt/

Attributes

current version
3.9
website
http://www.vex.net/~cthuang/tcom/

See Also

https://stackoverflow.com/questions/16839030/invalid-argument-when-using-tcom-from-tcl-script-activetcl

Compiling TCom
Tcom examples for Microsoft Excel
TcomOffice
tcom Allows Emacs as Editor for MS Outlook
Web automatic testing using TCOM extention
Printing DYMO Labels with Tcl and tcom
Matthias Hoffmann - Tcl-Code-Snippets - tcom & wmi - Examples
tcom Allows Emacs as Editor for MS Outlook
MNO I put together a really simple set of scripts/Emacs Lisp at: The functionality should be obvious from the title ;-)
How one discovers the API for a COM-exporting application
Stefan Vogel's page
explains on his page how to convert MSProject-Files to CSV-format and how to use the IE-object with callbacks via ::tcom::bind
Stefan Vogel's page
has interesting uses of bind
Howto export Microsoft Outlook contacts to XML using tcom and tDom
Running WEAP from Tcl with tcom
An example for the water planning software WEAP:
COMet
a COM explorer for Tcl that relies on tcom. Designed to discover and explore COM interfaces.
ADO (Visual Fox Pro oledb) - read .dbf with tcom
Advanced browser management
an example of using the ::tcom::ref getactiveobject command to get a reference to an already running instance of Internet Explorer.
tcom
Another tcom (humor)
Instancier des objets COM avec Tcom
TCOM package - 800700a4 - No more threads can be created in the system.
tcom server
Create com objects using tcl

Remote exec with tcom and WMI

Automating JMP via Tcl and tcom (PDF)

Download

http://www.vex.net/~cthuang/tcom/

Since March 2002, tcom is a part of the ActiveTcl Batteries Included distribution.

tcom was previously available as a static runtime, without the dependency on an external DLL. Any volunteers to provide that again?

Improved TCOM extension , Victor Wagner

jbr 2013-08-27: I've forked tcom on github and added features to allow passing binary buffers as byte arrays.

Installation

Place tcom/lib/Banking and tcom/lib/tcom a location that Tcl looks in when searching for libraries.

The binary requires MSVCP60.DLL, the Visual C++ 6.0 runtime. If another Windows application didn't already install it, see How to obtain the Visual C++ 6.0 run-time components

At that point, it should be possible to

package require tcom

Description

tcom is only available for Windows.

Many of its users agree that "this package has been incredibly helpful" (to quote one comp.lang.tcl posting).

Question: TclScript (Unanswered)

Version 3.8 includes an experimental implementation of a Tcl Active Scripting engine. The engine is written mostly in incr Tcl using tcom's COM server framework.

IIS might be able to interpret this ASP:

<%@ LANGUAGE = "TclScript" %>
<%
    Response write "Hello world!"
%>

Chin Huang: reports that using incr tcl 3.8b3, one can do the following in Internet Explorer:

<html>
    <head>
        <title>TclScript Test</title>
        <script language="TclScript">
            proc setText {newValue} {
                text1 value $newValue
            }
        </script>
    </head>
    <body>
        <input id="text1" name="text1" disabled="1">
        <p>
        <input type="button" id="button1" name="button1" value="Hello"
            onclick="setText hello">
        <input type="button" id="button2" name="button2" value="World"
            onclick="setText world">
    </body>
</html>

which is likely enough support to replace JScript with Tcl.

MHo 2006-04-04: Is there a equivalent way for this to work with Mozilla?

Type Conversion

phk: type conversion can be tricky

method Export needs a boolean value as argument

$rep Export 0
> 0x80020005 {Unknown error}

As Ronald Dauster showed me, there is a way to force a var to be boolean type (Tcl internally):

set FALSE false
if {$FALSE} {}

$rep Export $FALSE

nl: try this

proc FALSE {} {return [expr 1 == 0]}
proc TRUE {}  {return [expr 1 == 1]}
proc MAYBE {} {return [expr ([clock seconds]%2) == 0]}; #:-)
$rep Export FALSE

glennj: You probably meant

$rep Export [FALSE]

Null

If you need a NULL, use the command:

::tcom::null

It returns a token that tcom will interpret as a NULL.

empty string

If you have issues where "" is not accepted as the empty string, try: string range " " 0 0.

Speed Considerations

Alan Grunwald 2012-11-14:

I use tcom now and again to export the contents of a tablelist to a Word document and have always just gritted my teeth and put up with how slow it is. However, today I discovered Automating Word Tables for Data Insertion and Extraction , describing how to speed things up by entering the data as delimited text and the converting to a table. It's LOTS faster.

I also export tablist contents to Excel, and doing them one cell at a time is also really slow. Buoyed by my success with Word I found How to transfer data to an Excel workbook by using Visual Basic .NET , describing how to speed that up, which looks as though it sends an array to a multi-cell range. Does anyone know how I would set up a VB-style array from Tcl to use this method?

APN: The very latest TcomOffice package (0.5a3) has an example of this (sending arrays to a cell range). Unfortunately I don't remember exactly where. You will have to look through the examples and test scripts.

Alan Grunwald: Thanks Ashok, I'll have a look for the example.

Terminating a COM Server

Tip: To terminate a COM server cleanly, you must drop all references. If you stored the handle in a local variable, the reference is released when execution leaves the variable's scope. If you stored the handle in a global variable, you can release the reference by unsetting the variable, setting the variable to another value, or exiting the Tcl interpreter. Some applications, such as Excel, require you explicitly terminate them by calling "$application Quit", otherwise they continue to run even after dropping all references.

Out-of-Process COM objects

Klaus Maria Hansen asked about out-of-process COM objects. Chin Huang answered that, yes, tcom can create these:

From a tclsh, create the COM object and register it in the running object table using the "::tcom::object create -registeractive" command. Keep this tclsh running for as long as you want the COM object to be alive.
To access your COM object, client programs must get a reference to it using the "::tcom::ref getactiveobject" command.

Binary Data

Laurent Riesterer 2004-03-25:

I had a struggle to have a COM object correctly accepting binary blob. Here is a method which works

set data [binary format "II" 1 2]
append data "someinfo"
$comObject method $data

The [binary format] object creates a ByteArray Tcl_Obj type and if you do not convert it to a string (be careful to use string bytelength to get the length as string length changes the type), then tcom smartly detects the ByteArray and send a type VT_ARRAY|VT_UI1 to the COM object.

PYK 2013-10-19: Whatever the case may have been in the past, currently [string length] is smart enough to to force a string conversion of ByteArray Tcl_obj types if they are pure (no associated string representation). Therefore, if the length is needed, [string length] is the thing to use.

Empty String: Type Conflict

Error '0x80020005 Type conflict' when using empty string as parameter

HaO 2012-12-18: When I call a method with an empty string, I often get the error:

% $h SetValue {}
0x80020005 Typkonflikt

To cure this, one may use:

% $h SetValue [::tcom::variant bstr {}]

::tcom::import

The ::tcom::import command creates new commands using the type information from a type library. The ::tcom::import command returns the namespace in which it created the new commands. By default, the namespace is the type library name. This example assumes ComponentABC.dll contains a type library named ComponentABCLib:

% ::tcom::import ComponentABC.dll
ComponentABCLib

To list the commands ::tcom::import created:

% info commands ComponentABCLib::*
::ComponentABCLib::Interface1 ::ComponentABCLib::Interface2
::ComponentABCLib::ObjectXYZ

This command creates an object and obtains an interface pointer to the object's default interface:

set object [::ComponentABCLib::ObjectXYZ]

Given an object reference, you can query for specific interfaces:

set interface1 [::ComponentABCLib::Interface1 $object]
set interface2 [::ComponentABCLib::Interface2 $object]

Examples

trade ideas, for developers
A complete example. This Tcl code uses tcom to receive, augment, and display streaming real-time data from an ActiveX control.

In a c.l.t. posting, Chin Huang offers this illuminating example of use of tcom to retrieve Office document properties:

package require tcom

set filePropReader [::tcom::ref createobject DSOleFile.PropertyReader]
set properties [$filePropReader GetDocumentProperties {c:\tst.xls}]
puts [$properties Name]
puts [$properties AppName]

This example uses a COM object server which you can download from http://support.microsoft.com/support/kb/articles/Q224/3/51.ASP .


Martin Lemburg demonstrates with

% package require tcom
3.3
% set excel [::tcom::ref createobj Excel.Application]
::tcom::handle0x01337EF0
% $excel Run "Module!Macro" arg1 arg2 arg3 ... argn
result

how easy it is to invoke an Excel macro with Excel's built-in "Run" method. He notes, however, that "The only complication is to find out the 'path' to that macro. The XLA or XLM file or the workbook or worksheet containing the macro must be opened or given in that path!"

Note that it's sometimes beneficial/necessary/advisable/... to

$excel Visible 1

Here's how to dump an Excel chart to an image file:

set excel [tcom::ref createobj Excel.Application]
set Workbooks [$excel Workbooks]
set Book [$Workbooks Open {C:\path\to\my_spreadsheet.xlsx}]
set worksheets [$Book Worksheets]
[[[[$worksheets Item "My charts"] ChartObjects] Item [expr 1]] Chart] Export {C:\some\path\dumpchart.png}

I'm sure the last line looks ugly to many people, so here is a suggestion:

proc evalChain {args} {
    set argStr [join $args]
    set newArgs [list]
    while {[set idx [string first -> $argStr]] >= 0} {
        lappend newArgs [string range $argStr 0 [expr {$idx - 1}]]
        set argStr [string range $argStr [expr {$idx + 2}] end]
    }
    lappend newArgs $argStr
    set result ""
    foreach arg $newArgs {
        set result [uplevel 1 $result $arg]
    }
    return $result
}

Now you can do:

evalChain $worksheets Item Chart->ChartObjects->Item \[expr 1\]->Chart->Export {C:\\some\\path\\dumpchart.png}

Hmmm, maybe not a huge improvement... :-(


Fabrice Pardo offers this slightly more elaborate example of Excel invocation:

set application [::tcom::ref createobject Excel.Application]
$application Visible 1  

set workbooks [$application Workbooks]
set workbook [$workbooks Add]
set worksheets [$workbook Worksheets]
set worksheet [$worksheets Item [expr 1]]

set cells [$worksheet Cells]
set i 0
foreach row {1 2 3} {
    foreach column {A B C} {
        $cells Item $row $column [incr i]
    }
}

$workbook SaveAs {c:\tst.xls}
$application Quit

Sacha Schär shows how to add something into an existing workbook:

package require tcom

set excel [::tcom::ref createobj Excel.Application]
#$excel Visible 1

set workbooks [$excel Workbooks]
set workbook [$workbooks Open {c:\tst.xls}]
set worksheets [$workbook Worksheets]
set worksheet [$worksheets Item [expr 1]]

set cells [$worksheet Cells]
$cells Item 2 B {my additional text}

$workbook Save
#$workbook SaveAs {c:\tst2.xls}

$excel Quit

Many COM methods declare specific data types for their parameters. For example, if a method declares an int parameter, then tcom converts the argument to the native integer representation when calling the method. Some methods declare parameters of type VARIANT, which can hold different data types, such as string, int, or double. Hopefully, the method can interpret the argument in whatever form of data type is passed in the VARIANT. However, some methods interpret the argument based on the data type in the VARIANT. For example, the Excel method to access a cell declares the column parameter as a VARIANT. If a string is passed in the column parameter, the method considers it to specify the name of a column (A, B, C, ...). If an int is passed, the method interprets it as a numeric column index (based from 1). You might be tempted to address a column in Excel by number:

$cells Item $row 1 "Hello"

This works if the internal representation of the literal value 1 is an integer. In this case, tcom passes an integer value to the Item method, which considers the integer value to be a numeric column index. However, the bytecode compiler keeps a single copy of each literal value in a Tcl script or procedure body. Suppose you execute a command which changes the internal representation of the literal value 1 to a string. If you invoke the Item method after that, tcom passes a string consisting of the single character "1" to the method. The Item method interprets a string argument as the name of a column (A, B, C, ...), but there is no column named "1", so it returns error code 0x800a03ec. It's good style to address columns using letters instead. If you really want to address columns by number, you can ensure you're passing an integer value by doing:

$cells Item $row [expr 1] "Hello"

Lars H: This sounds like a classical example of what goes wrong when an extension ignores the everything is a string paradigm. If the string 1 and the int 1 may have different semantics, then there should on the Tcl side be distinct strings representing the two (e.g. "string 1" and "int 1", probably with a preferred interpretation as lists). Heuristics for trying to figure out what was the intent for an ambiguous value such as "1" are OK, but they must not depend on the internal representation.

HaO 2012-09-03: maybe, the command ::tcom::variant may help (see tcom server):

$cells Item $row [::tcom::variant int 1] "Hello"

Another example from Chin Huang, in this case to add a (new) worksheet at the end of the workbook:

package require tcom
set application [::tcom::ref createobject "Excel.Application"]
set workbooks [$application Workbooks]
set workbook [$workbooks Add]
set worksheets [$workbook Worksheets]
set lastWorksheet [$worksheets Item [$worksheets Count]]
set worksheet [$worksheets Add [::tcom::na] $lastWorksheet]

Example: Open MSWord

David Bigelow explained in comp.lang.tcl how to open MSWord with tcom:

package require tcom

set ::application [::tcom::ref createobject "Word.Application"]
$::application Visible 1

set ::docs [$::application Documents]
set ::doc [$::docs Open "[pwd]/test.doc" [::tcom::na] [expr 1]]
 # NOTE: the [expr 1] option in this example toggles the file to read only.

Example: Insert Table into Word

Miko howto insert a table in a new Word document:

set ::application [::tcom::ref createobject "Word.Application"]
$::application Visible 1
set ::docs [$::application Documents]
set ::doc [$::docs Add  ]
#ActiveDocument.Tables.Add Range:=Selection.Range, NumRows:=2, NumColumns:= 3
#translation:
[$::doc Tables] Add  [[$::application Selection] Range]  [set NumRows  2] [set NumColumns  3]

Example: Extract Data from Excel

AM 2004-05-11: I have always stayed far away from COM objects and the like, but as users keep asking about ways to import MS Excel files to get their data into a user-interface, I decided to have a look at tcom. With the help of this page, I came up with the following simple script to extract data from a haphazard Excel file:

# Extract some cells from an Excel file
#
package require tcom

set app [::tcom::ref getobject [file nativename [file join [pwd] "test_tcom.xls"]]]
set ws  [$app Worksheets]
set ws1 [$ws  Item [expr 1]]

set cells [$ws1 Cells]

foreach row {1 2 3 4} {
   foreach col {A B C} {
      set cell [$cells Item $row $col]
      puts "Row/column ($row/$col): [$cell Value]"
   } 
}
$app Close

Some comments:

  • MS Excel complained about the file not existing at first: it does not "know" about the current directory that the Tcl script is using.
  • Most tcom commands return some sort of handle, so you have to go down the object hierarchy to finally get at the data.

But once you are over this hurdle, it is very nice to be able to deal with such arcane stuff! (The real problem now is: what data are going to be passed from the Excel sheet to my user-interface?)

Example: Create a Chart In Excel

ramsan proposes how to create a chart in Excel (assuming you have already connected with the commands found previously in this page):

set chart [[$application Charts] Add]
$chart ChartType 65 ;# xlLineMarkers
# this is equal to: set range [$cells Range "D4:G7"]
set range [$cells Range [$cells Item 1 [expr 4]] [$cells Item 5 [expr 7]]]
$chart SetSourceData $range 2 ;# xlColumns
set xrange [$cells Range [$cells Item 2 [expr 3]] [$cells Item 5 [expr 3]]]
for { set i [expr 1] } { $i <= 4 } { incr i } {
    [[$chart SeriesCollection] Item $i] XValues $xrange
}
set axe [[$chart Axes] Item 1]    ;# xlPrimary
$axe HasTitle True
[[$axe AxisTitle] Characters] Text "Nº nodos"

set axe [[$chart Axes] Item 2]    ;# xlSecondary
$axe MinimumScaleIsAuto True
$axe MaximumScale 0.04
$axe HasTitle True

[[$axe AxisTitle] Characters] Text "Error(%)"
$chart HasTitle True
[[$chart ChartTitle] Characters] Text "Comparación resultados"
#$chart Location 2  "Hoja1" ;# xlLocationAsObject
[$chart ChartArea] Copy ;# copy the image of the chart to clipboard

When you want to modify the example, you will need to learn additional Excel com functions and constants. You can learn more inside Excel Tools->Macros->Visual Basic Editor and check the Help or the objects browser. Maybe you need to install additional help from the installation disk.

Example: Close Excel

PataElmo 2007-11-28: I have some interesting code for handling Excel through the tcom interface. This little proc allows you to control Excel close to how you would in VB. Could also be useful for other microsoft VB Apps.

proc ExcelCmd {App command} {
    set clist [split [regsub -all {\(([^()]+)\)} $command { \1}] .]
    set string $App
    while {[llength $clist]>0} {
        if {[llength [lindex $clist 0]]>1} {
            if {[expr {! [catch {expr {int([lindex [lindex $clist 0] 1])}}]}]} {
                set string [$string [lindex [lindex $clist 0] 0] [expr [lindex [lindex $clist 0] 1]]]
            } else {
                set string [$string [lindex [lindex $clist 0] 0] [lindex [lindex $clist 0] 1]]
            }
        } else {
            set string [$string [lindex $clist 0]]
        }
        set clist [lreplace $clist 0 0]
    }
    return $string
}

Usage:

set App [::tcom::ref createobject Excel.Application]
ExcelCmd $App Visible(0)
ExcelCmd $App Workbooks.Add
ExcelCmd $App Workbooks.Item(1).Worksheets.Item(2).Delete
ExcelCmd $App Workbooks.Item(1).Worksheets.Item(2).Delete
ExcelCmd $App Workbooks.Item(1).Worksheets.Item(1).Name("First")
ExcelCmd $App Visible(1)

Tyzio 2008-01-09: I took the code from PataElmo and improved it a little bit to be more universal:

proc ExcelCmd {{App ""} {command ""} } {
    if {$App eq "" && $command eq ""} {
        return -7
    }
    if {$App eq ""} {
        set App [::tcom::ref createobject Excel.Application]
    } elseif {$command eq ""} {
        set command $App
        set App [::tcom::ref createobject Excel.Application]
    }
    
    # Remove ()
    set command [regsub -all {\(([^()]+)\)} $command { \1}]
    
    # Clean the command line to have good quote chars and remove colons which are not string part
    set temp [regsub -all {(\".*),(.*\")} $command \\1\xd7\\2] 
    set temp [string map [list {, } { } {,} { } \xd7 {,} {\"} \\" \" {\"}] $temp]
    
    set clist [split $temp .]
    set string $App
    while {[llength $clist]>0} {
        if {[llength [lindex $clist 0]]>1} {
            foreach c [lrange [lindex $clist 0] 1 end] {
                if {[string is integer $c]} {
                    lappend cl "\[expr $c\]"
                } else {
                    lappend cl $c
                }
            } 
            set string [eval $string [lindex $clist 0 0] [join $cl]]
            unset cl
        } else {
            set string [$string [lindex $clist 0]]
        }
        set clist [lreplace $clist 0 0]
    }
    return $string
}

Usage (Create Pie Chart):

set App [::tcom::ref createobject Excel.Application]
set chtOb [ExcelCmd $App {ActiveSheet.ChartObjects.Add(100, 100, 250, 175)}]
ExcelCmd  $chtOb {Chart.ChartType(5)}
ExcelCmd  $chtOb {Chart.SeriesCollection.NewSeries}
set serCol [ExcelCmd  $chtOb {Chart.SeriesCollection(1)}]
set range [ExcelCmd  $App {ActiveSheet.Range(A2,F2).Value}]
$serCol XValues $range
set range [ExcelCmd  $App {ActiveSheet.Range(A3,F3).Value}]
$serCol Values $range
$serCol Name {CHART PIE}

Example: ROT object

christopher.owens at analog.com 2004-04-14: It looks as though all you need to do to communicate with an object from the ROT (Windows' "Running Object Table") is use

::tcom::ref getactiveobject "Appname.Application"

instead of

::tcom::ref createobject "Appname.Application"

You apparently would want to do this in cases where the object is a DLL that's already in use by an application, but also presents a COM interface.

Example: Microsoft Dictionary Object

Scott Nichols:

Below is an example on how to access Microsoft's VB dictionary object using tcom. It is very similar to Perl's associative array or Tcl's array, except it seems to only support one value per key. I have not tested its performance.

package require tcom

# Create the Dictionary -- Similar to a PERL associative array.
set d [::tcom::ref createobj scripting.dictionary]

# Add some key value pairs
$d Add keyboard Logitech
$d Add motherboard Asus

# Check if an item exists then add it.
if { ! [$d Exists memory] } {$d Add memory PNY}

# Retrieve all items
set items [$d Items]

# Retrieve all keys 
set keys [$d Keys]

# Replace an Item's value with a new value
if { [$d Exists motherboard]} {$d Item motherboard HP}

For more information on how to use Microsoft's Dictionary object please read: http://www.rickclark.org/articles/dicobj.pdf

Example: Outlook Contacts

LEM: This piece of code retrieves the full name of contact number one in your Outlook contacts list. Tested on Outlook 2002.

package require tcom
set outlook [::tcom::ref getactiveobject Outlook.Application]
set outlookinfo [::tcom::info interface $outlook]
# Prints long list of available methods, one of which is GetNamespace
$outlookinfo methods
#Magic string MAPI found on MSDN - only valid argument
set ns [$outlook GetNamespace "MAPI"]
set nsinfo [::tcom::info interface $ns]
#Prints long list of methods, GetDefaultFolder is one of them
$nsinfo methods
# The magic number 10 means the Contact folder
set contacts [$ns GetDefaultFolder 10]
set items [$contacts Items]
set item [$items Item 1]
set iteminfo [::tcom::info interface $item]
# Prints long list of available properties, including FullName
$iteminfo properties
$item -get FullName

Example: Outlook Contacts (2)

LEM: I have elaborated somewhat on my Outlook Contacts code

# Features:
#   Starts Outlook if not already running.
#   Retrieves all your contacts and creates one button for each
#   If you make a change to your contacts the gui updates
# Tested on:
#   TCOM 3.8, TCL/TK 8.4.7, WinXP, Outlook 2002
# Bugs:
#   I believe the handle passed to the "item" procedure is
#   dead because tcom automatically release references when
#   moving out of a variable's scope.  

proc log {string} {if {$::debug} {.t insert end "$string"}}
# Two procedures to see which properties and methods are available on an object
proc getproperties {handle} {set infohandle [::tcom::info interface $handle];$infohandle properties}
proc getmethods    {handle} {set infohandle [::tcom::info interface $handle];$infohandle methods}

proc eventhandler {args} {
    set event  [lindex $args 0]
    set handle [lindex $args 1]
    if       {$event == "ItemAdd"}    {item add    $handle
    } elseif {$event == "ItemChange"} {item change $handle
    } elseif {$event == "ItemRemove"} {item remove $handle
    } else   {
        log "Unrecognized event: $args\n"
    }
}

proc item {action handle} {
    log "Event: $action, $handle\n"
    # Could do something smart, but for now just redraw GUI completely
    draw_gui .f
}

proc init_outlook {} {
    if {[catch {set outlook [::tcom::ref getactiveobject "Outlook.Application"]} msg]} {
        log "Outlook not running, trying to start\n"
        if {[catch {set outlook [::tcom::ref createobject "Outlook.Application"]} msg]} {
            log "Failed starting Outlook: $msg\n"
        } else {
            log "Outlook started OK\n"
        }
    }

    # The magic string MAPI was found on MSDN - only valid argument
    set ns [$outlook GetNamespace "MAPI"]
    # The magic number 10 means the Contact folder, refer MSDN
    set contacts [$ns GetDefaultFolder 10]
    # Must keep items global, if not the reference is released by tcom
    set ::items [$contacts Items]
    # Bind to changes on the items collection
    ::tcom::bind $::items eventhandler
}

proc draw_gui {w} {
    if {[winfo exists $w.f]} {
        destroy $w.f
    }
    pack [frame $w.f] -fill x -expand yes
    set i 0
    # $items is a handle to a collection object. Thus we can use ::tcom::foreach.
    ::tcom::foreach item $::items {
        pack [button $w.f.b[incr i] -anchor w -text [$item -get FullName]] -side top -fill x -expand yes
    }
}

package require tcom
set debug 1
pack [frame .f] -fill x
if {$::debug} {pack [text .t] -fill both -expand yes}
init_outlook
draw_gui .f
wm title . Contacts
bind . <Control-c> "console show"

Example: Form an Email

Arnt Witteveen:

I put together this little script to create an email with some content. (I leave the email open here, but you could take it from here to send automatically etc.)

As far as developing this goes, I based this on the scripts by LEM above, and on Outlook Object Model . If MSDN moves everything around again, just search for "outlook object model". You'll find the outlook 2007 object model as well that way.

And I can recommend COMet also referenced below, esp. the starpack. Once COMet is started, look for the Outlook.Application COM objects in the tree, click 'create user object', name it testOutlook or something, then under its methods find 'CreateItem', Invoke this method, give it 0 as parameter and name the object testMail, and then on testMail find and invoke the Display method, then find and change the body property (or invoke the Body method instead), and that is pretty much what the script below does, done interactively. Great for testing stuff.

Anyway, on to the script:

package require tcom

proc removeNumbers { inString } {
  return [regsub -line -all -- "^\[0-9\]*" $inString " " ]
}

proc formatMethods { inInterface } {
  set theList [removeNumbers [join [$inInterface methods] "\n"] ]
  return [regsub -line -all -expanded -- {^\s*(\S*)\s*} $theList "\\0\n    " ]
}

proc listMethods { inObject } {
  set theInterface [::tcom::info interface $inObject]
  puts "************************************"
  puts -nonewline "Methods for interface [$theInterface name]:\n"
  puts [formatMethods $theInterface]

  #also list properties
  puts -nonewline "Properties for interface [$theInterface name]:\n  "
  puts [regsub -line -all -- "^\[0-9\]*" [join [$theInterface properties] "\n"] " " ]
}

if {[catch {set outlook [::tcom::ref getactiveobject "Outlook.Application"]} msg]} {
  puts "Outlook not running, trying to start\n"
  exit -1
    if {[catch {set outlook [::tcom::ref createobject "Outlook.Application"]} msg]} {
      puts "Failed starting Outlook: $msg\n"
      exit -1
    } else {
  }
}

set outlook [::tcom::ref getactiveobject Outlook.Application]

# Prints long list of available methods, one of which is GetNamespace
listMethods $outlook

set theNewMailItem [$outlook CreateItem 0]
listMethods $theNewMailItem

$theNewMailItem Display
$theNewMailItem Body "aaa\nbbb"
$theNewMailItem Subject "testtest"

Example: PowerPoint: Export Slides to Images

Beware 2011-08-24:

set r "your powerpoint file"
set pp [::tcom::ref createobject "PowerPoint.Application"]
$pp Visible 1
set filename [file nativename [file normalize $r]]
set pres [$pp Presentations]
set p [$pres Open $filename]
set slides [$p Slides]
set slide1 [$slides Item [expr 1]]
$slide1 Export [file nativename [file normalize "image.gif"]] GIF [expr 120] [expr 90]
$p Close
$pp Quit
unset pp

DNS Server/Zone access using TCOM via WMI.

ASG 2011-09-02 06:47:34:

Below are Tcl procedures that use tcom to create, list and remove a DNS Zone using WMI using the ConnectServer mechanism (as apposed to using a WMI moniker). These examples expect the DNS server to be integrated with an Active Directory. If your server is not using AD, change the line $objClassProperties Item DsIntegrated Value 1 to $objClassProperties Item DsIntegrated Value 0.

package require tcom

proc listZones {WMI_DNSServer DNSUsername DNSPassword DomainToList} {

    set ::dotString [ string repeat " ." 15 ]

    proc FormatText {text} {
        return [ string replace $::dotString 0 [expr {[ string length $text ]-1}] $text ]
    }

    set wbemFlagReturnImmediately [scan \x10 %c]

    # only use the following when you don't won't need to check Count property.
    # it reduces the WMI memory resource usage when processing a large collection of objects.
    set wbemFlagForwardOnly       [scan \x20 %c]

    set objSWbemLocator [::tcom::ref createobject "WbemScripting.SWbemLocator"]
    set objSWbemServices [$objSWbemLocator ConnectServer $WMI_DNSServer {\root\MicrosoftDNS} $DNSUsername $DNSPassword]

    set colItems [$objSWbemServices ExecQuery "SELECT * FROM MicrosoftDNS_Zone WHERE ContainerName='$DomainToList'" WQL \
                 [expr {$wbemFlagReturnImmediately}]]

    set countVal [$colItems Count]
    puts "ExecQuery returned $countVal items"

    ::tcom::foreach objItem $colItems {
        set propSet [ $objItem Properties_ ]
        puts "  [ FormatText "Name" ] : [ [ $propSet Item  Name ] Value ]"
        puts "  [ FormatText "ZoneType" ] : [ [ $propSet Item  ZoneType ] Value ]"
        puts "  [ FormatText "MasterServers" ] : [ [ $propSet Item  MasterServers ] Value ]"
    }        
 }

proc createZone {WMI_DNSServer DNSUsername DNSPassword ipAddress domainNames } {

    foreach singleAddress $ipAddress {
            lappend ipAddresses $singleAddress
    }

    set objSWbemLocator [::tcom::ref createobject "WbemScripting.SWbemLocator"]
    set objSWbemServices [$objSWbemLocator ConnectServer $WMI_DNSServer {\root\MicrosoftDNS} $DNSUsername $DNSPassword]
    set objDNSServer [$objSWbemServices Get {MicrosoftDNS_Server.name='"."'}]                
    set objDNSZone [$objSWbemServices Get {MicrosoftDNS_Zone}]

    # Spawn an instance of the input parameters that are expected for the 'CreateZone' method
    set objInParametersSpawned [[[[$objDNSZone Methods_] Item CreateZone] InParameters] SpawnInstance_]

    # Not sure why, but for some reason we need to manually add the expected parameters when using tcom and TCL.
    # So create a object of SWbemPropertySet.
    set objClassProperties [$objInParametersSpawned Properties_]

    # Add the required properties, specifying the appropriate CIMtypes
    $objClassProperties Add ZoneName 8 ; # 8 = string
    $objClassProperties Add ZoneType 19 ; # 19 = UINT32
    $objClassProperties Add DsIntegrated 11 ; # 11 = boolean
    $objClassProperties Add IpAddr 8 1 ; # 8 = string, 1 = array

    # ZoneName = String representing the name of the zone.
    [$objClassProperties Item ZoneName] Value $domainNames 
    # ZoneType = Zone forwarder.
    [$objClassProperties Item ZoneType] Value 3                                         
    # DsIntegrated = Indicates whether zone data is stored in the Active Directory or in files. 
    # If TRUE, the data is stored in the Active Directory; if FALSE, the data is stored in files.
    [$objClassProperties Item DsIntegrated] Value 1
    # IpAddr = IP address(es) of the master DNS Server for the zone. This parameter is an Array type.
       [$objClassProperties Item IpAddr] Value $ipAddresses

       set objOutputParams [$objDNSZone ExecMethod_ CreateZone $objInParametersSpawned]

}

proc removeZone {WMI_DNSServer DNSUsername DNSPassword DomainToRemove } {

    set wbemFlagReturnImmediately [scan \x10 %c]
    set objSWbemLocator [::tcom::ref createobject "WbemScripting.SWbemLocator"]
    set objSWbemServices [$objSWbemLocator ConnectServer $WMI_DNSServer {\root\MicrosoftDNS} $DNSUsername $DNSPassword]

    set colItems [$objSWbemServices ExecQuery "SELECT * FROM MicrosoftDNS_Zone WHERE ContainerName='$DomainToRemove' AND Name='$DomainToRemove' AND ZoneType=4" WQL \
                 [expr {$wbemFlagReturnImmediately}]]

    # Due to our selective Query there is most likely only one item in the set,
    # however using a foreach this is an easy way to get at it.
    ::tcom::foreach objItem $colItems {                
        $objItem Delete_ ;# not sure how to get at the documented Err object. Delete_ doesn't return anything.
    }   
}

createZone 192.168.50.2 {NERO\Steve} {Password01} {202.203.204.205 202.203.204.206} blue.yahoo.com
listZones 192.168.50.2 {NERO\Steve} {Password01} blue.yahoo.com
removeZone  192.168.50.2 {NERO\Steve} {Password01} blue.yahoo.com 

Example: Automate VideoRedo

ET: Here is an example script I use which automates the commerical scan of an mpg video using VideoRedo (shareware):

The header below turns this program into a droplet (see here: Droplets) and so the program is stored in a .bat file. Then one can drop an mpg file onto this .bat file icon and the result will be 2 files, a project file with all the commericals VideoRedo thinks it found set in cut mode, and then it will output an edited file using the same basename. The if 1 at the beginning is so one can easily edit the file to switch between running in the background or foreground where one can watch what it is doing.

::if 0 {
start wish "%~f0" %* & exit
:: }

wm wi .
proc wait {time} {
      for {set n 0} {$n < 50} {incr n} {
      after [expr ( $time / 50 )]
      update  
    } 
}

proc say {x} {
    tk_messageBox -message "say-------------\n$x" 
}

proc doit {file} {
    package require tcom
    if 1 {
        set vrds [::tcom::ref createobject VideoReDo.VideoReDosilent]
        set vrd [$vrds VRDInterface]
    } else {
        set vrd [::tcom::ref createobject VideoReDo.Application]
    }
    global text1 text2
    
    #console show
    
    toplevel .top
    entry .top.e1 -textvariable text1
    entry .top.e2 -textvariable text2
    button .top.b1 -text quit -command exit
    
    pack .top.e1 .top.e2 .top.b1 -expand yes -fill both -side top
    set text1 phase
    set text2 123
    
    set n 1
    set outfile "[file rootname $file]_adscanned$n[file extension $file]"
    set proj "[file rootname $file]_ad_proj$n.Vprj"
    #say "ok\n   $outfile    "
    
    $vrd FileOpen $file
    $vrd StartAdScan 1 1 0
    
    set n 0 ; set text1 scan
    while true {
        puts "scan $n"
        set text2 $n
        if {[$vrd IsScanInProgress] == 0} break
        wait 1000
        incr n
    }
    puts "done scan"
    
    $vrd WriteProjectFile $proj
    #say "$proj"
    
    $vrd Filesaveasex $outfile 1
    
    set n 0 ; set text1 output
    while true {
        puts "out $n"
        set text2 "$n          [expr int([$vrd OutputPercentComplete])] %"
        if {[$vrd IsOutputInprogress] == 0} break
        wait 1000
        incr n
    }
    puts "done output"
    set text1 done
    bell
    update
    
    unset vrd ;# closes com connection and vrd exits
    unset vrds ;# closes com connection and vrd exits
    wait 15000
    return
    
    
    #$vrd -get AudioAlert
    #$vrd -set AudioAlert false

    #$vrds VersionNumber
     

}

if {$argc > 0} {
    set f [lindex $argv 0]
    #tk_messageBox -message "file is:\n$f" 
    doit $f
}
exit

Bug: Characters Beyond Basic ASCII (fixed in 2003)

Unfortunately, in german versions of Windows 2000, one have to deal with 'Umlauts' (Ä, Ö, Ü etc.). So the following code fragment leads to an error:

::tcom::ref getobject "WinNT://$domain/$group,group"

if $group contains Domänen-Admins, for example. Does anyone know if this has something to do with encodings, or with the implementation of tcom, or whatever?

Matthias Hoffmann 2003-10-29: VBScript with its internal COM-Access has no problems under such circumstances. TCom 3.9b11 fixes this problem.

tcom and optcl

LES 2005-07-18: This extension is very interesting, but I find it difficult to use. I wish the docs had a lot more examples.

Can someone show how to declare VBA variables and assign values to them? For example:

Selection.Find.Font.ColorIndex = wdAuto

LES 2005-08-03: After no less than 3 days googling with no reward and struggling more with tcom than with Word, I found the solution to my problem: use a numeric value instead of the "literal" value:

set  ::hSELECTION  [$::hWORD  Selection]
set  ::hSELECTIONFIND  [$::hSELECTION  Find]
set  ::hSELECTIONFINDFONT  [$::hSELECTIONFIND  Font]
$::hSELECTIONFINDFONT  ColorIndex  [expr 0]

In other words, ColorIndex [expr 0] works, but ColorIndex wdAuto doesn't. I don't know why. It is also unclear to me why all the examples above make use of [expr] to declare a numeric value. I used just numbers (ColorIndex 0) in all my tests, including other commands (WindowState 1), and it worked every time.

Along the way, I ran into optcl and accomplished what I wanted in five minutes. It was that much easier because optcl has much better documentation and because its interface is a lot more intuitive, I mean, a lot more similar to actual VBA, so the VBA manuals really help. In comparison, all those four lines of tcom code above became just one with optcl:

$::hWORD -with Selection.Find.Font : ColorIndex wdAuto

Eh. Maybe there is an equally short and easy way to accomplish that with tcom, but heck if I know what it is. I felt really clueless all the time with the little existing documentation.

I also learned that numeric vs. literal values trick thanks to optcl's own version of MS's "Object Explorer", namely the command tlview::loadedlibs. MS Word has such tool, but browsing objects with Tcl is more fun. :-)

Last, but not least, optcl seems to be a lot faster than tcom. In my tests, I had snack play sounds between certain steps of the program and saw that tcom would take more or less 4 or 5 seconds just to "createobject", then another 2 full secs just to open a document, before it finally ran the desired commands. Running it several consecutive times didn't make it any faster. In comparison, optcl would do everything in one second.

So, tcom may have its merits, but if one wants to fool around with COM, I can only recommend that one go with optcl instead. It'll be a much easier and fruitful time.

APN I would agree that optcl is more intuitive. However, no work has been done on it for a long time and tcom seems to be in active development. Also, I've had no success getting callbacks to work from IE using optcl and landed up using tcom for that reason.

DKF: Logically, that should be possible to do as a single line:

[[[$::hWORD Selection] Find] Font] ColorIndex [expr 0]

The problem with the literal is that it is a named constant needs mapping into Tcl somehow (e.g., via set wdAuto [expr 0] for a “cheap hack” method) yet Tcl has a really quite different attitude to what a literal constant is (we use the string value directly, not its mapping into some arbitrary number).

Question: SMS

Arlie L. Codina 2009-02-10:

package require tcom

# Create the COM object
set sms [tcom::ref createobject mCore.SMS

# get the License property (it returns an object)
set license [$sms License]

# Set properties on the license object
$license Company "JOHN SMITH"
$license LicenseType "LITE-DISTRIBUTION"
$license Key "11-11-11-112"

The script above works fine but when I add the following:

% set message [tcom::ref createobject mCore.Message]
0x80040154 {Class not registered}

% set inbox   [tcom::ref createobject mCore.Inbox]
0x80040154 {Class not registered}

This the original VB 6 script:

Dim objMessage As mCore.Message
Dim objInbox As mCore.Inbox

Please help.

PT: The VB code above declares two variables to have the 'types specified. Tcl doesn't have types. So don't try to declare a variable with a type. The equivalent code will be as for the VB but without any types (as you might do in VBScript). So something like: set msg [$sms Messages 1] or whatever returns a message to you.

Question: Export Internet Explorer Bookmarks (Unanswered)

escargo 2003-08-05: Can tcom be used to script Internet Explorer to export its bookmarks? I have a process I go through to turn my IE bookmarks into HTML pages (that includes running a Perl script over them under Cygwin), and I would like to automate the process a bit more than I currently can. If tcom will let me script the export of the bookmarks, that takes most of the pain out of doing it.

Question: Open Office Spreadsheet

Does anyone know of any resources explaining how to write/create an open office spreadsheet (.ods) using tcl?

I have imported data using Mk4tcl but would like to avoid the intermediate step of exporting to an excel file format using the above.

pcam you Have looked into Drive OpenOffice Calc with tcluno ?

Question: tcom and TclPro (Unanswered)

Johannes: I need to use TclPro1.4 to wrap my applications which use tcom. I have not been able to get tcom to work with TclPro, I wonder if anyone can help and show me how to install tcom in TclPro such that when I have the command package require tcom, tclpro can find it. Thanks

Maybe you should ask it at Ask, and it shall be given # 2

Question: Message Dialog (Unanswered)

Chris Joyce 2004-03-29:

I'm trying to read text from a message dialog, or any object on the screen through Tcl for the purposes of testing. (I need to use this for text verification, with automated mouse and keyboard movements) How would I do this in Tcl?

Question: Events (Unanswered)

Is tcom unable to introspect on Events?

Question: Passing an Integer

MHo Does someone know how to tell the WSH's Run method to wait for the application to quit? At least if starting a document (.html in this case), not a program, the call returns immediately, even specifiying the wait-flag... To clarify:

set wsh [::tcom::ref createobject "WScript.Shell"]
$wsh Run test.html 3 1 ;# <---- the 1 doesn't help here... hm

Even calling with -1 won't work (VB's TRUE is -1 or 0xffff). 0xffff won't work at all... [expr 0xffff] leads to nothing... Meanwhile I've noticed that this flag is completely useless even in an VBScript....

Question: Type Mismatch error

Chin Huang explains management of COM errors in [L1 ].


shavi - 2016-07-28 18:51:19

I get a type mismatch error when I try to do the following. Can anyone please help?

The imported DLL file:

BLE_CreateBond(

            /* [out] */ unsigned int *passkey,
            /* [retval][out] */ int *pnErrRet) = 0;

TCL script: $UEP BLE_CreateBond pass

I think the error is due to return type being unsigned.

Thanks