Linux Application Development For The Enterprise (Charles River Media Programming)

 < Day Day Up > 


The CLX component library forms the core of the Kylix product, apart from the Delphi and C++ compilers. It is the component library that makes the application development robust and faster. CLX is equipped with prebuilt classes for almost every type of programming need, including visual and non-visual components; components for accessing external resources such as printers, database access components, components for distributed application development, and many more. In addition, CLX provides a very robust component building architecture for us to custom-build components specific to our needs or buy components from third-party vendors. It is this component-based technology that has led to a revolution in the application development for the Windows platform. Now, this technology is extended by Kylix and other similar systems to the Linux platform. The rest of this chapter is going to discuss the CLX component framework for building Linux desktop and database applications. In Chapter 5, a detailed discussion was presented on creating objects on the stack and on the heap, with relative advantages and disadvantages. The CLX objects should always be created on the heap, thus making effective memory management. In short, in Delphi, the ‘Create’ method should be used to create an object of the specific class on the heap and return a reference to that object and the ‘Destroy’ method is used to delete the object from the heap, and in C++ the same result is achieved by using the new operator for creating objects on the heap and the delete operator to delete the objects.

Figure 6.19 displays a very basic hierarchy of CLX components. The figure only shows the major branches in the hierarchy and is not complete in any way. The following subsections are going to discuss the detailed component architecture.

Figure 6.19: CLX class hierarchy.

CLX Sub-libraries

The CLX object library contains four main subdivisions; the BaseCLX provides classes that form a basis for the rest of the architecture, the VisualCLX component set provides the complete GUI functionality, the DataCLX is a set of components that allows the user to work with database objects, and the NetCLX set of components is used to build networking and distributed applications. It is not appropriate to attempt to correlate the component hierarchy diagram with these four component subdivisions, because the component, hierarchy provides the architectural relationship among the components, whereas the four subdivisions discussed in this section focus on the functional relationship among the components.

BaseCLX

As specified earlier, the BaseCLX is considered to form the foundation for the entire CLX component architecture. The classes in this subdivision provide the users great flexibility in understanding the runtime behavior of the CLX objects, through easy access to their Run Time Type Information (RTTI). It is these classes that impart robustness to the runtime library.

TObject

The TObject class is an abstract base class and forms the basis of the entire CLX framework, which means that every CLX component must be derived from TObject or one of its descendants. This class encapsulates all the basic functionality required by any class to fit into the CLX class framework. This includes providing the object identity (Run Time Type Information or RTTI) such as the name of the class that the object represents, its parent class, the size in bytes that an instance of this object occupies on the heap, and so on. This class does not have any properties, but provides necessary methods to know an object’s identity. The ClassName method retrieves the name of the class that the object represents, as a short string, and does not have any arguments. For example, if the object is an instance of TButton class, this method would return TButton as the value of the short string. The ClassType method retrieves a reference to the object’s class definition; in Delphi, it is a reference to TClass data type, whereas in C++ it is a pointer to the TMetaClass data type. In C++, TClass is defined as a typedef of a pointer to TMetaClass. However, usually the developers do not have to call the ClassType method directly because the functionality offered by this class is already provided through the static methods of TObject class. The InstanceSize method returns the size in bytes, of memory required to instantiate this object, and does not require any arguments. The ClassNameIs(AnsiString className) method returns a Boolean value indicating whether the object’s class name is the one specified in the method argument. The InheritsFrom(TClass ancestorClass) method returns a Boolean value indicating whether the object’s class is inherited from the class specified in the argument.

TPersistent

Objects that can save their state information to persistent storage, such as a stream or file, are known as persistent objects. Classes which directly descend from TObject cannot save their state (unless exclusively programmed in the descendant class). This is an important property required by all the components that should be available during design time because we learned that components owned by the form at design time are saved to the form file with their state information. The ability to persist their state information is imparted to the objects in the TPersistent class, which is a direct descendant of TObject. The DefineProperties(TFiler filer) method provides the ability to objects to save the unpublished data to stream (for later retrieval). It should be noted that the TPersistent class objects are not necessarily components that appear in the Object Inspector; they might be simply non-visual objects that do not have to be manipulated through the Object Inspector. Because the Object Inspector can display only published properties of components, the unpublished properties do not even appear in the Object Inspector. The component writers override this method in their component classes, and in their implementation, they specify a method that can write a property to a stream, through the TFiler object’s DefineProperty method. In other words, for every property they write to a stream, it is necessary to make a separate call to the TFiler object’s DefineProperty method within the overridden DefineProperties method implementation. A form’s components are saved to the form file only when the components are owned by the form, which is the case with all the components dropped onto the form during design time. But how does the form designer know the component that owns others? By executing the GetOwner method of the component’s class, the owner component is returned. This method is introduced in the TPersistent class, and is overridden by the descendant classes.

At this time, it is important to understand the following concept: Due to the object-oriented nature of the component framework, a particular branch in the class hierarchy can be extended to any extent as desired, and where we are satisfied with the functionality, we can finally implement the object. For example, objects of TPersistent class cannot be instantiated because the class does not represent any business functionality; rather, it imparts specific behavior to the classes that inherit this class. Therefore, either we can directly implement some classes with TPersistent as the base class, or we can further extend the chain to derive more complex classes. For example, the TCollection and TCollectionItem classes are directly inherited from TPersistent, and can be used collectively to build object collections. Even though the TCollection and TCollectionItem classes can be theoretically instantiated, normally one or more of their derived classes are used to create collections in the respective functional areas, as in the case of a collection of database field definitions or index definitions belonging to a dataset. The TGraphicObject class derived from the TPersistent class acts as an abstract base class for graphic objects such as TBrush, TPen, and TFont, which are used to represent the Brush, Pen, and Font properties of any object that can render a drawing surface known as canvas. The TCanvas abstract class is derived directly from TPersistent acts as the abstract base class for any other object that needs to render a canvas for drawing graphic images or writing text with graphic fonts. There is another class, TGraphic, which is derived from TPersistent, and acts as an abstract base class for those classes that needs to store an image and display them when needed, such as TBitmap and TIcon. Classes that directly descend from TPersistent cannot appear in the Component palette, because they do not have the behavior built for this purpose. However, objects of such classes can be instantiated during runtime or can be accessed as properties of other components that are accessible through the Component palette. Therefore, a new branch of classes is inherited from TPersistent, incorporating the ability to the classes to be hosted in the Component palette. The class that forms the ancestor of this branch is TComponent and is discussed in the following subsection.

TComponent

As specified earlier, the TComponent class introduces new features to the descendant classes: the ability to be hosted in the Component palette, the ability to own and manage other components, and the ability to save themselves to the form file and load again when the form is reopened. The ability to load and save is built upon the persistence provided by the ancestor class. The ability to own and manage other components means the ability to save and load the owned components and destroy them when need be. The ability of components that descend from TComponent class to appear in the Component palette should not be confused with the visual components that interact with the users. A component may be visual or non-visual. The direct descendants of TComponent are nonvisual components, which means that although their properties can be accessed through the Object Inspector, they do not have any visual interface to the user when the application is running. During design time, a visible icon may be accessible to the user for the purpose of identifying them, placing them onto the form, and selecting them in order to set their properties; however, during runtime, even the icon is not visible. Therefore, the non-visual components can be placed anywhere on the form. Some of the direct descendants of TComponent class include TApplication, which represents the current Kylix application; the TMainMenu class to identify the main menu component in a GUI application, TScreen to facilitate the interaction of the application with the desktop screen, the components belonging to the centralized user interface action management such as TAction, TActionList, and TActionLink, and the components belonging to the database connectivity such as TSQLConnection, TSQLDataSet, TSQLQuery, TField, TDataSource, and so on. The nonvisual components are used in almost all the applications, and details on some of these components are discussed throughout this chapter, while some distributed technology-related components are discussed in later chapters.

As discussed in the previous chapter, most of today’s powerful object-oriented languages (with a few exceptions) leave the responsibility of object destruction to the programmer, who might make mistakes, such as deleting an object while it has some active references or not deleting at all. However, component architectures such as CLX (and Borland’s Visual Component Library on Windows platform) brought in the concept of object ownership by other objects. With this concept, the owner of an object is made responsible for destroying all the objects owned by it, either when it goes out of scope automatically or is destroyed explicitly. However, this principle is not applicable to all the objects developed in these component frameworks. The object ownership is introduced at component level and is therefore a feature of the TComponent class. For this reason, objects of classes descending directly from the TObject class or TPersistent class do not participate in the object ownership phenomenon.

The Components property of the TComponent class represents an array of components it owns, and each individual component is accessible using a zero-based index. The number of components owned by the component is identified by the ComponentCount property. Because the component may itself be owned by another component, the Owner property indicates the component that owns this component, and the ComponentIndex property is an integer representing the index of this component in its owner’s Components array. In a typical CLX GUI application, all the components (visual and nonvisual) placed on the form are automatically owned by the form, which takes the responsibility of destroying them when the form is itself destroyed.

VisualCLX

The VisualCLX component set is a wealth of productivity tools and lets the developers tap into the Windows-like development features on the Linux platform. Apart from the base classes that introduce the GUI features for displaying data, accepting user input, and generating events, the components that the end user can use (and inherit from) include the basic widgets such as edit box, label, list box, combo box, list view, tree view, data aware components such as data grid and dataset navigator, and data aware counterparts of the basic widgets discussed earlier. In addition, there are components that are designed exclusively for user-generated events, such as button, radio button, speed button, checkbox, main menu, pop-up menu, some prebuilt common GUI dialogs, and many more.

TControl

Although a component may or may not be visual, a control is a visual component that the user can see and manipulate during runtime and that is implemented as a descendent of the TControl class, which is in turn inherited from the TComponent class. Because a control is visual, it introduces properties that can change its appearance, such as the Color, Font, Height, Width, and the coordinates of the top left corner, identified by the properties Top and Left. There is also a property known as Align that indicates how the control should be aligned with respect to its parent control. Similar to the concept of object ownership introduced at the component level, the concept of parent-child relationship is introduced at the control level. A parent control is one that is responsible for saving the child controls contained in it to a streaming object when the parent control is itself saved to the stream. However, the parent of a control should be a windowed control, such as a descendant of the TWidgetControl class. The controls are mainly two types: those that accept keyboard input from the user and serve as GUI widgets and those that do not accept keyboard input. The first type of controls are called the widget controls and form the main stream of GUI controls; these controls need more system resources because they have more user interaction. The TWidgetControl is an abstract base class that exposes the underlying GUI widget through the Handle property. The second type of controls, called the graphic controls, consume low system resources. The TGraphicControl class directly descends from the TControl class and introduces the ‘Canvas’ property, which provides a drawing surface for the graphic control.

TWidgetControl

It has been mentioned earlier that TrollTech’s Qt library is the underlying foundation for Borland’s CLX framework, and therefore the CLX widget controls represent the underlying Qt widgets. The TWidgetControl forms the ancestor of all the GUI widget controls and probably has the biggest branch of controls in the CLX component framework. Every user-interface control has to descend from this class. Usually, the GUI controls that programmers use in their applications are not direct descendants of this class; rather, they descend from an intermediate class that implements all the features of the final implementation class as protected methods, and the final implementation class unlocks the power of the intermediate class by publishing the necessary features. For example, the TCustomEdit class, inherited from TWidgetControl, is designed with full functionality of an edit control. However, creating objects of this class is prohibited, because this class implements features common to all types of edit controls and does not expose behavior specific to one particular type. The TEdit class, which publishes (and implements any overridden methods) of TCustomEdit, should be used to create edit control objects. The Handle property of the TWidgetControl component provides a pointer to the underlying Qt widget and can be used when making direct calls to the Qt widget library functions.

Forms and Frames

A form is the main interfacing control (or main window) in a typical GUI application and is identified by the characteristics of the abstract base class TForm. Objects of this class are not directly instantiated; rather, every time a GUI application is created through the IDE, a new descendant class of TForm is created with the default name TForm1 in a separate unit, and an object of this new class is created with the name Form1 in the main application (program) file. The reason for doing this is that the TForm is a generic class and provides the features of how a form should behave. However, the form in a typical application will have a number of add-on features, embedded components, and methods, which are easy and convenient to add in the application specific form class instead of the basic form class. As we keep adding these additional features through the IDE (or even manually), they are actually added to the class definition, thus customizing our application-specific form. For example, if we drop a data grid from the component palette onto the form in the form designer, an instance of the Component is created and added as member to the form. The form name, the form file name, and the form object name are all changeable as needed by the developer. If we add more forms to the application, the IDE automatically creates a new inherited form (from TForm), each time giving a different name to the form class and form file. For every new form, a .xfm file and the corresponding .pas file are created if the application is being developed in Delphi, and a .xfm file, and a .h header file with the corresponding .cpp unit file are created if the application is being developed in C++. In an Object Pascal program, the unit file itself contains the class type declaration, followed by the implementation of the class methods, whereas in the C++ application, the class declaration is stored in the header file and the implementation is stored in the .cpp program file. Every time a new form is added, the IDE automatically maintains a list of forms to be created automatically when the application is run and saves this list in the project options. Figure 6.20 displays the ‘Project Options’ dialog showing the (default) scenario with an application that has two forms created during design time, and added to the Auto-Create Forms list in the left side box, while the right side box showing the Available Forms list is empty.

Figure 6.20: Project Options dialog.

What this means is that when the application is started, all the forms listed in the Auto-Create list are automatically instantiated, using their respective class definitions, and maintained in memory, whether the forms are used or not or even if the forms are used at a later time. But this is not always a desirable situation if the application has more forms and should be run with optimal usage of memory. In such cases, we can choose the specific forms not to be instantiated when the application is started by moving those form names from the auto-create list to Available Forms list. By doing this, we are taking responsibility to instantiate the form(s) later. It is always best that the main form of the application be instantiated, even though other forms will be instantiated at run time. The other forms can be created during runtime and displayed as required. Listing 6.1 shows sample code in Delphi and C++ used to create forms during runtime. The code is written in the OnClick event handler of a button placed on the Form1. When the form is created, the Application variable is passed to the constructor as Owner of the form, which can be related to the earlier discussion on ownership of the components; the Application variable is an instance of the TApplication component, and controls the entire application. Every form created in the application is owned by the Application instance, and each form would in turn own the components placed on it during design time or created using the form as owner during runtime.

Listing 6.1

// Delphi code procedure TForm1.Button1Click(Sender: TObject); begin if Form2 = nil then Form2 := TForm2.Create(Application); Form2.Show; end; // C++ code void __fastcall TForm1::Button1Click(TObject *Sender) { if (!Form2) Form2 = new TForm2(Application); Form2->Show(); }

From the listing, it should be noted that first we make sure that the form is not already created and then create the form using the form’s constructor. If we do not do this, every time this code is executed, a new instance of the form will be created and assigned to the same form variable, leaving the previous instances of the form in memory without destroying them, thus leading to the memory leak situation. You can try commenting the if statement and rebuilding your application. Then, every time the button is clicked, a new instance of the form is created and displayed. If the if statement is used as coded in the listing, then the first time the button is clicked, the form is created and displayed, and for every subsequent click of the button, the same form is displayed.

The Show method of the form displays the form in modeless style, which means that the input focus may be shifted to the other forms of the same application (by using the mouse or the keyboard) while this form is displayed. There may be situations that require the current form to be closed before shifting input focus to the other forms. Such a display style of the form is called modal style. In such cases, the form should be displayed using the ShowModal method instead of the Show method.

In an application, the form is the highest-level GUI component, and most of the form’s generic behavior is implemented in the TCustomForm class and exposed through the TForm class. The FormStyle property classifies the form whether it is part of a single document interface (SDI) application or a multiple document interface (MDI) application. In a SDI application, each form is displayed as a separate entity and the forms are not tied together, whereas in a MDI application, one main form acts as the parent window containing one or more child form windows, and the child form windows cannot be displayed outside the main form window. The FormStyle property is fsNormal if it is part of a SDI application, or it may have a value fsMDIForm or fsMDIChild indicating that it is the main form or child form in a MDI application. The MDIChildren property is an array of child forms maintained within the main MDI form, and the MDIChildCount property returns the number of child form windows currently active. Both these properties are applicable only if the current form is an MDI parent form. Whether the form is resizable or can be minimized and maximized is determined by the BorderStyle property. If a TMainMenu component is added to the form, it can be accessed through the Menu property. The form’s transitional state information such as whether it is being created, is being shown, is activated, and so on may be obtained from the FormState property.

A frame is another widget control similar to the form, and is identified by the ‘TFrame’ class. A frame is very similar to a form in many ways, with a few exceptions as outlined here.

Common Dialogs

The CLX component library is shipped with prebuilt dialogs for some of the most commonly used functions, such as opening a file, saving a file to the disk, selecting a font, selecting a color, and so on. These are hosted on the ‘Dialogs’ page of the Component palette and will be discussed in this section to give an insight on how to use these dialogs. More dialogs may be built by developers using the component framework. It is fairly simple to use these dialogs, as they provide a familiar and consistent interface to enable the developers to perform common file operations. Each of these dialogs displays the respective dialog box when the Execute method is executed on the component. The user is provided with options to make a decision on the file (such as choosing a directory or file to open or setting a file name to save), and as long as the user is active with the dialog, the Execute function does not return. When the user completes the choice and clicks a button such as ‘OK’ or ‘Cancel’, the dialog completes its action and returns a Boolean value indicative of the choice made by the user.

The TOpenDialog is used to display the File Open dialog box. The dialog enables the user to choose a file from the list of displayed files in the current directory. It also enables the user to navigate different directories on the file system and choose one or more files. Using the available properties, the runtime functioning of the dialog can be customized as required. The InitialDir property is a string indicating the default directory that should be displayed when the dialog is displayed first. The FileName property contains the name of the file (including the path) that has been most recently selected. The Filter property can be used to allow the dialog to display only files having specific file extensions. Although the dialog may be configured to select files of any extensions, it is the responsibility of the application to handle the selected file properly. The Options property is a set of Boolean options, one or more of which can be set to make feature-rich dialogs. For example, the TOpenDialog permits the user to select only one file by default; however, this can be changed to allow the user to select more than one file at a time by setting the option ofAllowMultiSelect to true, in which case the user can use the SHIFT or CTRL key to select multiple files from a directory. When multiple files are selected, they are listed in the Files property, which is a TStrings object. Either the FileName property or the Files property is filled with the appropriate file name—or file names as the case may be—if the user clicks the OK button after selecting the files or files. On the other hand, if the user clicks the Cancel button, these properties do not have any values, and the Execute method returns the Boolean value false. The TSaveDialog is used to save a file to disk and behaves very similarly to the TOpenDialog in its options and property settings. In addition to those discussed here, there are other properties that these two dialogs support.

The TFontDialog and TColorDialog are similar to each other and are different from the File Open and File Save dialogs. The TFontDialog has only one important property, Font, used at runtime to identify the font selected by the user in the form of a TFont object. Similarly, the TColorDialog has the Color property, which identifies the color selected by the user in the form of a TColor object. TFindDialog and TReplaceDialog are similar and together give a find-and-replace feature similar to those found in many text and document editors. These dialogs provide an edit box in which the find text (and replace text in case of the replace dialog) can be entered by the user and that has a few buttons that initiate and control the search process. The dialog only provides a way to get the search string (and replace string in case of replace dialog) and a few options for the user; it is the responsibility of the application developer to implement the search and replace features accordingly. Dialogs are very useful in providing built in applet-style code involving user attention. Although the font and color dialogs are inherited from the TQtDialog abstract base class, all the other dialogs are inherited from the TCustomDialog class. Both the base dialog classes can be further extended to create custom dialogs. In either case, the Execute method should be overridden to provide the actual functionality required by the specific custom dialog.

DataCLX

Automating the database development process for the Linux platform is really a valuable innovation that Borland has built with the CLX object library. A number of visual and nonvisual components are designed for this purpose. A quick overview of these components is provided in this section, with a detailed discussion later in this chapter. Developing database applications is a composition of three tasks: first, establishing and maintaining database connection; second, accessing the underlying database objects and providing a uniform access method to the end-user application; and third, presenting the data to the user in the most effective way and permitting updates in a controlled and secure way.

The most important part of the first task is to design a framework that will connect to the backend database in a database-neutral way, which means changing the backend database without having much impact on the other two business components in the system. In addition, the framework should be able to support high-performance and flexible connectivity with the underlying database. The new ‘dbExpress’ architecture designed by Borland not only provides a high-performance, flexible and database-neutral architecture, but is also cross-platform compatible between Linux and Windows versions of the CLX framework. The most important part of the second task is the support provided by the comprehensive set of data access components which function as conduits (or data pipes) between the data supplier (which is the database) and the data consumers (the data controls). These components enable the developers to build either two-tier and three-tier applications or multitier Web-based applications transforming data from the database to XML documents. The most important aspect of the third task is the set of data controls that present the data to the user. These include data grid, data-enabled edit controls, combo box, list box, and so on. Collectively, all these components are considered as ‘DataCLX’. These are the components that are located in the ‘Data Access’, ‘Data Controls’, and ‘dbExpress’ pages of the Component palette. Only the ‘Data Controls’ page components are visual; all others are nonvisual.

NetCLX

Apart from developing simple desktop and database applications, Kylix 3 is capable of developing high-end networking and Internet-based applications. The platform supports almost all the networking protocols. The components located in the ‘Internet’, ‘WebSnap’, ‘WebServices’, ‘Indy Clients’, ‘Indy Servers’, ‘Indy Misc’, ‘Indy Intercepts’, and ‘Indy I/O Handlers’ pages are all the networking components used in building high-end networking and Web applications. Some of these components will be discussed and demonstrated in future chapters while working with distributed application development.

Some Most Frequently Used CLX Objects

It is really difficult to explain the features of Kylix in a single chapter. However, the first half of this chapter attempts to introduce the users of Linux to the power of this RAD development platform as a whole; we will focus attention on database application development in the second half of the chapter.

Strings and String Lists

The TStrings class is a collection of individual strings and associated object pointers. It is a virtual base class containing all pure virtual methods, and therefore we can only create instances of one or more of its descendant classes. The TStringList class is derived from TStrings and is widely used to work with a collection of strings. The Strings property is an array of the contained strings, and the Objects property is an array of object pointers. The Count property is a read-only property and indicates the number of strings in the list. The Strings array and the Objects array are accessed by an index, which varies from 0 to 1 less than the Count. The individual strings can all be concatenated together and retrieved as a single-string object in which the individual strings are separated by commas. The string list can also be used to store name-value pairs, in which case the Names and Values properties are used to retrieve the names and the corresponding values. In this process, the same index should be used to retrieve a name and its corresponding value. A name-value pair is of the form Name=Value. When the strings are loaded in this form, the complete string can be accessed through the Strings array property, and the name and value can be separately accessed through the Names and Values array properties, respectively.

The Add method lets you add a string to the item list, while the AddObject method lets you add an object with an associated string (e.g., object name). The signatures of these methods in Delphi and C++ are shown in Listing 6.2. To add an object to the list, the object has to be created separately, and then the object pointer (or reference) is passed along as parameter to the method call.

Listing 6.2

// Delphi code function Add(const Str: string): Integer; function AddObject(const Str: string; newObject: TObject): Integer; // C++ Code int __fastcall Add(const AnsiString S); int __fastcall AddObject(const AnsiString Str, System::TObject* newObject);

Both the methods return the index of the added string (and object). When the list is destroyed, the contained strings are destroyed, but the associated objects are not destroyed. As they are not owned by the string list object, they have to be destroyed separately by the developer. As discussed in the previous chapter, to avoid a memory leak situation it is always recommended that the objects be destroyed before destroying the string list because once the string list is destroyed, the object pointers are not available to access the objects for the purpose of destroying them. However, if the objects assigned to the string list are owned by another component, then the owner component takes care of destroying the objects, and the developer does not have to worry about it. The Insert and InsertObject method, are similar to the add methods, but are used to insert a string in the middle of the list, at the location specified by the index passed as the first argument to the method. The Delete method may be used to delete an item from the string list, and the Move method may be used to move an item from one index position to another. When an item is moved from one index position to another, all the affected items also change their index positions automatically. The Exchange method should be used to swap strings at two index positions specified as arguments to the method. There are two methods—SaveToFile and LoadFromFile—that are used to save the string list to a file and load again from the file. Similarly, there are two more methods SaveToStream and LoadFromStream, which are used for saving the string list to a stream object and loading the string list again from the stream.

When the objects are accessed using the Objects property, the returned object pointer must be typecast to the appropriate object pointer because the pointer returned as the array element references the object to be of type TObject. The C++ code segment shown in Listing 6.3 adds a TButton object to the string list; when retrieved later, the object is typecast to the TButton type.

Listing 6.3

// First create a string list and then assign a TButton object to it TButton *Button1 = new TButton(this); // 'this' pointer refers to the owner form TStringList *stList = new TStringList(); stListàAddObject(Button1àName, Button1); . . . . . . . . . . . . . . . . . . . . . . . . TButton myButton = (TButton)stListàObjects[I]; // I is the index of the object AnsiString buttonName = stListàStrings[I];

Object Lists

Apart from the string list, which stores strings and associated objects, there is another class—TList—that is used exclusively for storing a list of object pointers. The list stores void pointers in C++ (and Pointer data types in Delphi) and is used for containing lists of any object type. When retrieving the object pointers, the individual items should be typecast to the appropriate object pointer type. It is recommended (though not necessary) that all the items in a single list object be of the same object pointers, otherwise it might become difficult to retrieve the object from the item pointer because of the typecasting requirement. The Add and Insert methods add a new object pointer to the end of the list and at the specified index location, respectively. These methods only add the specified pointer to the list; the actual object pointer should be obtained by creating the object before attempting to add to the list. The Move and Exchange methods are used to move the index position of a pointer to a new location and swap two index positions, respectively. The Items property is an array of (void) pointers and is indexed on a zero-based index. For example Items[2] will retrieve the third object pointer in the list. The Count property indicates the number of items in the list. The Delete method should be used to delete the list items using the item index, while the Remove method is employed to delete an item using the (void) pointer. In either case, only the item pointer is removed from the list; all other items following the deleted item in the list are moved up in the index. Also, it is very important to keep in mind that delete is performed only on the item in the list and the actual object is to be deleted explicitly by the programmer before deleting the item from the list. Also, deleting the list object itself does not delete any of the objects whose pointers it maintained; the individual objects have to be deleted before deleting the list object. If one does not want the index of other items affected by the delete process, the particular list item be set to null value (nil value in Delphi) instead of deleting it. The Pack method is used to delete all the null items in the list and shrink it to contain only valid object pointers. The Clear method is used to delete all the pointers from the list.

The TObjectList class is inherited from the TList object and provides additional features to the developers. An object of TObjectList class not only stores the object pointers, but maintains the objects also, if the ‘OwnsObjects’ property is set to true. The ‘Remove’ method is overridden to accept the actual object pointer rather than the void pointer. Also, the ‘Items’ array property retrieves the actual object pointer instead of the void pointer. In all respects, it is similar to its parent class, except that it frees the memory occupied by the objects that are maintained by the list.

Streaming in CLX

Streams are data structures that enable efficient and fast saving of the raw data for easy access at a later time. Streams are considered to be one kind of I/O (input and output) system, similar to disk files. The advantage of streams over the files is that streams are data structures that are maintained in the memory; hence, they provide very fast access to the data. Almost any kind of data can be stored in a stream. Streaming is the process by which an object’s state can be saved to a stream. In a typical scenario, an object is saved to a stream, the stream is then transmitted over the network to another computer, and the object can be rebuilt on the remote computer in its own address space. The CLX component library comes with built-in stream objects for easy use. The abstract base class is TStream, from which TStringStream, TMemoryStream, and TFileStream are inherited to work with string objects, in-memory streams, and disk files, respectively. The TStream class defines all the features needed to read from and write to disk files and in-memory data structures on the heap. The stream objects are designed to permit the applications to read from and write to any arbitrary position within the memory managed by the stream. As mentioned earlier, streams are useful for saving data of any kind, such as ASCII characters, binary code, graphic images, and so on. The streams consider the data as only a stream of information; it is the responsibility of the programmer to interpret the data in a stream—either while writing to the stream or while reading from the stream.

The ‘TStream’ class also defines methods to work in conjunction with components and filers for loading and saving components in simple and inherited forms. Normally, these methods are called automatically by global routines that initiate the task of component streaming. However, they can also be called directly to initiate the streaming process, particularly by the component writers during custom component development. These are ReadComponent, ReadComponentRes, WriteComponent, WriteComponentRes, and so on. The ReadComponent method is called to read the data values from the stream and assign them as property values of the components, and the ReadComponentRes method is called to read the data in a resource file format. These methods are called by the global ReadComponent and ReadComponentRes functions, respectively. The WriteComponent and WriteComponentRes methods work in a similar way to write to the stream.

It should be noted that the CLX stream objects are common to both Delphi and C++ languages and should not be confused with the standard C++ I/O streams, which are supported by Kylix by default.

CLX Object Event Model

An event is the result of an action performed by an entity or an object in the system. For example, when the user clicks the mouse button or a button component on the application main window, or the mouse moves over a component, these are all considered events. In these examples, they are all external events. On the other hand, when the database connection is established, a table is opened for update, or the values in a record are changed, these situations are called internal events. The internal events may happen as a consequence of an external event or as a consequence of a timer object, which automatically triggers the event. One of the strengths of the Kylix 3 platform is the event model that supports the application’s actions in response to an event generated by external and internal objects or by the system-generated events. It is this event-driven model that makes Kylix a successful RAD tool for developing robust Enterprise-class applications, which would otherwise be a complex task to achieve with the underlying programming languages alone. The event model contains primarily three tasks: first, capturing the events when they occur; second, implementing what to do when the event occurs in the event handler methods; and third, establishing the link between these two tasks, so that when an event occurs, the corresponding event handler is executed. The first task is the job of component writers, while the second is performed by the application developers. In other words, from the point of view of the component writers, events are hooks that they provide for application developers to interact with the component as the component’s state changes from the point that it is created to the point that it is destroyed. The third task is performed to tightly integrate the first two tasks, which is accomplished by the CLX component architecture. An event handler is a method associated with the event; typically, it is a method on a specific instance of the component class and is executed only if implemented. The event architecture of Kylix directs the component writers to ensure that if an event is not implemented, the default action is taken by the component without causing abnormal termination of the application; however, the program may not give the results expected. An example of this would be an application that requires that the File Open dialog be displayed when a button is clicked. To achieve this task, the application developer has to implement the OnClick event of the button component where the appropriate code to execute the Execute method of the file open dialog is written. If the event handler is implemented, then it is executed when the button is clicked; otherwise, the application does nothing.

Let us see how events generated by the operating system are communicated to the object. System events are those generated by the operating system or signals originated directly from the underlying widget layer. In other words, event notifications originating from the Qt widgets are known as signals, whereas those originated by the core Linux operating system are termed system events. Because the Qt object library is built over the underlying operating system (and hiding the operating system internals), even the event notifications generated by Linux are treated as special Qt signals while communicating to the CLX components. These signals are translated (by the Qt library) to be of objects of type events or event objects, which contain the information about the event that occurred. The internals of these event objects are beyond the scope of this book. The type of event that is triggered by Linux is identified by the type of event object translated by the Qt library. This event object is passed over to the signal handler of the event (typically the EventFilter method defined at the TCustomControl level), which further translates this event to the form (and name) that is defined by the component’s classes and are displayed in the Object Inspector by the IDE.

When the application developer double-clicks on the event name in the Object Inspector, the IDE automatically adds an empty event handler with the parameters specified (by the component developer) in the event type definition, and with a name created by prefixing the name of the component to the event name and excluding the literal On. For example, if the event is identified by the name OnClick in the Object Inspector’s event page, and the name of the component is OKButton, then the empty event handler is named as OKButtonClick. The first parameter in the event handlers is always the object that has generated the event, and is passed as a reference (or pointer in C++) to the TObject type.

When the component is designed, the event is declared as a method pointer and is published as a property of the component. In the previous example, the OnClick event is published as a property named OnClick. For this reason, we can also assign event handlers at runtime by assigning a method to the specific property. However, the assigned method should have the same signature that the event handler is expected to have. More than one event can share the same event handler if the event handler signature is the same for all such events and if such assignment is functionally acceptable by the application.

Having learned about the event-driven architecture of Kylix, we can spend a little more time on the different types of events that most commonly occur. Among all the event types, the events initiated by user’s actions play a key role in a typical GUI application. The two devices that the user interacts with throughout an application’s life are the keyboard and the mouse. As explained earlier, these events are captured by the Linux operating system and X-Windows and are wrapped into the form of CLX component events by the CLX component library. When using a RAD tool such as Kylix, it is with this component library that we interact, and therefore we are not concerned about the low-level events generated by the operating system and the underlying graphics package. There are three events that could be triggered when a key is pressed: the OnKeyDown, OnKeyPress, and OnKeyUp events. All three events are not relevant for every type of control and for every key on the keyboard; therefore, only the relevant key events are triggered based on the control type and the key pressed. For the TEdit and TMemo controls, all three events occur, as these controls are meant to receive keyboard input. The events are triggered in the same sequence as specified here. The OnKeyDown event is triggered when a key is pressed on the keyboard, while the specific control (such as a button or edit control) is on focus and before the OnKeyPress event is triggered. The event handler designed for this type of event receives the key pressed as a parameter; the key might be any of the keys on the standard keyboard and includes the mouse buttons. There are virtual key constants used to identify the keyboard keys, such as VK_LBUTTON, VK_RBUTTON, VK_MBUTTON to identify the left, right, and middle buttons of the mouse, or VK_ESCAPE to identify the ESCAPE key, or VK_RETURN to identify the ENTER key, and so on. The state of the control keys such as the SHIFT key, or CTRL key, or ALT key, and the left, middle, and right mouse buttons is passed as another parameter in the form of a set. Because it is passed as a set, the state of each of these control keys can be checked. This event may be considered as a pre-KeyPress event where we can prepare to actually work with anything that is entered. Also, this event is triggered by every key on the keyboard, whether that key represents an ASCII character or not. On the other hand, the OnKeyPress event is triggered only for keys that have an ASCII code assignment and does not have any other parameter to identify any of the control keys. As mouse buttons do not have associated ASCII codes, mouse button clicks do not generate this event. The OnKeyUp key is triggered when the pressed key is released, and the parameters of this event handler are similar to the OnKeyDown event handler.

The other type of user events is those generated by the mouse button clicks, the OnMouseDown, OnMouseMove, and OnMouseUp events. These events are also triggered by the control over which the mouse pointer was placed when the event occurs. The OnMouseDown event occurs when a mouse button is clicked while the mouse pointer is on the control. The OnMouseMove event occurs when the user moves the mouse while the mouse pointer is over the control. The OnMouseUp event occurs when the user releases the mouse button that was pressed while the mouse pointer was placed over the control. In all these events, the first parameter is the object that generated the event. The second parameter is the mouse button that is clicked, in the case of OnMouseDown and OnMouseUp event handlers, while the OnMouseMove event handler does not have this parameter, as it is not relevant in this case. The next parameter is the state of the Shift keys in the form of a set (as explained earlier), and the next two parameters are the X and Y pixel coordinates of the mouse pointer on the client area of the control that generated the event. In the case of the OnMouseMove event handler, the X and Y coordinates represent the new location to which the mouse is moved on the client area of the control.

The other types of most frequently used events are those generated when the user initiates the drag-and-drop operation between controls in the application. These events are explained in more detail in a later section in this chapter during a discussion of the drag-and-drop operation.

CLX Exception Handling

Although computers have been designed to be intelligent, logic-based machines, it is a well-known fact that it is the human brain (or several human brains to be very specific) that design them and make them function as desired. If we do not tell the computer what to do in a particular situation—either through a default action or through a specific action—obviously the computer is confused about what to do next. This confusion results in conditions that a computer identifies as exceptions. Exceptions are those programming situations that are faced by the computer when executing any application for which no programming logic has been implemented; hence, the computer does not know what to do. Programming a computer is a task that is spread across several levels (or layers) of detail and complexity, ranging from the lowest level of machine-code interpreted by the processor to a higher level such as a fourth-generation language or even higher. Programming each level involves special skills of developers exclusive to that level, in addition to general programming skills. Exceptions that occur at a lower level are generally caught at a higher level. For example, a divide-by-zero exception occurring at the processor level is usually caught and handled by the program written in C++ or Delphi or Java or any other language. The exceptional conditions may arise from a variety of reasons, including invalid data, null objects, memory access violations, and so on. When an error condition occurs, the program raises an exception by creating an exception object and (internally) rolls back the stack to the most recent point where the exception was handled. The block of program where the exception is handled will have the opportunity to diagnose the exception condition based on the exception object type. Usually, a descriptive message is also provided in the exception object, which can be displayed or saved to log file for the benefit of the user.

A checklist for attempting to avoid exception conditions is given here.

Blocks of code that are guarded against exceptions are known as protected blocks. Depending on the application requirements and the involved code, often it is possible to group separate lines (or blocks) of code into a block and guard them through the exception handlers. The modern object-oriented programming languages provide very robust exception-handling features. Listing 6.4 displays the sample structure of an exception-handling block in a CLX application in both Delphi and C++ languages.

Listing 6.4

// CLX exception handling in Delphi language try . . . . . . except on <exception specifier> do begin . . . {code to handle the exception} . . . end; end; // CLX exception handling in C++ language try . . . . . . catch (<exception specifer>) { . . . // code to handle the exception . . . }

In Delphi, the block to be guarded against exceptions must be coded using the try . . . except on . . . end block, while the same thing is achieved in C++ using the try . . . catch block. The exception specifier is a parameter of an exception object or an object of one of its descendant classes. The use of exception objects is demonstrated by creating a small application in Kylix 3 Delphi IDE. From the ‘New Items’ dialog, create a standard GUI application by choosing the ‘Application’ type. Two list boxes, one edit control, and two buttons are dropped onto the form from the Component palette. One list box is used to store a list of items, and the other is used to display messages. The edit box is used for the user to enter an item number from the list, to select the item. One button is used to code the logic for retrieving the desired item from the list, in a try . . . except on . . . end block. Any errors that occur in retrieving the item detail from the list are caught in the except block. By catching the error, part of the task of exception handling is done; the remaining part is to inform the user about the error. In the except block, a custom message is displayed in the message list box, in addition to displaying the message retrieved from the exception object. The second button is used to clear the message list box. The project is named ‘ExceptionTestApp.dpr’, and the unit file is named ‘ExceptionTest.pas’. Listing 6.5 displays the source code for this application. This listing is available on the accompanying CD-ROM.

Listing 6.5: ExceptionTestApp.dpr

unit ExceptionTest; interface uses SysUtils, Types, Classes, Variants, QTypes, QGraphics, QControls, QForms, QDialogs, QStdCtrls; type TExceptionTestForm = class(TForm) ProductList: TListBox; MessageList: TListBox; GetProduct: TButton; ClearMessageList: TButton; ProdNbrEdit: TEdit; Label1: TLabel; procedure FormCreate(Sender: TObject); procedure GetProductClick(Sender: TObject); procedure ClearMessageListClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var ExceptionTestForm: TExceptionTestForm; implementation {$R *.xfm} procedure TExceptionTestForm.FormCreate(Sender: TObject); begin ProductList.Items.Add('Borland Kylix'); ProductList.Items.Add('Borland C++Builder'); ProductList.Items.Add('Borland Delphi'); ProductList.Items.Add('Borland JBuilder'); end; procedure TExceptionTestForm.GetProductClick(Sender: TObject); var product: String; var prodNbr: Integer; begin try prodNbr := StrToInt(ProdNbrEdit.Text); product := ProductList.Items[prodNbr-1]; MessageList.Items.Add('Found item in the list : '+product); except on E:Exception do begin MessageList.Items.Add('Wrong product number entered !!!'); MessageList.Items.Add(E.Message); end; end; end; procedure TExceptionTestForm.ClearMessageListClick(Sender: TObject); begin MessageList.Clear; end; end.

When the project is built in the IDE, an executable is created with the name ‘ExceptionTestApp’ in the directory where the project files are saved, which can be executed by the following command-line syntax.

$ ./ExceptionTestApp

The displayed main window of the application is shown in Figure 6.21.

Figure 6.21: Exception Test Application main window.

Creating GUI Applications

Having looked at the capabilities of the Kylix 3 platform, it is now time to discuss the steps involved in building desktop applications on the Linux platform, as outlined here.

The following subsections provide more details on topics related to the most commonly required concepts.

The System Global Variables

The Kylix 3 IDE automatically creates and manages a few global variables in almost every application (with a few exceptions); these are Application, Screen, Mouse, and Clipboard. When an application is developed in Borland Kylix, the IDE automatically creates a global variable named as Application. Although this global variable is created for most of the application types, the console application and the shared object library (or CLX package library) do not have such a variable, as it is not relevant in these cases. The purpose of the Application variable is to make accessible information specific to the application itself, and due to the global scope of the variable, it is available throughout the application. The Application variable definition is not unique across the Kylix platform, and different types of application units represent different objects under the name of Application variable. Depending on the type of application created through the IDE, the appropriate unit (or header file if it is a C++ application) containing the Application variable definition is automatically included in the project. Therefore, when using additional units (or including unit header files if it is a C++ application), the programmer has to take precautions not to explicitly include the units that contain other types of Application variable. For example, a simple desktop GUI application project should not contain an Apache Web server application unit file because they each represent different application types. Any attempt to use mixed application units through the IDE will be prevented by the IDE; however, if the programmer performs such things outside the IDE by manually editing the project file, then the project file may be considered corrupt because the IDE will not be able to compile such projects. In short, an application type and the Application variable have one-to-one correspondence, with the exclusion of the console application and shared library application.

When a standard desktop application (of type SDI or MDI) is created, the Application variable represents an object of TApplication class, which is defined in the QForms unit and simplifies the interface between the developer and the windowing environment. The class provides fundamental support to create and run the application by encapsulating the behavior necessary for processing the operating system-generated events, context-sensitive online help, and exception handling. Similarly, when a Web server application is developed, the Application variable represents an object of TCGIApplication defined in the CGIApp unit, or an object of TApacheApplication class, which is defined in the ApacheApp unit, and provides the framework necessary for a CGI Web module or Apache Web module accordingly.

Although the TApplication class is a component by nature, it is not available in the Component palette for design-time manipulation by the developers, as it is automatically handled by the IDE and is not expected to be controlled by the developers. Some of the properties of the Application are set at design time through the Project Options dialog, which was discussed earlier in the chapter. It was mentioned earlier that every time a form is added to the project, by default the IDE puts it in the ‘auto create’ list in the Project Options. All such forms to be created automatically when the application starts execution are created by the CreateForm method of the Application variable. This method takes two arguments: the Meta class definition of the specific form to be instantiated and a reference to the form variable. When a form unit is created, a form variable is created with a default name automatically in the Delphi unit file (or C++ unit header file). This name is accessed in the Object Inspector as the Name property of the respective form. When we change the form name in the Object Inspector, it changes the form variable name automatically. The project source file (the Delphi or C++ main program file and not the project options file) contains one line of code calling the CreateForm method on the Application variable for every ‘auto create’ style form. If it is intended to create only the main form of the application automatically, and not every form, delete the respective lines that call the CreateForm method from the application main program file (or remove them from the ‘auto-create’ list in the Project Options dialog as mentioned earlier). These forms should be created manually later in the application at the appropriate time, but before first use; any attempt to use such a form before it is created will cause access violation errors. When the form is created manually in the application, make the Application as the owner of the form, so that it is destroyed properly when the application exits gracefully.

Every Linux GUI application using Kylix 3 will have another important global variable, Screen, which is an instance of the TScreen class, created automatically when the application is created in the IDE. Similar to the Application variable, this component is also not available for manipulation through the Object Inspector. Through the properties of the Screen variable, it is possible to access the screen-related resources such as monitors, cursors, fonts, and resolution, using the respective property names. It is also possible to access other forms and data modules of the application through the appropriate properties. It is a usual practice to change the cursor appearance when the application is executing certain types of functions, to indicate to the user what the application is doing. For example, when a client application is waiting for the server to retrieve data from the database and send it back to the client, the cursor may be changed to a different shape (such as the hour-glass shape that is displayed by most applications), indicating to the user that the request is in process. This is done by changing the Cursor property of the Screen variable to crHourGlass; some of the other cursor shapes available are crArrow, crCross, crIBeam, crSQLWait, crHandPoint, and more. Once the data is displayed to the window, the cursor should be changed back to default shape (the shape before it was changed), indicating that the request processing is complete. This is similar to the default practice followed by many GUI applications that let the user know that the request is in process; it also indicates indirectly the length of time that the request processing is taking, or when the request failed due to a network related problem (in which case the cursor takes an unusually long time to return back to default shape). This is explained here with a code snippet. Listing 6.6 shows sample C++ code to illustrate the usage of this property. It should be noted that the two parts of the code are usually separated by other application code and that, as long as the cursor is displayed with the changed shape, the user may be locked from doing anything else. However, by running the process that waits for data to be retrieved from the server in a different thread, the user may be released to do other work in the main process; this needs to implement multi-threaded application development, which is the subject of another chapter.

Listing 6.6

ScreenàCursor = crHourGlass; // to indicate that the request is in process . . . . . . . . . . . . try { . . . . . . . . . . . . // After the request processing is complete . . . . . . . . . . . . } __finally { Screen®Cursor = crDefault; // to indicate that the request is complete }

The Forms property is an array of forms currently displayed in the application, and a specific form can be accessed as a reference to the ancestor TForm class. The ActiveForm property returns the form that currently has focus. The Fonts property is a TStrings object presenting a list of names of all the screen fonts currently installed in the system. Although the Screen global variable is available directly for the Delphi programs, due to some naming conflict problem, the screen device is accessed by making a call to the GetScreen() global function in C++ programs. The GetScreen() function does not take any arguments and can be executed wherever the Screen global variable is accessed. This method returns a pointer to the TScreen object and can be used safely in place of the Screen global variable.

The mouse is accessed through another global variable Mouse, which is an instance of the TMouse class. The Mouse variable provides properties that expose the mouse characteristics, or how the application can respond to mouse messages. For example, the CursorPos property retrieves the current mouse cursor position; the Boolean property DragImmediate may be set to true to indicate that the drag operation should immediately start when the left button is pressed or should start after the mouse is moved a certain specified number of pixels after pressing the left button (as set in the value of the DragThreshold property).

The clipboard is a temporary memory used by the operating system to enable copying data (text, images, objects, and so on) from one application to another or from one part of the application to another. The clipboard is accessed through an instance of the TClipboard class, obtained by executing the global function Clipboard(). It should be noted that any number of times this function is called from an application, always the same clipboard instance is accessed, as conceptually the clipboard is the same memory area managed by the operating system. This function is defined in the QClipbrd unit, which should be included in your Delphi application, or the corresponding QClipbrd.hpp header file must be included in the C++ application, which is automatically included when the clx.h header file is included by the IDE-created projects.

The Assign method of TClipboard class is used to copy a persistent object to the clipboard. The method takes a single argument of TPersistent type. Typically, it is used to copy a picture from the corresponding bitmap file. While the SetComponent method is used to copy a CLX component to the clipboard, the GetComponent method is used to retrieve the component from the clipboard. The SetComponent method takes one argument, the component to be copied. The GetComponent method takes two arguments: the first one is the owner component that owns the new copy of the component, and the second argument is the parent control that contains the component. As discussed earlier, the owner is the component that is responsible for destroying the component, and the parent is the control that contains the component within itself, such as a form, a panel, a group box, and so on. However, before a component is copied to the clipboard, it is necessary to register the class of the component with the streaming system so that it can be streamed. This is done by executing the RegisterClass method (to register one class) or the RegisterClasses method (to register more than one class through an array of classes).

Implementing Drag and Drop

Drag and drop is a very popular feature that most GUI applications implement on different operating systems. The specialty of this feature is that it gives the user the feeling that he is actually dragging an object over to another object and dropping it there. But in the view of the programmer, it is nothing more than the result of setting a few properties and implementing a few events on the object being dragged and on the object that is accepting the dragged object. Here is how it happens. The control that is dragged using the mouse pointer is known as the drag source, while the control on which the drag source object is dropped is known as drop target or drag target. For the drag source object to participate in the drag-and-drop operation automatically, its DragMode property must be set to dmAutomatic. If this property is set to dmManual (which is default), then the drag-and-drop operation does not start until the BeginDrag method is executed on this control in the OnMouseDown event handler. Thus, by default, the drag-and-drop operation is disabled on a control. The BeginDrag method takes two arguments. The first argument is a Boolean value indicating whether the drag operation should start immediately when the mouse is dragged while holding the left mouse button; the second argument is an integer value indicating that the drag operation should be delayed until the mouse is moved by a specified number of pixels after holding the left mouse button. The OnMouseDown event handler receives five arguments as discussed earlier; however, in the present context, the Sender argument is the drag source object, which should be cast to the appropriate object type before using its properties and methods. Except while implementing custom drag and drop (explained later in the section), it is always recommended to use the automatic drag-and-drop operation.

There are a few events that should be considered on the drop target object to make the drag-and-drop operation complete. Because the purpose of this operation is to transfer data from the source control to the target control, the event handlers on the target object will allow the target control to accept (or reject) the drag object and, if the drag object is accepted, then to extract the data from it. The OnDragOver event is fired on every control on the form once the drag-and-drop operation begins and the left mouse button is held. However, only the control for which this event handler is implemented will have the opportunity to accept the drag object. This event handler accepts several parameters. The Sender parameter is the object for which the event is being implemented; this is the object that might act as the drop object. The Source parameter is the drag object. The next two parameters are the mouse position coordinates. The fifth parameter indicates the drag state of the mouse with respect to the control, such as whether the mouse is just entering the control, is leaving the control, or is moving over the control. The last parameter to this event handler, named Accept, is Boolean with default value true and indicates whether the drop target is accepting the drag source object or not. Thus, even if the event handler is implemented, we might reject the drag source object by setting this last parameter Accept to false. The OnDragDrop event is fired on the drop target object if it is set to accept the drag source, and the user releases the left mouse button (which was held since the drag operation started) while the mouse pointer is on the drop target object. This event handler receives four parameters: the first parameter Sender is the drop target object, the second parameter Source is the drag source object, and the next two parameters are the position coordinates of the mouse pointer. Listing 6.7 displays sample C++ code demonstrating simple drag and drop. This listing is available on the accompanying CD-ROM.

Listing 6.7: Simple Drag and Drop

//--------------------------------------------------------------------- #include <clx.h> #pragma hdrstop #include "SimpleDrangAndDrop.h" //--------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.xfm" TSimpleDragAndDropForm *SimpleDragAndDropForm;

//---------------------------------------------------------------------

__fastcall TSimpleDragAndDropForm::TSimpleDragAndDropForm(TComponent* Owner) : TForm(Owner) { } //--------------------------------------------------------------------- void __fastcall TSimpleDragAndDropForm::ListBox1DragOver(TObject *Sender, TObject *Source, int X, int Y, TDragState State, bool &Accept) { Accept = true; } //--------------------------------------------------------------------- void __fastcall TSimpleDragAndDropForm::ListBox1DragDrop(TObject *Sender, TObject *Source, int X, int Y) { ListBox1->Clear(); for (int i=0; i < ComboBox1->Items->Count; i++) { AnsiString item = ComboBox1->Items->Strings[i]; ListBox1->Items->Add(item); } } //--------------------------------------------------------------------- void __fastcall TSimpleDragAndDropForm::Button1Click(TObject *Sender) { ListBox1->Clear(); } //---------------------------------------------------------------------

In this example, the combo box is the drag source, and the list box is the drop target. When the mouse cursor is dragged from the combo box to the list box, the items in the combo box are copied to the list box as implemented in the OnDragDrop event handler of the list box. The drag operation starts automatically because the DragMode property of the drag source object is set to dmAutomatic. A button is used on the form, and its OnClick event handler is implemented just to clear the items in the list box to enable testing the program multiple times.

In a more complex scenario, the default behavior of the drag-and-drop operation may not be sufficient for a variety of reasons, including one that necessitates the passing of a different object (other than the drag source object) as the Source parameter to the drop target’s event handler, or one that necessitates implementing dragging from multiple source objects to a single drop target object within the same application, or one that necessitates implementing dragging objects from different forms implemented in different shared libraries or executables to a single drop target located in a different shared library or executable. This is achieved by using the TDragObject class or its descendant class and by implementing the OnStartDrag event handler on the drag source object. If the TDragObject class is directly used, it provides minimal customization, whereas to have complete control of customization it is necessary to derive a class from TDragObject and use it as the drag source object.

Creating Database Applications

Traditionally, the concept of database applications was limited to large databases hosted on the mainframes, serving their client applications through the architecture exclusively built to run on the mainframes. However, as a result of the rapid development and advancement of relational database systems, the concept has extended to smaller computers than the mainframe, and now a home-based personal computer is capable of hosting very large databases somewhat closer to a mid-size corporation’s database if not to the large Enterprise systems. Thus, database applications have become the majority of the software industry’s development work. Also, the software industry is blessed with a myriad of database systems on the market with a wide range of features in terms of largest database, maximum number of simultaneous connections, data replication, and so on. In addition to providing the database system, every database vendor also provides a way to connect to the database, which is usually termed as the client connectivity API (application programming interface). Because data in a database management system is under the control of the database system and is usually stored in a vendor-specific format, it is necessary for the client applications to know the connectivity API in order to be able to connect to the database and work with the database objects. These APIs are unique to each of the database systems and hence pose a challenge to the programmer community to learn programming with different APIs while working with different database systems. There have been standard APIs recommended by several organizations, such as the ODBC (Open Data Base Connectivity) standard for programming with traditional languages. To make use of the ODBC standard API, database vendors have to provide appropriate driver software to connect to their database systems. Although the ODBC API has been successfully implemented and used on the Windows platform, the UNIX developer community did not embrace it with ease, due to performance issues or unavailability of drivers for different databases, or other reasons. The JDBC (Java Data Base Connectivity) API has been introduced with the Java development platform; however, it is primarily used for connecting with the Java-based applications and is the subject of Chapter 7 Building Distributed Java Applications.

Being developed and supported by the UNIX developer community, the Linux operating system has undergone similar circumstances as the UNIX platform. However, Linux has been perceived by many software professionals and users as a simple desktop-based UNIX, as opposed to one capable of running a large Enterprise UNIX system on a personal computer. It is the same perception that led to the invention of very powerful windowing desktop packages such as KDE and GNOME. Although the Java development APIs take care of database connectivity for Linux, due to the platform independent architecture of Java, the needs of all non-Java–based applications should be addressed separately and precisely, if Linux is to be perceived by the industry as a desktop operating system. Fortunately, the introduction of the Kylix platform on the Linux operating system has made this task easy through its ‘dbExpress’ database connectivity framework. Because Kylix has been modeled after Windows-based Delphi, the dbExpress framework is also coupled with the powerful dataset-based data access architecture, and almost all the features available in Windows platform are available on the Linux platform. The database architecture provided by the Kylix platform enables the programmers to develop database-independent applications very easily. For example, an application developed in Oracle database using Kylix database architecture can easily be migrated to DB2 UDB (or any other database supported by Kylix) with minimal coding changes. The Kylix database architecture consists primarily of three sets of components; one set is the database connection components, the second set is the data access components, and the third set is the data-aware controls. The database connection components are used to establish the connection and work with the underlying objects such as tables, stored procedures, execute queries on the database, and so on. These include a connection component, table, query, stored procedure components, and so on. The data-aware controls are the visual components that present the retrieved data in a visually perceivable form and permit the user to interact with them to make updates to the data. These include a data grid, record navigator, edit control, combo box, and so on. Finally, the data access components play the intermediary role between the other two sets, as data conduits (or data pipes). One end of the pipe is the supplier, and the other end is the consumer of data; with the concept of data conduits, it is very easy to change either end of the conduit without affecting the other end. The Windows-based Delphi and C++Builder developer community is very familiar with this concept. For the benefit of Linux developers, the rest of this chapter is going to provide details of this architecture and about developing database applications using this architecture.

Datasets and Their Characteristics

A dataset is designed to contain a record structure and a set of records that conform to this structure. In a way, a dataset represents a database table or a set of records retrieved as a result of executing a query or stored procedure on the database. The base class that represents a dataset is the TDataSet class, which contains all pure virtual methods and hence cannot be instantiated. It is designed to provide the basic framework of a dataset, in order for the descendants to fit into the unique architecture. A descendant of the TDataSet class might be specific to the architecture that is being implemented, such as the dbExpress architecture; in this case, it is the TCustomSQLDataSet. Similarly, on the Windows version of Delphi, the descendant class TCustomADODataSet provides the base functionality to work with datasets using the Microsoft ADO framework. In this book, we discuss the dbExpress-based chain datasets. The next level of descendant of TCustomSQLDataSet would be a class to represent a table object (as in TSQLTable), a class to represent a query object (as in TSQLQuery), or a class to represent a stored procedure object (as in TSQLStoredProc). Although all these objects might ultimately contain a set of records, it is important to understand that the way the set of records is filled in this object is different in each of the cases. A dataset that represents a table will fill the records when the name of the table is set—after establishing the connection—and therefore would need a property to hold the name of the table. A dataset that represents a query will fill the records when the query is executed on the database and therefore needs properties (and methods) to access the query string, pass arguments to the query, and so on. Similarly, a dataset that represents a stored procedure will fill the records when the stored procedure is executed and is coded to retrieve a set of records from the database, and therefore needs properties (and methods) to access the stored procedure name, pass arguments to the stored procedure, and so on. Thus, the first level of abstraction (the TDataSet class) introduces the basic features necessary for a dataset object, and the next level of abstraction (dependent on the data access architecture) introduces the features necessary for the architecture (in this case the dbExpress architecture). The third level of abstraction (or sub-classing a dataset to a table, query, or stored procedure) introduces the special features required by the specific descendant class. Although the discussion in this chapter is limited to certain specific classes, the developers are encouraged to create their own descendant classes of the dataset to meet their exclusive programming needs.

The properties and methods of a dataset (or its descendant) object enable the developers to work through the record structure, navigate the record set, and update individual records if the specific user is permitted by the database. The dataset level events provide an opportunity to the developer to write code (and hence control the behavior of this object) to perform data validation at several stages—forced cleanup or refresh of the dataset and so on—and therefore represent significant tasks performed on the dataset throughout the lifetime of the object, such as an event to be fired when the dataset is opened or an event to be fired before inserting a new record into the dataset. Although a number of events are provided, giving us flexibility to handle the dataset’s behavior throughout its lifetime, we only implement the event handlers for the required ones in our application. Another significant feature of this component is that it stores a record buffer locally, which could be used in a descendant class to enable us to disconnect from the database after getting the data and work in an offline mode, if desired. However, such advanced features are available only if provided by the descendent classes.

The table-type datasets (identified by the TSQLTable class and its descendants) are static snapshots of data read from the database objects and filled into the local data structure at a specific point of time. The properties and methods of this type of datasets enable us to connect to a specific database (or connection) component, set the name of the table that we are interested in reading (and/or updating online), and refresh the data as frequently as required in the application. When a table is opened in online mode, the updates performed on the table are immediately applied to the database if transaction support is disabled, or the updates may be grouped into transactions if the transaction support is enabled. While a table is opened in offline mode, the updates do not happen immediately; rather, they are applied at a later point of time. The query-type datasets (identified by the TSQLQuery class and its descendants) are mainly two types—queries that retrieve a set of records meeting a certain selection criteria and queries that perform data manipulation statements such as updating, deleting records in the database, and inserting new records. These types of datasets provide more flexibility to the programmer in terms of data access or updates because the queries can be executed on multiple tables (termed as table joins) at a time. Data retrieved from a database using query objects is usually offline, and the updates have to be programmed separately. However, it is also possible to process a query that retrieves records for update, if this feature is supported by the underlying database. The queries that perform data manipulation statements do not retrieve a record set; rather, they provide a result code of execution and any associated message thrown by the underlying database. While a result code will help in determining whether the query is successfully executed or not, in case of error conditions, the message and result code together will help in identifying the appropriate error or obtaining support from the database vendor. These types of datasets implement properties and methods to connect to a specific database (or connection) component, hold the string that represents a SQL statement, hold the input arguments to the statement, and execute the statement on the database. Because the dataset is derived from the TDataSet class, the records returned from a select query can be navigated in a way very similar to the table-type datasets. The stored procedure-type datasets (identified by the TSQLStoredProc class and its descendants) are used to represent individual code blocks to perform more complicated tasks than a simple query or update; these may include multiple queries, control statements, etc., and are usually implemented in a procedural language specific to the database. The stored procedure objects permit the developers to pass input arguments and to specify output arguments that will accept multiple return values after execution. The stored procedure object also accepts a return record set very similar to a query. All of the tasks that follow are enabled through the implemented properties and methods: connecting to a specific database (or connection) component, containing input arguments to the stored procedure, containing placeholders to accept multiple return values, and executing the stored procedure on the database. Because the dataset is derived from the ‘TDataSet’ class, the records returned from a stored procedure also can be navigated in a way very similar to the table-type datasets.

The Dataset States

The dataset objects keep changing their state throughout the process of adding, updating, or deleting records. The state of the dataset is represented by the State property, and it is helpful for developers to know the current state of the dataset in order to avoid related exception conditions. Some frequently occurring dataset states are dsInactive, dsBrowse, dsEdit, dsInsert, and dsOpening, in addition to other states that are not mentioned here because they are specific to certain circumstances or temporary states used by the datasets internally. The dsInactive state of the dataset means that the dataset is closed and therefore does not permit the performance of any task related to the data or the record structure. The only permitted tasks are those that bring the dataset to one of the active states, such as opening the dataset. This is done by setting the Active property of the dataset to true or by executing the Open method. When the dataset is in the process of opening and is not completely open, the dataset attains the state of dsOpening; at this time, the dataset is still not ready for access. During the process of opening, the dataset will attempt to fill the record set if the dataset represents a table or is a query or stored procedure that retrieves one or more records. After the dataset is open and there is one or more record in the dataset, the record pointer is placed to point to the first record. The active record in a dataset is always the record to which the record pointer is currently pointing. The record pointer is accessible only when the dataset is open and contains a record set of one or more records. The dsBrowse state of the dataset means that the dataset is open and available for browsing, which is the default state when the dataset is first open, or after a successful execution of the Post or Cancel method subsequent to a change in a record’s values. In this state, the records in the dataset cannot be changed. When the dataset is pointing to a valid record and the Edit method is executed, then the dataset attains the dsEdit state. This indicates that the record located at the current record pointer (also known as the active record) is open for edit and therefore permits changes to that record. Any changes made to the current record will be applied to the dataset only after executing a successful Post method and will be cancelled after executing a successful Cancel method. It is important to note that the active record is only a record buffer holding a copy of the current record whose values will be applied to the corresponding record of the dataset after executing the Post method. If the Post method fails execution, then the changes in the current record buffer will not be applied to the corresponding record. When the Cancel method is executed, the record buffer is cleared without making the update to the dataset and bringing the dataset to the dsBrowse state. When a new record is created in the dataset by executing the Append or the Insert method, a new record buffer is created, and the dataset state is changed to dsInsert. In this state, one is permitted to set the values of one or more fields in the dataset. If the dataset has associated indexes, the key columns representing the indexes should be set in an empty record as a minimum requirement for the record to be posted successfully through the Post method. There may also be other fields that need to be filled based on the description of the associated database object in the underlying database. While making changes to the active record when the dataset is in dsEdit state, normally the fields representing the primary keys (or secondary keys) may not be changed because they uniquely identify the record. The dsInsert and dsEdit states of the dataset are only applicable to the active record. When the dataset is in one of these two states, it is not possible to successfully execute methods that try to move the record pointer. We have to Post or Cancel the changes before we try to navigate the dataset records.

A dataset is said to be connected to a database in synchronous mode if the connection is active while working with the dataset. In this mode, setting values of the properties or executing methods on the dataset will make these changes directly to the underlying database objects, if the database is not in transaction mode. However, if the database is in transaction mode, the changes are applied only when the transaction is committed, even though they are posted to the dataset. Here it is very important to know that the CLX dataset and the associated database objects (such as tables or stored procedures) are two entities, and the CLX dataset maintains a copy of the data retrieved from the associated database objects. However, in the case of synchronous mode connection, the changes posted to the dataset are sent immediately to the database server. It is the server that decides whether the changes are to be kept in transaction for a later commit to the tables or are to be committed immediately, based on whether the database is configured to be (and is currently running) in the transaction mode or the database currently does not maintain transactions, respectively. On the other hand, if the connection between the dataset and the database is terminated after retrieving the data, or if the dataset is populated from the database through a middle tier, then the dataset is considered to be in asynchronous mode with respect to the database. In this case, because the dataset is not directly connected to the database, any changes applied to the dataset will not directly update the database; rather, a middle tier will apply the changes later asynchronously.

Additional Properties and Methods of TDataSet

Many of the features of the TDataSet component have been discussed in the previous subsections. However, some additional properties, methods, and events of this component are presented in this subsection to help the readers better understand this functionality. The Active Boolean property indicates whether the dataset is open or not, and setting this property to true opens the dataset, which is also equivalent to executing the Open method. If the dataset is a table, this action retrieves the records from the database table. The series of actions that take place when a dataset is opened includes generation of a BeforeOpen event, setting the dataset state to dsBrowse, establishing a way to fetch the data typically by opening a cursor, and generating an AfterOpen event.

Seting this property to false closes the dataset, which is also equivalent to executing the Close method. The series actions that take place when a dataset is closed include generation of a BeforeClose event, setting the dataset state to ‘dsInactive’, closing the cursor, and generating an AfterClose event. The Bof Boolean property indicates that the current record pointer is pointing to the first record, if the value is true, and the Eof Boolean property indicates that the current record pointer is pointing to the last record, if the value is true. The Fields property is an array of TField objects and identifies each of the individual fields of the dataset record structure through the index value of the field in the array; a field in a dataset is analogous to a column in the database table. On the other hand, the FieldValues property is also an array of variant values, and each field in the array is accessed through the field name. The values of individual fields can be set or retrieved through this property. The FieldDefList property represents a flattened view of the field definitions of the dataset, while the FieldDefs property represents a hierarchical view of the field definitions; both these properties should only be used to access the field definitions and not for accessing the field values. The Filter property is a string containing a selection criterion and is used as a filter to retrieve records from the table that match the criteria, and the FilterOptions property is used to indicate how the Filter property should be interpreted while applying the filter string. The IsUniDirectional Boolean property indicates whether the dataset reprsents a unidirectional dataset, which is always true for TCustomSQLDataSet and its descendants, as the dbExpress framework is built on the concept of unidirectional datasets. The RecNo property returns the active record number, the RecordCount property returns the number of records in the currently open dataset, and the RecordSize property returns the record size in bytes. These three properties should be implemented by the descendant dataset because the base TDataSet class does not provide any functionality for them.

The Append, Insert, and Edit methods should be used to create a new record at the end of the dataset, insert a record after the current record, and open the current record in edit mode, respectively. For these methods to be executed successfully, the dataset must be in dsBrowse state. If the dataset is in dsEdit or dsInsert state, then it should be brought to the dsBrowse state by executing the Post or Cancel method, as required by the application, before attempting to add a new record or edit another record for update. The ClearFields method clears the data of all the fields in the current record, if the record is in dsEdit or dsInsert state. The Delete method deletes the current record. The FieldByName and FindField properties both are used for retrieving the TField object of a field whose name is passed as an argument to either of these methods. While the FieldByName method raises an exception if the requested field does not exist, the FindField method returns null (nil in Delphi) for the same condition. The IsEmpty method is used to determine if the dataset is empty, which can also be determined by examining the value of RecordCount property. The four navigational methods First, Last, Next, and Prior are used to move the record pointer to the first record, to the last record, to the next record from the current position, and to the previous record from the current position, respectively. The navigational methods should only be executed when the database is in dsBrowse state.

The TDataSet component provides a set of events to facilitate data validation. These events are triggered at the appropriate times when the dataset passes through specific states or conditions. When the dataset is being opened, the ‘BeforeOpen’ event is triggered before the dataset is opened, and the AfterOpen event is triggered after the dataset is successfully opened. The BeforeInsert and AfterInsert events are triggered, respectively, before and after adding a new record to the dataset as a result of executing the Append or Insert method. The BeforeEdit and AfterEdit events are triggered respectively before and after editing the record as a result of executing the Edit method. The BeforeDelete and AfterDelete events are triggered respectively, before and after deleting a record as a result of executing the Delete method. The BeforePost and AfterPost events are triggered, respectively, before and after successfully posting updates to the current record, and the BeforeCancel and AfterCancel events are triggered, respectively, before and after successfully cancelling the changes made to the current record. Finally, the BeforeClose and AfterClose events are triggered on the dataset, respectively, before and after successfully closing the dataset.

The dbExpress Framework

The dbExpress framework provides a high-speed and small-footprint database access technology, using a concept known as unidirectional cursors. Therefore, the dataset components in this family are unidirectional and do not provide caching (or buffering) of records. For this reason, these datasets can only be navigated forward, which means the only navigational methods permitted on them are Next and Last. Any attempt to navigate backward, such as using the First and Prior methods, will raise an exception. In addition, attempts to position the record pointer at a particular record location are also not permitted, thus disabling the use of bookmarks. Because record buffering is not supported, filters and lookup fields are also not supported. Accessing data in these datasets is very fast due their unidirectional nature; therefore, the applications built on this framework demand less resources even for larger volumes of data access. The dbExpress components cannot be directly connected to data-aware controls (unlike other frameworks such as ODBC and ADO-based (ActiveX Data Objects) datasets, which are familiar to Windows-based Delphi and C++Builder programmers). Undoubtedly, the dbExpress components are faster; however, what about those applications that need direct connectivity to the data-aware controls, or caching of records for sorting, filtering, using bookmarks, and so on? The Kylix programmers are not deprived of these features. However, the features are provided through the use of another component known as the client dataset or a pair of components known as the client dataset and provider duo, as explained in detail in the following subsections.

The components supplied with the dbExpress framework include a TSQLConnection component used to establish a connection with the database, a TSQLDataSet component that provides a generic dataset component capable of being substituted in place of other dataset descendants, a TSQLTable component that represents a table in the connected database, a TSQLQuery component to execute a query on the connected database, and a TSQLStoredProc component that represents a stored procedure in the connected database, among others. These are the dbExpress components most commonly used when developing database applications.

What Is a Client Dataset?

The TClientDataSet component plays a very important role in the CLX database framework and implements a database-independent dataset. This dataset can be used in different ways in different circumstances—to obtain data from a provider component of a server module or to obtain data from independent data files, or it can work as an in-memory table. When used along with a provider component, the client dataset functions like a data consumer located in the client application, while the provider plays the role of server module. When used with independent data files or as an in-memory table, it functions like a standalone dataset, and this feature enables developers to create briefcase model applications where they can retrieve the data from a provider when the application is online with the server, save the data to one or more data files, and detach the client application from the server module. If the application is developed to work in offline mode, then the client module can be restarted later in offline mode when the user is not connected to the server. In the offline mode, the user can work with a local copy of the dataset that was retrieved earlier when the client was connected to the database, save it to local disk, and when connected to the server again, the dataset(s) can be synchronized with the server to resolve updates made by the client applications. However, it is the responsibility of the developer to make the application work in the offline mode and design the server module to be capable of handling simultaneous offline mode access by several users to the same dataset. This means that if more than one user makes offline copies of the same dataset at the same time or during overlapping time intervals, the designer should adopt an appropriate strategy for resolving updates to the same data records by different users. Another use of TClientDataSet component is to develop standalone applications that just use data files alone or in-memory tables. In-memory tables are very useful data structures for storing tabular data with navigation capabilities similar to database tables. Finally, there is another use of the TClientDataSet component in providing for caching (or buffering) of records from a unidirectional dataset in order to develop a traditional two-tier application model. Because the dbExpress datasets are unidirectional in nature, this feature is very useful when developing two-tier applications with dbExpress components.

The Dataset Provider Component

A dataset provider component plays the intermediary role between a client dataset residing in the module and the dataset object connected to the database server. The functionality of a dataset provider is incorporated in the TDataSetProvider component, which is used in developing multitier applications, and applications with unidirectional datasets. In a client-server model, the dataset provider is the server-side equivalent to the client dataset on the client-side. It provides data from the server side dataset to the client dataset and resolves updates to the dataset or database-server. In other words, it serves as a data broker between a remote database server and a client dataset. The dataset provider interacts with the datasets through the IProviderSupport interface, which is a Delphi interface and perceived as a pure virtual class by a C++ program; it is implemented by the dataset that provides data to the provider. In other words, any dataset that needs to provide data to a client dataset through a provider component must implement this interface. In a three-tier application, the client module contains a client dataset component (apart from a data source component to connect the client dataset to data-aware controls), and the server module contains a dataset provider for every dataset that needs to communicate with the client module. The server module constitutes the middle-tier in the three-tier model and the database server plays the third-tier. The application that acts as the middle-tier should implement the IAppServer interface, which is a Delphi interface and perceived as a pure virtual class by a C++ application; it also should choose an appropriate protocol to communicate with the client application. In the current release of Kylix, it is possible to implement the middle-tier server module using the SOAP (Simple Object Access Protocol) architecture, and the client can be implemented as a SOAP client application. This scenario will be demonstrated using an example that is discussed in a later chapter dedicated exclusively to Web services development. The developers may choose to build their own application server module implementing the IAppServer interface with their choice of protocol. In the current chapter, simple two-tier application development will be presented using the client dataset and provider combination of components. Figure 6.22 shows the typical scenario of a three-tier client-server application.

Figure 6.22: Three-tier client-server architecture in Kylix 3.

From the figure, it can be noticed that by connecting the dataset object to a provider component, we are providing a controlled interface of the dataset to the client application (or its client dataset object). In the case of a two-tier application, tier I and tier II will both reside in the same module. In this case, an application server object is internally created and maintained.

Characteristics of TClientDataSet

Because the TClientDataSet component is derived from the TDataSet class, it supports all the functionality provided by the TDataSet class. This subsection presents some more features introduced in this class, which were not discussed earlier. The AppServer property returns the IAppServer interface pointer as implemented in the application server, whether it is explicitly created or implicitly created (as in the case of a two-tier application). It is usually not necessary for the application to call methods on this interface because they are automatically invoked while setting properties on the client dataset object or while executing methods on the client dataset object. The client dataset maintains a local cache of the records received from the provider. When the user makes changes to data records, the changes are actually performed in this local cache maintained by the client dataset. The ChangeCount property represents how many such data changes are pending to be synchronized with the database since last time they were synchronized (or cancelled). In this sense, every field value that is different from the corresponding value of that field since last synchronization (or cancellation) counts toward a change to the local copy of the data. The ChangeCount property is reset to zero when any of the following actions take place—when the client dataset is synchronized with the database by executing the ApplyUpdates method, cancels all pending changes by executing the CancelUpdates method, or makes changes permanent by merging the delta packet with the data packet with a call to MergeChangeLog method (but still not synchronized with the database). The CommandText property is a SQL Query string, or a table name or stored procedure name that is sent to the provider to be run on the target dataset that the provider represents if the provider permits the client dataset to send dynamic queries. In this case, the command text is executed when the Execute method is called or the dataset is opened, whichever is the appropriate case. One of the provider options, poAllowCommandText, indicates whether the provider permits the client dataset to send dynamic query strings. If this value is not included in the provider options, it means the provider does not accept client-specific queries, and hence the CommandText property has no impact on the execution of the query on the target dataset. The Data property represents the data held in the client dataset in a transportable format; the source of the data must be a provider component or a file on the disk or the data entered manually by the user in an in-memory dataset. The Delta property represents the change log of the client dataset and contains all changes made to the data by the user. When the client dataset is synchronized with the provider, the contents of Delta are sent to the provider. The Delta value is cleared when the synchronization is successful. If the synchronization is only partially successful or fails completely, then the Delta property contains all the unsuccessful updates. The Boolean property LogChanges indicates whether the client dataset must maintain a separate change log to contain the changes made by the user, whose default value is true for client datasets that work with remote providers. However, if this property is false, the changes made by the user are immediately applied to the Data property and the resulting Data packet is not usable for synchronizing changes with the database. Therefore setting this property to false is only useful in two-tiered file-based or in-memory tables, to conserve resources. The ProviderName property identifies the name of the provider object with which the client dataset communicates.

A client dataset object fetches the records in terms of batches, known as packets, from the associated provider. When the user is scrolling the data in a visual data grid and reaches the end of the current data packet (or when the application attempts to access records beyond the current data packet), the client dataset fetches the next set of records in the form of another data packet. This process is automatic and happens without the knowledge of the user. This condition is indicated by the Boolean property FetchOnDemand, which is true by default. However, if the application needs to have control on fetching the next set of records, then this property should be set to ‘false’ and the application should execute the GetNextPacket method as and when required. If this property is false and the provider does not include BLOB (Binary Large Object) fields by default, then the BLOB fields must be fetched by explicitly calling the FetchBlobs method, or similarly if the provider does not include nested detail datasets by default when this property is false, then the nested detail datasets must be fetched by explicitly calling FetchDetails method. The provider includes BLOB fields or nested detail datasets when the respective provider options poFetchBlobsOnDemand and poFetchDetailsOnDemand are not included.

As mentioned earlier, the client dataset is used to make the application participate in the ‘briefcase’ style. This is done by saving the data to a local file when the application is connected to the server module and then later loading the data from the file when the application is started in ‘offline’ mode. The FileName property identifies the name of a file that stores the data of the client dataset. If the dataset always reads from and writes to a specific file, then this property is useful for setting the file name. If a valid file name is set, then every time the client dataset is closed, the data is written to the same file, and every time the client dataset is opened, the data is read from the same file. If the same file is not always used, then the SaveToFile and LoadFromFile methods are used to save data to and read data from a file, respectively; these two methods accept the file name as an argument.

The Params property represents a collection of TParam objects, each of which represents an input (or output) parameter as required by the corresponding query or stored procedure whose results are coordinated with the provider component. The Params can be set manually or automatically. To set the Params automatically, execute the FetchParams method, which sets the parameters as required by the target dataset object. If we choose to set the Params manually during design time or runtime, it is necessary to set each of the Param objects exactly as expected by the target dataset object. Any mismatch in the data type of the Param object would result in a runtime exception being raised. If the target dataset object is a table (instead of a query or stored procedure), then the Param objects can be used to represent individual fields and limit records of selection by setting specific values to these Param objects.

The Execute method of the client dataset triggers execution of a SQL command on the provider. If the provider’s Options property includes poAllowCommandText, and the CommandText property contains a valid SQL command along with the Params property to supply necessary parameters to the command identified by the CommandText property, then the SQL command is executed by the provider. Otherwise, by default, the SQL command or stored procedure associated with the underlying dataset object is executed. This method is designed to run SQL commands that do not return a result set. When the method is executed, first a BeforeExecute event is triggered on the client dataset, which gives the developer an opportunity to build custom data objects to be passed to the provider before execution of the command begins. Then the provider is passed the execution request, which triggers a BeforeExecute event on the provider. In this event handler, the provider has the opportunity to retrieve the custom data objects sent by the client dataset and take any necessary action before processing the request. Then the provider executes the command (either the command associated with the target dataset or the command set by the client dataset). Next, the AfterExecute event is triggered on the provider where it can package custom data objects to be sent back to the client dataset. Finally, the AfterExecute event on the client dataset is triggered where it receives the custom data objects packaged by the provider. When the provider executes the command, any output parameters are set in the Params property of the client dataset. Here, the purpose of using custom data objects to be transported between the client dataset and the provider is truly custom in nature and not required to perform the default tasks that automatically happen during the whole process. For example, the Params property is used for the input and output parameters needed for executing the query or stored procedure or a custom command, and it is not necessary to send the parameters through custom data objects.

Characteristics of TDataSetProvider

The previous subsection presented many of the characteristics of the client dataset component. It is now time to discuss some of the important characteristics of the dataset provider component to draw a complete picture of the client-server communication process. The DataSet property identifies the dataset object (such as the table, query, or stored procedure) whose data is provided to the client dataset in a controlled way. For this reason, the dataset object should have implemented the IProviderSupport interface methods (without using the default implementation done by the ancestor TDataSet class). The Options property enables the developers to specify some configuration options to the provider object in terms of how it communicates with the dataset or to what extent it exposes itself to the client dataset. Some of these options were already discussed earlier; some others include values such as poCascadeUpdates, which indicates that the provider should update the detail records automatically when the key values of a record in the master table are updated and if the provider represents the master table in a master-child relationship and the underlying database supports cascaded updates as part of referential integrity constraints, while the poCascadeDeletes option is used in a similar sense to delete the child records associated with the master record. If the poDisableEdits option is included, the client application will not be permitted to make edits to the associated dataset object. The dataset provider object generates events when specific actions take place on the associated target dataset, such as the AfterUpdateRecord event, which is triggered after a record is updated in the target dataset and is used to implement any specific processing after a record is updated successfully. The events on the provider object are similar to the database trigger objects that occur when specific actions take place on the associated database object. These events provide a unique opportunity to implement an alternate way to use database triggers (without regard to the type of database server), or when the associated database does not support triggers.

What Is a Data Module?

A data module is a nonvisual component that serves as a container to host other nonvisual components. In this sense it is similar to a form, which serves as a container to host visual components. As mentioned earlier, nonvisual components dropped onto a form may cause inconvenience during design time while working with the visual components, even though they do not have any impact during runtime. The data access and data connectivity components are all nonvisual in nature and are frequently used in database applications. Therefore, it is strongly suggested that an instance of the TDataModule in an application be used to host all the nonvisual components, which also facilitates centralized access to single instances of nonvisual components. Data modules can be used in the client applications as well as server applications. Just as all the forms used in a GUI application are automatically instantiated in the application main function, the data module is also instantiated in the same way. If the statements representing the data module instantiation are removed from this function manually, then it is the responsibility of the developer to instantiate the data module separately. After instantiation of the data module, it can be accessed through a global or local variable (based on how it is instantiated), and its contents are accessed using the . notation in Delphi applications or ® notation in C++ applications. This way, all the forms and modules in the application can access the same data module.

Establishing a Database Connection

The dbExpress framework includes a set of drivers, one for each of the supported databases, which currently includes Borland InterBase™, Oracle, IBM DB2 Universal Database™, IBM Informix, PostgreSQL/RedHat Database™, and MySQL™ with the Enterprise Edition of the product. However, the set of drivers included with a different edition may be different, and the readers are recommended to check with Borland Software Corporation for more details. In addition, some drivers may also be available from organizations committed to providing open source software to the Linux platform. The Kylix driver configuration files are installed in the .borland subdirectory within the home directory of the user who installed Kylix; these are ‘dbxdrivers’ and ‘dbxconnections’ files. The first file contains definitions of all the installed drivers for the dbExpress framework, while the second file contains all the connection definitions as configured on the system. The difference between the two files is explained here. A driver is a shared library file (containing the .so file extension) built to the driver specification laid out by dbExpress framework and connects to the vendor-supplied database client software in conjunction with the vendor-specific driver parameters. For this reason, for every database server supported by the Kylix dbExpress framework, there is a driver definition entry in the ‘dbxdrivers’ file. On the other hand, a connection definition specifies how to connect to a database instance or a database schema using the driver software. Because usually a database server supports more than one database instance or schema, we can have more than one connection definition in the ‘dbxconnections’ file relevant to the same database server. For example, if we are connecting to an Oracle database server, a connection to each of the services (defined through Oracle client software) is considered as a different connection in dbExpress terminology. Similarly, the Borland Interbase databases are identified by the files having .gdb extensions, and we can define different connections to different Interbase databases. However, they all establish connections through the same driver software. For these reasons, it is rarely necessary to change the driver configuration file, but we have to update the connection configuration file every time we add or update a connection. Usually, the connection configuration file can be updated through a wizard in the IDE, and therefore it is not necessary to manually update the file. The driver configuration file may need to be changed if a driver’s specific parameters change or when installing a new dbExpress driver. The steps involved in setting up a dbExpress connection are outlined here.

Two-Tier Database Application

In a typical two-tier application, all the three components—TClientDataSet, TDataSetProvider, and the dataset connected to the database—are contained in the same single client module. The dbExpress framework also contains a component named TSQLClientDataSet, which provides the combined functionality of all the three components specified before. However, if the TSQLClientDataSet is used in place of the three components, we have less control in customizing some of the functionality that we would have had while using the components individually, but it may be really handy and simple to use a single component, as is usually the case in small applications. The TSQLClientDataSet component is derived from the TCustomClientDataSet and not from the TSQLDataSet tree. This is why it is able to incorporate the record-buffering feature of a client dataset. At this time, we will create an example to demonstrate the use of dbExpress components, as outlined in the steps here.

Compile the program and run to execute. The displayed window looks similar to Figure 6.24. The SQL statement to create the sample table is provided in Listing 6.8, while Listings 6.9 and 6.10 provide the ‘.cpp’ and ‘.h’ files for the main form. Because the data module has no associated code other than just a constructor, it is not presented here but can be viewed on the screen. The limitation of this simple demo application is that it is not programmed to handle updates to the table.

Figure 6.24: Main form for SimpleDbxDemo application (Runtime).

This example demonstrates several features of dbExpress framework as explained here.

  • Use of client dataset and provider with associated dataset in the same application.

  • Use of SQLClientDataSet in place of three components as discussed earlier.

  • Use of data module to contain nonvisual components.

  • Dynamically changing the source of data input to a data source component.

Listing 6.8: create_addressBook.sql

CREATE TABLE MY_ADDRESS_BOOK (first_name VARCHAR2(30) NOT NULL, last_name VARCHAR2(30 NOT NULL, street_address VARCHAR2(40), city_name VARCHAR2(40), state_code VARCHAR2(2), country VARCHAR2(20), home_phone VARCHAR2(15), work_phone VARCHAR2(15) );

Listings 6.9 and 6.10 are available on the accompanying CD-ROM.

Listing 6.9: SimpleDbxDemo.cpp

//--------------------------------------------------------------------------- #include <clx.h> #pragma hdrstop #include "SimpleDbxDemo.h" #include "SimpleDbxDataModule.h" //--------------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.xfm" TForm1 *Form1; //--------------------------------------------------------------------------- __fastcall Tform1::Tform1(Tcomponent* Owner) : Tform(Owner) { } //--------------------------------------------------------------------------- void __fastcall Tform1::FormShow(Tobject *Sender) { DataModule1->SQLConnection1->LoginPrompt = false; DataModule1->SQLConnection1->Connected = true; DBNavigator1->DataSource = DataModule1->DataSource1; DBGrid1->DataSource = DataModule1->DataSource1; } //--------------------------------------------------------------------------- void __fastcall Tform1::Button1Click(Tobject *Sender) { if (DataModule1->ClientDataSet1->Active) DataModule1->ClientDataSet1->Active = false; DataModule1->DataSetProvider1->DataSet = DataModule1->SQLTable1; DataModule1->DataSource1->DataSet = DataModule1->ClientDataSet1; DataModule1->DataSource1->DataSet->Active = true; } //--------------------------------------------------------------------------- void __fastcall Tform1::Button2Click(Tobject *Sender) { if (DataModule1->ClientDataSet1->Active) DataModule1->ClientDataSet1->Active = false; DataModule1->DataSetProvider1->DataSet = DataModule1->SQLQuery1; DataModule1->DataSource1->DataSet = DataModule1->ClientDataSet1; DataModule1->DataSource1->DataSet->Active = true; } //--------------------------------------------------------------------------- void __fastcall Tform1::Button3Click(Tobject *Sender) { if (DataModule1->ClientDataSet1->Active) DataModule1->ClientDataSet1->Active = false; DataModule1->DataSetProvider1->DataSet = DataModule1->SQLDataSet1; DataModule1->SQLDataSet1->CommandText = "MY_ADDRESS_BOOK"; DataModule1->SQLDataSet1->CommandType = ctTable; DataModule1->DataSource1->DataSet = DataModule1->ClientDataSet1; DataModule1->DataSource1->DataSet->Active = true; } //--------------------------------------------------------------------------- void __fastcall Tform1::Button4Click(Tobject *Sender) { if (DataModule1->ClientDataSet1->Active) DataModule1->ClientDataSet1->Active = false; AnsiString queryString = "SELECT * FROM MY_ADDRESS_BOOK "; queryString = queryString + " WHERE STATE_CODE = 'TX'"; DataModule1->SQLClientDataSet1->CommandText = queryString; DataModule1->SQLClientDataSet1->CommandType = ctQuery; DataModule1->DataSource1->DataSet = DataModule1->SQLClientDataSet1; DataModule1->DataSource1->DataSet->Active = true; } //--------------------------------------------------------------------------- void __fastcall Tform1::Button5Click(Tobject *Sender) { if (DataModule1->ClientDataSet1->Active) DataModule1->ClientDataSet1->Active = false; if (DataModule1->SQLClientDataSet1->Active) DataModule1->SQLClientDataSet1->Active = false; } //---------------------------------------------------------------------------

Listing 6.10: SimpleDbxDemo.h

//--------------------------------------------------------------------------- #ifndef SimpleDbxDemoH #define SimpleDbxDemoH //--------------------------------------------------------------------------- #include <Classes.hpp> #include <Qcontrols.hpp> #include <QStdCtrls.hpp> #include <Qforms.hpp> #include <DB.hpp> #include <DBXpress.hpp> #include <SqlExpr.hpp> #include <QDBCtrls.hpp> #include <QDBGrids.hpp> #include <QextCtrls.hpp> #include <Qgrids.hpp> //--------------------------------------------------------------------------- class Tform1 : public Tform { __published: // IDE-managed Components Tbutton *Button1; Tbutton *Button2; Tbutton *Button3; Tbutton *Button4; TDBGrid *DBGrid1; TDBNavigator *DBNavigator1; Tbutton *Button5; void __fastcall FormShow(Tobject *Sender); void __fastcall Button1Click(Tobject *Sender); void __fastcall Button2Click(Tobject *Sender); void __fastcall Button3Click(Tobject *Sender); void __fastcall Button4Click(Tobject *Sender); void __fastcall Button5Click(Tobject *Sender); private: // User declarations public: // User declarations __fastcall Tform1(Tcomponent* Owner); }; //--------------------------------------------------------------------------- extern PACKAGE Tform1 *Form1; //--------------------------------------------------------------------------- #endif

Using the TClientDataSet as an In-Memory Table

Another very interesting feature that is worth demonstrating is the ability of TClientDataSet component to function as an in-memory table. In this context, the client dataset is a complex data structure and is available for normal non-database oriented applications as an in-memory structure without the need to connect to any database. The steps to build the demo application are described here.

Listing 6.11: InMemoryTable.pas

unit InMemoryTable; interface uses SysUtils, Types, Classes, Variants, QTypes, QGraphics, QControls, QForms, QDialogs, QStdCtrls, DB, QGrids, QDBGrids, QExtCtrls, QDBCtrls, DBClient; type TForm1 = class(TForm) CreateDataset: TButton; DeleteDataset: TButton; SaveDataset: TButton; LoadDataset: TButton; DataSource1: TDataSource; DBNavigator1: TDBNavigator; DBGrid1: TDBGrid; OpenDialog1: TOpenDialog; SaveDialog1: TSaveDialog; CloseDataset: TButton; procedure AddAField(dataSet: TClientDataSet; fldName: String; fldType: TFieldType; size: Integer; reqd: Boolean); procedure CreateDatasetClick(Sender: TObject); procedure DeleteDatasetClick(Sender: TObject); procedure SaveDatasetClick(Sender: TObject); procedure LoadDatasetClick(Sender: TObject); procedure CloseDatasetClick(Sender: TObject); private { Private declarations } myDataSet: TClientDataSet; public { Public declarations } end; var Form1: TForm1; implementation {$R *.xfm} procedure TForm1.AddAField(dataSet: TClientDataSet; fldName: String; fldType: TFieldType; size: Integer; reqd: Boolean); var field: TField; begin {This method adds a new field to the dataset} field := nil; case fldType of ftString: field := TStringField.Create(dataSet); ftSmallint: field := TSmallintField.Create(dataSet); ftInteger: field := TIntegerField.Create(dataSet); ftDate: field := TDateField.Create(dataSet); ftDateTime: field := TDateTimeField.Create(dataSet); ftFloat: field := TFloatField.Create(dataSet); ftBlob: field := TBlobField.Create(dataSet); ftBoolean: field := TBooleanField.Create(dataSet); end; if field <> nil then begin field.Name := fldName; field.FieldName := fldName; field.DataSet := dataSet; field.Required := reqd; field.Size := size; end; end; procedure TForm1.CreateDatasetClick(Sender: TObject); begin {Method to create the new dataset} if myDataSet <> nil then myDataSet.Destroy; myDataSet := TClientDataSet.Create(Self); AddAField(myDataSet, 'First_Name', ftString, 25, True); AddAField(myDataSet, 'Last_Name', ftString, 25, True); AddAField(myDataSet, 'Street_Address', ftString, 40, False); AddAField(myDataSet, 'City_Name', ftString, 40, False); AddAField(myDataSet, 'State_Code', ftString, 2, False); AddAField(myDataSet, 'Country', ftString, 20, False); AddAField(myDataSet, 'Home_Phone', ftString, 15, False); AddAField(myDataSet, 'Work_Phone', ftString, 15, False); myDataSet.CreateDataSet; myDataSet.Open; DataSource1.DataSet := myDataSet; end; procedure TForm1.DeleteDatasetClick(Sender: TObject); begin if myDataSet <> nil then begin if myDataSet.Active = True then myDataSet.Close; myDataSet.Destroy; myDataSet := nil; end; end; procedure TForm1.SaveDatasetClick(Sender: TObject); var currDir: String; begin { Determine the current working directory from where the program is executed} currDir := ExtractFileDir(Application.ExeName); { If the dataset is nil or is not open, then exit the procedure} if myDataSet = nil then System.Exit; if myDataSet.Active = False then System.Exit; { Set the initial directory of the open dialog to current directory} SaveDialog1.InitialDir := currDir; { Save the dataset with the selected name} if SaveDialog1.Execute = True then myDataSet.SaveToFile(SaveDialog1.FileName); end; procedure TForm1.LoadDatasetClick(Sender: TObject); var currDir, fileName: String; begin { if the dataset is not created yet, create one} if myDataSet = nil then myDataSet := TClientDataSet.Create(Self); { if the dataset is already open, close it first} if myDataSet.Active = True then myDataSet.Close; { Determine the current working directory from where the program is executed} currDir := ExtractFileDir(Application.ExeName); { Set the initial directory of the open dialog to current directory} OpenDialog1.InitialDir := currDir; { Load the dataset from the selected file} if OpenDialog1.Execute = True then begin fileName := OpenDialog1.FileName; myDataSet.LoadFromFile(fileName); DataSource1.DataSet := myDataSet; end; end; procedure TForm1.CloseDatasetClick(Sender: TObject); begin if ((myDataSet <> nil) and (myDataSet.Active = True)) then myDataSet.Close; end; end.


 < Day Day Up > 

Категории