Making database date and time entries in one field is problematic. Databases may provide fields designed for only a date, only a time, or for both. Some offer only the date and time option. Generally, the combined date and time field is another implementation of a floating-point field. The integer value represents a date and the fractional part represents time.
TDateTime d(0);//set date to zero
String fs("mm/dd/yyyy hh:nn:sssss");
ShowMessage(d.FormatString(fs));
Getting a time and date into a DateTime field isn't straightforward, and
examples don't exist.
Here's the TQuery's SQL property:
SELECT e.StartedOn, e.StartedOn AS StartedTime FROM 'employee.db' eNotice the cloned field? I include the DateTime field twice. The second occurrence is assigned the alias, or name, StartedTime. The first field enters the date and the other enters the time, but only one field is actually posted to the database file. There a few pitfalls to avoid, and I'll do my best to cover all of them. I'll reference the first field as the date field and the clone as the time field. After you set up the TQuery, double-click on it. Right-click in the resulting field editor dialog box and add all persistent fields. Click on one of the fields and press [F11] to open the Object Inspector. Set the DisplayFormat property for the date and time fields as mm/dd/yyyy and hh:nn, respectively. Doing so lets the DateTime field contain both date and time while displaying only the date in one field and only the time in the other. I also set the time field's EditMask property to !90:00;1;--I use the date-verification routines discussed in the April "DB->Dilemmas" in lieu of an edit mask on the date field.
Now you can set up the OnSetText events for each field. The pseudo code for both is as follows:
DateField_OnSetText
Enter a valid date.
Check the time field.
If time field is null
Get current system time.
Else
Get time from the time field.
If dataset is in edit or insert mode
Add the new time to the date entry.
TimeField_OnSetText
Enter a valid time
Check the date field
If date field is null
Get current system date
Else
Get date from the date field
If dataset is in edit or insert mode
Add the new time to the date field
Notice
that both handlers add the time to the date, preserving accurate date and time
values while allowing the user to type in two fields for each entry. Converting
the pseudo code to C++ yields the code shown in Listing A. This code
manages the date and time field and keeps the time entry combined into the date
field.
Listing A: OnSetText event handlers for date and cloned time fields
void TimeField_SetText(TField *Sender,
const AnsiString Text)
{ String DateEntry,TimeEntry;
if (qEmployeeSTARTEDON->IsNull) //Date field null
DateEntry = Date().FormatString("mm/dd/yyyy");
//use today as default
else
DateEntry =
qEmployeeSTARTEDON->AsDateTime.DateString();
//Get date value
if(Text.IsEmpty())
TimeEntry +=
Time().FormatString("hh:nn"); //Current Time
else
TimeEntry += Text; //Use the entry
DateEntry += " " + TimeEntry; //combine date, time
Sender->AsString = DateEntry; //update text
//to Time field
TDataSet * ds = Sender->DataSet; //get data set
//object from field
//if in edit mode, set date field to include time
if (ds && (ds->State == dsInsert ||
ds->State == dsEdit))
qEmployeeSTARTEDON->AsDateTime =
StrToDateTime(DateEntry); //add time to
//date field
}
//---------------------------------------------------
void DateSetText(TField *Sender,
const AnsiString Text)
{ TDateTime d;
String DateEntry(Text);
if (!MakeValidDate(DateEntry, d, 4)) //date entry,
//return variable, digits in year
{ ShowMessage("Please Enter a Valid Date");
Abort();
}
else
{ DateEntry +=
qEmployeeSTARTEDON->AsDateTime.FormatString(
" hh:nn");
Sender->AsString = DateEntry;
}
}
The last part of the question related to adding default values to new records. This is easy to do, and it works smoothly. Since the process is essentially the same for both the date field and its clone, I'll show the solution for the date field only.
Ideally, a default value is visible to the operator but not to the database until the user modifies at least one field. Such operation lets the user scroll to the new record in a TDBGrid, for example and see default values. However, if no fields are modified, scrolling off the field has no affect on database--that is, the entering default values don't trigger the insertion of a new record.
It turns out the OnGetText event is ideally suited for this type of operation. The following code is an example of the OnGetText handler for the Date field:
void OnGetText(TField *Sender,
AnsiString &Text,bool DisplayText)
{ const String dFmt ("mm/dd/yyyy");
if(Sender->IsNull)
Text=Date().FormatString(dFmt);
else
Text=Sender->AsDateTime.
FormatString(dFmt);
}
This particular handler creates a default value whenever the date field is
null. It might be preferable to create the default only if the TField's data
set is in the dsInsert State. In either case, the code creates default
values that meet the criteria we described earlier.
There's only one negative side effect I found using this solution: The
OnGetText event fires when you insert or append a new record. It fires again
when the object or cell displaying the data receives focus. In a time field,
that means the default value may change between the time the user inserts a
record--creating the default value--and actually posts the record. If the time
cell receives focus, and the time has changed since the default was created,
the default value will also change.
Actually there isn't such a property in the TDBGrid. However, you can easily achieve your objective with a single line of code attached to the BeforeInsert event of a TTable or TQuery object:
void BeforeInsert(TDataSet *DataSet)
{
ShowMessage("Aborted");//optional
Abort();
}
When running your application from within the IDE, you may get a nasty message
following execution of the Abort method. However, the error isn't a problem
when the program runs outside C++Builder's IDE. To disable the error message
within the IDE, clear the Break On Exception property under Environment
Options.
The Abort method attached to a TDataSet provides a great way to disable new records. The data cursor won't move beyond the last record in a data set to a new record. I haven't seen any side effects from this usage, except the time this great big guy wanted to add a new record and got upset--but that's another story.
Yes, there is. There are also more complicated approaches. You could build an OLE automation engine, for example (I'll discuss the OLE automation option in a future column). However, when I really need to get the job done quickly without busting the budget, I use another approach that's amazingly simple and powerful. I copy a data set's contents to a TRichEdit object. Then I can easily copy the contents of the rich text object to the Windows Clipboard and use it in any application that will accept pasted text. Listing B shows the generic code for this mini-report generator. Listing C contains the code I use to copy the contents of a rich text object to the Windows Clipboard.
Listing B: Creating tab-delimited text in a rich text object based on the contents of a Tdataset
void MakeReport(TDataSet *ds, TRichEdit *rte)
{ if (ds) //make sure dataset exists
{ int i;
String s;
const char tab = 9; //tab character
ds->Active = false; //seems to be necessary
//with text tables
ds->Active = true;
s = ds->Name; //get name of table
s += " "; //add space between table name
//and date/time
s += Now().FormatString("dd mmmm yyyy hh:nn:ss");
rte->Lines->Clear(); //clear contents
rte->Lines->Add(s); //add table name, date,
//and time to first line
if (!(ds->Eof) &&(ds->Bof))
{ ds->First(); //reposition dataset on
//first record
int FieldCount = ds->FieldCount-1;
while (!ds->Eof)
{ s="";
for(i=0;i<=FieldCount;i++)
{ s += ds->Fields[i]->AsString;
if (i<FieldCount) //don't add tab on
//last field
s += tab; //create tab-delimited string
}
rte->Lines->Add(s); //add text to
//rich text object
ds->Next();
} } } }
void btnView_fCustomerClick(TObject *Sender)
{ //call mini-report code with TTable and TRichText object
MakeReport(dynamic_cast<TDataSet*>(tbl), rte);
}
Listing C: Copying the contents of a TRichText object to the Windows
Clipboard
void CopyToClipBoard(TRichEdit *rte)
{ Screen->Cursor = crHourGlass;
rte->SelectAll(); //select all text
rte->CopyToClipboard(); //do copy
rte->SelStart = 1; //unselect text
rte->SelLength = 0;
ShowMessage("Contents Copied");
Screen->Cursor = crDefault;
}
I like the tab-delimited format because it's so portable. It pastes into Excel
without a hitch. There's no question about where one column ends and another
begins. Pasted into Microsoft Word, appropriate tab settings create an instant
report, and the output is easily formatted into a Word table.
There are other advantages to using the rich text edit object. It offers users a report mechanism for quick, simple reports that are easily printed or ported to most applications. Users can edit the output and add notations with relative ease. There are more methods with more muscle, but few of them can be implemented in 15 minutes, and none of them offer as much flexibility.