The Persistence of Objects PDF
The Persistence of Objects PDF
ON THE COVER
5 The Persistence of Objects Robert Vivrette
Users hate to perform boring tasks over and over again. And so do programmers! Learn to use Delphis RTTI capabilities to create generic, object-oriented, registry-aware components without drowning in drudgery yourself.
30 At Your Fingertips
Searching Multiple Directories, etc. John P. Gambrell This months tips include the finer points of the FileSearch function and the importance of good parenting.
FEATURES
14 Informant Spotlight
Down to the Metal Jay Cole Heres an exotic one. Tired of C/C++ programmers claiming supremacy in low-level programming? Would you believe using assembly to create a dynamic (run-time) compiler?
REVIEWS
32 The Birth of Raptor
Product Review by Alan Moore, Ph.D. Book Review by Alan Moore, Ph.D.
37 Hidden Paths of Delphi 3 37 Special Edition Using Delphi 2 and Special Edition Using Delphi 3
Book Review by Alan Moore, Ph.D.
22 Greater Delphi
Whats Multi-Generational Architecture, Anyway? Bill Todd This fifth and final installment explains what really sets InterBase apart, by exposing the problems its structure was designed to solve. Feel lucky? Grab a wad of cash, and get ready for ATM roulette.
DEPARTMENTS
2 Delphi Tools 4 Newsline 40 File | New by Richard Wagner
25 DBNavigator
Trying on TeeChart Cary Jensen, Ph.D. Theres more to Delphi 3 than its marquee players. Among the unsung is TeeChart, which builds charts at either design time or run time, from any data available to your application. Heres how.
Delphi
T O O L S
New Products and Solutions
TCompLHA 2.0 and TCompress Component Set 3.5 Ship from SPIS Ltd
SPIS Ltd, makers of the TCompress Component Suite for Delphi and C++Builder, has released TCompLHA 2.0 and TCompress Component Set 3.5. TCompLHA supports Delphi 1, 2, and 3, as well as C++Builder. It helps create and manage archives compatible with the freeware LHArc and LHA utilities archives which can also be used by utilities including WinZip. One-step methods such as Scan, Compress, Expand, Delete, and Verify simplify archive management. Key properties such as ArchiveName, CompressionMethod, Confirm, and FilesToProcess provide control over how files are processed, including wildcard expansion and user interface customization. TCompLHA also offers segmented (multidisk) archive processing. Additional features such as full archive verify and safe mode processing enable TCompLHA to be used in communications applications for the Internet. TCompLHA ships with a demonstration, source examples, and Help files. SPIS Ltd also announced a new version of their compression components for Delphi 1, 2, and 3, and C++Builder. In addition to new Delphi 3 and C++Builder support, TCompress 3.5 adds a compression method, a rich text BLOb compression component, and new utility functions for more flexible archive management. TCompress 3.5 provides native components for the creation of multi-file compressed archives, as well as database, file, resource, and in-memory compression using streams. Two compression methods (RLE and LZH) are built in, with hooks for adding custom compression formats. TCompress 3.5 also includes drop-and-play components for automatic database BLOb, image, rich text, and memo compression, based on the Delphi VCLs TDBMemo, TDBRichText, and TDBImage components. TCompress 3.5 includes a demonstration, source code, examples, and Help files. SPIS Ltd Price: TCompLHA 2.0 is US$65 for registration; US$40 for TCompLHA component source. TCompress Component Set 3.5 is US$65 for registration; US$40 for TCompress component source; and US$34 for source of the compressed DB controls. Fax: +64-3-384-5138 E-Mail: [email protected] Web Site: http://www.spis.co.nz
new or existing applications. Remember This uses Delphis Run-Time Type Information (RTTI) to automatically handle any component. It relies on a unique and extensible StateSaver technology to customize the saving of all Delphi standard controls. Remember This is available in a Delphi edition; a C++Builder edition is scheduled for release in the 3rd quarter of 1997. Both versions include online Help, sample applications, unlimited support, and a 60-day moneyback guarantee. A trial version is available from Design Systems Web site.
Design Systems Price: The developers kit of the Delphi or C++Builder version is US$119; with source code its US$179. A bundle of both versions is US$139; the bundle with source code is US$199.
Delphi
T O O L S
New Products and Solutions
ISBN: 0-672-31114-3 Price: US$29.99 (599 pages) Phone: (800) 545-5914 or (317) 581-5801
decompressing streams of data on-the-fly. Unlike other libraries for handling ZIP files, Abbrevia components are written in Delphi, and will compile directly into an application. Abbrevia requires no additional files be shipped with an application that uses its components. This product ships with documentation, online Help, free technical support, and a 60-day
News
L I N E
October 1997
Scotts Valley, CA Borlands first quarter revenues for Fiscal 1998 were US$41,970,000, compared to revenues of US$38,146,000 for the first quarter of the previous fiscal year. (First quarter revenues were up 10 percent over the first quarter of fiscal year 1997.) Net income for the first quarter of fiscal year 1998 was US$79,000, break-even in terms of earnings per share. This is compared with a net loss of US$21,809,000 (or US$.60 per share) for the first quarter of fiscal 1997. The companys cash balance on June 30, 1997 was US$76,600,000, compared to an ending cash balance of US$54,400,000 on March 31, 1997. The increase in cash was primarily the result of the
closing of the privately placed equity financing of approximately US$25 million, and exercises of employee stock options of about US$1.6 million. When Delbert W. Yocam, Chairman and CEO, joined Borland in December 1996, he pledged a return to profitability in fiscal year 1998, which began on April 1, 1997. To achieve this goal, he cut 30 percent of the workforce, and initiated new costreduction programs.
building, managing, and deploying middle-tier client/server applications. The new update to Entera 3.2 is available to all registered Borland Entera support subscribers. Borland plans to ship a new version of Entera later this year. For more information, visit Borlands Web site, located at http://www.borland.com.
On the Cover
Delphi 3
By Robert Vivrette
our parents taught you to be persistent. Its the only way to get noticed these days! Well, persistence isnt just a good quality for humans; computer users have come to expect a certain level of persistence in the programs they use as well. Think about the applications you use every day. No doubt each has its own problems with persistence that annoy you. For example, I use CompuServe for Windows to manage my CompuServe mail, and I am always irritated at the number of places within the program where I have to perform boring tasks over and over again; when I launch the program, it never leaves me where I left off. I have to click on the Mail Center button to look at my list of incoming mail; and whenever I add attachments to messages, the Open dialog is never looking at the right directory. Virtually every program I use has these kinds of minor shortcomings. Im sure when they were developed, saving the existing state of certain controls was not considered a priority. In fact, many programmers consider such programming to be drudgery. Yet programming an application to be persistent is a vital piece of your job as an application developer. Your application might be the greatest thing since they put soap in Brillo pads, but if it requires users to unnecessarily provide repetitive input to the program, it will quickly become an annoyance. Here are just a few of the kinds of information that a program should save: User information (name, address, passwords, etc.) Sizes and locations of forms (including the main application form) States of program options History lists of file Open and Save dialogs
5 October 1997 Delphi Informant
Last directory visited by Open and Save dialogs History list of recently-accessed document files Custom wildcard specifications for file Open and Save dialogs Typefaces used in the application Last visited tab in tab-sheet controls The list is virtually endless, and I could go on with additional items. Probably the best indicator of when you have a problem with persistence is to watch a user run your program repeatedly. How many times did they have to perform the same steps? How many times did they have to resize a form, or change to another tab? Every place where there was a repetitive action is a place where you could save that state from the previous execution of the application.
On the Cover
only the registry, but if youre stuck on .INI files (or are developing 16-bit applications) the same principles apply. The format of the registry is a tree structure. Each branch of the tree (or key in registry terminology) holds information about a particular user, application, or piece of hardware. At the end of these keys, there are Name/Value pairs. This is where persistent data is saved. For example, for the HKEY_CURRENTUSER\ControlPanel\Colors key, there is a list of key names (e.g. ActiveBorder, ActiveTitle, AppWorkspace, etc.) that have color assignments as values. When you open the Control Panel and access the colors applet, Windows retrieves these color assignments as your color options. This article assumes you have a basic understanding of how the registry works, so before we go any farther, I must provide this warning: The registry holds data essential to the operation of Windows; if you dont know what youre doing and you accidentally corrupt data in the registry, you quite possible might render Windows inoperative. Therefore, please make certain you understand how the registry functions and make backups of the registry before doing any experimentation. Windows 95 and NT have a program called RegEdit to manipulate the registry database. The online Help for RegEdit will help you if you have questions on its use. With that out of the way, lets go on. So we want to save persistent application data, and we want to use the registry to hold that data. How can you get a component to save pieces of itself (properties, values, etc.) automatically? Well, there have been many efforts in the Delphi community to create persistent objects, and to understand the issues a bit more, lets look at some of these along with their advantages and disadvantages. add registry support. If you want to add registry support to ListBoxes, ComboBoxes, StringGrids, RadioButtons, and CheckBoxes, you will have to create a new subclassed control for each of these and add the same registry support code that you did for TRegistryEdit. The code you add would be essentially identical, but would of course save the checked state of a CheckBox and RadioButton, the list of items in a ListBox and ComboBox, etc. However, when you add all these new controls to your form, youre adding quite a lot of redundant registry support code. Remember, each subclassed control you added had its own registry support! Also, your Component palette will have to house all these new registry-aware components. In addition, you still will need to manage (in code) each of these controls. Your FormCreate method might start to look like this:
procedure TForm1.FormCreate(Sender: TObject); begin Edit1.LoadFromRegistry; Edit2.LoadFromRegistry; Edit2.LoadFromRegistry; Edit2.LoadFromRegistry; ListBox1.LoadFromRegistry; StringGrid1.LoadFromRegistry; CheckBox1.LoadFromRegistry; CheckBox2.LoadFromRegistry; CheckBox3.LoadFromRegistry; CheckBox4.LoadFromRegistry; CheckBox5.LoadFromRegistry; end;
Obviously, this adds a lot of maintenance responsibilities when developing the application, and it will be prone to error (particularly if youre saving and restoring dozens or hundreds of values). Another disadvantage with this approach is that the new registry-aware control is only going to save data that you as the component developer have programmed it to save. For many controls this is obvious: An Edit component saves its Text property, a CheckBox saves its Checked state, and so on. But what if someone using your registry-aware control wants to save the Edit components ShowHint property, or the CheckBoxs Enabled property? They probably would have to toss your control and make their own subclassed version saving the values they want to save. Obviously, this is not a great example for promoting code re-use. Now, granted this kind of idea could have worked only if the Delphi development team decided to add automatic registry support to the component hierarchy. They could have added some kind of RegistryKey property in the TPersistent class (or somewhere else nearby) and then all components descending from it would have that same basic behavior. However, it would have clearly made the VCL a bit more cluttered. Who knows, they may have not even thought of it. Regardless, it isnt there now so we have to find another solution.
A Better Solution
Lets step back a bit and look at the problem with a broader perspective. We want to save data related to controls,
On the Cover
but as we already learned, we dont want to have to create new registry-aware versions of each control. This means that our improved solution must work with unmodified components and should require the absolute minimum amount of maintenance. Enter the Persistent Object Manager (ObjMgr). For those of you who have used my IniOut Component Property Manager, the Persistent Object Manager will be familiar. Although IniOut is a shareware product, I wanted Delphi programmers to get a feel for some of the concepts it uses. As a result, I created a completely new component, based loosely on the ideas used in IniOut. That way, I can provide all the source code necessary to show you how it works in this article without sabotaging the success of IniOut. First, lets go through a brief example of how ObjMgr works. Then we can dig into how its implemented in code. by dropping down this combo box, we automatically see all the controls that are present on the form (see Figure 3). We will start with the User Name edit box (edtName). When you select that component, the Property combo box is populated with all the supported properties of that control (see Figure 4). In this case, we want to manage the Text property. After choosing this property, we provide the registry subkey that this value will be saved as. Lets enter Software\Informant\ObjMgrDemo\UserData for this value. Then, we provide the name of this particular key (in this
On the Cover
bars (Volume and Balance). Note that the registry subkey value will be the same for each item (more on this later). After specifying each of these, the property editor would look like Figure 6.
Figure 6: All the items we want to manage.
We dismiss the property editor by clicking OK. Now there is only one thing left to do: We need to tell ObjMgr when to read values from the registry and when to save them. The FormCreate and FormDestroy methods are good spots; all we do is add a single line of code to each to access the Load and Save methods of ObjMgr as follows:
procedure TForm1.FormCreate(Sender: TObject); begin ObjMgr1.Load; end; procedure TForm1.FormDestroy(Sender: TObject); begin ObjMgr.Save; end;
case, well call it UserName). Lastly, we have a field that allows us to provide a default value for the property. It wouldnt make sense to provide a default for a user name, so we will leave this blank. After all these steps have been done, the property editor would look like Figure 5. Now, lets add all the other items we want to manage. Specifically, we would want to save the Text properties of the Address, City/State/Zip, and Stereo Brand Name edit boxes. Also, lets save the Position property of each of the two track
8 October 1997 Delphi Informant
Now run the application (see Figure 7). See what happened? ObjMgr set up the form as we requested through its property editor. Because this is the first time the program has been executed, there is no entry in the registry for any of this data. Therefore each control was given its default values. Now lets enter data in each field (see Figure 8) and close the application. When the application closed, the FormDestroy method was triggered, which told ObjMgr to save its managed properties to the registry. Now if you run RegEdit, youll note that the registry data in Figure 9 has been entered.
On the Cover
The location of these keys comes, of course, from the subkey field data we provided to ObjMgr. Now comes the cool part: Run the program again. See what happened? Because the registry key existed along with all the values we provided to ObjMgr, each of the controls has been restored to how it was when the form last closed. This is because we told ObjMgr to load its values in the forms FormCreate method. Keep in mind that all we needed to do to accomplish this was to add an ObjMgr component to the form, use its property editor to provide some basic information of properties to manage, then add two lines of code (one each in the FormCreate and FormDestroy methods). We used stock controls from Delphis Component palette and turned them into persistent objects. Not bad! Furthermore, if you were to expand on the form to hold a few more controls, and you wanted to save property values from those controls, you only need to access ObjMgrs property editor and add those items!
procedure TObjMgrDialog.FormShow(Sender: TObject); var a : Integer; begin // Add the form name to the list so we // can access its properties. cmbComponent.Items.Add(TheObjEd.Designer.Form.Name); // Populate combo with all components on form. with TheObjEd.Designer.Form do for a := 0 to ComponentCount-1 do cmbComponent.Items.Add(Components[a].Name); ValidateAll; // Are there items in the items managed list? if lbItemsManaged.Items.Count > 0 then begin // Select the first one. lbItemsManaged.ItemIndex := 0; // Simulate a click on it to populate other controls. lbItemsManagedClick(lbItemsManaged); end; end;
included when saving or restoring from the registry. If it isnt valid, its skipped.
How It Works
Now that weve seen ObjMgr at work, lets check under the hood to see how it works. I wont be covering every aspect of ObjMgr, but rather hitting the highlights. But fear not! If you want to use ObjMgr yourself, the complete source code is available on (see the end of this article for details). The key piece of ObjMgr is of course the property editor that you see when you double-click on the component (again, see Figure 2). The whole purpose of this property editor is to manage the contents of a single TStrings object. This string list holds the essential bits of information necessary for ObjMgr to save and restore properties from the registry. Although this list is internal to the component and you never see it directly, a brief discussion of its structure would be beneficial. Each item in this string list holds information for a single property being managed. The string is a collection of smaller strings separated by semicolons. Each string list contains the following items: Component Name Property Name Registry SubKey Key Name Default Value Valid Flag In our previous discussion, therefore, the first item we entered into ObjMgr would be stored internally like this:
edtName;Text;Software\Informant\ObjMgrDemo\UserData;UserName;;1
First, there is the component name (edtName) then a semicolon, followed by the property name (Text), another semicolon, and so on. Towards the end, you see two semicolons together. This is where the default is stored, and we didnt provide one for edtName. Last, there is a 1, which is a flag to tell ObjMgr whether the item is valid. If the item is valid, its
9 October 1997 Delphi Informant
On the Cover
procedure TObjMgrDialog.lbItemsManagedClick( Sender: TObject); begin UpdateComponentsCombo; UpdatePropertiesCombo; UpdateSubKeyCombo; UpdateKeyNameEditBox; UpdateDefaultsEditBox; end;
When you install ObjMgr into the Delphi Component Library, youre adding Objmgr.pas, which references Objmgr2.pas. At run time, however, the only file that needs to be included in a project is Objmgr2.pas. Because that is where the internal string list and registry code are, Delphi knows that it doesnt need to link in all of the design-time material into your run-time executable. If you looked at the compiled DCU files for each of these units, you would see that Objmgr.dcu is about 17KB and Objmgr2.dcu is only 6KB. Because the design-time and runtime material has been segregated in this way, we save the 17KB of Objmgr.dcu that doesnt link into the final executable.
Figure 11: Calling the Update methods when a new item is clicked.
extract the individual piece were looking for the name of the component, in this case. Referring to the Form Designer again, we obtain a reference to the component (which can be either a component on the form or the form itself ). Next, we use the GetPropList function in Delphis Typinfo.pas unit. This function returns all properties matching a certain criteria in its class or its ancestors. That criteria is a set of the supported property types ObjMgr can handle (specifically, tkInteger, tkChar, tkString, and tkLString). If you call this function and provide a nil as the last argument, the function simply returns the number of properties for the component. After we have this count, we allocate a chunk of memory of the appropriate size and then call the function again this time including a pointer to that block of memory. Then its just a matter of scanning through this block of data (records of type TPropInfo) and extracting the name of each property we encounter. After that, we make sure that we free the memory block; the whole section is wrapped in a try..finally construct to make sure this happens. Briefly then, whenever a user clicks on an item in the Items Managed list box to the left, ObjMgr needs to fill each control on the right with the appropriate data (if available). Obviously, if youre adding a new item to the list, some of the controls will have no data to display. But after youve entered a few items, you will be able to scroll up and down the list and all the controls on the right would be automatically updated. The effect is much like that of data-aware controls. In the preceding paragraphs, I discussed how UpdatePropertiesCombo works, but there is actually a similar Update method for each of the controls on the form. This is shown in Figure 11. One additional routine I wanted to comment on is NewRegSubKeyCheck. This method is called whenever the user hits J in, or exits from, the Registry Sub-Key field in the property editor. We want the drop-down list to show all the subkeys entered so far. This routine looks at any entry into the drop-down combo box and checks to see if it already exists in the list. If it does, that item is selected. If it doesnt, the new string is added to the list. This is a good general purpose routine for this kind of drop-down combo box management.
The Units
The code for ObjMgr is split up into two units: Objmgr.pas and Objmgr2.pas. The first unit houses all the behavior of the property editor as a whole, i.e. the way the controls work, the way it allows you to add and delete items managed, and so on. The second unit, Objmgr2.pas, contains the internal string list of managed properties as well as the registry-support code.
10 October 1997 Delphi Informant
On the Cover
If it is a string, we simply use GetStrProp (also defined in Delphis Typinfo unit) to return the current value of that string property. If it is of Char type, we use GetOrdProp and convert the ordinal value that comes back into a Char value. If it is an Integer property, we want to do a little more than simply return a number. In the event that a user wants to save off a Color or Cursor property, it wouldnt be very handy to require the user to lookup that the color clFuchsia is the number 16711935. Instead, we use some handy functions provided with Delphi that will convert a numeric value to and from either a Color or Cursor. Then it is simply a matter of determining if we are looking at a TColor or TCursor and calling the appropriate conversion routine (ColorToIdent or CursorToIdent both defined in Graphics.pas). Because we add this extra step, we now will have a registry entry that says that the Color key is clFuchsia instead of the meaningless value 16711935. Also, we can use the same technique when asking the user for a default value for such a property. In the Default Value edit box in the property editor, we can enter clYellow and the property editor will make the appropriate conversion. Getting even more difficult would be enumerated types, which would allow you to manage something like a components Align property. When you chose the Align property, the Default Value edit box could become a drop-down list populated with all the valid elements of that enumerated type; in this instance it would hold alBottom, alClient, alLeft, alNone, alRight, and alTop. You would also need to convert these to and from textual representations (e.g. alClient as a string) into its actual type (alClient as an item of the type TAlign). IniOut takes those extra steps to be a far more comprehensive solution. It manages virtually any property type (including floating-point properties, sets, enumerated types, classes, methods, and compound properties types such as Fonts, StringLists, and so on. It also allows you to add nonpublished properties, so you can add variables at run time to its list of managed properties. As a special offer to readers, I have decided to provide a free copy of the registered version of IniOut to any Delphi Informant subscriber. The shareware version has a few limitations, but the registered version is completely functional and includes compiled units for all versions of Delphi. The registered version doesnt include the source to IniOut, but it is available for a modest fee. For those of you who have previously paid for a registered copy of IniOut, dont fret! The money you paid for the registered version will be applied toward the cost of the source code version if you wish to get the IniOut source. To get your free registered copy of IniOut, simply send me an e-mail at [email protected]. Include in your e-mail the number directly to the left of your subscription expiration date. The number should look something like C 12345. I will send you the registered version of IniOut by return e-mail.
Conclusion
With the Persistent Object Manager, making a Delphi object or application persistent is as easy as dropping a component on a form. Saving persistent data helps users of your applications be more productive and eliminates annoying repetitive chores or input of data. In the past, making objects persistent might have been a chore that you as an application developer would rather not deal with. Now you have no excuse! The files referenced in this article are available on the Delphi Informant Works CD located in INFORM\97\OCT\DI9710RV.
Robert Vivrette is a contract programmer for Pacific Gas & Electric, and Technical Editor for Delphi Informant. He has worked as a game designer and computer consultant, and has experience in a number of programming languages. He can be reached via e-mail at [email protected].
12
On the Cover
S1 : string; CmpName : string; PrpName : string; SubKey : string; KeyName : string; DefVal : string; begin Reg := TRegistry.Create; try Reg.RootKey := HKEY_CURRENT_USER; // Loop through all items managed. for A := 0 to Items.Count-1 do begin S1 := Items[A]; // Verify item is valid. if not IsValid(S1) then Continue; // Extract the individual elements from the item. CmpName := Parse(S1,ppComponent,';'); PrpName := Parse(S1,ppProperty,';'); SubKey := Parse(S1,ppSubKey,';'); KeyName := Parse(S1,ppName,';'); DefVal := Parse(S1,ppDefault,';'); // Check to see if this is a form. if CmpName = (Owner as TForm).Name then // Yes - use the form. TmpCmp := (Owner as TForm) else // Find the component on the form. TmpCmp := (Owner as TForm).FindComponent(CmpName); // Couldn't find component - go on to next. if TmpCmp = nil then Continue; // Get the info record on this component. PropInfo := GetPropInfo(TmpCmp.ClassInfo,PrpName); if PropInfo = nil then Continue; try Reg.CloseKey; // Open the Subkey. if Reg.OpenKey(SubKey,False) then // Does this key name exist? if Reg.ValueExists(KeyName) then // Yes - set the property. SetPropFromString(TmpCmp,PropInfo, Reg.ReadString(KeyName)) else // No it doesn't exist - use the default. SetPropFromString(TmpCmp,PropInfo,DefVal) else // Couldn't open the key - use the default. SetPropFromString(TmpCmp,PropInfo,DefVal); except end; end; finally Reg.Free; end; end; procedure TObjMgr.Save; var Reg : TRegistry; A : Integer; TmpCmp : TComponent; PropInfo : PPropInfo; S1 : string; CmpName : string; PrpName : string; SubKey : string; KeyName : string; begin Reg := TRegistry.Create; try Reg.RootKey := HKEY_CURRENT_USER; // Loop through all items managed. for A := 0 to Items.Count-1 do begin S1 := Items[A]; // Verify item is valid. if not IsValid(S1) then Continue; // Extract the individual elements from the item. CmpName := Parse(S1,ppComponent,';'); PrpName := Parse(S1,ppProperty,';'); SubKey := Parse(S1,ppSubKey,';'); KeyName := Parse(S1,ppName,';'); // Check to see if this is a form. if CmpName = (Owner as TForm).Name then // Yes - use the form. TmpCmp := (Owner as TForm) else // Find the component on the form. TmpCmp := (Owner as TForm).FindComponent(CmpName); // Couldn't find component - go on to next. if TmpCmp = nil then Continue; // Get the info record on this component. PropInfo := GetPropInfo(TmpCmp.ClassInfo,PrpName); if PropInfo = nil then Continue; try Reg.CloseKey; if Reg.OpenKey(Subkey,True) then Reg.WriteString( KeyName,GetPropAsString(TmpCmp,PropInfo)); except end; end; finally Reg.Free; end; end; end.
13
Informant Spotlight
Delphi 2 / Delphi 3
By Jay Cole
elphi generates incredible code. It can pass procedure parameters in registers instead of setting up a stack frame; it uses pointer arithmetic instead of array index calculations when it can. All in all, the code Delphi generates is at least on a par with any C/C++ compiler out there. Are you tired of C/C++ programmers claiming you cant do low-level programming efficiently in Delphi? They maintain that only languages such as C/C++ are suited for low-level tasks, and that Delphi is great for user-interface programming and prototyping, but for timecritical, serious applications, only C/C++ or assembler will do. Well, you can out-perform even the craftiest C/C++ programmer if you are willing to break a few rules. So what can you do when you need even more performance? Obviously, first visit the algorithm used to solve the problem. A well designed algorithm can create orders-ofmagnitude improvements in performance; the classic example is a bubble sort versus a quick sort. That is exactly what we at OpNek did with NitroSort: We invented a new sorting algorithm that was several times faster than the traditional quick sort. But that wasnt enough. We couldnt get the performance numbers we wanted without breaking some of the cardinal rules of computer science, and generating some very tight code. (Note: The material in this article applies only to Delphi 2/3 running on Windows 95/NT.) new code, in memory, on-the-fly, based on a small, embedded language specific to the problem being addressed. The technique is simple: Allow users to specify what they want to accomplish through a small language geared to the problem at hand. In NitroSort, this is a sorting language that allows the user to specify the file type, record size, and sort keys, among other things. Then the system allocates a block of executable memory, and compiles the machine code directly into the allocated memory block. Finally, a standard function pointer in Delphi is assigned to the allocated memory block, and is called as a function pointer. Essentially, this is what Borland did with Turbo Pascal 3.0 when it compiled and ran within the DOS environment. Sound difficult? After all, who knows machine code? Who knows how to write a compiler? How does one debug and test the code? Actually, it looks more difficult than it is. Surprisingly, all this is fairly easy for most embedded-language applications. With tools such as Visual Parse++ for the parsing and Turbo Debugger for generating the machine code and debugging compiler output, the process is quick and straightforward. (This articles references to the Turbo Debugger refer specifically to the Turbo Debugger for Win32. It allows assembling and disassembling of 32-bit instruction codes. Generally,
Informant Spotlight
the more complex and general a language becomes, the more difficult the compilation and debugging becomes.) what memory location or what register. Its not difficult to generate the machine code by hand, but its tedious. Fortunately, theres an easier way. I dont think in, or write, machine code, so I cheat. Turbo Debugger has a built-in facility that makes this step easy. Load the Turbo Debugger, select File | Open, then open any executable program (even if it wasnt created in Delphi). If the program wasnt compiled with Turbo Debugger information, the message Program has no symbol table will be displayed. Click on the OK button if that message box appears. If the selected program has Turbo Debugger information embedded, the debugger will show source code instead of CPU instructions. In that case, select View | CPU to switch from source-level to CPU-level debugging. A screen similar to Figure 1 will appear immediately after opening the file and viewing the CPU window. Now, to see how the assembly statement would be represented in machine code, enter it into the debugger:
ADD EDX, 200
By pressing A, an input box is displayed with the character A already in it. Enter the rest of the ADD statement as shown in Figure 2. After pressing J, the assembly statement is transformed into machine code. The first column in the CPU window is the instruction pointer for the machine, i.e. the location of the add instruction in memory (:00410000). The next column contains the machine code bytes equivalent to ADD EDX, 00000200. The final column is the assembler statement itself. So, in our example, we have:
:00410000 81C200020000 add edx, 00000200
The $81 byte is the add instruction we saw earlier in the Turbo Assembler reference. The $C2 byte indicates the EDX register, and $00200000 is the 32-bit value represent-
The OpCode on the left is the machine code that represents the instruction (in the Instruction column). If the computers instruction pointer were currently pointing at a memory location containing $81, and it was to execute, the CPU would know to perform an ADD instruction, and the remaining bytes immediately after the $81 would tell it what and how to add. To perform this operation, generate a $81 hex followed by the mod r/m and the immediate value as a 4-byte integer. The mod r/m is an indicator to tell whether the operation is to memory, or to a register, and
15 October 1997 Delphi Informant
Informant Spotlight
fCodeBuff := VirtualAlloc(nil, codeSize, MEM_COMMIT + MEM_RESERVE, PAGE_EXECUTE_READWRITE);
ing the hex number 200. (Remember, on the Intel platform, words are stored backwards.)
By examining how Delphi generates a formula, we can size up the machine code involved. If the code Delphi generated isnt efficient, a better algorithm or method can be substituted and typed into the debugger (as shown in Figure 2). For this example, a small console application is created, containing all the operators to be implemented: addition, multiplication, subtraction, and division. The application is compiled with Turbo Debugger information embedded, and pulled into Turbo Debugger for examination. By doing this, the source code and the associated assembly language generated by Delphi are viewed together. The project options are shown in Figure 4. I knew Delphi would do some optimizations over and above what I would handle in this simple example. So, I put some floating-point assembly-language code inside an asm block I knew I would need in the code generation process. By doing this, I can translate the other needed instructions at the same time, instead of typing them in separately. Dont execute this code! It would most likely result in a nasty crash. Now, with a basic understanding of how Delphi generates the code, an efficient formula parser can be implemented. Before proceeding, make sure the compiler tab has all three Debugging options checked. With the sample code in Figure 5, the machine code needed to implement the next part of our formula parser is displayed. First, compile this program; go into Turbo Debugger and select File | Load for the executable. Then select View | CPU and begin stepping through the code until the call to Eval in Figure 5. At this point, use 7 to step into the function. A display similar to Figure 6 will appear.
Informant Spotlight
Here, Delphi generates some pretty simple uses Forms; code. It loads the EAX register {$R *.RES} with the address var of the variable a,b,c,d, tmpResult : Double; A. It uses a combination of function Eval : Double; begin FLD, FSTP, b := a * b + c * d; FADD, FDIV, asm FMUL, and FMULP ST(1), ST FDIVP ST(1), ST FSUB. These FADDP ST(1), ST operators load FSUBP ST(1), ST values on the end; Result := a + b; floating-point end; stack from a memory pointbegin b := 12.445; er, pop the valc := 13.345; ues off the stack d := 445014.112; to a memory tmpResult := Eval; WriteLn(tmpResult); address, and end. perform binary arithmetic operFigure 5: The sample code. ations on the floating-point stack. Most assembly language texts will give a complete treatment of these floating-point operators. Now all the information is present to generate a user-defined function in machine code.
program ConsoleApp;
create this instruction in memory, write out the first two bytes for the instruction, then the address of the variable to push. When the machines instruction pointer (IP) comes to that instruction ($DD,$05), it will push the value contained at that memory location to the floating-point stack, just as if Delphi had generated it. So, all the building blocks necessary to generate userdefined code are now in place. The only thing that remains is the formula parser and code generator. The formula compilers code model will push the left side, then the right side of each expression to the floating-point stack. It will perform the arithmetic operation on the top two stack elements and push that result back to the floatingpoint stack. For example, in the expression x + y, the value for x, then the value for y, would be pushed onto the stack. The + operation would be performed on the values for x and y, and the resulting value would be pushed back to the floating-point stack. This is a traditional post-fix stack execution model. Its not always the most effective code, but because the floating-point processor on a Pentium takes one or three clock cycles to perform these instructions, the code will be quick. Optimization is outside the scope of this article, so its just brute force for now. Even this brute-force method, however, would outperform the cleverest interpreted scheme.
Each function in Delphi has function entry and exit code. All the entry and exit code information can simply be copied directly to the formula compiler. All Delphi functions must adhere to this standard calling methodology as long as they are not stdcall type functions. Delphi subtracts eight from the stack pointer, and loads the EAX register with the address of A upon entry into this function. It then performs several floating-point stack operations, and finally, moves the result to the ESP stack, as well as pushing the result on to the floating-point stack, the code after:
Result := a+b
With the value on the floating-point stack, the function result could easily be used in an expression instead of just an assignment. Finally, it pops off two four-byte integers, so the stack frame is returned to the original setting before calling. The user-defined procedure generated in the formula compiler will need to mimic the entry and exit code of the function, and, based on the user formula, generate machine code similar to that in Figure 6, between the entry and exit code. Take one of the instructions as an example of how to use the machine code:
DD0560164200 fld qword ptr [00421660]
The first two bytes are the FLD (Load Real) instruction ($DD followed by $05). The remaining bytes are the address of the Double variable push onto the floating-point stack. To
17 October 1997 Delphi Informant
Informant Spotlight
TCodeGenerator = class(TObject) fCodeBuff, fCodeBuffEnd, fDataBuffPtr, fCurrCodeBuffOfs : Pointer; fCodeBuffSize : Integer; public constructor Create(const codeBuffSize : Integer); destructor Destroy; override; function GenBytes( const bytes : array of Byte) : Boolean; function GenWords( const words : array of Word) : Boolean; function GenQuads( const quads : array of Cardinal) : Boolean; function AllocDataSpace(numBytes : Longint) : Pointer; function GetFunctionAddress : Pointer; end;
together a real grammar, parser, and lexical analyzer. But, for this example, a quick and dirty, recursive-descent parser is best for illustration. Note the glaring omission of almost all error-checking and recovery code. This is also left as a problem for the reader, as there is not sufficient space to include it. Full source code for this article is available (see end of article for details). Its best to explain this overall process through an example. Take the expression A+B. The parser would push the value in A, then the value for B to the floating-point stack. The + operator generates an FADDP ST(1), ST that takes the top two values off the floating-point stack, adds them, then pushes the resultant value back to the floating-point stack for the next operation. The entry and exit code for the function is the same as the code generated by Delphi. The actual code generated for the mathematical operators is quite small. In fact, most of the code is spent in the mundane task of lexing the input stream into recognizable tokens. This grammar is rudimentary; it doesnt handle the negate operator or the power operator, let alone mathematical functions such as Sine, Cosine, etc., but this article is not about how to write grammars.
definition of the code generator. Basically, the constructor reports the combined size of the code and data space. In parsing the formula, the compiler will begin to generate bytes, words, and quad-words to the code space, and sub-allocate data space from the block of memory allocated for the machine code. By calling the Gen... functions, the machine code is generated as the expression is parsed. When done with this step, only the exit code and return must be generated before the code is ready to execute as a standard Delphi function. The AllocDataSpace function allocates a block of numBytes size from the end of the code buffer. The code is generated from the beginning of the block to the end. The data is allocated from the end of the block to the beginning. As long as these two pointers dont overlap, there is enough space to generate code and data. If the program runs out of space, the Gen... functions return False. Of course, it would be nice to have some simple error recovery functions. Its not difficult, but its left as a problem for the reader. The Create constructor uses the VirtualAlloc function, described previously, to allocate an executable memory block. Finally, calls to GetFunctionAddress get the pointer to the memory block containing the generated machine code, so a function pointer can be assigned to the newly generated code, and subsequently be called and executed. The implementation code is shown in Listing Two, beginning on page 20.
Now that the types are declared, all that remains is assignment and execution. To assign the function pointer, cast the pointer returned by the GetFunctionPointer method to type TDoubleFunction:
FuncPtr := TFunctionType(codeGenObj.GetFunctionPointer);
Now if the run-time formula will have access to internal variables, it must use the previous symbol table functions to set them (before the compilation process). A symbol table consists of each variable name and its address. Right after the parser is created, but before the users formula is compiled and executed, add the internal variables and their respective addresses to the symbol table:
aParser := TParser.Create; aParser.AddSymbol('X', @x); aParser.AddSymbol('Y', @y); if aParser.ParseFormula('x+y * 2'); ...
Because this example uses the variables x and y, and uses the address of the variables (not the values), the values dont need
Informant Spotlight
to be initialized in any other way than just setting them in the Pascal code. For example:
for x := 1 to 100 do begin for y := 1 to 100 do begin tmpDouble := FuncPtr; end; end;
call to FMULP, followed by a push of the Y variable and a call to FADDP. Finally, the resultant value is pushed to the stack, and the function exit code is executed. The RET function returns to the function call in the OnClick event. It is now an official compiler! It has also produced some pretty efficient code not quite to the level of Delphi, but close.
Because the compiler is accessing x and y via their address in the function routine, the assignment in the Delphi for loop is sufficient to evaluate the formula versus the current values in x and y.
into the formula input box. Next, set a breakpoint at this statement:
tDouble := funcPtr;
This code is located in the OnClick event for the Calculate button in the inner-most loop. By setting the breakpoint here, the debugger will stop short of the newly generated routine. Now, run the program by pressing the Calculate button. The debugger will break at the line above. Select View | CPU in Turbo Debugger and switch to mixed-mode source and assembly. Press 7 to step into the generated code. A screen similar to that shown in Figure 9 will appear. First is the generated function entry code, ADD ESP, FFFFFFF8. Next is the push of the variable X twice and a
19 October 1997 Delphi Informant
Informant Spotlight
compiler such as Delphi or C/C++ will be available. It takes some time, but small steps add up quickly.
begin fSymbolTable.Delete( fSymbolTable.IndexOf(UpperCase(sym))); end; function TParser.GetChar : char; begin if (fCurrPos <= Length(fInputStr)) then begin Result := fInputStr[fCurrPos]; Inc(fCurrPos); end else Result := Chr(0); end; procedure TParser.UnGetChar; begin Dec(fCurrPos); end; function TParser.Lexan : TTokenType; var t : char; tokenStr : string; begin Result := ttNone; while True do begin t := GetChar; t := UpCase(t); if (t in [' ', Chr(9)]) then begin // Skip the white space. while (t in [' ', Chr(9)]) do t := GetChar; UnGetChar; end else if (t in ['0'..'9']) then begin // We have a number. tokenStr := ''; while (t in ['0'..'9', '-', '.']) do begin tokenStr := tokenStr + t; t := GetChar; end; UnGetChar; try Result := ttNumber; fFloatVal := StrToFloat(tokenStr); except on EConvertError do Result := ttError; end; Break; end else if (t in ['A'..'Z']) then begin // We have a character, so we have an ID. tokenStr := ''; while (t in ['A'..'Z','0'..'9']) do begin tokenStr := tokenStr + t; t := GetChar; t := UpCase(t); end; UnGetChar; if (LookUp(tokenStr, fAddrVal)) then Result := ttId else Result := ttError; Break; end else case t of '+' : Result '-' : Result '/' : Result '*' : Result '(' : Result ')' : Result
The files referenced in this article are available on the Delphi Informant Works CD located in INFORM\97\OCT\DI9710JC.
Jay Cole is the owner of OpNek Research, a consulting and Financial Markets firm that consults for many Fortune 500 companies. OpNek also produces the highspeed sort utility NitroSort. For more information on OpNek or NitroSort, visit http://www.nitrosort.com.
:= := := := := :=
20
Informant Spotlight
Chr(0) : Result := ttDone; else Result := ttNone; Break; end; end; procedure TParser.SyntaxError(anErrorStr : string); begin fError := True; MessageDlg(anErrorStr, mtError, [mbOk], 0); end; function TParser.ParseFormula(aFormula : string) : Boolean; begin // Generate the entry code. fError := False; fInputStr := aFormula; fCurrPos := 1; fCodeGen.GenBytes([$83, $c4, $f8]); // ADD ESP, $FFFFFFF8 lookAhead := Lexan; if (lookAhead <> ttDone) then Expr; // Generate the exit code. We must have space to store // result, and that space is the ESP position that we // backed up. We also need to leave the result on the // floating-point stack. fCodeGen.GenBytes([$dd,$1c,$24]); // FSTP QWORD PTR [ESP] fCodeGen.GenBytes([$9B]); // WAIT fCodeGen.GenBytes([$dd,$04,$24]); // FLD QWORD PTR [ESP] // Pop off the 8 stack bytes and return. fCodeGen.GenBytes([$59]); // POP fCodeGen.GenBytes([$5a]); // POP fCodeGen.GenBytes([$c3]); // RET Result := not fError; end; procedure TParser.Expr; begin Term; while (True) do begin if (lookAhead = ttAdd) then begin Match(ttAdd); Term; // Generate Add code. fCodeGen.GenBytes([$de,$c1]); // FADDP ST(1), ST end else if (lookAhead = ttSubtract) then begin Match(ttSubtract); Term; // Generate Subtract Code. fCodeGen.GenBytes([$de,$e9]); // FSUBP ST(1), ST end else Break; end; end; procedure TParser.Term; begin Factor; while (True) do begin if (lookAhead = ttMultiply) then begin Match(ttMultiply); Factor; // Generate the multiply code. fCodeGen.GenBytes([$de,$c9]); // FMULP ST(1), ST end else if (lookAhead = ttDivide) then begin Match(ttDivide); Factor; // Generate the divide code. fCodeGen.GenBytes([$de,$f9]); // FDIVP ST(1), ST end else Break; end; end; procedure TParser.Factor; var tmpPtr : ^double; begin if (lookAhead = ttLParen) then begin Match(ttLParen); Expr; Match(ttRParen); end else if (lookAhead = ttNumber) then begin // Add the number to the data space and then generate // the load code for that number. tmpPtr := fCodeGen.AllocDataSpace(SizeOf(fFloatVal)); tmpPtr^ := fFloatVal; fCodeGen.GenBytes([$dd,$05]); // FLD QWORD PTR [tmpPtr] fCodeGen.GenQuads([Cardinal(tmpPtr)]); Match(ttNumber); end else if (lookAhead = ttId) then begin // Get the ID's address. Generate the push for the // value held in the Id address space. fCodeGen.GenBytes([$dd,$05]); // FLD QWORD PTR [@id] fCodeGen.GenQuads([Cardinal(fAddrVal)]); Match(ttId); end else begin // We have a syntax error. Let the user know. SyntaxError('Unrecognized token at ' + IntToStr(fCurrPos)); end; end; procedure TParser.Match(t : TTokenType); begin if (lookAhead = t) then lookAhead := Lexan else begin // Syntax Error SyntaxError('Expecting '+cTokToStr[t]); end; end; procedure TParser.GetProcAddress( var funcPtr : TDoubleFunction); var ptr : Pointer; begin ptr := fCodeGen.GetFunctionPointer; funcPtr := TDoubleFunction(ptr); end;
ECX EDX
21
Greater Delphi
InterBase / Delphi
By Bill Todd
Dwelling in Isolation
When you work with transactions, picking a transaction-isolation level is critical. It controls how or if it sees changes made by other transactions. The TransIsolation property of Delphis Database component lets you choose one of three levels. The first isolation level is tiDirtyRead. If you choose this isolation level, your transaction can see every change made to the database by other transactions including those that havent yet committed. This is undesirable, because it allows your transaction to read a value that may yet be rolled back, rendering that value incorrect. Thats why many servers dont support dirty reads. The second isolation level is tiReadCommitted, which allows your transaction to see only those values written to the database by transactions that have committed. This ensures that the values used have become a permanent part of the database. However, it doesnt protect your transaction from reading a value, then having another transaction change the value again, rendering it incorrect. The third and most restrictive isolation level is tiRepeatableRead. With repeatable read, your transaction is guaranteed that a value will remain constant, no matter how many times its read from the database. Another
Greater Delphi
way of looking at repeatable-read isolation is that it gives your transaction a snapshot of the database at a moment in time, and thats the way your transaction will see the database throughout its life. But a database server built on the locking model can provide repeatable-read isolation only by preventing other transactions from updating the database until your transaction commits or rolls back. Not all database servers support all these isolation levels. To determine what each level maps to for your server, see the Delphi online Help for the TransIsolation property. InterBase doesnt support dirty reads; selecting tiDirtyRead with InterBase will produce the read-committed level. and leaves the original version intact. When a transaction updates a row in the database, it also stores its transaction number as part of the record. InterBase does not actually write a complete new copy of the row; instead, it writes a difference record, which contains only the fields that have changed. This conserves space in the database. As a read transaction requests each row in a table, the database software checks to see if the transaction number for the latest version of the row is greater than the number of the transaction thats requesting it, and if the transaction was committed at the time the read transaction started. If it is greater, or if the transaction that created the latest version was active, the database software looks back through the chain of prior versions until it encounters one whose transaction number is less than that of the transaction trying to read the row, and whose transaction status was committed at the time the read transaction started. When the database manager finds the most recent version that meets these criteria, it returns that version. The result is repeatable-read transaction isolation without preventing updates during the life of the read transaction. This is what sets InterBase apart from other database servers. Consider the following example of a row for which four versions exist:
Tran=100 (status=committed) Tran=80 (status=active when read started) Tran=60 (status=rolled back) Tran=40 (status=committed when read started)
Assume that a read transaction with transaction number 90 attempts to read this row. The read transaction will not see the version of the row created by transaction 100, because the update that created this version took place after transaction 90 began. Transaction 90 also will not be able to read the version created by transaction 80, even though it has a lower transaction number, because transaction 80 has not yet committed. Although the version for transaction 60 still exists on disk, transaction 60 has rolled back, and rolled back versions are always ignored. Therefore, the version that transaction 90 will read is the version created by transaction 40. In this example, transaction 80 wont be allowed to commit. When transaction 80 attempts to commit, the database manager will discover that transaction 100 has committed, and transaction 80 will be rolled back.
Crash Proof
Another significant advantage of InterBases versioning architecture is instantaneous crash recovery. Locking-model databases support transaction processing by maintaining a log file that contains information about all changes made to the database. If the database software crashes and is restarted, it must scan the entire log file to determine which transactions were active at the time of the crash, then roll them back. This can take some time. InterBase doesnt maintain a log file, because it doesnt need one. The record versions in the database provide all the information
Greater Delphi
Figure 1: The BDE Administrator. Changing the InterBase SQL Link driver DRIVER FLAGS setting to 512, changes the default mode from read-committed (the default) to snapshot.
needed to roll back an active transaction. This means that if an InterBase server crashes, it will restart instantly, because the only recovery it must perform is to scan the transaction-inventory pages and change the status flags of all active transactions to rolled back. No changes to the data are required, because any record version whose status is rolled back will be ignored when the record is read.
An Unfortunate Default
The ability to provide repeatable-read transaction isolation without blocking update transactions is obviously a big advantage for InterBase. However, the BDE doesnt take advantage of it. For some strange reason, the BDE InterBase driver defaults to read-committed transaction isolation. To overcome this, you must use the BDE Administrator to set the DRIVER FLAGS property of the InterBase SQL Link driver to 512 (see Figure 1). This changes the default mode from read-committed to snapshot.
Conclusion
InterBases multi-generational architecture truly sets it apart from other database servers. The ability to provide repeatable-read transaction isolation without blocking update transactions makes it ideal for executive-information systems or any situation that must mix long-running read transactions with updates. Its instantaneous crash recovery is another big advantage for systems in which maximum up-time is important. Just remember that when you access InterBase with the BDE, you must set DRIVER FLAGS to get these advantages.
Bill Todd is President of The Database Group, Inc., a database consulting and development firm based near Phoenix, AZ. He is a Contributing Editor of Delphi Informant; co-author of Delphi 2: A Developers Guide [M&T Books, 1996], Delphi: A Developers Guide [M&T Books, 1995], Creating Paradox for Windows Applications [New Riders Publishing, 1994], and Paradox for Windows Power Programming [QUE, 1995]; and a member of Team Borland, providing technical support on CompuServe. He has also been a speaker at every Borland Developers Conference. He can be reached on CompuServe at 71333,2146, on the Internet at [email protected], or at (602) 802-0178.
24 October 1997 Delphi Informant
DBNavigator
Delphi 3
Trying on TeeCharts
A New Component Gets Its Due
ith its new interfaces, COM support, packages, remote data-broker technology, and support for creating Web server extensions, Delphi has taken Windows software development to a higher level. Unfortunately, these important improvements can easily obscure some of the wonderful new, updated components available in Delphi 3. Among these are TeeCharts, which Ill address this month, and Decision Cube components, which Ill cover in next months column. This is not, however, a comprehensive treatment; that would require hundreds of pages. To get the most from TeeChart, read the teeMach TeeChart version 3 Chart Guide. This guide, which is in Microsoft Word format, is installed with Delphi 3. Its default location is C:\Program Files\Borland\Delphi 3\TeeChart\TChartV3.DOC. (If you dont have Word, use the Windows 95 WordPad, or download the Word Viewer from http://www.microsoft.com.) code and the 16- and 32-bit versions of TeeChart are part of TeeChart Pro. For purchase information, right-click a TeeChart component and select About TeeChart , or visit http://ourworld.compuserve.com/homepages/dberneda. TeeChart is a parent to one or more series, or graphs. Consequently, its possible to define more than one graph with a single TeeChart component. Each series defines a chart type and data. The version of TeeChart that ships with Delphi includes 11 series: Line, Area, Point (scatter), Bar (Bar, Pyramid, and Cylinder), Horizontal Bar (Bar, Pyramid, and Cylinder), Pie, Shape, Fast Line, Arrow, Gantt, and Bubble. Each series type has methods for defining graph dimensions. While these can be manipulated at run time, its much easier to prepare a TeeChart at design time, using the Chart Editor shown in Figure 1. To display this editor, either double-click a TeeChart component (Chart, DBChart, or DecisionGraph), or right-click the TeeChart and select Edit Chart. The Chart Editor has two primary pages. The first page, labeled Chart (again, see Figure 1), permits you to create, remove, and copy series, as well as define overall properties for the chart. The second page, labeled Series, applies to the current series
TeeChart Basics
Delphi 3 ships with TeeChart version 3, a powerful component for displaying and printing graphs. Four components are related to TeeChart in Delphi 3; three of these are associated with the following classes: TChart, TDBChart, and TDecisionGraph. The fourth component, TQRChart on the QReport page of the Component palette, is not an actual TeeChart. Rather, its an interface object, much like DataSource, that points to a TChart or TDBChart. You can include the Delphi 3 version of TeeChart in the applications you create, royalty-free. This version, however, doesnt ship with source code. Also, it doesnt work with Delphi 1 or 2. Both the source
25 October 1997 Delphi Informant
DBNavigator
selected on the Chart page. For example, if a series named Series1 is defined on the Chart page and highlighted, selecting Series permits you to customize Series1. Likewise, if a series named Series2 is selected on the Chart page, the Series page permits you to control the characteristics of Series2. The Series page, shown in Figure 2, displays only those properties appropriate for a given series type. For example, if the selected series is a pie chart, the Series page displays controls for customizing a pie chart. You can switch between the various defined series by using the ComboBox at the top of the Series page.
TeeChart Step-by-Step
The following steps demonstrate how to use a TeeChart to create a pie chart. Create a new project. Place Panel, DBChart, and Query components on the main form of the project. Set the Panels Align property to alBottom, and its Caption property to a blank string. Next, set the DBCharts Align property to alClient. Define the data. Set the Querys DatabaseName property to DBDEMOS. Next, define a query that asks the question What is the relative contribution of each country to overall sales? Calculate the sum of the amounts paid in the orders table, and summarize this value for each country in which customers reside. The following query performs this calculation, so enter it in the SQL property of the Query:
SELECT FROM WHERE GROUP BY CUSTOMER."Country", SUM(ORDERS."AmountPaid") "CUSTOMER.DB" CUSTOMER, "ORDERS.DB" ORDERS (CUSTOMER.CustNo = ORDERS.CustNo) CUSTOMER."Country"
Figure 1: Use the Chart Editor to define the series for a TeeChart.
Now set the Querys Active property to True. While this last step isnt essential for creating the chart, it permits you to see it at design time. Define the DBChart properties. Right-click the DBChart object, and select Edit Chart. Define a new series by clicking the Add button on the Series sub-page of the Chart page. TeeChart displays the TeeChart Gallery shown in Figure 3. Select Pie from the TeeChart Gallery, and click OK. This returns your view to the Chart Editor. With Series1 now selected, go to the Series page of the Chart Editor. To link to the data provided by the Query, select the Data Source page of the Series page, and select Dataset from the displayed ComboBox. TeeChart now permits you to select a specific Dataset, the field to use for labels, and the field that represents the quantities to graph. Set Labels to Country, and set Pie to SUM OF AmountPaid. Your screen should resemble Figure 4. Now click the Close button on the Chart Editor to accept the changes. Because the Query is active, the data it returns is immediately visible in the DBChart component (see Figure 5). Run the form by pressing 9; notice that some of the labels overlap. This can be solved by rotating the angle of the pie chart. Close the application and return to the designer. Rotate the pie chart. Pie-chart rotation is subject to the properties of the series itself, rather than those of the DBChart component. Once defined, a series can be selected directly in
DBNavigator
Figure 6: With the TrackBar, a user can adjust the pie-chart rotation at run time.
Figure 4: Defining the data to display in a pie chart, using the Chart Editor.
series is visible when its Active property is set to True. Normally, you have only one series active at a time. However, you can overlap charts, if you want, by setting more than one series to Active. While overlapping series provide a powerful analytical tool for users, theyre useful only under certain circumstances. The series of a given TeeChart share characteristics such as scale, background gradient, and so forth; you should permit the display of overlapping series only when their characteristics are compatible. The next example demonstrates how to create a second series a bar chart with the DBChart created earlier. A bar chart is used to display relative quantities, much like a pie chart. TeeChart permits you to base each series on a different Dataset or even the same Dataset, if the data is consistent with the chart style you choose.
the Object Inspector, permitting you to control its properties at design time. Therefore, you could select Series1 in the Object Inspector, and set the RotationAngle property to a value other than 0. (The Chart Editor also permits you to modify this property.) However, like all published properties, this one can be modified at run time, as well. Providing a run-time interface to rotate the chart offers the user extra flexibility; lets do that, rather than limit the chart to a static, design-time adjustment. Place a TrackBar in the Panel. Set its Min property to 0, and its Max property to 360. Next, set the TrackBar TickStyle property to tsNone. Double-click the TrackBar to create an OnChange event handler, and modify it as follows:
procedure TForm1.TrackBar1Change(Sender: TObject); begin Series1.RotationAngle := TrackBar1.Position; end;
Create a new series. Right-click the TeeChart, and select Edit Chart. Click the Add button, then select Bar from the TeeChart Gallery. Press OK to return to the Chart Editor. Now that you have two series, you can select which is active by using the check box next to the series name, in the Series sub-page of the Chart page (refer to Figure 1). Uncheck the check box next to Series1, so that only Series2 is displayed. Set the properties. Select the Series page of the Chart Editor. If Series2 is not selected in the ComboBox, select it. Move to the DataSource page. Set Data Source to Dataset, Dataset to Query1, Labels to Country, and Bar to SUM OF AmountPaid. If the data could be further divided, such as amount paid by year, you could set X to Year. In this case, leave X blank. Your screen should resemble Figure 7. Now select the General page of the Series page. Uncheck the Show in Legend check box to turn off the legend for Series2. Now return to the Series sub-page of the Chart page. Uncheck Series2, and check Series1. Again, the pie chart is displayed. Accept the Chart Editor by clicking Close. Add a button. Add a button to the panel at the bottom of the form. Set the caption of this button to Show Bars. Double-click this button, and enter the event handler shown
Run the form. When you move the TrackBar, the pie chart rotates (see Figure 6).
DBNavigator
procedure TForm1.Button1Click(Sender: TObject); var i: Integer; begin for i := 0 to DBChart1.SeriesCount - 1 do DBChart1.Series[I].Active := False; if Button1.Caption = 'Show Bars' then begin Button1.Caption := 'Show Pie'; Series2.Active := True; TrackBar1.Enabled := False; end else begin Button1.Caption := 'Show Bars'; Series1.Active := True; TrackBar1.Enabled := True; end; end;
procedure TForm1.Button2Click(Sender: TObject); var i: Integer; begin if Series3 = nil then begin Series3 := TBarSeries.Create(Self); DBChart1.AddSeries(Series3); Series3.AddBar(16,'West',clBlue); Series3.AddBar(25,'Central',clRed); Series3.AddBar(35,'NorthEast',clYellow); Series3.AddBar(25,'South',clGreen); end; for i := 0 to DBChart1.SeriesCount -1 do DBChart1.Series[i].Active := False; Series3.Active := True; end;
Next, call the TBarSeries constructor, and assign it to the instance variable. Finally, use AddSeries to add this series to your TeeChart. These last two steps look like this:
Series3 := TBarSeries.Create(Self); DBChart1.AddSeries(Series3);
in Figure 8. Now run the form. When you click Show Bars, the bar chart appears, as shown in Figure 9. Clicking this button a second time reverts the display to the pie chart.
Once youve created the series and added it to the TeeChart, you can work with the properties and methods of the series to produce the chart. The methods and properties available depend on the type of series youve created. After you finish setting properties, you can make a particular series visible by setting its Active property to True. Although more than one series at a time can be active, you will generally want to turn off other series by setting their Active properties to False. This technique is demonstrated in the TEECHART project, which is available for download (see the download instructions at the end of this article). Attached to the button labeled Add/Show New Bar the code shown in Figure 10, which tests for the existence of a series named Series3. If it doesnt exist, Series3 is created and populated with data by calling the TBarSeries method AddBar. Finally, all series associated with the TeeChart are made inactive; then the new one is made active. The result is shown in Figure 11. If you inspect the OnClick event handler in Figure 10, youll notice that its possible to control series either individually
where TChartSeries is one of the following classes: TAreaSeries, TBarSeries, TCircledSeries, TCustomBarSeries, TCustomSeries, TFastLineSeries, THorizBarSeries, TLineSeries, TPieSeries, or TPointSeries. For example, to add a new bar-chart series to TDBChart1, you would first declare a TBarSeries instance variable in the forms type declaration. For example, you could add the following line to the public section of your forms type declaration:
Series3: TBarSeries;
28
DBNavigator
procedure TForm1.Button3Click(Sender: TObject); var i: Integer; begin if Series4 <> nil then Series4.Free; Randomize; Series4 := TPointSeries.Create(Self); DBChart1.AddSeries(Series4); Series4.ShowInLegend := False; for i := 1 to 100 do Series4.AddXY(i,i*2 + Random(i),IntToStr(i),clRed);
Figure 11: A bar chart created at run time, using TChartSeries methods.
procedure TForm1.Button2Click(Sender: TObject); var i: Integer; begin if DBChart1.SeriesCount < 3 then begin DBChart1.AddSeries(TBarSeries.Create(Self)); TBarSeries(DBChart1.Series[2]). AddBar(16,'West',clBlue); TBarSeries(DBChart1.Series[2]). AddBar(25,'Central',clRed); TBarSeries(DBChart1.Series[2]). AddBar(35,'NorthEast',clYellow); TBarSeries(DBChart1.Series[2]). AddBar(25,'South',clGreen); end; for i := 0 to DBChart1.SeriesCount -1 do DBChart1.Series[i].Active := False; DBChart1.Series[2].Active := True; end;
for i := 1 to 100 do Series4.AddXY(i,i*2,IntToStr(i),clBlue); for i := 0 to DBChart1.SeriesCount -1 do DBChart1.Series[i].Active := False; Series4.Active := True; end;
though an instance variable, or through the Series property of the TeeChart (an array of pointers to the series owned by the TeeChart). Although a TBarSeries variable was declared in the preceding example, there are alternatives. For example, the code in Figure 12 would produce the same effect without the need for an instance variable. As mentioned previously, each TChartSeries descendent has different methods, depending on the type of series it represents. For example, when creating a point chart, you cant call AddBar; you would use AddXY instead. This is demonstrated in Figure 13, which is associated with the button labeled Add/Show New Point in the TEECHART project. This code creates a new series each time, using Delphis builtin random-number generator. This example shows that you can create a chart using any data, not just that stored in a Dataset. In fact, with the appropriate Add method for the series youre creating, you can define the contents of a chart based on any valid data, regardless of origin. Figure 14 shows a chart produced by this code.
Figure 14: A point chart created at run time, using random numbers.
Delphi applications. Furthermore, TeeChart is flexible enough to permit your charts to be built at either design time or run time, from any data available to your application. Next month, DBNavigator takes a look at the Decision Cube components available in the Client/Server and Enterprise editions of Delphi 3. Among these components is the DecisionGraph, which is itself a TeeChart descendant. The files referenced in this article are available on the Delphi Informant Works CD located in INFORM\97\OCT\DI9710CJ.
Cary Jensen is President of Jensen Data Systems, Inc., a Houston-based database development company. He is author of more than a dozen books, including Delphi In Depth [Osborne/McGraw-Hill, 1996]. Cary is also a Contributing Editor of Delphi Informant, as well as a member of the Delphi Advisory Board for the 1997 Borland Developers Conference. For information concerning Jensen Data Systems Delphi consulting and training services, visit the Jensen Data Systems Web site at http://idt.net/~jdsi. You can also reach Jensen Data Systems at (281) 3593311, or via e-mail at [email protected].
Conclusion
Using one of the TeeChart components, you can quickly create business graphs and charts, and include them in your
29 October 1997 Delphi Informant
At Your Fingertips
Delphi / Object Pascal
By John P. Gambrell
How does the parent/child relationship work for Delphi windows controls?
Every windows control includes the property Parent that specifies its parent control. Controls assigned to another control via the Parent property become child controls to that parent. For example, RadioButton components placed inside a GroupBox are the children of that GroupBox; thus, the RadioButtons relate to each other, so only one can be checked at a time. When a parent control is destroyed, all the child controls assigned to that parent are destroyed as well. This example demonstrates the relationship between parent and child controls with the use of two GroupBoxes and a few RadioButtons, as shown in Figure 3. Clicking on the Change Parent of RadioButton3 button reassigns RadioButton3s Parent property to the other
Figure 2: The OnClick event handlers for the Add and Begin Search buttons.
30 October 1997 Delphi Informant
At Your Fingertips
procedure TForm1.btnChangeParentClick(Sender: TObject); begin try with RadioButton3 do begin if Parent.Name = 'GroupBox2' then begin if Checked then Checked := False; Parent := Groupbox1; end else begin if Checked then Checked := False; Parent := GroupBox2; end; Label1.Caption := Parent.Name + ' is the parent of RadioButton3'; end; except ShowMessage('GroupBox1 does not exist.'); end; end; procedure TForm1.btnDestroyClick(Sender: TObject); begin Groupbox1.Free; end; end.
GroupBox; it then relates to the RadioButtons in the other GroupBox (not something you want to practice, obviously). Clicking on the Destroy Groupbox1 button calls the Destroy method for GroupBox1, which in turn destroys its children RadioButtons. See Figure 4 for the code assigned to the OnClick event handlers for the Change Parent of RadioButton3 and Destroy Groupbox1 buttons. The files referenced in this article are available on the Delphi Informant Works CD located in INFORM\97\OCT\ DI9710JG.
Figure 4: The OnClick event handlers for the Change Parent of RadioButton3 and Destroy Groupbox1 buttons.
John Gambrell is a consultant with Ensemble Corporation, specializing in client/server and Internet application development. John can be reached at [email protected].
31
know, I know Delphi is already RAD. But before you get too complacent with the Delphi IDE and the various built-in tools Borland provides, youd better check out Raptor, the latest in a series of powerful development tools from Eagle Software. If youve used the Component Development Kit (see Robin Karlins review in the May 1997 Delphi Informant) or the component testing tool, reAct, you are already familiar with Eagle Softwares commitment to excellence and innovation. Their latest creation, Raptor, is much more than simply another useful tool it has the potential to revolutionize how you work in Delphi 3, and to considerably enhance your productivity. Lets find out how by examining its major features. Raptor has two main components: a greatly enhanced Object Pascal editor, and a series of panels that surround the editor, providing many powerful utilities. Figure 1 shows how Raptor and its panels integrate with the Delphi IDE. Both the enhanced editor and the panels deserve a good deal of attention; lets begin with the editor.
32
Figure 1: Raptor adds considerable functionality to Delphis editor (center right) and powerful panels (center and bottom).
As youve probably figured out, M is the trigger key. If you want, you can reconfigure it to VM (a request I made as a beta-tester.) Most of the templates are static and obvious (they always expand the same way each time). The last one (sf ) deserves special mention, as it is dynamic (my term). Like many other templates, sf (or set to false) inserts the current contents of the Clipboard before the assignment, := False;. Figure 2 lists these and other templates with their Figure 2: A small sampling of Raptors comprehensive set of keyboard expansions. Note that {paste} indicates the point in templates. a dynamic template where text will be inserted from the Clipboard; note also that many templates, including bl, appropriately place the cursor before or within the expanded text. If you dont like some of the keyboard assignments, you can easily change them; and with just a little more effort, you can create your own. As previously mentioned, these templates can be either static or dynamic. Figure 3 shows the template editor, where you can view, edit, or create new templates. Note the special shortcuts that are provided to add a Clipboard paste area to your template ({paste}, as shown in the previous examples), to insert the editing cursor at any location within the expanded code, or to add backspaces (hard and soft), tabs, character deletion, and date/time information. Raptor templates also allow you to place special markers in the code, to indicate insertion points. (After youve finished entering code at one insertion point, you can press E to jump to the next one.) Templates are also smart: They expand in your particular code style, which is configurable. Theyre also smart enough not to expand within comments or strings. You can bring up your own dialog box from within a Raptor template, or call specially created string functions. Clearly, these templates are a tremendous help in accelerating coding speed, but the good news doesnt stop here.
33 October 1997 Delphi Informant
Expansion var : boolean; begin {paste} := false; end; ShowMessage Application.CreateForm(T{paste}, {paste} ResourceString if not assigned({paste}) then exit a try..finally block {paste}.Handle := LoadBitmap(HInstance, PChar(' '));
Figure 3: "FMT" (one of over 600 keyboard templates) expands to "Format (X, [])" placing the cursor at the X.
How many of you have experienced this: While developing a new class, you find yourself constantly jumping back and forth between declarations in the interface section and the enabling code in the implementation section. Wouldnt it be great to be able to drop multiple markers wherever you are, so you can quickly jump from one active section of code to another? With Raptors editor, you can.
Figure 5: Raptors Hot List provides easy access to non-component units and classes.
the editor. Others, such as the Files and the Hot List, allow for quick and easy file imports into your current project (as well as other useful tasks). Lets briefly examine each. Raptors Search Files panel (see Figure 4) is considerably faster than Delphis. All the expected options search pattern, search path, file spec, case sensitivity, and subdirectory search are included. There are also speed buttons to quickly set the path, or search within the current project directory, the VCL directory, or the run-time library (RTL) directory. Other buttons provide a means to toggle various elements of the display, print results, get help, or examine the File Grep properties dialog box. The latter provides control over the visual display of the search results, the maximum number of entries, and the thread priority (the default is Highest, just short of Time Critical.) The Hot List panel (see Figure 5) allows you to organize and instantly access frequently used groups of files. You could have one hot folder for all your include files, another for all your utility files, and another for your packages. I use this feature for easy access to TurboPowers SysTools library with its many container classes, string functions, mathematical functions, and sorting routines. The Hot List panel includes speed buttons to create, delete, or rename folders, to add or remove files, and to open a single file or all the files in the current folder. In addition to the two specialized file-manipulation panels weve just examined, Raptor includes a full-featured Files panel (see Figure 6) that allows easy navigation through folders and files. Again, there are speed buttons to open,
Figure 8: The Statistics panel provides detailed time-tracking and productivity information.
word processor than in the Raptor/Delphi editor this session! Later I learned that I could adjust Raptors Statistics panel settings to pause after 0.1 minutes of inactivity (the default is three minutes). This produces a more accurate keystroke/minute average.
copy, rename, touch, and yes, delete files. It keeps track of your favorite directories, so if you want to add files to your current project from several directories, you can do so easily. You can even bring up a DOS prompt from this panel. As with other panels, there is a properties dialog box that gives you complete control over which buttons are displayed, and which actions require confirmation. When I described the enhanced code editor, I mentioned its bookmarks. Within the editor, you can manage and use bookmarks easily. In addition, there is a Bookmarks panel that provides a summary of all the bookmarks set in the current project and its files (see Figure 7). By double-clicking any of these bookmarks, you can easily jump to that location. Has source-file navigation ever been easier than this? The Statistics panel (see Figure 8) is fascinating and highly useful. In and of itself, it provides a powerful means of marketing this product to Delphi developers. This panel monitors your keystrokes and your use of Raptors features as you work on your project, monitoring and storing all kinds of time-tracking and editing statistics, including: lines of code added, deleted, or changed; actual keystrokes; code navigation keys, real and effective (based on shortcuts); total editing times (with or without Raptors features); and a summary of average keystrokes per minute and the increase in productivity by using Raptor. In case youre concerned about my apparent lack of typing skills, please be aware that I spent much more time in my
35 October 1997 Delphi Informant
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 on the Internet at [email protected].
TextFile
practical flavor. This will delight those who are put off by too much theory and text that falls into the nether regions of advanced topics. By contrast, the cadre of authors who produced Using Delphi 3 includes many Borland engineers among its numbers. The organization and content of the two books are similar, but they maintain their individuality. Both provide a very comprehensive and thorough introduction to Delphi programming, best suited for developers well versed in programming but still learning Delphi. However, if youve
mastered Delphis essentials, and are looking for something more esoteric, these books will probably disappoint you. Before you make any decisions, Ill discuss some of the specific topics covered in each. Using Delphi 2. Like most Delphi books that aspire to be comprehensive, Using Delphi 2 begins by presenting some of the basic principles of Windows programming using Delphi. Here youll find all the expected topics, from an introduction to ObjectUsing Delphi 2 & 3 continued on page 38
Both Secrets of Delphi 2 and the newest book, Hidden Paths of Delphi 3, concentrate on aspects of Delphi that are either poorly documented or not documented at all in Borlands manuals. Both contain information you would have a hard time finding elsewhere. While Secrets explores many aspects of Delphi pro37 October 1997 Delphi Informant
TextFile
nication; but theres an internal, event-driven system that kicks into gear as soon as an event is invoked. Question: What are notifiers, and how are they used in Delphi experts? Lischner shows us how notifiers provide information about major occurrences in the Delphi IDE, such as opening or closing units, opening or closing projects, and so on. He explains that addin experts have a special notifier that we can use to keep track of events and file manipulations. To make working with notifiers easier, Lischner creates TProjectNotifier and TModuleNotifier. These classes encapsulate the structure of these notifier classes in a way that makes them easily available when were writing experts. The various settings and events are available as properties, and many of the internal details are handled for us. In fact, there are event handlers for each notification in these classes. Have you heard about or worked with editor interfaces? If your expert will be modifying code, whether at the project or unit level, you must be concerned about text editing. Editor interfaces provide easy access to Delphis Code Editor. In demonstrating the uses of this interface, Lischner identifies some serious bugs in the first release of Delphi 3 (indicated by a bug graphic and the word warning in the margin). This alone should make this a worthwhile investment for anyone working with Delphi experts. No doubt, Lischner spent many hours crashing
his system so we wouldnt have to. This new book by Ray Lischner is right at the top of my list of favorite Delphi books, next to his Secrets of Delphi 2. Theres very little I can find to complain about. I would have liked to have seen information about which functions are available in which Delphi versions. However, the book clearly identifies itself as a Delphi 3 book, so I shouldnt necessarily have expected that. In conclusion, I recommend this
book very highly to every serious Delphi programmer. Even if youve never considered writing a Delphi expert, this book will show you why and when you should, and how to go about it. Alan C. Moore, Ph.D. Hidden Paths of Delphi 3 by Ray Lischner, Informant Press, 10519 E. Stockton Blvd., Ste. 100, Elk Grove, CA 95624, (916) 686-6610. ISBN: 0-9657366-0-1 Price: US$39.99 (350 pages, CD-ROM)
TextFile
39
File | New
Directions / Commentary
must upgrade for Delphi developers, Delphi 3 provides a host of new features and functionality that make it, perhaps, the best environment ever for developing Win32 applications. Yet, because no perfect development tool has been developed, it behooves us to note some of Delphi 3s shortcomings as well; in doing so, well be better informed about what to use, avoid, or work around. The VCL has always been a centerpiece of the Delphi architecture. Therefore, the robustness and flexibility of these components are critical factors in the products success. Delphi 3 introduced several new components to the VCL, but my experience has shown that some of these new controls dont meet Delphis usual standards. With that background, I offer some lessons learned from working with Coolbars and Decision Cube controls. Coolbars. My experience using TCoolbar and its related components (TToolbar, TSpeedbutton, and TToolbutton) has been quite frustrating. If you intend to use them, there are several issues to note. Using a TSpeedbutton on a TToolbar when the Flat property is set to True creates a vertical line up the center of the button when your system has a recent version of ctrl3d32.dll. You can avoid this TSpeedbutton glitch by using TToolbuttons, but these also have problems, such as confusing and sometimes inconsistent resizing. Another negative of the TToolbutton control is its reliance on using a TImageList for images. While there are some advantages to using imagelists, I find it much easier to work with single images for a toolbar button. For example, the seemingly simple task of moving a button between toolbars will usually force you to edit the .res file that that imagelist references. Adding additional controls (such as a TComboBox) to a TToolbar can cause problems if the toolbar already has TToolbuttons contained in it. I found changing the positioning of the combobox would often cause Out of Index and Access Violation errors. Then the toolbar became unstable at design time. I havent seen a freeware or shareware Coolbar replacement, but for Office97-like toolbars, a great solution is to use Jordan Russells TToolbar97 component suite. These freeware VCLs are my newest entries to the All-Wagner Team (see the August 1997 issue of Delphi Informant). Download them at http://members.aol.com/jordanr7/index.htm. Decision Cube controls. A second type of native VCL component to approach cautiously is the Decision Cube controls (TDecisionCube, TDecisionGrid, etc.). Ive determined, through company experience, that these controls work well for basic displaying of data, but if you intend to do much more with them watch out. As with the Coolbars, there are issues you should be aware of. For instance, accessing data and dimension cells, and working with data inside them, can be difficult; and much-needed events, such as OnSelectCell, arent fully implemented. Also, Help files and printed documentation, in some cases, arent correct in listing methods or properties supported. Thus, be sure to use the .INT files to get correct parameters for methods and functions. The Decision Cube controls are the lone group of VCLs created by Borland that dont have source code included with any Delphi package. Therefore, you dont have the same ability to correct or modify these controls as you do with other native VCLs. Aside: revisiting business objects. On a different note, I wanted to briefly touch on the subject of business objects. Earlier this year, I did a two-part series on creating and using business objects in Delphi. These two columns generated more interest on behalf of readers than anything else Ive written. Most people responding to me via e-mail have been interested in employing business objects, but would like further information on the specific techniques of doing so. Other readers have been using business objects with Delphi, and wanted to share their ideas with others. I am in the process of trying to link together these Delphi developers interested in discussing business objects, so if you have an interest, drop me a note at [email protected], and Ill put you in touch with this group of people. Richard Wagner Richard Wagner is Chief Technology Officer of Acadia Software in the Boston, MA area, and Contributing Editor to Delphi Informant. He welcomes your comments at [email protected].
40