1

I am using an in-memory TClientDataSet with a TStringField column which contains folders path (Delphi 7). When I create an index on this column the order is not what I am looking for. As an example I get :

c:\foo
c:\fôo\a
c:\foo\b

when I would like this order :

c:\foo
c:\foo\b
c:\fôo\a

So I searched a way to use my own compare field function.

Based on this RRUZ answer How to change the implementation (detour) of an externally declared function I tried the following :

type
  TClientDataSetHelper = class(DBClient.TClientDataSet);
  ...
  MyCDS : TClientDataSet;
  ...
// My custom compare field function
function FldCmpHack
(
  iFldType  : LongWord;
  pFld1     : Pointer;
  pFld2     : Pointer;
  iUnits1   : LongWord;
  iUnits2   : LongWord
): Integer; stdcall;
begin
  // Just to test
  Result := -1;
end;
...
---RRUZ code here---
...
procedure HookDataCompare;
begin
  HookProc
  (
    (MyCDs as TClientDataSetHelper).DSBase.FldCmp, <== do not compile !!!
    @FldCmpHack, 
    FldCmpBackup
  ); 
end;

When I try to compile I get an error (MyCDs as TClientDataSetHelper).DSBase.FldCmp : not enough actual parameters

I do not understand why this does not compile. Could you please help me ?

Is it even possible to "detour" IDSBase.FldCmp in DSIntf.pas ? Am i totally wrong ?

Thank you

EDIT

Finally, thanks to Dsm answer, I transformed the TStringFieldcolumn into a TVarBytesField in order to avoid doubling the buffer. Plus, when a TVarBytesField is indexed the order is based on the bytes value so I get the order I want. For having all child folders after a parent folder and before the next parent folder (c:\foo.new after c:\foo\b), I patched TVarBytesFieldlike this :

TVarBytesField = class(DB.TVarBytesField)
protected
  function GetAsString: string; override;
  procedure GetText(var Text: string; DisplayText: Boolean); override;
  procedure SetAsString(const Value: string); override;
end;

function TVarBytesField.GetAsString: string;
var
  vBuffer : PAnsiChar;
  vTaille : WORD;
  vTexte  : PAnsiChar;
  vI      : WORD;
begin
  Result := '';
  GetMem(vBuffer, DataSize);
  try
    if GetData(vBuffer) then
    begin
      vTaille := PWORD(vBuffer)^;
      vTexte := vBuffer + 2;
      SetLength(Result, vTaille);
      for vI := 1 to vTaille do
      begin
        if vTexte^ = #2 then
        begin
          Result[vI] := '\';
        end
        else
        begin
          Result[vI] := vTexte^;
        end;
        Inc(vTexte);
      end;
    end;
  finally
    FreeMem(vBuffer);
  end;
end;

procedure TVarBytesField.GetText(var Text: string; DisplayText: Boolean);
begin
  Text := GetAsString;
end;

procedure TVarBytesField.SetAsString(const Value: string);
var
  vBuffer : PAnsiChar;
  vTaille : WORD;
  vTexte  : PAnsiChar;
  vI      : WORD;
begin
  vBuffer := AllocMem(DataSize);
  try
    vTaille := WORD(Length(Value));
    PWORD(vBuffer)^ := vTaille;
    vTexte := vBuffer + 2;
    for vI := 1 to vTaille do
    begin
      if Value[vI] = '\' then
      begin
        vTexte^ := #2
      end
      else
      begin
        vTexte^ := Value[vI];
      end;
      Inc(vTexte);
    end;
    SetData(vBuffer);
  finally
    FreeMem(vBuffer);
  end;
end;
Community
  • 1
  • 1
NMD
  • 135
  • 11

1 Answers1

1

The message is telling you that FldCmp is a function, and it is expecting you to execute it, but it has not got enough parameters. I am sure that you already realised that and probably already tried to get the address of the function with the @ (like you do for FldCmpHack) and found that that does not work.

The reason for that is, I am afraid, that FldCmp is not a normal function. DSBase is actually an interface, which will have been assigned (looking at the source code) by a class factory. What you actually need is the real function itself and for that you need the real object that the class factory creates. And I am sorry, but I can't see any realistic way of doing that.

However, the DSBase field is only created if it has not been assigned, so you could, in theory, create your own IDSBase interface object, which is the way this type of problem is meant to be handled. That is a lot of work, though, unless you know class that the class factory produces and can descend from that.

A sneakier alternative is to override the Translate property and create some sort of hash (perhaps by translating the ASCII codes to their HEX values) so that the database keeps them in the right order

  TClientDataSetHelper = class(TClientDataSet)

  public
      function Translate(Src, Dest: PAnsiChar; ToOem: Boolean): Integer; override;

  end;
Dsm
  • 5,870
  • 20
  • 24
  • Thank you very much for your explanations. I tried to override `Translate` and, for testing purpose only, convert `ô`to #255 when `ToOem`is `True` and reverse when `ToOem` is `False`. And you are right `c:\fôo\a` comes after `c:\foo\b` ! So overriding `Translate` in a custom `TClientDataSet`is a serious alternative. And yes translating to hex values would do the job. However, `TStringField.GetValue` and `TStringField.SetAsString` use the same buffer for `Src` and `Dest`when calling `Translate` but I need `Dest` to be twice as long as `Src`. How can I do that ? – NMD Mar 16 '17 at 10:10
  • Well, I know how to do it, but it would be easier to explain if I had a bit more code, then I could test and prove, but essentially you need to create your own TStringField type (descended from the original) and override the GetValue, SetAnsiString and GetDataSize functions. So if you can supply how you define your fields I will be able to fill in the blanks. – Dsm Mar 16 '17 at 10:49
  • Sorry but I can't paste my code in a comment. At the moment, I am trying to find a solution in a very simple project before applying the modifications on the final project. I have a `TClientDataSet` with `Name=cds` and `IndexFieldNames=FOLDER` and one `TStringField` with `Name=cdsFOLDER`, `FieldName=FOLDER` and `Size=259`. I call `cds.CreateDataSet()` and then append folders `c:\foo`, `c:\fôo\a` and `c:\foo\b`. I get the result in a `DBgrid`. – NMD Mar 16 '17 at 11:31
  • You could modfy your question to include the code.That should be enough info. – Dsm Mar 16 '17 at 11:38
  • Actually, thinking about it all you should need to do is double the size in the database. Then your buffers will be big enough. You will need to do this anyway because it is the translated (double length) code that is stored. – Dsm Mar 16 '17 at 12:05
  • You will need to be careful in the translate routine, though, and create a temporary buffer to copy the source value from on entry. Otherwise, you risk overwriting a value before it is used. – Dsm Mar 16 '17 at 13:31
  • Excellent. :-). – Dsm Mar 16 '17 at 21:32