Composite waveform component for Delphi
Paul Milenkovic
Copyright 2002
TF32 is the
Time-frequency analysis for 32-bit Windows software program. A key element of that program is a wave plot
component that displays one or more time-synchronized waveform channels. Components that compute and display pitch,
time-frequency spectrogram, and an x-y articulatory display are in turn linked
to those waveform channels to produce a time-synchronized display where
analyses are automatically updated in response to changes in the
waveforms.
Components are autonomous
software modules that can be combined to construct an application program after
the fashion of Visual Basic, Delphi, Lab View, Visual Studio .NET and related
systems for software development.
Components are often implemented using object-oriented programming where
a class is a software template for generating instances of an object type
defined by that class. In this
circumstance, a customized component class is derived from a base class that is
part of a class framework. A control is
a component that produces a visual display in a graphical user interface (GUI)
and can interact with user input from the keyboard or mouse. Object-oriented programming is popular for
GUI development because a control may require a great deal of implicit behavior
that needs to be replicated for each style of control, and this implicit
behavior can be inherited from a base class in the framework, hiding the detail
work from the concern of the application programmer.
The TF32 wave plot
component is implemented in Delphi as a composite object made up of one or more
wave channel objects. Each wave channel
object displays a single waveform channel.
The wave plot object organizes the wave channel objects in its
collection to provide a stacked display of time-aligned waveforms. The number of wave channels is dynamically
allocated in response to making monaural, stereo, or multi-channel recordings
or when retrieving recorded waveforms from files.
A Delphi application is
typically constructed by object composition.
Using a software tool called the Form Designer, one or more component
objects can be selected and added to a form, an application main window. The Object Inspector is a tool for
customizing these objects by setting data values, the component
properties. Events are a special kind
of property that can link a component to its container form – when an
application is run, pressing a button control can invoke an event to cause an
action to take place on the form.
The Form Designer saves a
list of component objects that belong to the form along with keywords
specifying property values for each of those objects. Such values are stored in a separate .DFM file for each form
making up an application program. When
the application program is run and a form is created, the run-time library
incorporated into that program reads in the .DFM file for that form, creates
instances of the component objects contained in that form, sets data fields of
the form object to reference the component objects, and initializes the
components by setting data values to the stored properties.
The class definition for a
component object can declare property variables in the published section. Published properties will be automatically
saved and retrieved from the .DFM file.
This system works well for properties that are numeric values or
character strings and for components that are not containers for other
components. It also works for
properties that are references to other components on the form – this is a way
to link components together – although it requires coding an override of the
Notification method to keep track of when the referenced component objects get
destroyed.
What about components that
contain other components? Delphi allows
picking components from the palette and placing them inside panel
components. Invoking Files Open on a
.DFM file from Delphi will show a text representation of the components and
will show component definitions nested within a panel component definition,
which will be in turn nested within the definition for the main form. Each component definition will contain
keywords specifying property values for that component in the nested structure.
The wave plot is a
composite component with different requirements than the panel component. The number and layout of wave channel
components are controlled by the wave plot FileName property (specifies the
name of a waveform file containing one or more channels ) and the NChan
property (specifies the number of zero-valued waveform channels when the
FileName property is blank). It turns
out that is possible to override virtual methods of the wave plot base class (TWinControl
in this example) to allow the wave plot to manage the wave channel collection
while allowing properties of each wave channel object to be saved to the .DFM
file.
The first requirement is to
derive the wave channel class TWaveChan from a VCL class (such as
TCustomControl, a class derived from TWinControl that adds management of a
display context for drawing the waveform) and to define properties to be saved
in the .DFM file as published.
TWaveChan defines property ytic (plot vertical grid spacing) of type
Single and property readout (name of the numeric readout mode for the plot) of
type string. The next requirement is to
create wave channel objects in response to setting the FileName or NChan
properties of the wave plot object, setting both the Owner and Parent
properties of the wave channel object to the wave plot object, referenced by
parameter Self inside the wave plot class.
The Owner is passed as a parameter of the wave channel constructor while
the Parent property is set by assignment following wave channel construction.
The Owner and Parent
properties may seem confusing, but they contain object references that have
entirely different purposes. Parent has
meaning in terms of the underlying Windows API that the Delphi VCL hides
(perhaps too well). The parent is the
object that graphically contains another Windows control; setting the wave
channel Parent to the wave plot instance insures that the wave channels appear
as panes within the wave plot window.
Owner has meaning in terms of the Form Designer. Objects owned by the form can be dragged and
deleted by the Form Designer, even if they are placed within a composite
component as the case with a panel component.
If we create a component while the Form Designer is running, make the
containing form the Owner and set the Name property, a variable with that name
will appear in the code listing for the form class allowing form methods to
reference that component. This happens
even if a component is placed programmatically on a form in response to setting
a component property without the requirement of selecting that component from
the palette and placing it with the mouse.
Since we want to disable direct manipulation of individual wave channels
by the Form Designer and want to treat the entire wave plot as a unit, it is
necessary to make the wave plot the wave channel owner.
Were we to set the wave
channel Owner to the containing form while setting Parent to the wave plot, the
VCL would automatically save wave channel properties to the .DFM file. Since we are making wave plot the wave
channel Owner, it is necessary to override the GetChildren method of the base
class from which wave plot is derived.
Since wave channels are the only elements of the Controls list (the only
components having the wave plot as Parent), we code
{$IFDEF VER90}
procedure
TWavePlot.GetChildren(Proc: TGetChildProc);
{$ELSE}
procedure
TWavePlot.GetChildren(Proc: TGetChildProc;
Root: TComponent);
{$ENDIF}
var i: Integer;
begin
for i := 0 to Pred(ControlCount) do
Proc(Controls[i]);
end;
where VER90 refers to
Delphi 2.0 -- Delphi 3 and later
versions add the Root parameter to GetChildren.
GetChildren has a
misleading name. GetChildren does not
get the child controls at all --
instead it takes a function pointer called Proc as an argument and applies Proc
to those child controls that qualify for saving to the .DFM file. GetChildren gets called as part of a tree
traversal of nested child controls during the process of writing the .DFM file,
but the default GetChildren function considers only those child controls that
specify the parent form as Owner. The
above override of the GetChildren method of the wave plot class enables saving
the wave channel components that have wave plot as both Parent and Owner.
Now that we have saved the
wave channels to the .DFM file, how do we read them back in? The process that reads the .DFM file first
creates the object instance for each component and then sets property values
for that instance. The reader needs to
match class names input from the .DFM file with compiled object Pascal classes
in order to construct object instances for those classes. The global RegisterClass function needs to
be called with the class name TWaveChan as an argument in order to be able to
construct wave channel instances in response to reading the .DFM file. I call RegisterClass from the constructor of
the wave plot class and I invoke UnregisterClass from the destructor to clean
up.
The .DFM file reader also
needs to know how to set the Owner property of components that have a wave plot
as a Parent. This information is
supplied by overriding the GetChildOwner method of the wave plot class
according to
function
TWavePlot.GetChildOwner: TComponent;
begin
GetChildOwner := Self;
end;
The default GetChildOwner
returns nil, which instructs the reader to make the containing form the
owner. The override of GetChildOwner
directs the reader to make the wave plot the owner, the result we want.
To summarize, we give the
wave plot control over the wave channels by making the wave plot Owner of the
wave channels, and we enable saving those wave channels to the .DFM file by
overriding the wave plot GetChildren method.
We can retrieve wave channels from the .DFM file by invoking RegisterClass
on the wave channel class so the reader knows how to construct wave channel
objects and by overriding GetChildOwner to tell the reader that the wave
channel instances we are reading in are owned by the wave plot object. The need to override GetChildren and
GetChildOwner is a consequence of making the wave plot owner to the wave
channels. This owner relationship is
required to prevent the Form Designer from meddling with the wave channel
window panes of the wave plot display, but if we want to allow the Form
Designer to have control, we need to make the containing form owner of the wave
channels and we can dispense with overriding GetChildren and GetChildOwner.
There is one remaining
wrinkle. The reader cannot set
properties of already created wave channel objects; the reader creates objects
and sets properties of the objects it creates.
The reader also reads in the wave plot properties prior to creating wave
channel objects and setting their properties.
This creates a problem because setting the FileName and NChan properties
before inputting the wave channel objects will create a duplicate set of wave
channels. The solution is to detect
when the wave plot is being read from the .DFM file and to defer inputting the
file specified by FileName or loading zero-values into channels specified by
setting NChan until after this reading process is completed. The Boolean expression csLoading
in ComponentState will return True for
this condition. The wave plot Loaded
method gets called when the wave plot and its peer form components have been
completely read from the .DFM file. By
overriding Loaded, we can perform the deferred wave channel input specified by
FileName and NChan.
For reference information
on the use of GetChildren and GetChildOwner in writing a composite Delphi
control, see John M. Miano’s Component Writer’s FAQ at
http://www.ellipse-data.com/delphifaq/delphi_faq_component.html.