Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations Chris Miller on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

How to add an external event loop to Tcl/Tk GUIs embedded into C programs

Using Tcl/Tk From C program

How to add an external event loop to Tcl/Tk GUIs embedded into C programs

by  menczel  Posted    (Edited  )
Purpose:

To add a mechanism to the Tk library by which a C program can register a function that will become the main event loop of Tcl/Tk.

Rationale:

Tcl/Tk is very convenient for providing sophisticated GUIs to C programs. 'Guibuilder' makes designing graphical user interfaces in Tcl/Tk very easy. Also Tcl/Tk is truly platform independent: the same script will work under Windows, Linux and Mac OS. For this reason, using a Tcl/Tk GUI for C programs is IMO a viable and attractive alternative to using native C GUI libraries like Gtk.

There are two ways of integrating Tcl/Tk GUIs into C programs. Both have advantages and disadvatages. Actually, one of them simply does not work, I guess that may be considered a disadvantage. ;-)

The standard way is calling 'Tk_Main' from the main module of the C program and registering callbacks for events occuring in the main Tk window. For most applications this is sufficient. However, when you use this method, there seems to be no easy way to regularly call C modules for doing other tasks that must be done independent of the events of the user interface (e.g reading a data stream from the serial port, sending commands or state updates to hardware that your program controls, etc.).

Note: I have seen a comment in one of the docs which indicates that the function 'Tcl_CreateTimerHandler' could be used for this purpose. However, this call creates a one-shot event so the callback would have to set up its timer again and again after each call. Rather clumsy and probably not very efficient.

The Tcl/Tk library in theory provides another C interface method. You can do the creation and initialization of the interpreter and the Tk main window from your C code, then run a main loop in your app. In this case you are free to do any other task in the main loop, as long as you call 'Tcl_DoOneEvent' frequently enough so that events in the Tk main window get serviced. There is example code for this approach in the book 'Practical Programming in Tcl/Tk'.

However, according to my experience this approach does not work. I tested it using both the 8.4 and 8.5 versions of Tcl/Tk under Windows (MinGW development system). My programs crash when they try to create the main Tk window. I traced the problem to a specific Tcl/Tk library function, but have no idea how to fix it.(I could probably do it, but I have no intention to spend days or weeks on understanding, modifying and debugging the window management modules of the Tcl/Tk library.)

In order to solve this problem I have designed a simple method which can be added to the Tk library. We let 'Tk_Main' do all the arcane tasks necessary for starting a functional Tcl/Tk interpreter and creating a main window. Then, when everything already works, we simply hijack the main loop and replace it with our own.

You have to recompile the Tk library if you want to implement this mechanism. The added code does not interfere with other functions in the library, so the new version (theoretically) should be fully compatible with the standard distribution.

Changing the Tk library

Add the following code to 'tk.h' right before the section starting with the statement #include "tkDecls.h" (near the end of the file):

Code:
typedef void (* Tk_ExtEventLoop)(void);
  
#if defined BUILD_tk
  __declspec(dllexport) int Tk_RegisterEventLoop(Tk_ExtEventLoop func);
#else
  __declspec(dllimport) int Tk_RegisterEventLoop(Tk_ExtEventLoop func);
#endif

Add the following code to 'tkEvent.c' right before the function 'Tk_MainLoop' (at the end of the file):

Code:
static Tk_ExtEventLoop event_loop = NULL;

int
Tk_RegisterEventLoop(Tk_ExtEventLoop func)
{
  if (event_loop != NULL || func == NULL)
    return 0;
    
  event_loop = func;
  return 1;
}

Modify 'Tk_MainLoop' in 'tkEvent.c' to read as follows:

Code:
void
Tk_MainLoop(void)
{
    if (event_loop != NULL)
      (* event_loop)();		// preempt standard event loop

    while (Tk_GetNumMainWindows() > 0) {
	Tcl_DoOneEvent(0);
    }

    event_loop = NULL;		// paranoia
}

Recompile & install the Tk libraries.

The advantage of this mechanism is that only minimal changes are made in the library code, it is platform independent, and timing is under the absolute control of the C application.

How to add and use the external event loop

Following is an example program demonstrating the use of an external main loop.

Code:
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <tcl.h>
#include <tk.h>

static Tcl_Interp *interp = NULL;

static int time_init = 0;

static unsigned long time_base;

static void reset_timer_period(void)
{
  timeEndPeriod(1);
}

static int init_systime(void)
{
  MMRESULT res;

  if (time_init)
    return 1;
    
  res = timeBeginPeriod(1);
  if (res != TIMERR_NOERROR)
    return 0;
    
  time_base = timeGetTime();
  time_init = 1;
  atexit(reset_timer_period);
  
  return 1;
}

static unsigned long get_systime(void)
{
  if (! time_init)
    return 0;

  return timeGetTime() - time_base;
}

int tcl_app_init(Tcl_Interp *i)
{
  interp = i;

  /*
     Code must be added here to tell Tcl/Tk where to find
     the necessary libraries and scripts. Removed from
     this example, for clarity.
  */

  if (Tcl_Init(i) == TCL_ERROR)
    return TCL_ERROR;
    
  if (Tk_Init(i) == TCL_ERROR)
    return TCL_ERROR;
  
  /*
     Code to register callbacks for the GUI must be added
     here. Removed from this example, for clarity.
  */ 

  return TCL_OK;
}

#define TCL_CALL_INTERVAL	5
#define OUTPUT_INTERVAL		1000

void event_loop(void)
{
  unsigned long curr_time, last_tcl_call, last_output;

  last_tcl_call = last_output = curr_time = get_systime();
  
  while (1)
  {
    curr_time = get_systime();

    // check for time wrap-around, it is unlikely but possible

    if (curr_time < last_tcl_call)
    {
      last_tcl_call = last_output = curr_time;
      continue;
    }
    else if (curr_time < last_output)
    {
      last_tcl_call = last_output = curr_time;
      continue;
    }

    // check timeouts and call functions as needed

    if (curr_time - last_tcl_call >= TCL_CALL_INTERVAL)
    {
      last_tcl_call = curr_time;
      Tcl_DoOneEvent(TCL_ALL_EVENTS | TCL_DONT_WAIT);

      if (Tk_GetNumMainWindows() == 0)
        break;
    }
    else if (curr_time - last_output >= OUTPUT_INTERVAL)
    {
      last_output = curr_time;
      printf("time = %d\n", (int) curr_time);
    }
  }
}

int main(int argc, char *argv[])
{
  init_systime();

  if (! Tk_RegisterEventLoop(event_loop))
    exit(1);
    
  Tk_Main(argc, argv, tcl_app_init);

  return 0;
}

If you wish to see how this works you can download a working copy from my WEB page:

http://menczel.extra.hu/tkloop.zip

The archive contains the modified version of Tcl/Tk (based on source version 8.5.2) including files needed for program development, as well as a compiled version of the test program shown above. The stuff was compiled using MinGW (a Win32 port of GCC). Instructions for proper installation can be found in the accompanying README.
Register to rate this FAQ  : BAD 1 2 3 4 5 6 7 8 9 10 GOOD
Please Note: 1 is Bad, 10 is Good :-)

Part and Inventory Search

Back
Top