Data modules in Delphi even exist in versions specific for multitier applications remote data modules and server-side HTTP applications Web data modules.applica-Finally, remember that yo
Trang 1Both find methods use as parameters an array of constants Each array element sponds to one of the fields of the current index You can also pass only the value for the initialfield or fields of the index, so the following fields will not be considered.
corre-NOTE I won’t discuss these features in details, showing complete examples, because some of them
are limited to the BDE and makes sense only for local tables, not for SQL server–based tables Actually if you set a filter or a range over a Table connected with a SQL server, the BDE will try
to generate a proper select statement, avoiding fetching all the data and filtering it locally The problem is that this isn’t always possible and you lose most of your control, two good reasons
to use the Query component when working with SQL servers.
A Query with Parameters
When you need slightly different versions of the same SQL query, instead of modifying thetext of the Query (stored in the SQLproperty) each time, you can write a query with a parame-ter and simply change the value of the parameter For example, if you decide to have a userchoose the countries of a continent (using the Country table of the DBDEMOS database),you can write the following parametric query:
select *
from Country
where Continent = :Continent
In this SQL clause, :Continentis a parameter We can set its data type and startup value, usingthe editor of the Paramsproperty collection of the Query component When the Parameterscollection editor is open, as shown in Figure 13.1, you see a list of the parameters defined inthe SQL statement and set the data type and the initial value of these parameters
The form displayed by this program, called ParQuery and available on the companion CD,uses a list box to provide all the available values for the parameters Instead of preparing theitems of the list box at design time, we can extract the available continents from the same
F I G U R E 1 3 1 :
Editing the collection of
parameters of a Query
component
Trang 2database table as the program starts This is accomplished using a second query component,with this SQL statement:
select distinct Continent
procedure TQueryForm.ListBox1Click(Sender: TObject);
procedure TQueryForm.Query1BeforePost(DataSet: TDataSet);
var
StrNewCont: string;
begin
// add the continent, if not already in the list
StrNewCont := Query1.FieldByName (‘Continent’).AsString;
if ListBox1.Items.IndexOf (StrNewCont) < 0 then
ListBox1.Items.Add (StrNewCont);
Trang 3We can add a little extra code to this program to take advantage of a specific feature ofparameterized queries To react faster to a change in the parameters, these queries can be
optimized, or prepared Simply call the Preparemethod before the program first opens thequery (after setting the Activeproperty of the Query component to Falseat design time)and call Unprepareonce the query won’t be used anymore:
procedure TQueryForm.FormCreate(Sender: TObject);
it If you use the same query (even if a parametric one) over and over, the engine doesn’t need
to reprocess the query but already knows how to handle it
Master/Detail Structures
Often you need to relate tables, which have a one-to-many relationship This means that for
a single record of the master table, there are many detailed records in a secondary table Aclassic example is that of an invoice and the items of the invoice; another is a list of customers
F I G U R E 1 3 2 :
The ParQuery example at
run time
Trang 4and the orders each customer has made This is very common situation in database ming, and Delphi provides explicit support for it with the master/detail structure We’ll seethis structure for BDE Table and Query components, but the same technique applies toalmost all of the datasets available in Delphi.
program-NOTE The TDataSet class has a generic DataSource property for setting up a master data source,
but the Table component, for example, uses a different property (MasterSource) to express the same concept.
Master/Detail with Tables
The simplest ways to create a master/detail structure in Delphi is to use the Database FormWizard, selecting a master/detail form in the first page To accomplish the same effect manu-ally, place two table components in a form or data module, connect them with the same data-base, and connect each with a table In the MastDet example, I’ve used the customer and orderstables of the DBDEMOS database, and I’ve used a data module Now add a DataSource com-ponent for each table, and for the secondary table set a master source to the data source con-nected to the first table Finally relate the secondary table to a field (called MasterField) of themain table, using the special property editor provided
A Data Module for Data-Access Components
To build a Delphi database application, you can place access components and the aware controls in a form This is handy for a simple program, but having the user interface and the data access and data model in a single, often large, unit is far from a good idea For this
data-reason, Delphi implements the idea of data module, a container of nonvisual components I
already introduced in Chapter 1, “The Delphi 6 IDE.”
At design time, a data module is similar to a form, but at run time it exists only in memory The TDataModule class derives directly from TComponent, so it is completely unrelated to the Win- dows concept of a window (and is fully portable among different operating systems) Unlike a form, a data module has just a few properties and events For this reason, it’s useful to think of data modules as components and method containers.
Like a form or a frame, a data module has a designer This means Delphi creates for a data module a specific Object Pascal unit for the definition of its class and a form definition file that lists its components and their properties.
There are several reasons to use data modules The simplest one is to share data-access ponents among multiple forms, as I’ll demonstrate at the beginning of the next chapter This technique works in conjunction with visual form linking, the ability to access components of another form or data module at design time (with the File ➢Use Unit command) The second
Trang 5com-reason is to separate the data from the user interface, improving the structure of an tion Data modules in Delphi even exist in versions specific for multitier applications (remote data modules) and server-side HTTP applications (Web data modules).
applica-Finally, remember that you can use the Diagram page of the editor, introduced in Chapter 1, to see a graphical representation of the connections among the components of a data module, as you can see in this example for the MastDet application:
The following is the complete listing (only without the irrelevant positional properties) ofthe Data Module used by the MastDet program on the CD:
object DataModule1: TDataModule1
Trang 6TIP Starting with Delphi 5, you can also create a master/detail structure using the Data Diagram
view of a data module.
In Figure 13.3 you can see an example of the main form of the MastDet program at runtime I’ve placed data-aware controls related to the master table in the upper portion, andI’ve placed a grid connected with the detail table in the lower portion of the form This way,for every master record, you immediately see the list of the connected detail record, in thiscase all the orders by the current client Each time you select a new customer, the grid belowdisplays only the orders pertaining to that customer
A Master/Detail Structure with Queries
The previous example used two tables to build a master/detail form As an alternative, youcan define this type of join using a SQL statement After setting the master DataSource for
F I G U R E 1 3 3 :
The MastDet example at
run time
Trang 7the detailed query, you simply set up its SQL statement with a parameter having the samename of the field of the master dataset this data source refers to.
For this example (called Orders), I’ve joined the ORDERS.DB table with ITEMS.DB,which describes the items of each order The two tables can be joined using the OrderNofield When you generate the code, the program behaves exactly like the previous one, Mast-Det This time, however, the trick is in the SQL statements of the second query object:
select OrderNo, ItemNo, PartNo, Qty
from items
where OrderNo = :OrderNo
As you can see, this SQL statement uses a parameter, OrderNo This parameter is nected directly to the first query, because the DataSourceproperty of QueryItemsis set todsOrders, which is connected to QueryOrders In other words, the second query is considered
con-to be a data control connected con-to the first data source Each time the current record in thefirst data source changes, the QueryItemscomponent is updated, just like any other compo-nent connected to dsOrders The field used for the connection, in this case, is the field hav-ing the same name as the query parameter
Other BDE Related Components
Along with Table, Query, StoredProc, and DataSource, other components are on the DataAccess page of the Component palette, the BDE page I’ll cover these components in thenext chapter, but here is a short summary:
• The Database component is used for transaction control, security, and connection trol It is generally used only to connect to remote databases in client/server applica-tions or to avoid the overhead of connecting to the same database in several forms TheDatabase component is also used to set a local alias, one used only inside a program.Once this local alias is set to a given path, the Table and Query components of theapplication can refer to the local database alias This is much better than replicating thehard-coded path in each DataSet component of the program
con-TIP The Borland Database Engine (BDE) uses an alias to refer to a database file or directory You
can define new aliases for databases by using the Database Explorer or the Database Engine Configuration utility It is also possible to define them by writing code in Delphi that calls the AddStandardAlias and AddAlias methods of the Session global object, followed by a call
to SaveConfigFile to make the alias persistent The alternative is the low-level DbiAddAlias BDE function In some of the program of this chapter I’ll use the DBDEMOS database alias, which refers to Delphi’s demo database, installed by default in the C:\Program Files\Common Files\Borland Shared\Data directory.
Trang 8• The Session component provides global control over database connections for anapplication, including a list of existing databases and aliases and an event to customizedatabase login.
• The BatchMove component is used to perform batch operations, such as copying,appending, updating, or deleting values, on one or more databases
• The UpdateSQL component allows you to write SQL statements to perform variousupdate operations on the dataset, when using a read-only query (that is, when workingwith a complex query) This component is used as the value of the UpdateObjectprop-erty of tables or queries
Using Data-Aware Controls
Once you’ve set up the proper data-access components, you can build a user interface to let
a user view the data and eventually edit it Delphi provides many components that resemblethe usual Windows controls but are data-aware For example, the DBEdit component is sim-ilar to the Edit component, and the DBCheckBox component corresponds to the CheckBoxcomponent You can find all of these components in the Data Controls page of the DelphiComponent palette
All of these components are connected to a data source using the corresponding property,DataSource Some of them relate to the entire dataset, such as the DBGrid and DBNavigatorcomponents, while the others refer to a specific field of the data source, as indicated by theDataFieldproperty Once you select the DataSourceproperty, the DataFieldproperty willhave a list of values available in the drop-down combo box of the Object Inspector
NOTE In Chapter 18, “Writing Database Components,” we’ll discuss the technical details of these
controls, as we’ll see how to write custom data-aware components.
Notice that all the data-aware components are totally unrelated to the data-access ogy, provided the data-access component inherits from TDataSet This means that yourinvestment on the user interface is totally preserved when you change the data-access tech-nology What is true, however, is that some of the lookup components and an extended use
technol-of the DBGrid, displaying a lot technol-of data, only make more sense when working with local data,and should generally be avoided in a client/server situation, as we’ll see in the next chapter
Data in a Grid
The DBGrid is a grid capable of displaying a whole table at once It allows scrolling and gation, and you can edit the grid’s contents It is an extension of the other Delphi grid controls
Trang 9navi-You can customize the DBGrid by setting the various flags of its Optionsproperty andmodifying its Columnscollection The grid allows a user to navigate the data, using the scroll-bars, and perform all the mayor actions A user can edit the data directly, insert a new record
in a given position by pressing the Insert key, append a new record at the end by going to thelast record and pressing the Down arrow key, and delete the current record by pressingCtrl+Del
The Columnsproperty is a collection where you can choose the fields of the table you want
to see in the grid and set column and title properties (color, font, width, alignment, caption,and so on) for each field Some of the more advanced properties, such as ButtonStyleandDropDownRows, can be used to provide custom editors for the cells of a grid or a drop-downlist of values (indicated in the PickListproperty of the column)
An alternative to the DBGrid is the DBCtrlGrid component, a multirecord grid that canhost panels with other data-aware controls These controls are duplicated in each panel foreach record of the dataset I’ll discuss the DBCtrlGrid control at the end of this chapter
DBNavigator and Dataset Actions
DBNavigator is a collection of buttons used to navigate and perform actions on the database.You can disable some of the buttons of the DBNavigator control, by removing some of theelements of the VisibleButtonsset
The buttons perform basic actions on the connected dataset, so you can easily replace themwith your own toolbar, particularly if you use an ActionList component with the predefineddatabase actions provided by Delphi In this case, in fact, you get all the standard behaviors,but you’ll also see the various buttons enabled only when their action is legitimate
TIP If you use the standard actions, you can avoid connecting them to a specific DataSource
com-ponent, and the actions will be applied to the dataset connected to the visual control that rently has the input focus This way a single toolbar can be used for multiple datasets displayed by a form.
cur-Text-Based Data-Aware Controls
There are multiple text-oriented components:
• DBText displays the contents of a field that cannot be modified by the user It is a aware Label graphical control It can be very useful, but users might confuse this con-trol with the plain labels that indicate the content of each field-based control
Trang 10data-• DBEdit lets the user edit a field (change the current value) using an Edit control Attimes, you might want to disable editing and use a DBEdit as if it were a DBText, buthighlighting the fact that this is data coming from the database.
• DBMemo lets the user see and modify a large text field, eventually stored in a memo orBLOB (binary large object) field It resembles the Memo component and has full edit-ing capabilities, but all the text is rendered in a single font
• DBRichEdit is a component that lets the user edit a formatted text file; it is based on aRichEdit Windows common control and, in contrast to DBMemo, it allows text withmultiple fonts and paragraph styles
List-Based Data-Aware Controls
For letting a user choose a value in a predefined list (which reduces input errors), you can usemany different components DBListBox, DBComboBox, and DBRadioGroup are similar,providing a list of strings in the Itemsproperty, but they do have some differences:
• The DBListBox component allows selection of predefined items (“closed selection”),but not text input, and can be used to list many elements Generally it’s best to showonly about six or seven items, to avoid using up too much space on the screen
• The DBComboBox component can be used both for closed selection and for userinput The csDropDownstyle of the DBComboBox, in fact, allows a user to enter a newvalue, besides selecting one of the available ones The component also uses a smallerarea of the form because the drop-down list is usually displayed only on request
• The DBRadioGroup component presents radio buttons (which permit only one tion), allows only closed selection, and should be used only for a limited number ofalternatives A nice features of this component is that the values displayed can beexactly those you want to insert in the database, but you can also choose to providesome sort of mapping The values of the user interface (some descriptive strings stored
selec-in the Itemsproperty) will map to corresponding values stored in the database (somenumeric or character-based codes listed in the Valuesproperty) For example, you canmap some numeric codes indicating departments to a few descriptive strings:
object DBRadioGroup1: TDBRadioGroup
Trang 11of this component.
The usage of a DBRadioGroup control, with the settings discussed above, and a
DBCheckBox control is highlighted by the DbAware example
Creating Local Tables with FieldDefs
The DbAware example would be a rather simple program if it didn’t have an extra feature: It can create a new table for the DBDEMOS database Delphi allows you to set the definition of the fields of a table—its internal structure—at design time, using the collection editor of the FieldDefs property Once you’ve defined the fields, you can then right-click the table compo- nent at design time and select the Create Table command.
This list of field definitions is generally extracted from the database, but if you set the Defs property of the table to True, it will be saved in the DFM file along with the other table properties The effect of the StoreDefs property is more complex than it seems at first If you right-click the form, you’ll notice that its local menu offers an Update Table Definition option, along with the expected Delete Table and Rename Table That is, you can store the field defini- tions locally, but if the structure of the physical table changes, you should update this definition
Store-as well Until Delphi 4, the field definitions were invariably loaded from the databStore-ase table at run time; now you can preload them, speeding up the table opening However, if the local and the actual table definitions do not match, you can get in trouble.
In the DbAware example, I’ve used this technique to create a new database table, called ers, which stores data about the employees of a company This is the definition of the fields of the table, along with the other key properties:
Work-object Table1: TTable
Trang 12Regardless of the fact you might have created the table at design time, the program must do
so the first time it is executed on a different computer In practice, when the program starts, it checks whether the table already exists and creates one if it doesn’t:
procedure TForm1.FormCreate(Sender: TObject);
in other examples in this book, so you might want to run it once and create the table anyway.
Trang 13Using Lookup Controls
If the list of values is extracted from another dataset, then instead of the DBListBox andDBComboBox controls you should use the specific DBLookupListBox or DBLookupCombo-Box components These components are used every time you want to select for a field arecord of another dataset
For example, if you build a standard form for taking orders, the orders dataset will ally have a field hosting a number indicating the customer who made the order Workingdirectly with the customer number is not the most natural way; most users will prefer towork with customer names However, in the database, the names of the customers are stored
gener-in a different table, to avoid duplicatgener-ing the customer data for each order by the same tomer To get around such a situation, with local databases or small lookup tables, you canuse a DBLookupComboBox control (This technique doesn’t port very well to client/serverarchitecture with large lookup tables, as discussed in the next chapter.)
cus-The DBLookupComboBox component can be connected to two data sources at the sametime, one source containing the actual data and a second containing the display data Basically,I’ve built a standard form using the ORDERS.DB tables of the DBDEMOS database, withseveral DBEdit controls (you can as well use the Database Form Wizard to build this plainform) The example actually uses a Query component selecting most fields of the orders table
At this point we want to remove the standard DBEdit component connected to the tomer number and replace it with a DBLookupComboBox component (and a DBText com-ponent for understanding what exactly is going on) The lookup component (and the
cus-DBText) is connected with the DataSource for the order and with the CustNo field To letthe lookup component show the information extracted from another table,
CUSTOMER.DB, we need to add another table component referring to it, and new datasource connected to the table
For the program to work, you need to set several properties of the DBLookupComboBox1component Here is a list of the relevant values:
object DBLookupComboBox1: TDBLookupComboBox
Trang 14proper-provide multiple fields, as I’ve done in the example Only the first field is displayed as combobox text, but if you set a large value for the DropDownWidthproperty, the pull-down list of thecombo box will include multiple columns of data You can see this output in Figure 13.4.
TIP If you set the index of the table connected with the DBLookupComboBox to the Company
field, the drop-down list will show the companies in alphabetical order instead of number order This is what I’ve done in the example.
customer-What about the code of this program? Well, there is none Everything works just by ting the correct properties The three joined data sources do not need custom code Thisdemonstrates that using master/detail and lookup connections can be very fast to set up andvery efficient The only real drawback is that these techniques, particularly the lookup, can-not be used when the number of records becomes too large, particularly in a networked orclient/server environment Moving hundreds of thousands of records just to make a nice-looking lookup combo box probably won’t be very effective
set-NOTE In Delphi 6, both the TDBLookupComboBox and TDBLookupListBox controls have a
Null-ValueKey property, which indicates the shortcut that can be used to set the value to null, by calling the Clear method of the corresponding field.
Graphical Data-Aware Controls
Finally, Delphi includes two graphical data-aware controls:
• DBImage is an extension of an Image component that shows a picture stored in aBLOB field (provided the database uses a graphic format that the Image componentsupports, such as BMP and JPEG)
F I G U R E 1 3 4 :
The output of the
Cust-Lookup example, with the
DBLookupComboBox
showing multiple fields in
its drop-down list
Trang 15• DBChart is a data-aware business graphic component or the data-aware version of theTeeChart control built by David Berneda.
To demonstrate the use of the DBChart control, I’ve added this component to a simpleexample showing a data grid The application, called ChartDB, shows a pie chart with thesurface of each country of the COUNTRY.DB table, as you can see in Figure 13.5
The program has almost no code, as all the settings can be done using the specific nent editor, which has several options but is quite easy to use Here are some of the key prop-erties of the component, taken from the form description:
compo-object DBChart1: TDBChart
Legend.Visible = False
Align = alClient
object Series1: TPieSeries
Marks.ArrowLength = 8 Marks.Visible = True DataSource = Table1
XLabelsSource = ‘Name’
ExplodeBiggest = 3 OtherSlice.Style = poBelowPercent
The output of the ChartDB
example, which is based on
the TDbChart control
Trang 16What I’ve done is show the area field as the data source for the pie chart (the PieValues ValueSourceproperty of the series), use the name field for the labels (the XLabelsSourceproperty of the series), and condense all the countries with a value below 2 percent in a singlesection indicated as Others (the OtherSlidesubproperties).
As a minor addition to the code, I’ve added two radio buttons you can use to toggle
between the area and the population The code of the two radio buttons simply sets thesource of the series, after casting it to the proper series type, as in:
procedure TForm1.RadioPopulationClick(Sender: TObject);
begin
DBChart1.Title.Text [0] := ‘Population of Countries’;
(DBChart1.Series [0] as TPieSeries).PieValues.ValueSource := ‘Population’;
end;
The DataSet Component
Instead of focusing right away on the use of a specific dataset, I prefer starting with a genericintroduction of the features of the TDataSetclass, which are shared by all inherited data-accessclasses The DataSet component is a very complex one, so I won’t list all of its capabilities butonly discuss its core elements
The idea behind this component is to provide access to a series of records that are readfrom some source of data, kept in internal buffers (for performance reasons), and eventuallymodified by a user, with the possibility of writing back changes to the persistent storage Thisapproach is generic enough to be applied to different types of data (even non-database data)but has a few rules First, there can be only one active record at a time, so if you need toaccess data in multiple records, you must move to each of them, read the data, then moveagain, and so on You’ll find an example of this and related techniques in the section aboutnavigation
Second, you can edit only the active record: you cannot modify a set of records at the sametime, as you can in a relational database Moreover, you can modify data in the active bufferonly after you explicitly declare you want to do so, by giving the Editcommand to the dataset.You can also use the Insertcommand to create a new blank record, and close both operations(insert or edit) by giving a Postcommand
Other interesting elements of a dataset I will explore in the following sections are its status(and the status change events), navigation and record positions, and the role of the field objects
As a summary of the capabilities of the DataSet component, I’ve included the public methods
of its class in Listing 13.1 (the code has been edited and commented for clarity) Not all ofthese methods are directly used everyday, but I decided to keep them all in the listing In
Trang 17Chapter 18, I’ll also discuss the virtual methods of the protected portion of the class, whichwe’ll need to override to build custom dataset components.
➲ Listing 13.1: The public interface of the TDataSet class (excerpted)
TDataSet = class(TComponent, IProviderSupport)
public
// create and destroy, open and close
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Open;
procedure Close;
property BeforeOpen: TDataSetNotifyEvent read FBeforeOpen write FBeforeOpen; property AfterOpen: TDataSetNotifyEvent read FAfterOpen write FAfterOpen; property BeforeClose: TDataSetNotifyEvent
read FBeforeClose write FBeforeClose;
property AfterClose: TDataSetNotifyEvent read FAfterClose write FAfterClose;
// status information
function IsEmpty: Boolean;
property Active: Boolean read GetActive write SetActive default False; property State: TDataSetState read FState;
function ActiveBuffer: PChar;
property IsUniDirectional: Boolean
read FIsUniDirectional write FIsUniDirectional default False;
function UpdateStatus: TUpdateStatus; virtual;
property RecordSize: Word read GetRecordSize;
property ObjectView: Boolean read FObjectView write SetObjectView;
property RecordCount: Integer read GetRecordCount;
function IsSequenced: Boolean; virtual;
function IsLinkedTo(DataSource: TDataSource): Boolean;
// datasource
property DataSource: TDataSource read GetDataSource;
procedure DisableControls;
procedure EnableControls;
function ControlsDisabled: Boolean;
// fields, including blobs, details, calculated, and more
function FieldByName(const FieldName: string): TField;
function FindField(const FieldName: string): TField;
procedure GetFieldList(List: TList; const FieldNames: string);
procedure GetFieldNames(List: TStrings);
property FieldCount: Integer read GetFieldCount;
property FieldDefs: TFieldDefs read FFieldDefs write SetFieldDefs;
property FieldDefList: TFieldDefList read FFieldDefList;
property Fields: TFields read FFields;
property FieldList: TFieldList read FFieldList;
property FieldValues[const FieldName: string]: Variant
Trang 18read GetFieldValue write SetFieldValue; default;
property AggFields: TFields read FAggFields;
property DataSetField: TDataSetField
read FDataSetField write SetDataSetField;
property DefaultFields: Boolean read FDefaultFields;
procedure ClearFields;
function GetBlobFieldData(FieldNo: Integer;
var Buffer: TBlobByteData): Integer; virtual;
function CreateBlobStream(Field: TField;
Mode: TBlobStreamMode): TStream; virtual;
function GetFieldData(Field: TField;
Buffer: Pointer): Boolean; overload; virtual;
procedure GetDetailDataSets(List: TList); virtual;
procedure GetDetailLinkFields(MasterFields, DetailFields: TList); virtual; function GetFieldData(FieldNo: Integer;
Buffer: Pointer): Boolean; overload; virtual;
function GetFieldData(Field: TField; Buffer: Pointer; NativeFormat: Boolean): Boolean; overload; virtual;
property AutoCalcFields: Boolean
read FAutoCalcFields write FAutoCalcFields default True;
property OnCalcFields: TDataSetNotifyEvent
read FOnCalcFields write FOnCalcFields;
function MoveBy(Distance: Integer): Integer;
property RecNo: Integer read GetRecNo write SetRecNo;
property Bof: Boolean read FBOF;
property Eof: Boolean read FEOF;
procedure CursorPosChanged;
property BeforeScroll: TDataSetNotifyEvent
read FBeforeScroll write FBeforeScroll;
property AfterScroll: TDataSetNotifyEvent
read FAfterScroll write FAfterScroll;
// bookmarks
procedure FreeBookmark(Bookmark: TBookmark); virtual;
function GetBookmark: TBookmark; virtual;
function BookmarkValid(Bookmark: TBookmark): Boolean; virtual;
procedure GotoBookmark(Bookmark: TBookmark);
function CompareBookmarks(Bookmark1, Bookmark2: TBookmark): Integer; virtual; property Bookmark: TBookmarkStr read GetBookmarkStr write SetBookmarkStr;
// find, locate
function FindFirst: Boolean;
function FindLast: Boolean;
function FindNext: Boolean;
Trang 19function FindPrior: Boolean;
property Found: Boolean read GetFound;
function Locate(const KeyFields: string; const KeyValues: Variant;
Options: TLocateOptions): Boolean; virtual;
function Lookup(const KeyFields: string; const KeyValues: Variant;
const ResultFields: string): Variant; virtual;
// filtering
property Filter: string read FFilterText write SetFilterText;
property Filtered: Boolean read FFiltered write SetFiltered default False; property FilterOptions: TFilterOptions
read FFilterOptions write SetFilterOptions default [];
property OnFilterRecord: TFilterRecordEvent
read FOnFilterRecord write SetOnFilterRecord;
// refreshing, updating
procedure Refresh;
property BeforeRefresh: TDataSetNotifyEvent
read FBeforeRefresh write FBeforeRefresh;
property AfterRefresh: TDataSetNotifyEvent
read FAfterRefresh write FAfterRefresh;
procedure UpdateCursorPos;
procedure UpdateRecord;
function GetCurrentRecord(Buffer: PChar): Boolean; virtual;
procedure Resync(Mode: TResyncMode); virtual;
// editing, inserting, posting, and deleting
property CanModify: Boolean read GetCanModify;
property Modified: Boolean read FModified;
procedure Post; virtual;
procedure AppendRecord(const Values: array of const);
procedure InsertRecord(const Values: array of const);
procedure SetFields(const Values: array of const);
// events related to editing, inserting, posting, and deleting
property BeforeInsert: TDataSetNotifyEvent
read FBeforeInsert write FBeforeInsert;
property AfterInsert: TDataSetNotifyEvent
read FAfterInsert write FAfterInsert;
property BeforeEdit: TDataSetNotifyEvent read FBeforeEdit write FBeforeEdit; property AfterEdit: TDataSetNotifyEvent read FAfterEdit write FAfterEdit; property BeforePost: TDataSetNotifyEvent read FBeforePost write FBeforePost; property AfterPost: TDataSetNotifyEvent read FAfterPost write FAfterPost; property BeforeCancel: TDataSetNotifyEvent
read FBeforeCancel write FBeforeCancel;
property AfterCancel: TDataSetNotifyEvent
Trang 20read FAfterCancel write FAfterCancel;
property BeforeDelete: TDataSetNotifyEvent
read FBeforeDelete write FBeforeDelete;
property AfterDelete: TDataSetNotifyEvent
read FAfterDelete write FAfterDelete;
property OnDeleteError: TDataSetErrorEvent
read FOnDeleteError write FOnDeleteError;
property OnEditError: TDataSetErrorEvent
read FOnEditError write FOnEditError;
property OnNewRecord: TDataSetNotifyEvent
read FOnNewRecord write FOnNewRecord;
property OnPostError: TDataSetErrorEvent
read FOnPostError write FOnPostError;
// support, utilities
function Translate(Src, Dest: PChar;
ToOem: Boolean): Integer; virtual;
property Designer: TDataSetDesigner read FDesigner;
property BlockReadSize: Integer read FBlockReadSize write SetBlockReadSize; property SparseArrays: Boolean read FSparseArrays write SetSparseArrays; end;
The Status of a Dataset
When you operate on a dataset in Delphi, you can work in different states, indicated by aspecific Stateproperty, which can assume several different values:
dsBrowse indicates that the dataset is in normal browse mode, used to look at the dataand scan the records
dsEdit indicates that the dataset is in edit mode A dataset enters this state when the gram calls the Editmethod or the DataSource has the AutoEditproperty set to True, andthe user starts editing a data-aware control, such as a DBGrid or DBEdit When thechanged record is posted, the dataset exits the dsEdit state
pro-dsInsert indicates that a new record is being added to the dataset Again, this might pen when calling the Insertmethod, moving to the last line of a DBGrid, or using thecorresponding command of the DBNavigator component
hap-dsInactive is the state of a closed dataset
dsSetKey indicates that we are preparing a search on the dataset This is the state between
a call to the SetKeymethod and a call to the GotoKeyor GotoNearestmethods (see theSearch example later in this chapter)
dsCalcFields is the state of a dataset while a field calculation is taking place; that is, ing a call to an OnCalcFieldsevent handler Again, I’ll show this in an example
Trang 21dur-dsNewValue, dsOldValue, and dsCurValue are the states of a dataset when an update ofthe cache is in progress.
dsFilter is the state of a dataset while setting a filter; that is, during a call to an Recordevent handler
OnFilter-In simple examples, the transitions between these states are handled automatically, but it isimportant to understand them because there are many events referring to the state transitions.For example, every dataset fires events before and after any state change When a programrequests an Editoperation, the component fires the BeforeEditevent just before entering inedit mode (an operation you can stop by raising an exception) Immediately after entering editmode, the dataset receives the AfterEditevent After the user has finished editing and requests
to store the data, executing the Postcommand, the dataset fires a BeforePostevent, which can
be used to check the input before sending the data to the database, and an AfterPosteventafter the operation has been successfully completed
Another more general state-change tracking technique is to handle the OnStateChangeevent of the DataSource component As a very simple example you can show the current sta-tus with code like this:
procedure TForm1.DataSource1StateChange(Sender: TObject);
var
strStatus: string;
begin
case Table1.State of
dsBrowse: strStatus := ‘Browse’;
dsEdit: strStatus := ‘Edit’;
dsInsert: strStatus := ‘Insert’;
The Fields of a Dataset
I mentioned earlier that a dataset has only one record that is the current, or active, one Therecord is stored in a buffer, and you can operate on it with some generic methods, but toaccess the data of the record you need to use the field objects of the dataset This explainswhy field components (technically instances of class derived from the TFieldclass) play a
Trang 22fundamental role in every Delphi database application Data-aware controls are directly nected to these field objects, which correspond to database fields.
con-By default, Delphi automatically creates the TFieldcomponents at run time, each time theprogram opens a dataset component This is done after reading the metadata associated withthe table or the query the dataset refers to These field components are stored in the Fieldsarray property of a dataset You can access these values by number (accessing the array directly)
or by name (using the FieldByNamemethod) Each field can be used for reading or modifyingthe data of the current record, using its Valueproperty or type-specific properties, like AsDate,AsString, AsInteger, and so on:
strName := Table1.FieldValues [‘LastName’];
strName := Table1 [‘LastName’];
Creating the field components each time a dataset is opened is only a default behavior As
an alternative, you can create the field components at design time, using the Fields editor(double-click a dataset to see the Fields editor in action, or activate its local menu and choosethe Fields Editor command) After creating a field for the LastName column of a table, forexample, you can refer to its value by applying one of the AsXxxmethods to the proper fieldobject:
strName := Table1LastName.AsString;
Besides being used to access the value of a field, each field object also has properties forcontrolling visualization and editing of its value, including range of values, edit masks, dis-play format, constraints, and many others These properties, of course, depend on the type ofthe field—that it is, on the specific class of the field object If you create persistent fields youcan set some properties at design time, instead of writing code at run time, maybe in theAfterOpenevent of the dataset
NOTE Although the Fields editor is similar to the editors of the collections used by Delphi, fields are
not part of a collection They are components created at design time, listed in its published section of the form class, and available in the drop-down combo box at the top of the Object Inspector.
Trang 23As you open the Fields editor for a dataset, it appears empty You have to activate the localmenu of this editor to access its capabilities The simplest operation you can do is to selectthe Add command, which allows you to add any other fields in the dataset to the list of fields.Figure 13.6 shows the Add Fields dialog box, which lists all the fields that are available in atable These are the database table fields that are not already present in the list of fields in theeditor.
The Define command of the Fields editor, instead, lets you define a new calculated field,lookup field, or field with a modified type In this dialog box, you can enter a descriptive fieldname, which might include blank spaces Delphi generates an internal name—the name ofthe field component—which you can further customize Next, select a data type for the field
If this is a calculated field or a lookup field, and not just a copy of a field redefined to use anew data type, simply check the proper radio button We’ll see how to define a calculatedfield in the section “Adding a Calculated Field” and a lookup field in two following sections
NOTE A TField component has both a Name property and a FieldName property The Name property
is the usual component name The FieldName property is either the name of the column in the database table or the name you define for the calculated field It can be more descriptive than the Name, and it allows blank spaces The FieldName property of the TField component is copied to the DisplayLabel property by default, but this field name can be changed to any suitable text It is used, among other things, to search a field in the FieldByName method of the TDataSet class and when using the array notation.
All of the fields that you add or define are included in the Fields editor and can be used bydata-aware controls or displayed in a database grid If a field of the original database table isnot in this list, it won’t be accessible When you use the Fields editor, Delphi adds the decla-ration of the available fields to the class of the form, as new components (much as the MenuDesigner adds TMenuItemcomponents to the form) The components of the TFieldclass, ormore specifically its subclasses, are fields of the form, and you can refer to these components
F I G U R E 1 3 6 :
The Fields editor with the
Add Fields dialog box
Trang 24directly in the code of your program to change their properties at run time or to get or settheir value.
In the Fields editor, you can also drag the fields to change their order Proper field ordering
is particularly important when you define a grid, which arranges its columns using this order
TIP An even better feature of the Fields editor is that you can drag fields from this editor to the
surface of a form and have Delphi automatically create a corresponding data-aware control (such as a DBEdit, a DBMemo, or a DBImage) The type of control created depends on the data type of the field This is a very fast way to generate custom forms, and I suggest you try it out
if you’ve never used it before This is my preferred way to build database-related forms, much better than using the Database Form Wizard.
Using Field Objects
Before we look at an example, let’s go over the use of the TFieldclass The importance ofthis component should not be underestimated Although it is often used behind the scenes,its role in database applications is fundamental As I already mentioned, even if you do notdefine specific objects of this kind, you can always access the fields of a table or a query usingtheir Fieldsarray property, the FieldValuesindexed property, or the FieldByNamemethod.Both the Fieldsproperty and the FieldByNamefunction return an object of type TField, soyou sometimes have to use the asoperator to downcast their result to its actual type (likeTFloatFieldor TDateField) before accessing specific properties of these subclasses
The FieldAcc example is a simple extension of a form generated by the Database FormWizard I’ve added to it three speed buttons in the toolbar panel, accessing various fieldproperties at run time The first button changes the formatting of the population column ofthe grid To do this, we have to access the DisplayFormatproperty, a specific property of theTFloatFieldclass For this reason we have to write:
procedure TForm2.SpeedButton1Click(Sender: TObject);
procedure TForm2.SpeedButton2Click(Sender: TObject);
begin
ShowMessage (string (Table1 [‘Name’]) +’: ‘+ string (Table1 [‘Population’]));
end;
Trang 25When you access the value of a field, you can use a series of As properties to handle the
current field value using a specific data type (if this is available; otherwise, an exception israised):
These properties can be used to read or change the value of the field Changing the value
of a field is possible only if the dataset is in edit mode As an alternative to the As properties
indicated above, you can access the value of a field by using its Valueproperty, which isdefined as a variant
Most of the other properties of the TFieldcomponent, such as Alignment, DisplayLabel,DisplayWidth, and Visible, reflect elements of the field’s user interface and are used by thevarious data-aware controls, particularly DBGrid In the FieldAcc example, clicking the thirdspeed button changes the Alignmentof every field:
procedure TForm2.SpeedButton3Click(Sender: TObject);
tool-F I G U R E 1 3 7 :
The output of the FieldAcc
example after the Center
and Format buttons have
been pressed
Trang 26A Hierarchy of Field Classes
There are several field class types in VCL Delphi automatically uses one of them depending
on the data definition in the database, when you open a table at run time or when you use theFields editor at design time Table 13.1 shows the complete list of subclasses of the TFieldclass
TABLE 13.1: The Subclasses of TField
TADTField TObjectField An ADT (Abstract Data Type) field, corresponding to an object
field in an object relational database.
TAggregateField TField An aggregate field represents a maintained aggregate It is used
in the ClientDataSet component and discussed in Chapter 14,
“Client/Server Programming.”
TArrayField TObjectField An array of objects in an object relational database.
TAutoIncField TIntegerField Whole positive number connected with a Paradox
auto-incre-ment field of a table, a special field automatically assigned a different value for each record Note that Paradox AutoInc fields do not always work perfectly, as discussed in the next chapter.
TBCDField TNumericField Real numbers, with a fixed number of digits after the decimal
point.
TBinaryField TField Generally not used directly This is the base class of the next
two classes.
TBlobField TField Binary data and no size limit (BLOB stands for binary large
object) The theoretical maximum limit is 2 GB.
TBytesField TBinaryField Arbitrary data with a large (up to 64 KB characters) but
TDateField TDateTimeField Date value.
TDateTimeField TField Date and time value.
TFloatField TNumericField Floating-point numbers (8 byte).
TFMTBCDField TNumericField (New field type in Delphi 6) A true binary-coded decimal (BCD),
as opposed to the existing TBCDField type, which converted BCD values to the Currency type This field type is used auto- matically only by dbExpress datasets.
TGraphicField TBlobField Graphic of arbitrary length.
TGuidField TStringField A field representing a COM Globally Unique Identifier, part of
the ADO support.
Trang 27TABLE 13.1 continued: The Subclasses of TField
TIDispatchField TInterfaceField A field representing pointers to IDispatch COM interfaces,
part of the ADO support.
TIntegerField TNumericField Whole numbers in the range of long integers (32 bits) TInterfacedField TField Generally not used directly This is the base class of fields that
contain pointers to interfaces ( IUnknown ) as data.
TLargeIntField TIntegerField Very large integers (64 bit).
TMemoField TBlobField Text of arbitrary length.
TNumericField TField Generally not used directly This is the base class of all the
numeric field classes.
TObjectField TField Generally not used directly The base class for the fields
provid-ing support for object relational databases.
TReferenceField TObjectField A pointer to an object in an object relational database TSmallIntField TIntegerField Whole numbers in the range of integers (16 bits).
TSQLTimeStampField TField (New field type in Delphi 6) Supports the date/time
representa-tion used in dbExpress drivers TStringField TField Text data of a fixed length (up to 8192 bytes).
TTimeField TDateTimeField Time value.
TVarBytesField TBytesField Arbitrary data, up to 64 KB characters Very similar to the
TBytes - Field base class.
TVariantField TField A field representing a variant data type, part of the ADO
defini-Adding a Calculated Field
Now that you’ve been introduced to TFieldobjects and seen an example of their run-timeuse, it is time to build a simple example based on the declaration of field objects at designtime using the Fields editor We can start again from the first example we built, GridDemo,and add a calculated field The COUNTRY.DB database table we are accessing has both thepopulation and the area of each country, so we can use this data to compute the populationdensity
Trang 28To build the new example, named Calc, select the Table component in the form, and openthe Fields editor In this editor, choose the Add command, and select some of the fields (I’vedecided to include them all.) Now select the Define command, and enter a proper name anddata type (TFloatField) for the new calculated field, as you can see in Figure 13.8.
WARNING It is obvious that as you create some field components at design time using the Fields editor,
the fields you skip won’t get a corresponding object What might not be obvious is that the fields you skip will not be available even at run time, with Fields or FieldByName When a pro- gram opens a table at run time, if there are no design-time field components, Delphi creates field objects corresponding to the table definition If there are some design-time fields, how- ever, Delphi uses those fields without adding any extra ones.
Of course, we also need to provide a way to calculate the new field This is accomplished inthe OnCalcFieldsevent of the Table component, which has the following code (at least in afirst version):
procedure TForm2.Table1CalcFields(DataSet: TDataSet);
begin
Table1PopulationDensity.Value := Table1Population.Value / Table1Area.Value;
end;
NOTE Calculated fields are computed for each record and recalculated each time the record is loaded
in an internal buffer, invoking the OnCalcFields event over and over again For this reason, a handler of this event should be extremely fast to execute, and cannot alter the status of the dataset, by accessing different records A more time-efficient (but less memory-efficient) ver- sion of a calculated field is provided by the ClientDataSet component with “internally calcu- lated” fields, which are evaluated only once, when they are loaded, with the result stored in memory for future requests.
F I G U R E 1 3 8 :
The definition of a
calculated field in the
Calc example
Trang 29Everything fine? Not at all! If you enter a new record and do not set the value of the lation and area, or if you accidentally set the area to zero, the division will raise an exception,making it quite problematic to continue using the program As an alternative, we could havehandled every exception of the division expression and simply set the resulting value to zero:
if not Table1Area.IsNull and
Since we have defined some components for the fields, we can use them to customize some
of the visual elements of the grid For example, to set a display format that adds a comma toseparate thousands, we can use the Object Inspector to change the DisplayFormatproperty
of some field components to “###,###,###” This change has an immediate effect on the grid
at design time
Trang 30NOTE The display format I’ve just mentioned (and used in the previous example) uses the Windows
International Settings to format the output When Delphi translates the numeric value of this field to text, the comma in the format string is replaced by the proper ThousandSeparator character For this reason, the output of the program will automatically adapt itself to different International Settings On computers that have the Italian configuration, for example, the comma is replaced by a period.
After working on the table components and the fields, I’ve customized the DBGrid usingits Columnsproperty editor I’ve set the Population Density column to read-only and set itsButtonStyleproperty to cbsEllipsis, to provide a custom editor When you set this value, asmall button with an ellipsis is displayed when the user tries to edit the grid cell Pressing thebutton invokes the OnEditButtonClickevent of the DBGrid:
procedure TCalcForm.DBGrid1EditButtonClick(Sender: TObject);
begin
MessageDlg (Format (
‘The population density (%.2n)’#13 +
‘is the Population (%.0n)’#13 +
‘divided by the Area (%.0n).’#13#13 +
‘Edit these two fields to change it.’,
[Table1PopulationDensity.AsFloat, Table1Population.AsFloat,
Table1Area.AsFloat]), mtInformation, [mbOK], 0);
end;
Actually, I haven’t provided a real editor, but rather a message describing the situation, asyou can see in Figure 13.9, which shows the values of the calculated fields To create an edi-tor, you might build a secondary form to handle special data entries
F I G U R E 1 3 9 :
The output of the Calc
example Notice the
Population Density
calculated column, the
ellipsis button, and the
message displayed when
you select it.
Trang 31Lookup Fields
As an alternative to placing a DBLookupComboBox component in a form (discussed earlier
in this chapter under “Using Lookup Controls”), we can also define a lookup field, which can
be displayed with a drop-down lookup list inside a DBGrid component We’ve seen that toadd a fixed selection to a DBGrid, we can simply edit the PickListsubproperty of the Columnsproperty To customize the grid with a live lookup, instead, we have to define a lookup fieldusing the Fields editor
As an example, I’ve built the FieldLookup program, which has a grid displaying orders with
a lookup field to display the name of the employee who took the order, instead of the codenumber of this employee To accomplish this, I added to the data module a Table componentreferring to the EMPLOYEE.DB table Then I opened the Fields editor for the ORDERStable and added all the fields I selected the EmpNofield and set its Visibleproperty to False, toremove it from the grid (we cannot remove it altogether, because it is used to build the cross-reference with the corresponding field of the EMPLOYEE table)
Now it is time to define the lookup field If you’ve followed the preceding steps, you can usethe Fields editor of the ORDERS table and select the New Field command, obtaining the NewField dialog box (As an alternative in Delphi 5, it’s possible to use the Diagram page of theeditor, drop the two tables there, and drag a lookup relation from the ORDERS table to theEMPLOYEE table, connecting the two in the resulting New Field dialog box In Delphi 6,though, the lookup relation button is still part of the Diagram page but doesn’t seem to beworking at all.)
The values you specify in the New Lookup Field dialog box will affect the properties of anew TFieldadded to the table, as demonstrated by the DFM description of the field:
object Table2Employee: TStringField
Trang 32Handling Null Values with Field Events
Beyond a few interesting properties, the field objects have a few key events The OnValidateevent can be used to provide extended validation of the value of a field, and should be usedwhenever you need a complex rule that the ranges and constraints provided by the field can-not express This event is triggered before the data is written to the record buffer, whereasthe OnChangeevent is fired soon after the data has been written
Two more events, OnGetTextand OnSetText, can be used to customize the output of a field.These two events are extremely powerful: they allow you to use data-aware controls evenwhen the representation of a field you want to display is different from the one Delphi willprovide by default
An example of the use of these events is the handling of null value On SQL servers, ing an empty value or a null value for a field are two separate operations The latter tends to
stor-be more correct, but Delphi by default uses empty values and displays the same output for anempty or a null field Although this can be useful in general for strings and numbers, it becomesextremely important for dates, where it is hard to set a reasonable default value and where if theuser blanks out the field you might have an invalid input
The NullDates program displays a specific text for dates that have a null value and clearsthe field (setting it to the null value) when the user uses an empty string in input Here is therelevant code of the two event handlers for the field:
procedure TForm1.Table1ShipDateGetText(Sender: TField;
var Text: String; DisplayText: Boolean);
begin
F I G U R E 1 3 1 0 :
The output of the
Field-Lookup example, with the
drop-down list inside the
grid displaying values taken
from another database
table
Trang 33“position, movement.” You can move to the next or previous record, jump back and forth by
a given number of records (with MoveBy), or go directly to the first or last record of the dataset.These operations of the dataset are generally available in the DBNavigator component or inthe standard dataset actions, and they are not particularly complex to understand
F I G U R E 1 3 1 1 :
By handling the
OnGet-Text and OnSetText
events of a date field, the
NullDates example displays
a specific output for null
values
Trang 34What is not obvious, though, is how a dataset handles the extreme positions If you openany dataset with a navigator attached, you can see that as you move on record by record, theNext button remains enabled even when you’ve reached the last record It’s only when youtry to move forward after the last record that the current record apparently doesn’t changeand the button is disabled This is because the Eoftest (end of file) succeeds only when thecursor has been moved to a special position after the last record If you jump to the end withthe Last button, instead, you’ll immediately be at the very end You’ll see exactly the samebehavior for the first record (and the Boftest) As we’ll see in a while, this approach is veryhandy, as we can scan a dataset testing for Eofto be Trueand, at this point, we know we’vealso already processed the last record of the dataset.
NOTE Handling this special record positions before the beginning and after the end of the dataset,
which are called cracks, is very important (and quite confusing) when you write a custom
dataset, as we’ll see in Chapter 18.
Besides moving around record by record or by a given number of records, programs mightneed to jump to specific records or positions Some datasets support the RecordCountpropertyand allow movement to a record at a given position in the dataset using the RecNoproperty.These properties can be used only for datasets that support positions natively, which basicallyexcludes all client/server architectures, unless you grab all of the records in a local cache(something you’ll generally want to avoid) and then navigate on the cache As we’ll see in thenext chapter, when you open a query on a SQL server you fetch only the records you areusing, so Delphi doesn’t know the record count, at least not in advance
There are two alternatives you can use to refer to a record in a dataset, regardless of its type:
• You can save a reference to the current record and then jump back to it after movingaround This is accomplished by using bookmarks, either in the TBookmarkor the moremodern TBookmarkStrform This approach is discussed in the upcoming section
“Using Bookmarks.”
• You can locate a record of the dataset matching given criteria, using the Locate
method This even works after you close and reopen the dataset, because you’re ing at a logical (and not physical) level This approach is presented in the next section
work-Locating Records in a Table
To show you an example of the use of the Locatemethod, I’ve built the Search example,which has a table connected to EMPLOYEE.DB The form I’ve prepared has the data-awareedit boxes inside a scroll box aligned to the client area, so that a user can freely resize the formwithout any problems When the form becomes too small, scroll bars will appear automati-cally in the area holding the edit boxes Another feature is a toolbar with buttons connected to
Trang 35some of the predefined dataset actions available in the ActionList component plus two tom actions to host the search code.
cus-The searching capabilities are activated by the two buttons connected to custom actions.The first button is connected to ActionGoto, used for an exact match, and the second toActionGoNear, for a partial match In both cases, we want to compare the text in the edit boxwith the LastNamefields of the EMPLOYEE table If the local table has an index on the field(as in the specific case) Locatewill use it, but the method will work with or without indexes(only at a different speed)
If you’ve never used Locate, at first sight the help file won’t be terribly clear The idea isthat you must provide a list of fields you want to search, and a list of values, one for eachfield If you pass only one field, the value is passed directly, as in the case of the example:
procedure TSearchForm.ActionGotoExecute(Sender: TObject);
begin
if not Table1.Locate (‘LastName’, EditName.Text, []) then
MessageDlg (‘“‘ + EditName.Text + ‘“ not found’, mtError, [mbOk], 0);
end;
If you search for multiple fields, you have to pass a variant array with the list of the values youwant to match The variant array can be created from a constant array with the VarArrayOffunc-tion or from scratch using the VarArrayCreatecall This is a code snippet from the example:
Table1.Locate (‘LastName;FirstName’, VarArrayOf ([‘Cook’, ‘Kevin’]), [])
Finally, we can use the same method to look for a record even if we know only the initialportion of the field we are looking for Simply add the loPartialKeyflag to the Optionsparameter (the third) of the Locatecall
NOTE Using Locate makes sense specifically for local tables, but doesn’t port very well to
client/server applications In fact, on local tables this technique is rather optimized by letting the dataset read in only the records it is looking for, using local indexes On a SQL server, instead, similar client-side techniques imply moving all the data to the client application first, which is generally a bad idea Locating the data should be performed with restricted SQL statements You can still call Locate after you’re retrieved a limited dataset For example, you can search a customer by name after you’ve selected all the customer of a given town or area, obtaining a result set of a limited size There’s more on this topic in the next chapter, which is devoted to client/server development.
Trang 36The Total of a Table Column
So far in our examples, the user can view the current contents of a database table and manuallyedit the data or insert new records Now we will see how we can change some data in the tablethrough the program code The idea behind this example is quite simple The EMPLOYEEtable we have been using has a Salaryfield A manager of the company could indeed browsethrough the table and change the salary of a single employee But what will be the total salaryexpense for the company? And what if the manager wants to give a 10 percent salary increase(or decrease) to everyone?
These are the two aims of the Total example, which is an extension of the previous program.The toolbar of this new example has some more buttons and actions There are a few otherminor changes from the previous example I opened the Fields editor of the table and removedthe Table1Salaryfield, which was defined as a TFloatField Then I selected the New Fieldcommand and added the same field, with the same name, but using the TCurrencyFielddata type.This is not a calculated field; it’s simply a field converted into a new (but equivalent) data type.Using this new field type the program will default to a new output format, suitable for currencyvalues
Now we can turn our attention to the code of this new program First, let’s look at the code
of the total action This action lets you calculate the sum of the salaries of all the employees,then edit some of the values, and compute a new total Basically, we need to scan the table,reading the value of the Table1Salaryfield for each record:
MessageDlg (‘Sum of new salaries is ‘ +
Format (‘%m’, [Total]), mtInformation, [mbOk], 0);
end
This code works, as you can see from the output in Figure 13.12, but it has some problems.One problem is that the record pointer is moved to the last record, so the previous position inthe table is lost Another is that the user interface is refreshed many times during the operation
Trang 37Using Bookmarks
To avoid these two problems, we need to disable updates and to store the current position ofthe record pointer in the table and restore it at the end This can be accomplished using a
table bookmark, a special variable storing the position of a record in a database table Delphi’s
traditional approach is to declare a variable of the TBookmarkdata type, and initialize it whilegetting the current position from the table:
cally implemented as an opaque string, a structure subject to string lifetime management, but
it is not a string, so you’re not supposed to look at what’s inside it.) This is how you can ify the code above:
The output of the Total
program, showing the total
salaries of the employees
Trang 38To avoid the other side effect of the program (we see the records scrolling while the tine browses through the data), we can temporarily disable the visual controls connected withthe table The table has a DisableControlsmethod we can call before the whileloop startsand an EnableControlsmethod we can call at the end, after the record pointer is restored.
rou-TIP Disabling the data-aware controls connected with a table during long operations not only
improves the user interface (since the output is not changing constantly), it also speeds up the program considerably In fact, the time spent to update the user interface is much greater than the time spent performing the calculations To test this, try commenting out the DisableControls and EnableControls methods of the Total example, and see the speed difference.
Finally, we face some dangers from errors in reading the table data, particularly if the gram is reading the data from a server using a network If any problem occurs while retriev-ing the data, an exception takes place, the controls remain disabled, and the program cannotresume its normal behavior So we should use a try/finallyblock Actually, if you want tomake the program 100 percent error-proof, you should use two nested try/finallyblocks.Including this change and the two discussed above, here is the resulting code:
pro-procedure TSearchForm.ActionTotalExecute(Sender: TObject);
MessageDlg (‘Sum of new salaries is ‘ +
Format (‘%m’, [Total]), mtInformation, [mbOK], 0);
end;
Trang 39NOTE I’ve written this code to show you an example of a loop to browse the contents of a table, but
keep in mind that there is an alternative approach based on the use of a SQL query returning the sum of the values of a field When you use a SQL server, the speed advantage of a SQL call
to compute the total can be very large, since you don’t need to move all the data of each field from the server to the client computer The server sends the client only the final result.
Editing a Table Column
The code of the increase action is similar to the one we have just seen The Executemethod also scans the table, computing the total of the salaries, as the previousmethod did Although it has just two more statements, there is a key difference When youincrease the salary, you actually change the data in the table The two key statements arewithin the whileloop:
ActionIncrease-while not Table1.EOF do
The first statement brings the table into edit mode, so that changes to the fields will have
an immediate effect The second statement computes the new salary, multiplying the old one
by the value of the SpinEdit component (by default, 105) and dividing it by 100 That’s a 5percent increase, although the values are rounded to the nearest dollar With this program,you can change salaries by any amount—even double the salary of each employee—with theclick of a button
WARNING Notice that the table enters the edit mode every time the while loop is executed This is
because in a dataset, edit operations can take place only one record at a time You must finish the edit operation, calling Post or moving to a different record as in the code above At that time, if you want to change another record, you have to enter edit mode once more.
Customizing a Database Grid
Unlike most other data-aware controls, which are quite simple to use, the DBGrid controlhas many options and is more powerful than you might think The following sections exploresome of the advanced operations you can do using a DBGrid control A first example showshow to draw in a grid, a second one shows how to clone the behavior of a check box for a
Trang 40Boolean selection inside a grid, and the final example shows how to use the tion feature of the grid.
multiple-selec-Painting a DBGrid
There are many reasons you might want to customize the output of a grid A good example is
to highlight specific fields or records Another is to provide some form of output for fieldsthat usually don’t show up in the grid, such as BLOB, graphic, and memo fields
To thoroughly customize the drawing of a DBGrid control, you have to set its Drawingproperty to False and handle its OnDrawColumnCellevent In fact, if you leave thevalue of DefaultDrawingset to True, the grid will display the default output before themethod is called This way, all you can do is add something to the default output of the grid,unless you decide to draw over it, which will take extra time and cause flickering
Default-The alternative approach is to call the DefaultDrawColumnCellmethod of the grid, perhapsafter changing the current font or restricting the output rectangle In this last case you canprovide an extra drawing in a cell and let the grid fill the remaining area with the standardoutput This is what I’ve done in the DrawData program
The DBGrid control in this example, which is connected to the commonly used BIOLIFEtable of the DBDEMOS database, has the following properties:
object DBGrid1: TDBGrid
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject;
const Rect: TRect; DataCol: Integer; Column: TColumn;
State: TGridDrawState);
begin