If you've ever tried to build a large-scale, compute-intensive, or commercial application using Tcl/Tk, you probably had a difficult time of it. A pure Tcl/Tk script is terrific for writing small programs or for prototyping, but it is often inadequate for really big problems. This is due to several factors:
But programming a graphical user interface in pure C is time-consuming and error-prone. The job can be made somewhat easier by using Tcl/Tk's C interface, and having your C program call the Tcl/Tk library routines directly. Many people have done this, some successfully. The task is still tough, though, because unlike its scripting language, Tcl/Tk's C interface is not easy to use. Properly initializing the Tcl/Tk interpreter takes skill and finesse, and calling the interpreter from C is a dull chore.
And so the problem remains: Do you go for the speed and structure of C or the power and simplicity of Tcl/Tk?
The Embedded Tk system (hereafter ``ET'') was
created to resolve this conundrum.
ET is a simple preprocessor and small interface library that make it
easy to mix Tcl/Tk and C in the same program.
With ET, you can put a few commands of
Tcl/Tk code in the middle of a C routine.
ET also makes it very easy to write C functions that work as
new Tcl/Tk commands -- effectively allowing you to put pieces of
C code in the middle of your Tcl script.
These features gives you
the speed and structure of C with the power and simplicity of Tcl/Tk.
As an added benefit,
an application written using ET will compile into a stand-alone
executable that
will run on any binary-compatible computer, even
if the other computer doesn't have Tcl/Tk installed.
The ET system is designed to be easy to use.
To see this, let's look at the classic ``Hello, World!''
program, coded using ET.
2 An Example: ``Hello, World!''
void main(int argc, char **argv){
Et_Init(&argc,argv);
ET( button .b -text {Hello, World!} -command exit; pack .b );
Et_MainLoop();
}
If you compile and link these 5 lines, you'll get
a stand-alone executable that pops up a
``Hello, World!''
button, and goes away when the button is clicked.
Let's take this program apart to see how it works.
The first thing it does is call the Et_Init()
procedure.
This procedure performs the tedious and confusing work needed
to start up the Tcl/Tk interpreter, initialize widget bindings,
create the main window ``.'', and so forth.
The last line is a call to another procedure Et_MainLoop()
that implements the event loop.
(If you don't know what an event loop is, don't worry.
We'll have more to say about event loops in section 4.)
The most interesting part of the example is the middle line, the
one that looks like a call to a function named ET()
.
The ET()
function is special.
It looks and is used like a regular C function, but takes a Tcl/Tk
script as its argument instead of a C expression.
Its function is to execute the enclosed Tcl/Tk.
In this particular example, the ET()
function
creates the ``Hello, World!'' button.
Because of the ET()
function, we can't give the
``Hello, World!'' source code directly to a C compiler
and expect it to work.
We have to run it through a preprocessor first. Like this:
et2c hello.c > hello_.cThe et2c preprocessor converts the
ET()
function into real, compilable C code.
The preprocessor also takes care of some
other housekeeping details, like adding prototypes to the top of the
file so that we don't have to bother with
a #include
.
After it has been preprocessed, the source code can be compiled like
any other C program.
cc -O -o hello hello_.c et.o -ltk -ltcl -lXll -lmNotice that you must link the program with ET's
And that's all there is too it!
If you're restless to start programming and are the type of person who prefers to learn at the keyboard rather than from a book, this section is for you. It contains a terse overview of the features of ET. Peruse this section, glance quickly at the examples, and you'll be ready to start coding. You can use the rest of the article as a reference guide when you run into trouble.
On the other hand, if you are new to graphical interface programming, are a little unsteady with C, or just have a more deliberate and cautious attitude toward life, then you may prefer to lightly skim or even skip this section and focus instead on the tutorial-like text that follows.
The ET system consists of two things: the et2c
preprocessor and the
Among the support routines in Et_Init()
and Et_MainLoop()
for initializing the ET package and implement the event loop, respectively.
A third routine, Et_ReadStdin()
,
allows standard input
to be read and interpreted by the Tcl/Tk interpreter at run-time.
The Et_Interp
is a pointer to the Tcl/Tk interpreter used by ET.
Et_MainWindow
is the main window.
Et_Display
is the Display
pointer required
as the first argument to many Xlib routines.
ET also provides two global Tcl variables, cmd_name
and
cmd_dir
. These contain the name of the executable and the
directory where the executable is found.
The et2c preprocessor is used to convert an ET source
file into real C code.
It creates the illusion of giving the C language some new statements,
like ET_INSTALL_COMMANDS
and ET_PROC
and
some special new functions like ET()
.
The ET()
function is used as if it were
a regular C function, except that its argument is a
Tcl/Tk script.
The job of the ET()
is to execute the script.
ET()
returns either ET_OK
or ET_ERROR
depending upon whether the script suceeded or failed.
Similar routines ET_STR()
, ET_INT()
, and
ET_DBL()
also take a Tcl/Tk script
as their argument, but return the string, the integer,
or the double-precision floating point number
that was the result of the last Tcl/Tk command in the argument script.
Wherever the string %d(x)
occurs inside an
ET()
function, the integer C expression x
is converted to ASCII and substituted in place of the
%d(x)
.
Similarly, %s(x)
can be used to substitute
a character string, and %f(x)
substitutes
a floating point value.
The string %q(x)
works like %s(x)
except
that a backslash is inserted before each character
that has special meaning to Tcl/Tk.
The special construct
ET_PROC( newcmd ){...}
defines a
C function that is invoked whenever the newcmd
Tcl/Tk command is executed.
Formal parameters to this function, argc
and argv
,
describe the arguments to the command.
The formal parameter interp
is a pointer to the
Tcl/Tk interpreter.
If a file named ET_PROC
macros, the commands associated with
those macros are registered with the Tcl/Tk interpreter by
invoking ET_INSTALL_COMMANDS(aux.c)
after
the Et_Init()
in the
main procedure.
The statement ET_INCLUDE( script.tcl )
causes the Tcl/Tk
script in the file script.tcl
to be made a part of the
C program and executed at the point where
the ET_INCLUDE()
macro is found.
The external Tcl/Tk script is normally read into the C program
at compile-time and thus becomes part of the executable.
However, if the -dynamic
command-line option is given to the
et2c preprocessor, loading of the external
Tcl/Tk script is deferred to run-time.
Finally, at the top of its output files,
the et2c preprocessor inserts
#define
s that
make ET_OK
and ET_ERROR
equivalent to
TCL_OK
and TCL_ERROR
. This often
eliminates the need
to put ``#include <tcl.h>
''
at the beginning of files that use ET.
And that's everything in ET! All the rest is just detail.
Before we delve into the details of ET, it may be helpful to review the concept of an event loop and an event-driven program. Many ET users have never before written an event-driven graphical user interface (GUI) and may be unfamiliar with how such programs operate. If you are such a user, you may profit from this quick review. But if you are already familiar with event-driven programs, feel free skip ahead to section 5.
The only inputs to a GUI are ``events.'' An event is a notification that something interesting has happened. Events arrive whenever the mouse moves, or a mouse button is pressed or released, or a key of the keyboard is pressed, and so forth. A event-driven GUI differs from more familiar command-line programs in that its inputs (e.g. events) do not arrived in any predictable sequence. Any kind of events can arrive at any time, and the GUI program must be prepared to deal with them.
The code for an event-driven GUI can be divided into two parts: the initialization code and the event loop. The initialization code runs first and does nothing more than allocate and initialize the internal data structures of the application. As soon as the initialization code completes, the application enters the event loop. Within the event loop, the program waits for the next event to arrive, reads the event, and processes it appropriately. The loop then repeats. The event loop does not exit until the program terminates.
This is a schematic view of a typical GUI program:
main(){ /* Initialization code */ while( /* More work to do */ ){ /* Wait for the next event to arrive */ /* Read the next event */ /* Take appropriate action for the event just read */ } }Don't worry about the details here. Most of the event loop processing is handled automatically by Tcl/Tk and ET. The important things to know are that the event loop exists, it runs after the initialization code, and that it doesn't terminate until the program exits.
If you've never written an event-driven program before, and you are like most people, then you will have a little trouble at first. To help you get started, here are some important points to remember:
The initialization code does only one thing -- initialize. It creates the main windows of the application (but it doesn't draw the windows -- that happens in the event loop!) and it sets up internal data structures. But the initialization code should never wait for input or respond to an event. Waiting and reading inputs and responding to events should happen only in the event loop.
Everything that a GUI program does is in response to some event. Any C procedure or Tcl/Tk command that is called in response to an event is referred to as a callback. Because all inputs to a GUI program are in the form of events, the only place for user-initiated processing to occur is within the callback routines.
A callback should do its job quickly and then return. Otherwise, the event loop will not be able to respond to new events as they arrive, and the program will appear to ``hang''. If you have a callback that needs to execute for more than a few hundred milliseconds, you should either invoke the ``update idletasks'' Tcl/Tk command periodically within the callback, or you should break the callback's calculations up into several separate routines that can be invoked by separate events.
Once started, GUI programs tend to run for a long time -- hours, days,
weeks or even months. Hence, you should take special care to avoid
memory leaks. A memory leak occurs when you allocate a
chunk of memory from the heap using malloc()
but don't
return that memory to the heap using free()
when you are
done with it.
Because the memory was not released by free()
it can never
be reused.
When this happens, the amount of memory required by your application
will constantly increase, until at some point it will consume all
memory available, and then die.
Memory leaks are probably the most common bug in GUI programs (which is
why I mention them.)
The main()
routines for ET applications all look
pretty much alike.
Here's a template:
void main(int argc, char **argv){ Et_Init(&argc,argv); /* Start the Tcl/Tk interpreter */ /* Create new Tcl/Tk commands here */ /* Initialize data structures here */ /* Create windows for the application here */ Et_MainLoop(); /* The event loop */ }When you need to write an ET application, but you aren't sure where to begin, this template is a good starting point. Type in the above template and make sure you can successfully compile and run it. (The program that results from compiling the template creates a blank window that doesn't respond to any mouse or keyboard inputs. Its the equivalent of ``wish /dev/null''.) After you get the template running, slowly begin adding bits of code, recompiling and testing as you go, until you have a complete application.
Let's take a closer look at each line of the template, so that you can better understand what is going on.
The first line of main()
is a call to the Et_Init()
procedure.
The Et_Init()
procedure initializes the ET
system and the Tcl/Tk interpreter.
It must be called before any other ET function or statement.
The parameters are the argc
and argv
formal parameters of main()
.
Et_Init()
uses these parameters to look for command-line
options.
ET currently understands four:
send
command.
The application name is also used for processing X11 resources,
and as the default text on the application's title bar.
Notice the ``&
'' before the argc
parameter to Et_Init()
.
The number of command line arguments is passed to Et_Init()
by address, not by value.
This is so Et_Init()
can change the value of argc
.
Whenever Et_Init()
sees one of the above command-line options,
it removes that option from the option list in argc
and argv
.
Hence, after Et_Init()
returns, only application-specific
command line options remain.
For example, suppose you invoke an ET program like this:
myapp -quiet -display stego:0 file1.dataThe values of
argc
and argv
passed into the Et_Init()
function are:
argc = 5 argv = { "myapp", "-quiet", "-display", "stego:0", "file1.data", 0 }The
Et_Init()
function will see the -display stego:0
part and act upon it accordingly. It will then remove those fields
from the argument list, so that after Et_Init()
returns,
the values are these:
argc = 3 argv = { "myapp", "-quiet", "file1.data", 0 }In this way, the initialization code that follows
Et_Init()
never sees the ET-specific command line arguments.
After the Et_Init()
procedure comes the initialization
code.
Normally, you begin the initialization by creating and registering
all the new Tcl/Tk commands you will need.
The details are described in section 6.
Basically it involves replacing the comment in the template with
one or more ET_INSTALL_COMMANDS
statements.
Once you've created the new Tcl/Tk commands, you may need to
construct internal C data structures, or create linkages between
C variables and Tcl variables using Tcl's Tcl_LinkVar()
function.
Command-line options that haven't been removed by
Et_Init()
are often processed here, as well.
Finally, you will probably want to create the initial windows for
the application.
The ET()
function (see section 7)
and ET_INCLUDE()
procedure (see section 8)
are both good for this.
Of course, this is only a suggested outline of how to initialize your application. You should feel free to do something different if your program requires it. The only ground rule is that the initialization code shouldn't try to interact with the user. Instead, use callback routines to respond to user inputs.
The last line of main()
is a call to the
Et_MainLoop()
procedure.
Et_MainLoop()
implements the event loop.
It will not return until the program is ready to exit.
One of the first things people tend to do with ET is
create new Tcl/Tk commands, written in C, that do computations that
are either too slow or impossible with a pure Tcl.
This is a two-step process.
First you have to write the C code using the ET_PROC
construct.
Then you have to register your new Tcl/Tk command with the Tcl/Tk
interpreter using the ET_INSTALL_COMMANDS
statement.
We will consider each of these steps in turn.
To help illustrate the concepts, this section introduces a
new sample program: the decimal clock.
The decimal clock displays the current time of day as a decimal
number of hours. For instance,
8:30am displays as ``8.500
''.
11:15pm shows as ``23.250
''.
And so forth.
A screen shot of this program is shown
in figure 6.1.
We'll begin by looking at the main procedure for the decimal clock program.
void main(int argc, char **argv){ Et_Init(&argc, argv); ET_INSTALL_COMMANDS; ET( label .x -bd 2 -relief raised -width 7 pack .x proc Update {} { .x config -text [DecimalTime] after 3600 Update } Update ); Et_MainLoop(); }As you can see, the main procedure is just a copy of the program template from section 5, with some of the comments replaced by actual initialization code. The first initialization action is to invoke the special ET statement
ET_INSTALL_COMMANDS
. Don't worry about what this
does just yet -- we'll return to it a little later.
The second initialization action is a single
ET()
function containing 7 lines of Tcl/Tk.
This Tcl/Tk code does three things:
Update
that
updates the label widget to show the current time, and then arranges
to call itself again after 0.001 hours (3.6 seconds).
Update
procedure once in order to
initialize the text of the label widget, and to start the
periodic updates.
The core of the decimal clock program is a new Tcl/Tk command,
DecimalTime
, that returns the current time of day
as a decimal number of hours.
This new command is written in C, using the special
ET_PROC
construct of ET. The code looks like this:
#include "tcl.h" #include <time.h> ET_PROC( DecimalTime ){ struct tm *pTime; /* The time of day decoded */ time_t now; /* Number of seconds since the epoch */ now = time(0); pTime = localtime(&now); sprintf(interp->result,"%2d.%03d",pTime->tm_hour, (pTime->tm_sec + 60*pTime->tm_min)*10/36); return ET_OK; }The magic is in the
ET_PROC
keyword.
The et2c preprocessor recognizes this keyword and
converts the code that follows it into a compilable C function that
implements the Tcl/Tk command.
In general, you can create new Tcl/Tk commands using a
template like this:
ET_PROC( name-of-the-new-command ){ /* C code to implement the command */ }You could, of course, construct approprate C functions by hand, but that involves writing a bunch of messy details that detract from the legibility of the code. The
ET_PROC
mechanism is much easier to write and
understand, and much less subject to error.
Though they do not appear explicitly in the source code, every
function created using ET_PROC
has four formal parameters.
This parameter is an integer that holds the number of arguments
on the Tcl command that invokes the function.
Its role is exactly the same as the argc
parameter to
the main()
function of a standard C program.
Like argc
before it, this parameter works just like the
argv
parameter to main()
.
The variable argv[0]
contains the name of the the
command itself (``DecimalTime
'' in this
example), argv[1]
contains the name of the first
argument, argv[2]
contains the name of the second
argument, and so forth up to argv[argc]
which is a
null pointer.
This parameter is a pointer to the Tcl/Tk interpreter.
It has type ``Tcl_Interp*
''.
The interp
parameter has many uses, but is most often used to set the
return value of the Tcl/Tk function.
(Note that you have to #include
either
<tcl.h>
or
<tk.h>
somewhere in your
source file in order to use the interp
parameter, since
one of these header files are needed to define the fields of the
Tcl_Interp
structure.)
This is a pointer to the Tk_Window
structure that defines
the main window (e.g. the ``.'' window) of the application.
It has a type of ``void*
'' and will need to be
typecast before it is used.
On the other hand, it is seldom used, so this isn't normally a problem.
The decimal clock example uses the interp
formal parameter on the sixth line of the ET_PROC
function.
In particular, the DecimalTime
function writes its
result (e.g. the time as a decimal number) into the result
field of interp
.
It's OK to write up to about 200 bytes of text into
the result
field of the interp
parameter,
and that text will become the return value of the Tcl/Tk
command.
If you need to return more than about 200 bytes of text, then
you should set the result using one of the routines from the
Tcl library designed for that purpose:
Tcl_SetResult()
,
Tcl_AppendResult()
, or
Tcl_AppendElement()
.
(These routines are documented by Tcl's manual pages
under the name ``SetResult''.)
If all this seems too complicated, then you can choose to
do nothing at all, in which case
the return value defaults to an empty string.
Another important feature of every ET_PROC
function is
its return value.
Every ET_PROC
should return either ET_OK
or ET_ERROR
, depending on whether or not the
function encountered any errors.
(ET_OK
and ET_ERROR
are #define
constants
inserted by et2c and have the save values as
TCL_OK
and TCL_ERROR
.)
It is impossible for the DecimalClock
function to fail,
so it always returns ET_OK
, but most ET_PROC
functions can return either result.
Part of Tcl's result protocol is that if a command
returns ET_ERROR
it should put an error message in
the interp->result
field.
If we had wanted to be pedantic, we could have put a test in the
DecimalTime
function to make sure it is called with
no arguments.
Like this:
ET_PROC( DecimalTime ){ struct tm *pTime; /* The time of day decoded */ time_t now; /* Number of seconds since the epoch */ if( argc!=1 ){ Tcl_AppendResult(interp,"The ",argv[0], " command should have no argument!",0); return ET_ERROR; } /* The rest of the code is omitted ... */ }New Tcl/Tk commands that take a fixed format normally need to have some checks like this, to make sure they aren't called with too many or too few arguments.
We've seen how
the ET_PROC
constuct will create a new
Tcl/Tk command.
But that command must still be registered
with the Tcl interpreter before it can be used.
Fortunately, ET makes this very easy.
ET uses the ET_INSTALL_COMMANDS
keyword to register
ET_PROC
commands with the Tcl interpreter.
The et2c preprocessor converts the
ET_INSTALL_COMMANDS
keyword into a sequence of C
instructions that register every ET_PROC
in the current file.
In the main()
procedure of the
decimal clock example, the ET_INSTALL_COMMANDS
keyword that immediately follows the Et_Init()
function
is used to register the DecimalTime
command.
As it turns out, DecimalTime
is the only ET_PROC
function in the same source file, but even if there had be 100
others, they would have all been registered by that single
ET_INSTALL_COMMANDS
statement.
The ET_INSTALL_COMMANDS
keyword can also be used to
register ET_PROC
functions in separate source files,
simply by putting the name of the source file in
parentheses after the ET_INSTALL_COMMANDS
keyword.
Like this:
ET_INSTALL_COMMANDS( otherfile.c );A larger program will typically have many
ET_INSTALL_COMMANDS
statements immediately following the Et_Init()
function,
one statement for each file that contains ET_PROC
functions.
One recent commercial project used 33
ET_INSTALL_COMMANDS
statements following the
Et_Init()
function!
Before leaving this section,
let's briefly summarize the steps needed to create new Tcl/Tk commands
in C using ET.
First you create one or more commands using the ET_PROC
construct, as follows:
ET_PROC( name-of-the-new-command ){ /* C code to implement the command */ return ET_OK; /* Don't forget the return value! */ }Then, you register these commands with the Tcl interpreter using an
ET_INSTALL_COMMANDS
statement after the
Et_Init()
function call within main()
.
Like this:
ET_INSTALL_COMMANDS( name-of-file-containing-ET_PROCs.c );And that's all you have to do!
The ET_PROC
construct lets you
put a C routine in the middle of Tcl/Tk.
The next section will take a closer look at ET()
which
allows you to put Tcl/Tk in the middle of a C routine.
If you've been keeping up with the examples, you've already seen
the ET()
function used twice to insert a few lines of
Tcl/Tk in the middle of a C procedure.
But the ET()
function can do a lot more, as this
section will show.
The first thing to note about ET()
is that, just like
a real C function, it has a return value.
ET()
returns an integer status code which is either
ET_OK
or ET_ERROR
depending on whether the
enclosed Tcl/Tk was successful or failed.
(ET()
might also return
TCL_RETURN
, TCL_BREAK
,
or TCL_CONTINUE
under rare circumstances.)
The status return of ET()
is nice, but in practice it
turns out to be mostly useless.
What you really need is the string value returned by the enclosed Tcl/Tk
script.
That's the purpose of the ET_STR()
function.
The ET_STR()
function works a lot like ET()
.
You put in a Tcl/Tk script as the argument, and the script gets executed.
But instead of returning a status code, ET_STR()
returns a
pointer to a string that was the result of the last Tcl/Tk command
in its argument.
The ET_STR()
function turns out to be a very handy mechanism
for querying values from Tcl/Tk.
For instance, suppose your program has an entry widget named
``.entry
'' and some piece of C code needs
to know the current contents of the entry.
You can write this:
char *entryText = ET_STR(.entry get);Or imagine that you need to know the current size and position of your main window. You might use code like this:
int width, height, x, y; sscanf(ET_STR(wm geometry .),"%dx%d+%d+%d",&width,&height,&x,&y);Does your C routine need to know the value of a Tcl variable? You could use the cumbersome
Tcl_GetVar()
function,
but it's much easier to say:
char *zCustomerName = ET_STR(set CustomerName);Possible uses for
ET_STR()
seem limitless.
But, there are two subtleties with ET_STR()
that programmers
should always keep in mind.
The first is that the Tcl/Tk script in the argument is executed at
Tcl's global variable context level.
This means that all of the Tcl/Tk variables ET_STR()
creates, and the only Tcl/Tk variables it can access, are global
variables.
This limitation also applies to the regular ET()
function, and to
two other function we haven't talked about yet:
ET_INT()
and ET_DBL()
.
ET provides no means for C code to access or modify local variables.
On the other hand, this has not proven to be a serious hardship in
practice.
The second subtlety with ET_STR()
is more dangerous, but
fortunately applies to ET_STR()
only.
Recall that ET_STR()
returns a pointer to a string,
not the string itself.
The string actually resides in memory that is held deep within the
bowels of Tcl/Tk.
The danger is that the next Tcl/Tk command may choose
to change, deallocate, or reuse this memory, corrupting the value
returned by ET_STR()
.
We say that the return value of ET_STR()
is
``ephemeral.''
One way to overcome the ephemerality of ET_STR()
is by making a copy of the returned string.
The strdup()
function is good for this.
(Unfortunately, strdup()
is missing from a lot of C libraries.
You may have to write your own string duplicator.)
In place of the examples given above, you might write
char *entryText = strdup( ET_STR(.entry get) );or
char *zCustomerName = strdup( ET_STR(set CustomerName) );The
strdup()
function uses malloc()
to get the
memory it needs, so if you use this approach,
be sure to free()
the value when you
are done to avoid a memory leak!
The other way to overcome the ephemerality of ET_STR()
is simply not to use the returned string for very long.
You should be safe in using the returned string as long as you don't
invoke any other Tcl/Tk commands, or return to the event loop.
Code like this
sscanf(ET_STR(wm geometry .),"%dx%d+%d+%d",&width,&height,&x,&y);is OK since we need the return value only for the duration of the
sscanf()
function and sscanf()
doesn't
use Tcl/Tk.
In addition to ET()
and ET_STR()
, the
ET system provides two other functions named ET_INT()
and ET_DBL()
.
Both take a Tcl/Tk script for their argument, as you would expect.
But ET_INT()
returns an integer result and
ET_DBL()
returns a floating-point value (a
double
).
In a sense, these two functions are extensions of ET_STR()
.
In fact, ET_INT()
does essentially the same thing as
int v = strtol( ET_STR(...), 0, 0);and
ET_DBL()
is equivalent to
double r = strtod( ET_STR(...), 0);Because
ET_INT()
and ET_DBL()
return a value, not
a pointer, their results are not ephemeral nor subject to the
problems that can come up with ET_STR()
.
We've seen how ET_STR()
, ET_INT()
and
ET_DBL()
can be used to pass values from Tcl/Tk back
to C.
But how do you go the other way and transfer C variable values
into Tcl/Tk?
ET has a mechanism to accomplish this too, of course.
Within the argument to any ET()
function (or
ET_STR()
or ET_INT()
or ET_DBL()
),
the string ``%d(x)
'' is special.
When ET sees such a string, it evalutes the integer C expression
``x
'', converts the resulting integer into
decimal, and substitutes the integer's decimal value for the original
string.
For example, suppose you want to initialize the Tcl/Tk variable
named nPayment
to be twelve times the value of a C
variable called nYear
.
You might write the following code:
ET( set nPayment %d(12*nYear) );As another example, suppose you want to draw a circle on the canvas
.cnvs
centered at (x,y) with radius r.
You could say:
id = ET_INT( .cnvs create oval %d(x-r) %d(y-r) %d(x+r) %d(y+r) );Notice here how the
ET_INT()
function was used to record
the integer object ID returned by the Tcl/Tk canvas create
command.
This allows us to later delete or modify the circle by
referring to its ID.
For example, to change the fill color of the circle, we could execute
the following:
ET( .cnvs itemconfig %d(id) -fill skyblue );
If you want to substitute a string or floating-point value into
an ET()
argument, you can use %s(x)
and %f(x)
in place of %d(x)
.
The names of these substitutions phrases were inspired by the
equivalent substitution tokens in the standard C library
function printf()
.
Note, however, that you cannot specify a field-width, precision, or
option flag in ET()
like you can in printf()
.
In other words, you can use conversions like
%-10.3f
in prinf()
but not in ET()
.
The ET()
function will accepts only
specification, such as %f
.
But the ET()
function does support a conversion specifier
that standard printf()
does not:
the %q(x)
substitution.
The %q
works like %s
in that it expects its argument to be a null-terminated string,
but unlike %s
the %q
converter
inserts extra backslash characters into the string in order
to escape characters that have special meaning to Tcl/Tk.
Consider an example.
char *s = "The price is $1.45"; ET( puts "%q(s)" );Because
%q(s)
was used instead of
%s(s)
, an extra backslash
is inserted immediately before the ``$
''.
The command string passed to the Tcl/Tk interpreter is therefore:
puts "The price is \$1.45"This gives the expected result. Without the extra backslash, Tcl/Tk would have tried to expand ``
$1
'' as a variable, resulting in an error message like this:
can't read "1": no such variableIn general, it is always a good idea to use
%q(...)
instead of %s(...)
around strings that originate from outside the program--you never know
when such strings may contain a character that needs to be escaped.
And that's everything there is to know about the
ET()
function and its siblings.
In case you missed something amid all the details, here's a
10-second review of the essential facts:
ET()
executes Tcl/Tk code and returns a
success/failure code.
ET_STR()
, ET_INT()
and ET_DBL()
do the same, but return a string, and integer or a double which
was the result of the last Tcl/Tk command executed.
ET_STR()
is ephemeral.
%s(...)
, %d(...)
and
%f(...)
insert string, integer and double C expressions
into the argument of ET()
and its siblings.
%q(...)
works like %s(...)
but adds backslashes before characters that are special to Tcl/Tk.
ET_INCLUDE()
, that allows you incorporate whole
files full of Tcl/Tk into your application.
In the sample programs seen so far in this article,
Tcl/Tk code in an
ET()
function was used to construct the main window.
This works fine for the examples, since their windows are
uncomplicated and can be constructed with a few lines code.
But in a real application, or even a more complex example,
the amount of Tcl/Tk code needed to initialize the program's windows
can quickly grow to hundreds or thousands of lines.
It is impractical and irksome to put this much code into an
ET()
statement, so the ET system provides another
way to get the job done: the ET_INCLUDE()
statement.
The ET_INCLUDE()
statement is similar in concept to the
#include
statement in the C preprocessor.
Both take a filename as their argument, and both read the named
file into the original source program.
The ET_INCLUDE()
statement expects its file to
be pure Tcl/Tk code, though.
Its job is to turn the Tcl/Tk source into a form that the
C compiler can understand, and to arrange for the Tcl/Tk to
be executed when control reaches the ET_INCLUDE()
statement.
An example may help to clarify this idea.
In the decimal clock program (way back
at the beginning of section 6),
there are 7 lines of Tcl/Tk in an ET()
function used
to create the application's main window.
Now suppose we move those 7 lines of Tcl/Tk into a separate
file named ET()
function with an
ET_INCLUDE()
statement that references the new file
like this:
void main(int argc, char **argv){ Et_Init(&argc, argv); ET_INSTALL_COMMANDS; ET_INCLUDE( dclock.tcl ); Et_MainLoop(); }When the et2c preprocessor sees the
ET_INCLUDE()
statement, it locates the specified file, reads that file into the
C program, and makes arrangements for the text of the file to be
executed as if it had all appeared within an ET()
function.
Well, almost like an ET()
function.
There are a couple of minor differences.
The ET_INCLUDE()
does not understand
the various %s(...)
substitutions as ET()
does.
Also, ET_INCLUDE()
is a true procedure, not a function.
It doesn't return a value like ET()
so
you can't use an ET_INCLUDE()
in an expression.
It is important to understand the difference between an
ET_INCLUDE()
statement like this
ET_INCLUDE( dclock.tcl );and the
source
command of Tcl/Tk, used as follows:
ET( source dclock.tcl );The
ET_INCLUDE()
statement reads the Tcl/Tk into the program at
compile-time, effectively making the Tcl/Tk code part of the executable.
The Tcl source
command, on the other hand, opens and
reads the file at run-time, as the application executes. This
makes the executable a little smaller, but it also means that the
file containing the Tcl/Tk must be available to the executable
whenever it runs.
If you move just the executable, but not the Tcl/Tk file, to
another computer, or even another directory, then it will no
longer work because it won't be able to locate and read the Tcl/Tk file.
The ability to read an external Tcl/Tk script and make it part of the executable program is an important feature of ET. But while you are developing and testing a program, it is sometimes convenient to turn this feature off and to have the application read its scripts at run-time instead of compile-time. That way, you can make changes to the Tcl/Tk script and rerun your program with the changes, but without having to recompile. You can do this using the -dynamic option to the et2c proprocessor. Whenever you run et2c with the -dynamic command-line option, it effective turns instances of the statement
ET_INCLUDE( filename.tcl );into the statement
ET( source filename.tcl );This feature has proven very helpful during development. But be careful to turn it off before doing your final build, or else you won't be able to move your executable to other machines!
There is just one other feature of the ET_INCLUDE()
statement that we need to discuss before moving on, and that is
the algorithm it uses to locate the Tcl/Tk source code files.
Just like the C preprocessor's #include
statement, the
ET_INCLUDE()
mechanism can include
files found in other directories.
The et2c preprocessor always looks first in the working
directory for files named by an ET_INCLUDE()
statement.
If the file is found there, no further search is made.
But if the file is not found, then et2c will also look
in all directories named in -I command line options.
For example, if you run et2c like this:
et2c -I../tcl -I/usr/local/lib/tcl app.c >app_.cand the
ET_INCLUDE( setup.tcl );then et2c will search for the
The Et_Init()
creates two new global
Tcl/Tk variables that many programs find useful.
This section will describe what all of these variables do, and
suggest ways that they can be used.
Perhaps the most useful of the global variables available in
ET is Et_Interp
.
This variable is a pointer to the Tcl/Tk interpreter, the one
created by Et_Init()
and used to execute all Tcl/Tk
commands within the program.
The Et_Interp
variable has the same value as the
interp
formal parameter found in every ET_PROC()
function.
The Et_Interp
variable is useful because you may
often want to call C routines in the Tcl/Tk library, and most
of these routines require a pointer to the interpreter as their
first parameter.
For instance, suppose in the initialization code you want to create
a link between the global C variable nClients
and a
Tcl/Tk variable by the same name.
Using the Et_Interp
variable as the first parameter
to the Tcl function Tcl_LinkVar()
, you could write:
Tcl_LinkVar(Et_Interp,"nClients",(char*)&nClients,TCL_LINK_INT);Having done this, any changes to the C
nClients
variable
will be reflected in the Tcl/Tk variable, and vice versa.
Perhaps the second most useful global varible is Et_Display
.
This variable contains the Display
pointer required as
the first argument to most Xlib routines.
It is used by daring, down-to-the-bare-metal programmers who like to
call Xlib directly.
Here's an example.
Suppose you want to create a new Tcl/Tk command,
PitchedBell
, that makes
the X terminal emit a beep with a pitch specified by its sole argument.
Once such a command is implemented, then the following Tcl/Tk code
would emit a single tone at the pitch of concert A:
PitchedBell 440Here a short piece of Tcl/Tk code that plays the opening bar of Beethoven's Fifth Symphony:
foreach pitch {784 784 784 659} { PitchedBell $pitch after 200 }You probably get the idea. Here's the code that implements the
PitchedBell
command:
#include <tk.h> /* Will also pickup <Xlib.h> */ ET_PROC( PitchedBell ){ XKeyboardControl ctrl; /* For changing the bell pitch */ if( argc!=2 ){ interp->result = "Wrong # args. Should be: ``PitchedBell PITCH''"; return ET_ERROR; } ctrl.bell_pitch = atoi( argv[1] ); XChangeKeyboardControl(Et_Display,KBBellPitch,&ctrl); XBell(Et_Display,0); XFlush(Et_Display); return ET_OK; }After checking to make sure it has exactly one argument, the
PitchedBell
command uses the
XChangeKeyboardControl()
function of Xlib to change the bell pitch.
It then rings the bell using the XBell()
Xlib function, and
finally flushes the Xlib message queue using XFlush()
to
force the bell to be rung immediately.
All three of these Xlib functions require a Display
pointer as their first argument, a role that is perfectly filled
by the Et_Display
global variable.
The third and final global C variable in ET is Et_MainWindow
.
This variable is a pointer to
the Tcl/Tk structure that defines the application's
main window.
Back in the days of Tk3.6, several Tcl/Tk library functions
required this value as a parameter.
But the Tcl/Tk library interface changed in the move to Tk4.0, so that
the main window pointer is no longer required.
Hence, the Et_MainWindow
variable isn't used much any more.
It has been kept around as an historical artifact.
Besides the 3 global C variables, ET also provides two
Tcl/Tk variables that are of frequent use:
cmd_name
and cmd_dir
.
The cmd_name
variable contains the name of the file
holding the executable for the application, and cmd_dir
is the name of the directory containing that file.
The cmd_name
and cmd_dir
variables are
useful to programs that need to read or write auxiliary data files.
In order to open an auxiliary file, the program needs to know the
files pathname, but it is not a good idea to hard-code a complete
pathname into the program.
Otherwise, the auxiliary file can't be moved without recompiling
the program.
By careful use of cmd_name
and/or cmd_dir
,
we can arrange to have auxiliary files located in a directory
relative to the executable, rather that at some fixed location.
That way, a system adminstrator is free to move the auxiliary file
to a different directory as long as the executable moves with it.
For example, suppose you are writing a program named
char *fullName = ET_STR( return $cmd_dir/../data/$cmd_name.db ); FILE *fp = fopen(fullName,"r");Using this scheme, both the executable and the datafile can be placed anywhere in the filesystem, as long as they are in the same position relative to one another. They can also be renamed, so long as they retain the same base name. This flexibility is a boon to system adminstraters, and also make the program less sensitive to installation errors.
There's one last feature of ET that we haven't discussed:
the Et_ReadStdin()
procedure.
If this procedure is called (with no arguments) in between
the calls to Et_Init()
and Et_MainLoop()
,
ET will make arrangements to read all data that appears
on standard input and interpret that data as Tcl/Tk commands.
You can use the Et_ReadStdin()
to implement the
interactive wish interpreter for Tcl/Tk.
The code would look like this:
main(int argc, char **argv){ Et_Init(&argc,argv); Et_ReadStdin(); Et_MainLoop(); }Let's call this program etwish in order to distinguish it from the standard wish that comes with Tcl/tk. The etwish program differs from wish in two ways. First, wish reads a set of 15 or so Tcl/Tk scripts from a well-known directory when it first starts up. Thus, to install wish, you have to have both the
ET_INCLUDE()
statements inside
the Et_Init()
function) so the external scripts are
no longer required. This does make the etwish executable
slightly larger (by about 64K bytes), but it also makes the
program much easier to install and administer.
The second difference between wish and the etwish program shown above is that etwish is always interactive. It will not read a script from a file given as a command line argument like standard wish will. But we can remove that difference using a little more code.
main(int argc, char **argv){ Et_Init(&argc,argv); if( argc>2 && (strcmp(argv[1],"-f")==0 || strcmp(argv[1],"-file")==0) ){ ET( source "%q(argv[2])" ); }else if( argc>1 ){ ET( source "%q(argv[1])" ); }else{ Et_ReadStdin(); } Et_MainLoop(); }This revised program serves as a great template for building customized editions of wish that have one or more new Tcl/Tk commands written in C. All you have to do is code the new commands using the
ET_PROC()
mechanism and insert a single
ET_INSTALL_COMMANDS
statement right after the
Et_Init()
.
We've already discussed the basics of compiling ET applications back in section 2 when we put together the ``Hello, World!'' example. Basically, all you do is preprocess your source files with et2c and then run the results through the C compiler. But that synopsis omits a lot of detail. This section fills in the missing information.
But before we begin talking about how to compile ET applications,
we need to first mention how to compile ET itself --
the et2c preprocessor and the
The source code to the et2c preprocessor is contained in a single file named et2c.c. The preprocessor is written in highly portable K&R C and should compile without change on just about any 32-bit architecture. All you have to do is this:
cc -O -o et2c et2c.c
Compiling the
Let's suppose, for the sake of discussion, that you selected the
source file
Recall that the stardard Tcl/Tk interpreter program, wish, reads and executes a series of Tcl/Tk scripts when it first starts up. These scripts set up default widget bindings, create procedures for handling menus, and so forth. The names of the directories from which these scripts are loaded are hard-coded in the wish executable. There are about 15 different startup scripts (the number varies from one version of Tcl/Tk to the next) and wish will not run without them.
But ET applications don't read the startup scripts at run-time.
Instead, a series of ET_INCLUDE()
statements inside
the Et_Init()
function bind the startup scripts into
an ET executable at compile-time.
This feature is what enables ET applications to run on machines that
do not have Tcl/Tk installed.
It is because of 15 or so startup scripts included by
ET_INCLUDE()
statements in the
ET library that we have to preprocess the library source code
using et2c.
But we also have to tell et2c what directories to use
when searching for the startup scripts.
If Tcl/Tk has already been installed on your system, then you
can find out the names of the startup script directories by executing the
following wish script:
#! wish puts $tk_library puts $tcl_libraryLet's suppose that the startup scripts are located in the directories
et2c -I/usr/local/lib/tcl -I/usr/local/lib/tk et41.c >et.c
After preprocessing the library source code, all that remains
is to compile it.
The library references the <tk.h>
header file,
which in turn references <tcl.h>
, so you
may have to add some -I options to the compiler command
line to specify the directories where these header files are located.
The following is typical:
cc -c -o et.o -I/usr/include/tcl -I/usr/include/tk et.c
Once you get et2c and
et2c appmain.c >temp.c cc -c temp.c -o appmain.o et2c appaux.c >temp.c cc -c temp.c -o appaux.o cc appmain.o appaux.o et.o -ltk -ltcl -lX11 -lm
If you're using a Makefile, you might want to redefine the default rule for converting C source code into object code to incorporate the et2c preprocessor step. Like this:
.c.o: et2c $<>temp.c cc -c -o $@ temp.cThe et2c does not harm files that don't use ET constructs, so this rule will work for every file in your project.
The et2c preprocessor attempts to save memory and
improve performance of your application by removing comments and
unnecessary spaces from the Tcl/Tk code inside
ET()
functions and loaded by ET_INCLUDE()
statements.
This mechanism works well most of the time, but it is not
foolproof.
It is theoretically possible for a valid Tcl/Tk script to be corrupted
by et2c's compression attempts.
If you experience trouble, and suspect that
et2c is messing up your Tcl/Tk code, then you can
turn script compression off using the
-nocompress command line option.
If it is your misfortune not to have an ANSI C compiler, you can
still use ET.
The source code to et2c
is pure K&R C and should work
fine under older compilers.
The source code to -K+R
option to et2c
and then give
a -DK_AND_R
option to the C compiler:
et2c -K+R -I/usr/local/lib/tcl -I/usr/local/lib/tk et40.c >et.c cc -DK_AND_R -I/usr/include/tcl -I/usr/include/tk -c et.cWhen compiling application code with an older compiler, just give the
-K+R
option to et2c
.
It is not necessary to give the -DK_AND_R
option to the
C compiler when compiling objects other than et.c
.
Besides the very simple ``Hello, World!'' and decimal clock programs presented above, ET is distributed with a number of non-trivial sample programs. This section will briefly overview what several of these example programs do, and why ET was important to their implementation. We won't try to explain the details of how the programs work, though. You can figure that out for yourself by looking at the source code.
There is a color chooser tool for X11 called
color.
The sources to color are in the files
The X11 Window System supports displays with over 280 quadrillion distinct colors (48 bits per pixel). But from this vast number, a few hundred colors are assigned English names like ``blue'' or ``turquoise'' or ``peachpuff''. All the rest are given arcane hexadecimal designations like ``#b22cd8517f32''. It is best to use colors with English names whenever possible.
The purpose of the color program it to help select colors with English names. At the top of the application is large swatch showing one of the 280 quadrillion X11 colors, together with either its English name (if it has one) or its hexadecimal value. Sliders on the lower left side of the window allow the user to vary the color of the swatch by changing various color components. On the lower right side of the window are six smaller swatches that show colors with English names that are similar to the color in the main swatch. Moving any of the six color component sliders causes the colors in all swatches, and the other sliders, to update in real time. Clicking on any of the smaller swatches transfers its color to the main swatch, updating all of the sliders and swatches appropriately.
In theory, there is nothing to prevent the color program
from being coded in pure Tcl/Tk, but in practice, such an
implementation would be much too slow.
For this reason, two key routines are coded in C.
The ET_PROC
command ChangeComponent
is
called whenever one of the color component sliders is moved.
This routine moves the other sliders, changes the color of the main
swatch, then computes close colors for the smaller swatches.
Another ET_PROC
command named ChangeColor
is called whenever the user clicks on one of the smaller swatches.
This routine changes the color of the main swatch, then updates
the sliders and the smaller swatches accordingly.
The example named tkterm implements a VT100 terminal emulator. The tkterm program can be used as a direct replacement for the more familiar emulator programs xterm or rxvt.
The sources for tkterm are contained in three separate files.
The main procedure is in
The tkterm program simulates the VT100 display using
an ordinary Tcl/Tk text widget.
C routines in ET()
functions to insert characters into their proper
places within the text widget.
The
The tkterm program is an example of an application that could not be coded in pure Tcl/Tk, since Tcl/Tk has no provisions for dealing with pseudo-TTYs or TTYs in ``raw'' mode. But even if it could, we would probably still want to use some C code, since it seems unlikely that a Tcl/Tk script would be able to process the VT100 escape sequences efficiently.
The perfmon program is a system performance monitor for
the Linux operating system.
It uses bar graphs to shows the amount of memory, swap space, CPU time
currently being used.
The display is updated 10 times per second.
There are two source code files for this application:
The main display of the perfmon program is implemented using
a Tcl/Tk canvas widget.
But for efficiency's sake, the logic that computes the current memory,
swap space, and CPU usages is all coded in C.
The C code obtains the system performance data by reading the files
ET()
function calls.
On a 90MHz Pentium and with an update frequency of 10 times per second, the prefmon program uses a negligible amount of the CPU time. So in addition to being a nifty desktop utility for a Linux workstation, this example demonstrates that Tcl/Tk applications can be very efficient.
The two programs tkedit and browser implement,
respectively, an ASCII text editor and a UNIX file browser utility.
Source code to these programs is in the files
Both of these programs could just as well have been implemented as
pure Tcl/Tk scripts, with no loss of features or performance.
(In fact, the browser can be used as pure script by invoking the
The tkedit and browser programs are examples of
how to convert a pure Tcl/Tk script into a stand-alone program using
ET.
The idea is very simple.
Your C code simply initializes ET, invokes your script using a
single ET_INCLUDE()
statement, and then enters the event
loop.
Like this:
void main(int argc, char **argv){ Et_Init(&argc,argv); ET_INCLUDE( browser.tcl ); Et_MainLoop(); }Compiling this code results in a stand-alone application that can be run on any binary-compatible machine.
ET, like Tcl/Tk, was originally written to support the open X11 windowing system only. But nowadays, people often need to write applications for popular proprietary windowing systems such as Windows95 or Macintosh. Beginning with release 4.1, Tcl/Tk supports these proprietary products, and so does ET. (Actually, only Windows95 is supported as of this writing. The author has no access to a Macintosh system on which to develop and test a Macintosh port.)
On a Macintosh, ET applications that don't call Xlib directly should
compile with little or no change.
The Mac won't support the Et_ReadStdin()
routine, or the
Et_Display
global variable, but then again, neither of
these make much sense on a Mac.
The application will compile in much the same way as it does for X11,
except that you should use the
More change is required to support Windows95, however.
The Windows version of ET doesn't contain either
Et_Init()
or Et_MainLoop()
.
Instead these functions will be invoked automatically.
An ET program for Windows should contain a single Et_Main()
procedure definition to do all its setup, and nothing more.
Hence, if your application used to look like this:
void main(int argc, char **argv){ Et_Init(&argc,argv); /* Your setup code here */ Et_MainLoop(); }then under Windows, it will look like this instead:
void Et_Main(int argc, char **argv){ /* Your setup code here */ }Besides that, and the obvious fact that
Et_Display
is
not supported, a Windows ET application should work just like an
X11 ET application.
It is compiled in the same way, except that you should use the
Over the past two years, many people have used ET to build programs from a mixture of Tcl/Tk and C. Projects have ranged in size from student programming assignments up to large-scale (100,000+ lines) commercial development efforts. In all cases, ET has proven to be an effective alternative to other GUI toolkits.
The original implementation of ET grew out of a programming contract from Lucent Technologies (formerly AT&T Bell Laboratories). Lucent Technologies was in turn funded under a contract from the United States Navy. Many thanks go to Richard Blanchard at Lucent Technologies and to Charlie Roop, Dave Toms and Clair Guthrie at PMO-428 for allowing ET to be released to the public domain.
The author can be reached at:
D. Richard Hipp
Hipp, Wyrick & Company, Inc.
6200 Maple Cove Lane
Charlotte, NC 28269
704-948-4565
drh@vnet.net