May 1998
by Jim Bailey

Date and time entries

How can I enter the date and time in a DateTime field? I know 05/01/1998 08:15:00 will work, but that's a cumbersome entry. I'd also like to set a default entry on new records using the current date and time.

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.

Day zero is what day?

Most Windows-based systems use the integer value 0 to represent midnight on December 30, 1899. Windows sees noon on that day as the value .5, or one half. You can see this for yourself with a test application. Execute the following code as part of a button's OnClick event:
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.

Cloning the date field

Let's talk about getting a date and time value plugged into a single field from two entry points. The solution is elegant, in my opinion--and it works, too. It requires the use of CachedUpdates, a TQuery object, and persistent fields. It also uses the OnSetText and OnGetText events. I used a Paradox table with a TimeStamp field for testing. A sample application, including source code, is available at www.cobb.com/cpb, as part of the file may98.zip. I'll reference the field in this article as a DateTime field named StartedOn.

Here's the TQuery's SQL property:

SELECT e.StartedOn,
e.StartedOn AS StartedTime 
FROM 'employee.db' e
Notice 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.

Disabling new records

I have a grid in my application. I want users to be able to edit the information, but I don't want them to add new records. Is there a property I can set on the TDBGrid to disable new records?

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.

Porting data To Excel

I occasionally need to move data from my C++Builder application to a spreadsheet, word processor, or other application. Is there a quick way to get information out of my database and into another application?

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.