Delphiversions.inc

 Delphi  Comments Off on Delphiversions.inc
Jul 262014
 

In the olden days[tm] there was a project for maintaining a Delphiversions.inc file which created human readable conditional defines from the VERxx conditional defines of the Delphi compilers. Since it seems to have vanished from the face of the Internet[tm], I have just added a new page to the Delphi Wiki which you can just copy to a file and use. It allows you to do things like this:

{$include 'Delphiversions.inc'}
{$IFDEF Delphi6up}
{$WARN unsafe_type off}
{$WARN unsafe_code off}
{$WARN unsafe_cast off}
{$ENDIF}

(This turns the annoying dotNET warnings off that are just so unnecessary for native code.)

For a more comprehensive version of such a file, see jedi.inc from Project JEDI.

 Posted by on 2014-07-26 at 13:28

Displaying a multi line error message without wasting space

 Delphi  Comments Off on Displaying a multi line error message without wasting space
Jul 232014
 

I have got a form that usually displays outputs from various sources, each of which has a frame for its own purpose. Now, if an error occurs with any of theses sources, I want to display that error message on the bottom of the form, just above the panel that contains the action buttons. It looks like this:

    P2  **********************************
        *      *          *              *
        *  F1  *   F2     *      F3      *
        *      *          *              *
        **********************************
        *                                *
        *            F4                  *
    L1  *                                *
        **********************************
    P1  *         buttons go here        *
        **********************************

F1, F2, F3, F4 are the output frames for four of the sources, there can be many of various sizes.

My current solution is two panels and a label:
P2 is client aligned and contains all the frames
L1 is bottom aligned and normally invisible
P1 is bottom aligned

If an error occurs, I set the label’s caption and make it visible.

That works fine, if the error messages are short and fit within the window. Unfortunately, that’s not always the case. If the messages are longer, I would like to automatically word wrap them and increase the label’s hight.

I was prepared to start using Canvas.TextExtend etc. to write code for all of that, but it turned out there is a very simple solution:

Set the label’s WordWrap property to true and make sure that its AutoSize property is also true. That way the VCL will automatically take care of both, the word wrapping and increasing the label’s height if necessary.

(I think TLabel.WordWrap was introduce with Delphi 2007, but it might be even older.)

 Posted by on 2014-07-23 at 15:08

Adding fields to a TDataset in code

 Delphi, dzLib  Comments Off on Adding fields to a TDataset in code
Jul 122014
 

The Delphi IDE allows you to add fields to a TDataset (descendant e.g. TTable, TQuery, TAdoTable etc.) by right clicking on the component and selecting “Add Field” or “New Field”. For a particular project I didn’t want to do that because I kept changing the query for which I want to add the fields. But since there was one calculated field I had to add fields to the dataset otherwise the OnCalcFields event wouldn’t be called. So I ended up adding the fields in code.

Basically that’s easy, you just create a TField descendant (e.g. TStringField,TAutoIncField, TDateTimeField etc.) set the field name and dataset and that’s it. But there are some pitfalls:

var
  fld: TField;
  ds: TDataset;
begin
  // init Dataset
  // ...
  // add fields
  fld := TWideStringField.Create(ds);
  fld.Name := '';
  fld.FieldName := 'TE_Employee';
  ds.Fields.Add(fld);

While this looks fine, it will not work. You will get the error “Field does not have a dataset” (or similar, I don’t remember the exact error message). The reason is that adding the field to the Dataset’s Fields collection does not automatically tell the field to which dataset it belongs. For that you need to assign the field’s Dataset property.

var
  fld: TField;
  ds: TDataset;
begin
  // init Dataset
  // ...
  // add fields
  fld := TWideStringField.Create(ds);
  fld.Name := '';
  fld.FieldName := 'TE_Employee';
  fld.Dataset := ds;
  ds.Fields.Add(fld);

If you write it like this, it will seem to work, but you will get an access violation when the dataset is being destroyed. Can you spot why?

I had to trace into the RTL sources to find the problem: The field was added to the Fields collection twice, because setting its Dataset property also adds it to the Fields collection. So, when the field’s collection got freed, the Field was freed twice, resulting in the AV.

So the correct and working code is this:

var
  fld: TField;
  ds: TDataset;
begin
  // init Dataset
  // ...
  // add fields
  fld := TWideStringField.Create(ds);
  fld.Name := '';
  fld.FieldName := 'TE_Employee';
  fld.Dataset := ds; // this automatically adds the field to the Dataset's Fields collection.

You might be wondering about the line

  fld.Name := '';

I set the name of all components created in code to an empty string to avoid name collisions with any existing components or even a different instance of the component created by the same code. “A component with the name Edit1 already exists.” has bitten me too often.

Since I also used a TDbGrid to display the dataset, I also added Columns to the DBGrid to set the caption and column width. And since I am lazy (as any good programmer should be), I moved most of that code into a (sub-) procedure to make the code more readable (actually I did it so I had less to type, but the result is the same):

var
  TheDataset: TDataset;
  TheGrid: TDbGrid;

procedure TMyForm.InitDbGrid;

  procedure AddField(_fld: TField; const _Fieldname: string; const _Caption: string; _Width: Integer);
  var
    Col: TColumn;
  begin
    _fld.FieldName := _Fieldname;
    _fld.ReadOnly := True;
    _fld.Dataset := TheDataset;
    Col := TheGrid.Columns.Add;
    Col.Expanded := False;
    Col.FieldName := _Fieldname;
    Col.Title.Caption := _Caption;
    Col.Width := _Width;
  end;

begin
  AddField(TAutoIncField.Create(nil), 'SeNr', _('Number'), 50);
  AddField(TDateTimeField.Create(nil), 'TeArbeitstag', _('Date'), 70);
  AddField(TWideStringField.Create(nil), 'SeVon', _('Start'), 40);
  AddField(TWideStringField.Create(nil), 'SeBis', _('End'), 40);
  fld := TWideStringField.Create(nil);
  fld.FieldKind := fkCalculated;
  AddField(fld, 'Stunden', _('Hours'), 50);
  AddField(TWideStringField.Create(nil), 'PROJ_IDENT', _('Project'), 50);
  AddField(TWideStringField.Create(nil), 'SSchluessel', _('Activity'), 50);
  AddField(TWideStringField.Create(nil), 'SeBeschreibung', _('Description'), 200);
  ResizeGridColumns;
end;

And just to show why this code is actually useful, here are the ResizeGridColumns method and TheDatasetCalcFields and FormResize events.

procedure TMyForm.ResizeGridColumns;
var
  cnt: Integer;
  TotalWidth: Integer;
  i: Integer;
begin
  TotalWidth := 0;
  cnt := TheGrid.Columns.Count;
  for i := 0 to cnt - 2 do begin
    TotalWidth := TotalWidth + TheGrid.Columns[i].Width;
  end;
  TheGrid.Columns[cnt - 1].Width := TheGrid.ClientWidth - TotalWidth - 50;
end;

procedure TMyForm.FormResize(Sender: TObject);
begin
  ResizeGridColumns;
end;

procedure TMyForm.TheDatasetCalcFields(Dataset: TDataSet);
var
  Von: TdzNullableTime;
  Bis: TdzNullableTime;
begin
  Von.AssignVariant(Dataset['seVon']);
  Bis.AssignVariant(Dataset['seBis']);
  Dataset.FieldByName('Stunden').Value := (Bis - Von).ToHourStr(2);
end;

(TdzNullableTime is an extended record type from my dzlib library which is available from sourceforge if you are interested. It is declared in u_dzNullableTime.pas.

 Posted by on 2014-07-12 at 17:42

GORM experimental release

 Delphi  Comments Off on GORM experimental release
Jul 052014
 

GORM is an editor for .po files that was originally written by Lars Dybdahl and to which I have been contributing quite a few features in recent years. Since Lars is too busy to do a release and currently even the original download doesn’t seem to work, I have compiled the current sources and provide a download here:

Gorm_2014-07-05.zip

In addition to the features described on the Gorm homepage this new version can do the following:

  • Edit the po file header.
  • Auto translate, using Google translate (not sure whether it is still available) or Microsoft Translator. Both need an application key that is not part of Gorm.
  • Auto translate from a local translation repository, which is an MS Access database, one per language, that can be shared by multiple developers
  • Auto translate using translation memory, that is a po file that is automatically generated.
  • Load and save translations to be ignored from a ignore.po file.
  • In addition to English, I provide it with German localization, that, of course, is created with Gorm itself.

The sources are available from the dxgettext repository on sourceforge.
Of course I like Gorm much better than PoEdit and other alternatives. If there are any bugs, that’s probably my fault.

 Posted by on 2014-07-05 at 18:59

Comparing Variants

 Delphi  Comments Off on Comparing Variants
Jul 012014
 

I hate Variants, did I say that before? They just make everything more complex than necessary.

Did you ever try to compare two variants? E.g. you have got one that contains an empty string (”) and another that contains a date. Now you want to know whether they are equal or not. Simple?

  v1 := '';
  v2 := EncodeDate(2014,07,01);
  if v1 <> v2 then
    WriteLn('not equal')
  else
    WriteLn('equal');

Not so. The code above raised a EVariantConversionError exception because it could not convert an empty string into a date.

Let me introduce you to two Delphi RTL functions from Variants.pas:

VarSameValue and VarCompareValue.

(I bet you didn’t know about them, did you?)

Assuming these functions work as described, the above must be written like this:

  v1 := '';
  v2 := EncodeDate(2014,07,01);
  if not VarSameValue(v1, v2) then
    WriteLn('not equal')
  else
    WriteLn('equal');

Unfortunately they fail as well with exactly the same error. So all that’s left is implementing it myself.

function VarIsSame(_A, _B: Variant): Boolean;
var
  LA, LB: TVarData;
begin
  LA := FindVarData(_A)^;
  LB := FindVarData(_B)^;
  if LA.VType <> LB.VType then
    Result := False
  else
    Result := (_A = _B);
end;

Note that this too might not do what you expect:

  VarIsSame('3', 3);

will return false! That’s why it’s called VarIsSame rather than VarIsSameValue. It doesn’t try to convert variants of different types, it just checks whether they are the same.
For my use case this is exactly what I need.

Did I mention that I hate Variants?

 Posted by on 2014-07-01 at 13:40