![]() |
![]() |
![]()
| ![]() |
Sound + Vision Multimedia / Experts / Tool Services / Delphi 1-5
A Multimedia Assembly Line: Part II Generating a Component
In Part I we began building a Delphi sound expert (Wizard) that produces multimedia components. Based on a simpler expert, SndExprt 1.0, it adds sound-playing (.WAV file) capabilities to any component. We also dealt with the expert structure and the user interface. Before we take a detailed look at the code-generating engine, let's review that structure.
The sound-generating property, SoundPlayEvents, and its subproperties are arranged in a hierarchy. The main property added to each component, SoundPlayEvents, contains a series of properties, XSoundPlayEvent, where X is the name of one of the sound events supported. These properties, such as ClickSoundPlayEvent, EnterSoundPlayEvent, and ExitSoundPlayEvent, contain their own subproperties. Those that manage the playing of sounds are shown in Figure 1.
Figure 1: Subproperties of XSoundPlayEvent.
With SoundSource, the flags in fdwSound (SND_ALIAS, SND_FILENAME, and SND_RESOURCE) determine how the Filename property will be interpreted as a file name, a resource identifier, or an alias for a system event. Filename is a string holding the name of the file, resource, or other location. All of these subproperties are included in the?SoundPlayEvent property, where "?" is the name of one of the sound events.
How do we program our expert to generate a component with this new composite property? You'll notice we use several types and data structures from the unit, SndTypes.pas, introduced in Part I. Now let's explore some of the details.
Programming the Expert Engine In writing an expert, its engine is where the real programming generally takes place. It's also where I had to write most of the new code for the updated expert. In both the original and revised experts, I found it helpful to have a prototype of the type of component the expert would generate to which we could refer. That prototype included all the new functionality and properties we've been discussing.
After you've made all your choices and clicked Finish, the code for your new component is generated (CompGen1.pas) and opened in the Delphi IDE editor. There you can look at the code, edit it, or save it. Let's concentrate on the component-building code. Here is the most important routine in the engine:
procedure WriteALine(StringIn: string); begin TempMemoStr.Add(StringIn); end;
TempMemoStr is a StringList that collects all of the code to be saved in a file. Because we call this method for every line of generated code, the CompGen1.pas unit is now too large to include in its entirety. Instead, we'll highlight some of the more interesting routines. Compared to the earlier version, the WriteTypes method is expanded considerably. It writes the support types and a specific Sound Options type for each enabled event. In the WriteSoundOptions method, lines such as the following read the data collected from running the expert, and produce the proper types by using strings associated with the event to name them:
if spPlayReturn in SoundFactorsArray[j].SoundPlayOptions then AddSoundOption(EventPrefixes[j] + 'spPlayReturn');
The EventPrefixes array stores prefixes for each event with an appropriate name: "ck," "dd," and "do" are prefixes for the Click, DragDrop, and DragOver events, respectively.
There are other significant changes in this new version that are necessary to create the new property structure in the target components created. In the previous version, we simply added two properties to each generated component. Here we add just one, but it's a monster with multiple class properties, each of which has a number of subproperties. Figure 2 shows the new property from a newly produced component in the Object Inspector with a few of these subproperties shown.
The new Sound Options types we just discussed provide the basis for the most important subproperties used under each event subproperty. The work of writing the main component class and the host of event subclasses is done in the WriteNewClasses procedure. There are several nested procedures within this method, including WriteMainClass, which writes the new component class, and WriteSoundClasses, which writes a new class for each Sound Event. Let's examine that procedure, as it demonstrates several interesting issues. Pay particular attention to the nested procedure within WriteSoundClasses, WriteSoundClass. This latter procedure, shown in Listing One, writes each of the individual sound event classes.
As you can probably guess, this one procedure writes the bulk of the new property with all of its subproperties. The variable or changing portions of this code depend upon a number of arrays of strings: BasicEventNames, which includes the same event names used with the check boxes (Click, DragDrop, etc.); EventNames, which is the name of the actual method used to fire the event (DoEnter instead of Enter, for example); and a smaller array, ParametersArray, which contains the strings used for parameters when the inherited methods are called in the overridden event methods.
In this code, and in code we examined in Part I, EventMax is used a great deal. This constant represents the number of events supported minus one, because all of the iterations are zero-based. It's used in most of these arrays, as well as in most iterations. By using this constant, it will be easy to change this code by adding or deleting supported events. For example, take a look at WriteConstructors. A procedure nested within it, WriteSoundClassesConstructors, iterates through all possible events and writes a constructor for each one the user has selected. Similarly, the WriteEventHandlers procedure writes event handlers for those events.
The code in Listing One may seem like a lot of code for producing a series of rather simple classes, but it does more than just write all of these classes. Let's start with the individual classes. First, we need to determine which events will be sound-enabled by iterating through the check-box flags with:
for j := 0 to EventMax do if EventCheckBoxArray[j]= ecbsChecked then ...
Then we build an array that contains information on which events are sound enabled. We use the NewClasses variable to monitor how many are actually used. As we iterate through all of the possible events, we build that array:
NewClassArray[NewClasses] := BasicEventNames[j];
BasicEventNames contains the basic names of the enabled events. We use this array later to write the implementation code. The code that follows simply writes the remainder of the class declaration. When we reach the last line, we call a nested procedure with:
WriteSoundPlayEventsClass;
This procedure writes another class - the main class that contains all of the individual sound-enabled events and their subproperties. With this procedure, we make good use of the NewClassArray to create a property for each sound-enabled event. Figure 3 shows the generated code for this class.
TSoundPlayEvents = class(TPersistent) private FClickSoundPlayEvent : TClickSoundPlayEvent; public constructor Create(AOwner: TComponent); published property ClickSoundPlayEvent : TClickSoundPlayEvent read FClickSoundPlayEvent write FClickSoundPlayEvent; end; Figure 3: The TSoundPlayEvents class in a generated component.
You'll note that we have just one event - the Click event. We could just as easily have three or 12. We must create one more class - the component class. Some of the information for this class comes from the first page of our expert (see Part I), its class name, and its ancestor class. Other than that, the code is identical for any component created with this expert. Figure 4 shows a main component's class declaration.
TSoundButton = class(TBitBtn) private FSoundPlayEvents : TSoundPlayEvents; public constructor Create(AOwner: TComponent); override; procedure Click; override; published property SoundPlayEvents : TSoundPlayEvents read FSoundPlayEvents write FSoundPlayEvents; end; Figure 4: A new component's main class declaration.
Upon casual examination, this code is deceptively simple. Keep in mind, however, the many subproperties within the SoundPlayEvents property we just discussed. If you enable many events and then view all of the details, it could take up most of the space in the Object Inspector (again, refer to Figure 2).
Next, we need to create constructors for these classes. That chore is rather straightforward, so we'll skip a detailed discussion of it. One issue worth mentioning is that we need a way to handle the options on our pop-up dialog box concerning defaults (.WAV files or Yield). In the individual event classes, we handle that situation with the following code, using data collected in our expert:
if SoundFactorsArray[j].WavFileDefault then WriteALine(' FSoundFile := ''' + SoundFactorsArray[j].DefaultWavFile + ''';') else WriteALine(' FSoundFile := ''*.wav'';'); WriteALine(' FSoundSource := ssFilename;'); if SoundFactorsArray[j].Yield then WriteALine(' FYield := True;') else WriteALine(' FYield := False;');
Even more interesting to examine is the method that actually does the work, in this case the Click method (see Figure 5).
procedure TSoundButton.Click; var fdwSoundValue : DWORD; AhmodValue : HMODULE; AFileName : array [0..255] of Char;
function GetfdwSoundValue( ASoundPlayEvent: TSoundPlayEvent): DWORD; begin case ASoundPlayEvent.SoundSource of ssFilename: Result := SND_FILENAME; ssMemory: Result := SND_MEMORY; ssAlias: Result := SND_ALIAS; ssAliasID: Result := SND_ALIAS_ID; ssResouce: Result := SND_RESOURCE; end; end; { GetfdwSoundValue }
begin with SoundPlayEvents do begin fdwSoundValue := GetfdwSoundValue(ClickSoundPlayEvent); if (fdwSoundValue = SND_RESOURCE) then AhmodValue := ClickSoundPlayEvent.hmodValue else AhmodValue := 0; StrPCopy(AFileName, ClickSoundPlayEvent.SoundFile); if ClickSoundPlayEvent.yield then fdwSoundValue := (fdwSoundValue OR SND_NOSTOP); case ClickSoundPlayEvent.ClickSoundPlayOption of ckspPlayReturn: PlaySound(AFileName, AhmodValue, fdwSoundValue or snd_Async or snd_NoDefault); ckspPlayNoReturn: PlaySound(AFileName, AhmodValue, fdwSoundValue or snd_Sync or snd_NoDefault); ckspPlayNoSound: ; ckspPlayContinuous: PlaySound(AFileName, AhmodValue, fdwSoundValue or snd_Async or snd_Loop or snd_NoDefault); ckspEndSoundPlay: PlaySound(nil, AhmodValue, fdwSoundValue or snd_Async or snd_NoDefault); end; { case } end; inherited Click; end; Figure 5: An event handler override to play a sound.
There are various steps involved here. First, we use the local function, GetfdwSoundValue, to determine the sound source type (SoundSource), and return the corresponding flag. We then plug that flag into the last parameter of the PlaySound function along with other appropriate flags. Unless we use the SND_RESOURCE flag, the hmodValue is set to zero. One option (that we don't enable here) concerns default sounds: snd_NoDefault is always set. Of course, we could add an additional subproperty. That will have to wait for the next version.
Conclusion We haven't investigated every detail of the code-generating engine. However, the other procedures use similar approaches and these same string arrays to accomplish their purpose. This version represents a major enhancement of the previous one. We've extended the expert to handle other standard events besides the Click event. Unfortunately, we haven't provided a way to handle events that might apply to just a few components, let alone new events we write ourselves. This would be a difficult to accomplish.
It would also be nice to have a property editor to use with our new SoundsEvents property. Again, this would be tricky, because each property will have its own character with a different list of subclasses, as well as a different list of properties within each subclass. Still, there might be an appropriate and elegant means of doing this. I plan to continue to update this expert, so be sure to let me know what you'd like to see. In the meantime, enjoy using the many sound-enabled components you'll create using this tool.
The files referenced in this article are available for download.
Alan Moore is a Professor of Music at Kentucky State University, specializing in music composition and music theory. He has been developing education-related applications with the Borland languages for more than 10 years. He has published a number of articles in various technical journals. Using Delphi, he specializes in writing custom components and implementing multimedia capabilities in applications, particularly sound and music. You can reach Alan mailto:acmdoc@aol.com.
Begin Listing One - WriteSoundClasses routine procedure WriteSoundClasses; var j, NewClasses : Integer; NewClassArray : array [0..EventMax] of string;
procedure WriteSoundPlayEventsClass; var i: Integer; begin WriteALine(' TSoundPlayEvents = class(TPersistent)'); WriteALine(' private'); for i := 0 to (NewClasses-1) do WriteALine(' F' + NewClassArray[i] + 'SoundPlayEvent : T' + NewClassArray[i] + 'SoundPlayEvent;'); WriteALine('public'); WriteALine(' constructor Create(AOwner: TComponent);'); WriteALine(' published'); for i := 0 to (NewClasses-1) do begin WriteALine(' property ' + NewClassArray[i] + 'SoundPlayEvent : T' + NewClassArray[i] + 'SoundPlayEvent read F' + NewClassArray[i] + 'SoundPlayEvent'); WriteALine(' write F' + NewClassArray[i] + 'SoundPlayEvent;'); end; WriteALine('end;'); WriteALine(''); end; { WriteSoundPlayEventsClass }
begin { WriteSoundClasses } NewClasses := 0; for j := 0 to EventMax do if EventCheckBoxArray[j]= ecbsChecked then begin NewClassArray[NewClasses] := BasicEventNames[j]; inc(NewClasses); WriteALine(' T' + BasicEventNames[j] + 'SoundPlayEvent = class(TSoundPlayEvent)'); WriteALine(' private'); WriteALine(' FSoundFile : TFileName;'); WriteALine(' FSoundSource : TSoundSource;'); WriteALine(' FYield : Boolean;'); WriteALine(' F' + BasicEventNames[j] + 'SoundPlayOption : T' + BasicEventNames[j] +'SoundPlayOption;'); WriteALine(' public'); WriteALine( ' constructor Create(AOwner: TComponent);'); WriteALine(' published'); WriteALine(' property SoundFile: TFileName ' + 'read FSoundFile write FSoundFile;'); WriteALine(' property SoundSource: TSoundSource ' + 'read FSoundSource write FSoundSource ' + 'default ssFilename;'); WriteALine(' property Yield : Boolean ' + 'read FYield write FYield default False;'); WriteALine(' property ' + BasicEventNames[j] + 'SoundPlayOption : T' BasicEventNames[j] + 'SoundPlayOption read F' + BasicEventNames[j] + 'SoundPlayOption write F' + BasicEventNames[j] + 'SoundPlayOption;'); WriteALine('end;'); WriteALine(''); end; WriteSoundPlayEventsClass; end; End Listing One
|
![]() |
|