In the previous section, we've seen Tcl scripting used to create a simple GUI for SpecTcl using Tk. In this section we're going to explore how to connect Tcl Scripts with your compiled code (normally event processors).
There are two approaches to linking SpecTcl C++ code and Tcl scripts: You can share data (variables) between Tcl and C++ or you can add commands to SpecTcl/Tcl that you execute in your C++ code. We are going to look at both approaches by lookint at the problem of providing a linear calibration that turns a raw parameter into a calibrated parameter. We'll look at the following specific techniques:
Obtaining the value of Tcl variables and/or linking them to C/C++ variables.
The Treevariable subsystem.
Adding new compiled Tcl commands to the interpreter.
In our toy example, we need to provde and offset and a slope
to calibrate a single parameter. This section will show
how to obtain the values of two variables named
slope
and offset
defined in Tcl scripts at the C++ level.
![]() | NOTE |
---|---|
Before you think hard about extending and generalizing this code for your own use, you might check out the calibrated parameter SpecTcl plugin. |
We're going to show two ways to do this. First we'll ask Tcl to return the value of the variables. Second we'll link the variables to C++ variables. The key to all of this are the following classes:
SpecTcl
This class provides and application programming
interface (API) that provides access to
SpecTcl objects of interest. For this
example, and the subsequent examples, we will
need this class's getInterpreter
method.
Note that SpecTcl
is a
singleton object.
This means there can be only one.
The constructor for SpecTcl
is private. To get access
to the single instance, you must use the
getInstance
static
method of the class.
CTCLInterpreter
SpecTcl (and NSCLDAQ) include a C++ wrapping
of much of the Tcl API. This wrapping
is called Tcl++. The
CTCLInterpreter
,
is a wrapping of a Tcl_Interp,
or Tcl interpreter, object.
SpecTcl
::getInterpreter
returns a pointer to a CTCLInterpreter
object.
CTCLVariable
This class encapsulates a Tcl variable, or array and provides services for accessing, linking and, if derived, tracing the variable.
See the reference guides for detailed information about these classes.
Suppose we are writing a new event processor that will compute the calibrated parameter from the raw parameter. We're going to have a class definition for that event processor that looks like this:
Example 6-4. Calibration event processor
#ifndef CALIB_H #define CALIB_H #include <EventProcessor.h> #include <TreeParameter.h> class Calibrator : public CEventProcessor { private: CTreeParameter m_raw; CTreeParameter m_calibrated; public: Calibrator(); Bool_t operator()(const Address_t pEvent, CEvent& rEvent, CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder); private: double getSlope(); double getOffset(); }; #endif
If you've gone through the sections on writing event processors, this should be nothing new. We've added:
Two CTreeParameter
member
variables. m_raw
will
is a tree parameter for the raw parameter.
m_calibrated
is a tree
parameter for the calibrated parameter.
We've added a construtor whose job it will be to create the tree parameters properly.
We added two private methods,
getSlope
and
getOffset
that will insulate
our event processing code from the mechanisms of
fetching the calibration slope and offset values.
The constructor is pretty simple. Assuming the raw parameter is named raw and is from a 4K ADC e.g. and the calibrated parameter is named calibrated and represents energy in KeV between 0 and 2,000;
Example 6-5. Calibrated parameter constructor
#include <config.h> #include "Calib.h" ... Calibrator::Calibrator() : m_raw("raw", 4096, 0.0, 4095.0, "arbitrary"), m_calibrated("calibrated", 4096, 0.0, 2000, "KeV") {}
Let's look at the event processor. It will be registered in the event processing pipeline after whatever does the raw unpacking and produces the raw parameter. That means that our event processor has access to the raw parameter in the tree parameter rather than having to decode it again from the data:
Example 6-6. Calibrated parameter computatino:
Bool_t Calibrator::operator()(const Address_t pEvent, CEvent& rEvent, CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder) { if (m_raw.isValid()) { m_calibrated = getSlope() * m_raw + getOffset(); } return kfTRUE; }
Pretty simple. Note that we can only compute the calibrated parameter if the raw parameter has a value for this event. That's what the if statement checks.
Let's look at the getSlope
method.
Example 6-7. Getting the slope from a Tcl variable:
#include <SpecTcl.h> #include <TCLInterpreter.h> #include <TCLVariable.h> #include <cstdlib> ... double Calibrator::getSlope() { SpecTcl* pApi = SpecTcl::getInstance();CTCLInterpreter* pInterp = pApi->getInterpreter();
CTCLVariable slopeVar(pInterp, "slope", kfFALSE);
const char* slopeString = slopeVar.Get();
double slope; if (slopeString) { slope = std::atof(slopeString);
} else { slope = 1.0; } return slope; }
If you're not familiar with the classes being used some of this might be a bit confusing, so let's pick it apart step by step:
SpecTcl
class implements
a singleton object. For more on the Singleton
design pattern see e.g.:
https://msdn.microsoft.com/en-us/library/ee817670.aspx.
Singleton's don't have public constructors. They
have a method (in our case getInstance
)
that returns a poiner to the one allowed instance of
that class.
getInterpreter()
returns a
pointer to the Tcl++ object that wraps the
interpreter that SpecTcl uses for command processing.
This is the interpreter in which the variables
we are interested in will be defined. Note that
applications can, and often do, instantiate more than
one interpreter so it's important to do this to get the
right one.
CTCLVariable
wraps a variable
that lives in Tcl scripts. Note that this constructor
provides an interpreter in which the variable is
defined. The last parameter of the constructor is
a flag that indicates whether or not we want to trace
the variable as it changes. Tracing is beyond the
scope of this manual.
Note that creating an object like this does not ensure the variable exists, nor does it create it if it does not. This will be important to know in the lines of code that follow.
Get
method returns the
string representation of the variable's value.
Recall that in Tcl all objects have a string
representation. Not shown are a set of
flags that are passed to Get
.
By default, these flags look only at global variables.
That's what we want.
Previously I said that the creation of an instance
of CTCLVariable
does not
gaurantee the existence of that variable. It may never
have been given a value. If the variable has not
been given a value, then the return value from
Get
is a null pointer.
Get
was not a null pointer it is decoded as a
double. This code assumes the string is a value
representation of a floating point value. production
code might instead use std::strtod
which allows the caller to determine if the conversion
succeeded.
If the variable has not yet been set, the return
value from Get
is null
and a default value of 1.0 is
returned.
The code for offset is pretty much identical. As an exercise,
create a method getDouble
and
rewrite getSlope
and
getOffset
in terms of it.
Example 6-8. Getting the offset from a Tcl variable
double Calibrator::getOffset() { SpecTcl* pApi = SpecTcl::getInstance(); CTCLInterpreter* pInterp = pApi->getInterpreter(); CTCLVariable offVar(pInterp, "offset", kfFALSE); const char* offString = offVar.Get(); double offset; if (offString) { offset = std::atof(offString); } else { offset = 0.0; } return slope; }
The only difference from getSlope
is the name of the variable we care about and the default
value.
The code shown in these examples is perfectly fine. It will work. It does, however require that for each event with a raw parameter the Tcl interpreter has to give us the variable values and we need to convert those string values into doubles. We can use Variable Linking to speed this up.
Tcl allows us to associate a script variable with a C/C++ variable. When you do this, you specify the underlying type of the variable and Tcl constrains values to valid values for that type. To access the value of a Tcl variable that is linked, you just need to get the value of the local C/C++ variable.
To make all of this work for use we'll:
Add member variables to the class for the slope and offset.
Modify the constructor to link member variables to the variable. Note that links are always one-to-one.
Provide a class destructor to undo the link as storing data to the variable after our class is destroyed is likely to make a segfault happen. Note that this is good practice though in an event processor you probably could get away without it as most event processors exist for the length of the program's run.
Modify getSlope
and
getOffset
to return
the values of the linked variables. While we
could reference those variables directly in the
operator()
, we choose not
to so that the method of obtaining the variable
values is opaque to that method.
Let's look at the changes to the header:
Example 6-9. Calibrator header with linked variables:
#ifndef CALIB_H #define CALIB_H #include <EventProcessor.h> #include <TreeParameter.h> class Calibrator : public CEventProcessor { private: CTreeParameter m_raw; CTreeParameter m_calibrated; double m_slope; double m_offset; public: Calibrator(); virtual ~Calibrator(); Bool_t operator()(const Address_t pEvent, CEvent& rEvent, CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder); private: double getSlope(); double getOffset(); }; #endif
The three lines added to the header are emphasized above.
The constructor must be modified to link the variables to the Tcl Variables. Note that the code also ensures there are initial values for the variables that make sense:
Example 6-10. Constructor for linked variables
Calibrator::Calibrator() : m_raw("raw", 4096, 0.0, 4095.0, "arbitrary"), m_calibrated("calibrated", 4096, 0.0, 2000, "KeV") { SpecTcl* pApi = SpecTcl::getInstance(); CTCLInterpreter* pInterp = pApi->getInterpreter();CTCLVariable slope(pInterp, "slope", kfFALSE); m_slope = 1.0;
slope.Link(&m_slope, TCL_LINK_DOUBLE);
CTCLVariable offset(pInterp, "offset", kfFALSE); m_offset = 0.0;
offset.Link(&m_offset, TCL_LINK_DOUBLE); }
m_slope
forcing it to be a double.
If our event processor is destroyed, referencing the Tcl
variables slope
and
offset
will likely cause segmentation
faults. We therefore need code to undo the link
at destruction time:
Example 6-11. Destructor for linked variable calibrator
Calibrator::~Calibrator() { SpecTcl* pApi = SpecTcl::getInstance(); CTCLInterpreter* pInterp = pApi->getInterpreter(); CTCLVariable slope(pInterp, "slope", kfFALSE); slope.Unlink(); CTCLVariable offset(pInterp, "offset", kfFALSE); offset.Unlink(); }
Much of this code should be understandable. The
Unlink
removes any link the
variable may have with a C/C++ variable.
By using get getSlope()
and
getOffset()
methods, the
operator()
method body is unchanged.
The two getters, however become significantly simpler:
Example 6-12. Getting slope and offset for linked variables
double Calibrator::getSlope() { return m_slope; } double Calibrator::getOffset() { return m_offset; }
No explanation is needed.
In the previous section, we looked at how to get data from a Tcl variable using the SpecTcl and Tcl++ APIs to both SpecTcl and the Tcl interpreter. We used values stored in Tcl variables to compute a calibrated parameter.
In this section, we will introduce the treevariable subsystem. Daniel Bazin first designed and implemented this system as an extension to SpecTcl. Over time the code migrated into a contributed subsystsem and now, is a fully supported part of SpecTcl.
The idea of tree variables is similar to that of tree parameters. These object provide a hierachical namespace of variables. Each tree parameter can be treated as if it were a double. Tree parameters, in addition to having a value, have units of measure. A tree variable array class allows you to aggregate several tree parameters in to a single object.
Tree parameters are implemented as C/C++ variables that are linked to Tcl variables. In addition to the C/C++ variable they have units of measure.
We will use tree variables to manage a set of calibration parameters. We'll assume we have a treeparameter array of 16 elements that are called raw.0 ... raw.15. Associated with these 16 parameters are two tree variables of calibration constants, one, slopes is a tree variable array of 16 sloops while the other, offsets is a tree variable array of calibration offsets.
Note that by adding a second level of indirection, as with Tree parameters, more than one tree variable can be associated with a single Tcl variable. Thus the mapping of Tcl variables to tree parameters is one to many.
Let's look at a header for the calibrator we've described using tree parameters and tree variables.
Example 6-13. Header for calibrator using treevariables
#ifndef TREECALIB_H #define TREECALIB_H #include <EventProcessor.h> #include <TreeParameter.h> class TreeCalibrator : public CEventProcessor { private: CTreeParameterArray m_raw; CTreeParameterArray m_calibrated; CTreeVariableArray m_slope; CTreeVariableArray m_offset; public: TreeCalibrator(); Bool_t operator()(const Address_t pEvent, CEvent& rEvent, CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder); }; #endif
In addition to tree parameter arrays with the raw and calibrated parameters, we have tree variable arrays for the calibration values. The constructor must instantiate these properly. We're going to assume again, that the raw parameters come from a 12 bit digitizer of some sort.
Here's the constructor:
Example 6-14. TreeCalibratorConstructor
^Cfox@charlie:~/test/sample$ cat TreeCalib.cpp #include <config.h> #include "TreeCalib.h" TreeCalibrator::TreeCalibrator() : m_raw("raw", 12, 16, 0), m_calibrated("calibrated", 12, 16, 0), m_slope("slope", 1.0, "KeV/channel", 16, 0), m_offset("offset", 0.0, "KeV", 16, 0) {}
The form of the tree parameter constructor we are using says that the parameters are 12 bits wide (go from 0 through 4095). The tree variable array initializers create 16 slopes initialized to 1.0 and 16 offsets initialized to 0. The parameters will be calibrated in KeV.
Using Tree parameters and tree variables makes the
operator()
trivial to write:
Example 6-15. TreeCalibrator::operator()
Bool_t TreeCalibrator::operator()(const Address_t pEvent, CEvent& rEvent, CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder) { for (int i = 0; i < 16; i++) { if (m_raw[i].isValid()) { m_calibrated[i] = m_raw[i] * m_slope[i] + m_offset[i]; } } return kfTRUE; }
The really cool thing about tree parameters and tree variables from the programmer's point of view is that they mimic ordinary doubles and that arrays mimic ordinary arrays.
We'll continue to use our calibrated parameter as an example for how to add commands to SpecTcl. We've already seen that using tree parameters and tree variables provides a truly elegant solution.
Going back to our example of a single calibrated parameters: We're going to add a command called setcalib. setcalib accepts two double parameters. The first is the slope and the second the calibration offset.
This example is a bit involved:
We need an event processor that stores slope and offset and uses them to compute a calibrated parameter. The event processor must also provide a mechanism to set the slope and offset
We need a CTCLObjectProcessor
derived object. CTCLObjectProcessor
is a class that wraps Tcl compiled application specific
commands. All of SpecTcl's extensions to the Tcl
interpreter are implemented as classes derived from
CTCLObjectProcessor
.
The class we design and implement must also have a way to modify the slope and offset of the event processor.
We need to make the event processor and the command known to SpecTcl and Tcl respectively. Included in this are both modifications to the MySpecTclApp.cpp and Makefile files provided by the skeleton.
The header of the event processor look fairly normal:
Example 6-16. Event processor for command steered calibration
#ifndef CMDCALIBRATION_H #define CMDCALIBRATION_H #include <EventProcessor.h> #include <TreeParameter.h> class CmdCalibrator : public CEventProcessor { private: CTreeParameter m_raw; CTreeParameter m_calibrated; double m_slope; double m_offset; public: CmdCalibrator(); Bool_t operator()(Address_t pEvent, CEvent& rEvent, CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder); void setSlope(double slope); void setOffset(double offset); }; #endif
The constructor, in addition to constructing the tree parameters must initialize the sllope and offset to reasonable default values:
Example 6-17. Constructor for command steered calibration event proc.
CmdCalibrator::CmdCalibrator() : m_raw("raw", 4096, 0.0, 4095.0, "arbitrary"), m_calibrated("calibrated", 4096, 0.0, "KeV"), m_slope(1.0), m_offset(0.0) {}
The remainder of the event processor is equally trivial:
Example 6-18. Command drive event processor implementation
Bool_t CmdCalibrator::operator()(Address_t pEvent, CEvent& rEvent, CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder) { if (m_raw.isValid()) { m_calibrated = m_slope * m_raw + m_offset; } } void CmdCalibrator::setSlope(double slope) { m_slope = slope; } void CmdCalibrator::setOffset(double offset) { m_offset = offset; };
CTCLObjectProcessor
is the base
class for all Tcl++ command processors based on
Tcl_Obj* parameter lists.
Tcl_Obj* parameters are, however,
wrapped in CTCLObject
objects which
can do most of the type conversions needed.
Our command processor will contain a CmdCalibration event processor reference and will operate on the slope and offset in that object. This means that our header will look something like this:
Example 6-19. setcalib command processor header:
#ifndef SETCALIBCOMMAND_H #define SETCLIBCOMMAND_H #include <TCLObjectProcessor.h>class CmdCalibrator;
class SetCalibCommand : public CTCLObjectProcessor
{ private: CmdCalibrator& m_eventProcessor;
public: SetCalibCommand(CTCLInterpreter& interp, CmdCalibrator& evp);
int operator()(CTCLInterpreter& interp, std::vector<CTCLObject>& objv);
}; #endif
CTCLObjectProcessor
base class. This header provides the definition
of that class needed by derived classes.
CmdCalibrator
event processor.
We could also include the
CmdCalibration.h header.
We do not, however, need the compiler to know
the actual shape of the
CmdCalibrator
class when
it is compiling the header as we're going to hold
a reference to the actual event processor object
rather than the object itself.
Stylistically and practically, if you can get away with opaque forward definitions like this you should. In addition to the slight increase in compilation speed (the compiler doesn't have to include and compile the header at this time), there are circular definition problems that can be side stepped by using opaque forward references.
For example, suppose I have a class A that holds a reference to class B and class B holds a pointer to class A. There's no way to use header includes to resolve the need to define A to B's and B to A's header. Forward references such as the one shown above resolve this problem.
SetCalibCommand
is derived, as promised from
CTCObjectProcessor
.
CTCLObjectProcessor
constructor. Those two constraints determine
the argument signature for the constructor.
The command name is going to be hard coded in the implementation's call to the CTCLObjectProcessotr constructor. Note that it's more usual to allow the creator of the object to supply the command name to the constructor as this allows the application control over the command, and the namespace it might live in.
We hard code the command name for simplicity.
operator()
to be invoked when the command is executed.
That method is overriden by specific command
processors to provdee argument processing and
the logic of the command.
The operator()
method is
passed a reference to the interpreter and
a reference to the vector of command words
encapsulated in CTCLObject
objects. Note that the command words include
the command name word itself (objv[0]).
Let's have a look at the constructor. It's quite simple.
The base class constructor does all the work of registering
the command with Tcl and arranging for the
operator()
to be called inside
a try/catch block when the command is executed:
Example 6-20. setcalib command constructor:
#include <config.h> #include "SetCalibCommand.h" #include "CmdCalibration.h"#include <TCLInterpreter.h> #include <TCLObject.h> SetCalibCommand::SetCalibCommand(CTCLInterpreter& interp, CmdCalibrator& evp) : CTCLObjectProcessor(interp, "setcalib", true),
m_eventProcessor(evp)
{}
We made an forward reference to
CmdCalibrator
in our header and the header for
CTCLObjectProcessor
makes opaque forward references to both
CTCLInterpreter
and
CTCLObject
.
We have code in this implementation that needs to know the actual shape of these classes. You need to know the shape of a class if you are going to invoke its methods or refer to public data it may have.
The base class constructor we are using, requires the interpreter, on which the object will be registered as a command processor, the verb that will activate the command and a flag indicating whether or not you want the object immediately registered.
It is possible to register a command processor
object on more than one interpreter, but that's
beyond the scope of this manual. See the reference
material that covers Tcl++ for more information
about CTCLObjectProcessor
and the services it offers.
Now we turn our attention to the implementation of the command itself. Command processors provide two things back to the interpreter.
First they must return an integer status to the caller. This can be one of TCL_OK the command worked just fine. TCL_ERROR means the command detected an error and failed. Other return codes are used in command that provide flow control: TCL_RETURN indicates that the command requested a return from any proc the command was invoked in. TCL_BREAK indicates the command wants the inner loop that called this command to break out of the loop. TCL_CONTINUE means that the command wants the inner loop that called this command to skip the remainder of the loop body and start the next loop iteration (if appropriate to the loop condition).
Second, if desired, the command can have an interpreter result. Interpreter results are what will be substituted for the command if it's used inside a[] substitution. The interpreter result is also what's used as the error message in the event TCL_ERROR is returned. That's how we will use the interpreter command value as we have nothing interesting to return on success.
Here's a rough description of what our command will do:
Verify the number of command words is three. We need to have the command verb, the slope and the offset.
Extract the slope and offset from the command words.
Invoke the
setSlope
and
setOffset
methods of
the event processor to steer its subsequent
processing.
Note that we're going to centralize event handling by wrapping pretty much all of the code in a try catch block.
Example 6-21. setcalib Command processor implementation
SetCalibCommand::operator()(CTCLInterpreter& interp, std::vector<CTCLObject>> objv) { int status = TCL_OK;bindAll(interp, objv);
try { requireExactly(
objv, 3, "setcalib needs only slope and offset params" ); double slope = objv[1];
if (slope == 0.0) { throw "Slope equal to zero makes no sense";
} double offset = objv[2]; m_eventProcessor.setSlope(slope);
m_eventProcessor.setOffset(offset); } catch (CException& e) { interp.setResult(e.ReasonText()); status = TCL_ERROR; } catch (std::exception& e) { interp.setResult(e.what()); status = TCL_ERROR; }
catch (std::string msg) { interp.setResult(msg); status = TCL_ERROR; } catch (const char* msg) { interp.setResult(msg); status = TCL_ERROR; } catch (...) { interp.setResult("Unexpected exception type caught executing setcalib"); status = TCL_ERROR; } return status;
}
This initialization of status
ensures that if no error is detected, the return
code will be TCL_OK which indicates
success.
CTCLObject
objects
can be bound or unbound to interpreters. Some processing
of these objects can be done independent of an interpreter,
while the underlying API requires other calls to provide
an interpreter. We're just going to bind all
command words to the interpreter executing the command
so that it's available if we use any services that
require an interpreter.
The base class,
CTCLObjectProcessor
provides
a service method bindAll
that binds an interpreter to all elements of a vector
of CTCLObject
s.
CTCLObjectProcessor
provides
several utility methods for checking that your
command has been passed a valid number of parameters.
requireExactly
throws
an std::string
exception (the
value of the last parameter), if the size of
objv
is not exactly what
is specified by the second parameter.
requireAtLeast
and
requireAtMost
are also
provided to handle cases where a command may have
a variable number of parameters.
Note as well that the command requires three parameters
because objv[0]
is always the
command verb.
CTCLObject
provides type
conversion
operators that are capable of extracting the
various representations from the
Tcl_Obj* it wraps. In this case the
implicit type coniversion extracts the
double precision floating
point representation from that object.
If the required representation cannot be extracted
(e.g. the slope parameter might have the value
bad), a
CTCLException
is thrown.
This exception type is derived from
CException
.
Note that while we throw the exception here, it's caught a bit lower down in the code and turned into an error.
CTCLInterpreter
setResult
to set the
command result as well as setting the status
variable to TCL_ERROR so that
the interpreter knows an error occured.
Before continuing on, an important point of exception
handling is illustrated here. The principle that
your catch blocks should catch
as generic a base class as possible. Thus instead of
catching CTCLException
we catch
the ultimate base class CException
,
similarly we catch std::exception
the ultimate base class of C++ language exceptions.
The reason this is a good idea is that it insulates us as much as possible from knowing the exact exception types the code we use throws. There are cases where you must make an exception (no pun intended) to this rule.
Suppose you use an I/O package that throws a variety
of objects derived from iopkgexception
.
Suppose that an attempt to read past end of file
results in a derived type eofexception
being thrown. It can make sense to catch
eofexception
prior to trying
to catch the generic iopkgexception
because you need to process an eofexception
differently than a generic error.
Note as well the final catch(...) which catches unanticipated exception types and turns them into a generic error message and a return code of TCL_ERROR.
To make all of this work we need to:
Ensure that MySpecTclApp.cpp includes the headers for our event processor and our command processor.
Instantiate and register our event processor in the event procesing pipeline after the raw event decoding has been done.
Create an instance of our command processor registered on the SpecTcl command interpreter and given an instance of our event processor.
Make additions to the Makefile so that our event processor and our command processor get built into SpecTcl.
Including headers. Locate all of the #include directives towards the top of MySpecTclApp.h. Inser the following towards the bottom of those directives?
Instantiate and register the event processor.
We're going to statically create the event processor.
Registration will be done in the usual mannner
in CreateAnalysisPipeline
.
Just prior to CreateAnalysisPipeline
create an instance of the event processor. With intervening
comments at the front of that method stripped out, this
will look like:
CmdCalibrator calibration; void CMySpecTclApp::CreateAnalysisPipeline(CAnalyzer& rAnalyzer) { RegisterEventProcessor(*(new SimpleEventProcessor), "Test"); RegisterEventProcessor(calibration, "Calibrator"); }
We created this static instance of
CmdCalibrator
so that
we can find it when we make our command processor
Creating and adding our command processor.
MySpecTclApp.cpp has a
method; AddCommands
.
That method is responsible for adding commands
to the base Tcl command set.
Add a line that Creates an instance of our event processor using the interpreter that's passed in to that method. Here's what it looks like after the addition:
void CMySpecTclApp::AddCommands(CTCLInterpreter& rInterp) { CTclGrammerApp::AddCommands(rInterp); new SetCalibCommand(rInterp, calibration); }
Note that you should never
remove the call to
CTclGrammerApp::AddCommands
at the top of this method. That call adds SpecTcl's
command set.