I use DBGrid with Delphi XE2 to show some query results. In the result some (integer) Fields represent a duration in seconds that I want to show in some format like "hh:mm:ss" I don't know in advance the fields order since the queries may be different but I know the fields name that I want to format. What is the most efficient way to do it ?
2 Answers
While answer by @AndreaBoc is correct (but only answers one of your questions), it is somewhat repetitive: copy-pasting the same calculations looks an anti-pattern to me.
Repetitiveness may be removed by using http://docwiki.embarcadero.com/Libraries/XE4/en/System.Math.DivMod
procedure TForm1.PrettyPrintSeconds(Sender: TField;
var Text: string; DisplayText: Boolean);
var
hh,mm,ss: Word;
begin
DivMod( Sender.AsInteger, 3600, hh, mm );
DivMod( mm, 60, mm, ss );
Text := Format('%.2d:%.2d:%.2d',[hh,mm,ss]);
end;
Additionally, if you really want be efficient ( but then why using TDataset? ) you may replace too flexible Format function with 3 temp string variables and old-style Str procedure like outlined at http://docwiki.embarcadero.com/Libraries/XE4/en/System.SysUtils.IntToStr :-)
This should be set for http://docwiki.embarcadero.com/Libraries/XE2/en/Data.DB.TField.OnGetText
That is better than copy-pasting same operations twice and more, increasin both CPU time and chance to make silly typo.
Then there is the next part of question: don't know the fields order... but I know the fields name that I want to format. That means that you should tune this field after the query opened:
procedure TDataModule1.Query1AfterOpen(DataSet: TDataSet);
var F: TField;
begin
F := DataSet.FindField('MY-Name-For-Time');
if F = nil
then (* .... no such field - do something about it .... *)
else F.OnGetText := TForm1.PrettyPrintSeconds;
end;
- http://docwiki.embarcadero.com/Libraries/XE2/en/Data.DB.TDataSet.AfterOpen
- http://docwiki.embarcadero.com/Libraries/XE2/en/Data.DB.TDataSet.FindField
- http://docwiki.embarcadero.com/Libraries/XE2/en/Data.DB.TDataSet.FieldByName
From BeforeClose event you may remove (set to nil) this event handler similarly. Just for that rare check that the same TField object gets re-used for different purpose later. It should not happen except for some very exotic situations, but just to "clean after oneself".
There is less effective (from CPU point of view) but more VCL in spirit approach as well.
For the query in its AfterOpen event: look for the given field name like above, and if found then:
- save the result for the later use (as linear string search inside
FieldByNameis not the most effecient operation per se, so should not repeat it for every value displaying)- Proper but somewhat tedious way would be to make new
TTimeFieldproperty for the query owner. - under Win32 you may (ab)use
Int32propertyTDataSet.Tag, storing there the pointer via type-cast. That is fragile, unsafe and unportable code, but storingInteger,pointerandTObjectvalues in any of those variables is somewhat "old Delphi style" ( like in TStringList.Objects ) and it works, if you're careful and only interested in Win32.
- Proper but somewhat tedious way would be to make new
- add a calculated field of datetime type
- set
'hh:nn:ss'as format for that new field: http://docwiki.embarcadero.com/Libraries/XE2/en/Data.DB.TDateTimeField.DisplayFormat - Make the new field friendly to DBGrid and other db-aware controls
- Make the original field invisible or remove its column from the grid - you have the new field instead.
Then set OnCalcFields on the query like:
// 1. Delphi TDateTime is double and 1 day equals to 1.0
// 2. Multiplication is more efficient than division - do it by pen and paper and see
procedure TDataModule1.Query1AfterOpen(DataSet: TDataSet);
const coeff = 1.0 / ( 24 * 60 * 60 );
begin
MyCalcField.AsTime := MySourceField.AsInteger * coeff;
end;
Then you would be able to use the new virtual field in every place where Delphi routines would expect TDateTime value.
Comments
You can use OnGetText event of the field for you request:
procedure TForm1.Table1secondsGetText(Sender: TField;
var Text: string; DisplayText: Boolean);
var
seconds,hh,mm,ss:Integer;
begin
seconds := Sender.AsInteger;
hh := seconds div 3600;
mm := (seconds - (hh * 3600)) div 60;
ss := (seconds - (hh * 3600) - (mm * 60));
Text := Format('%.2d:%.2d:%.2d',[hh,mm,ss]);
end;
for example 185 seconds will be displayed in this way: 00:03:05
DisplayFormatproperty check a sample here stackoverflow.com/questions/3619835/…