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.