0

I am trying to develop a new TEdit-Component.

TDBFilterEdit = class(TEdit)

The component is meant to Filter an associated DataSet based on the string that is entered in its Edit-Field.

this is what my component looks like:

type
TDBFilterEdit = class(TEdit)
  private
    { Private-Deklarationen }
    fFilter:String;
    fDataSource:TDataSource;
    fDataSet:TDataSet;
    fText:string;
  protected
    { Protected-Deklarationen }
    procedure SetFilter(value:String);
    procedure SetDS(value:TDataSource);
    procedure FilterRecords(DataSet:TDataSet; var Accept:Boolean);
    procedure Change(Sender:TObject);
    procedure SetText(value:String);
  public
    { Public-Deklarationen }
    constructor Create(AOwner:TComponent);
  published
    { Published-Deklarationen }
    property Text:String read fText write SetText;
    property Filter:String read fFilter write SetFilter;
    property DataSource:TDataSource read fDataSource write SetDS;
  end;

Now, I am pretty Novice when it comes to component-development. My first Idea was to Override the OnFilterRecord-method of the Dataset as soon as the DataSource gets assigned to my component and trigger it whenever the text of my Edit-component changes.

procedure TDBFilterEdit.SetDS(value:TDataSource);
var
  myaccept:Boolean;
begin
  fDataSource:=value;
  fDataSet:=fDataSource.DataSet;
  if fDataSet=nil then Exit;

  fDataSet.OnFilterRecord:=FilterRecords;
  if Filter<>'' then fDataSet.OnFilterRecord(fDataSet,myaccept);
end;

My Problem is, I don't know how to make the component aware that its Text-property got updated. I tried overriding the OnChange-Method with following code

procedure TDBFilterEdit.Change(Sender:TObject);
begin
  Filter:=Text;
  inherited Change();
end;

however, to no avail so far.

1
  • The usual way of doing this is to write a method (that might be called DoOnChange) that's called within your Change and does whatever you want, including invoking any special event handlers that you might want to provide. Have a look at the various DoOnxxxx methods in the VCL and you should soon get the idea. Commented Feb 24, 2017 at 17:20

1 Answer 1

4

My Problem is, I don't know how to make the component aware that its Text-property got updated.

The Text property is inherited from TControl. When the property value changes, TControl issues a CM_TEXTCHANGED notification message to itself. Descendant classes can handle that message by either:

  1. using a message handler:

    type
      TDBFilterEdit = class(TEdit)
        ...
        procedure CMTextChanged(var Message: TMessage); message CM_TEXTCHANGED;
        ...
      published
        ...
        // DO NOT redeclare the Text property here!
        // It is already published by TEdit...
      end;
    
    procedure TDBFilterEdit.CMTextChanged(var Message: TMessage);
    begin
      inherited;
      // use new Text value as needed...
      Filter := Text;
    end;
    
  2. overriding the virtual WndProc() method.

    type
      TDBFilterEdit = class(TEdit)
        ...
      protected
        ...
        procedure WndProc(var Message: TMessage); override;
        ...
      end;
    
    procedure TDBFilterEdit.WndProc(var Message: TMessage);
    begin
      inherited;
      if Message.Msg = CM_TEXTCHANGED then
      begin
        // use new Text value as needed...
        Filter := Text;
      end;
    end;
    

As for the rest of your component, it should look more like this:

type
  TDBFilterEdit = class(TEdit)
  private
    { Private-Deklarationen }
    fDataSource: TDataSource;
    fDataSet: TDataSet;
    fFilter: String;
    procedure FilterRecords(DataSet: TDataSet; var Accept: Boolean);
    procedure SetDataSource(Value: TDataSource);
    procedure SetDataSet(Value: TDataSet);
    procedure SetFilter(const Value: String);
    procedure StateChanged(Sender: TObject);
    procedure UpdateDataSetFilter;
  protected
    { Protected-Deklarationen }
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    procedure WndProc(var Message: TMessage); override;
  public
    { Public-Deklarationen }
    destructor Destroy; override;
  published
    { Published-Deklarationen }
    property DataSource: TDataSource read fDataSource write SetDataSource;
    property Filter: String read fFilter write SetFilter;
  end;

...

destructor TDBFilterEdit.Destroy;
begin
  SetDataSource(nil);
  inherited;
end;

procedure TDBFilterEdit.FilterRecords(DataSet: TDataSet; var Accept: Boolean);
begin
  // ...
end;

procedure TDBFilterEdit.Notification(AComponent: TComponent; Operation: TOperation);
begin
  inherited Notification(AComponent, Operation);
  if Operation = opRemove then
  begin
    if AComponent = fDataSource then
    begin
      SetDataSet(nil);
      fDataSource := nil;
    end
    else if AComponent = fDataSet then
    begin
      fDataSet := nil;
    end;
  end;
end;

procedure TDBFilterEdit.SetFilter(const Value: String);
begin
  if fFilter <> Value then
  begin
    fFilter := Value;
    UpdateDataSetFilter;
  end;
end;

procedure TDBFilterEdit.SetDataSource(Value: TDataSource);
begin
  if fDataSource <> Value then
  begin
    SetDataSet(nil);

    if fDataSource <> nil then
    begin
      fDataSource.RemoveFreeNotification(Self);
      fDataSource.OnStateChange := nil;
    end;

    fDataSource := Value;    

    if fDataSource <> nil then
    begin
      fDataSource.FreeNotification(Self);
      fDataSource.OnStateChange := StateChanged;
      SetDataSet(fDataSource.DataSet);
    end;
  end;
end;

procedure TDBFilterEdit.SetDataSet(Value: TDataSet);
begin
  if fDataSet <> Value then
  begin
    if fDataSet <> nil then
    begin
      fDataSet.RemoveFreeNotification(Self);
      fDataSet.OnFilterRecord := nil;
    end;

    fDataSet := Value;

    if fDataSet <> nil then
    begin
      fDataSet.FreeNotification(Self);
      fDataSet.OnFilterRecord := FilterRecords;
      UpdateDataSetFilter;
    end;
  end;
end;

procedure TDBFilterEdit.StateChanged(Sender: TObject);
begin
  if fDataSource.DataSet <> fDataSet then
    SetDataSet(fDataSource.DataSet);
end;

procedure TDBFilterEdit.UpdateDataSetFilter;
begin
  if fDataSet <> nil then
  begin
    fDataSet.Filter := fFilter;
    fDataSet.Filtered := fFilter <> '';
  end;
end;

procedure TDBFilterEdit.WndProc(var Message: TMessage);
begin
  inherited;
  if Message.Msg = CM_TEXTCHANGED then
    Filter := Text;
end;

UPDATE: sorry, my bad. The CM_TEXTCHANGED message is only sent when the Text property is updated programmably in code. To detect when the user changed the text, you need to handle the Win32 EN_CHANGE notification instead:

  1. using a message handler:

    type
      TDBFilterEdit = class(TEdit)
        ...
        procedure CMTextChanged(var Message: TMessage); message CM_TEXTCHANGED;
        procedure CNCommand(var Message: TWMCommand); message CN_COMMAND;
        ...
      published
        ...
        // DO NOT redeclare the Text property here!
        // It is already published by TEdit...
      end;
    
    procedure TDBFilterEdit.CMTextChanged(var Message: TMessage);
    begin
      inherited;
      // use new Text value as needed...
      Filter := Text;
    end;
    
    procedure TDBFilterEdit.CNCommand(var Message: TWMCommand);
    begin
      inherited;
      if Message.NotifyCode = EN_CHANGE then
      begin
        // use new Text value as needed...
        Filter := Text;
      end;
    end;
    
  2. overriding the virtual WndProc() method.

    type
      TDBFilterEdit = class(TEdit)
        ...
      protected
        ...
        procedure WndProc(var Message: TMessage); override;
        ...
      end;
    
    procedure TDBFilterEdit.WndProc(var Message: TMessage);
    begin
      inherited;
      case Message.Msg of
        CM_TEXTCHANGED: begin
          // use new Text value as needed...
          Filter := Text;
        end;
        CN_COMMAND: begin
          if TWMCommand(Message).NotifyCode = EN_CHANGE then
          begin
            // use new Text value as needed...
            Filter := Text;
          end;
        end;
      end;
    end;
    

In fact, TCustomEdit already handles EN_CHANGE for you, and will call its virtual Change() method (to fire its OnChange event), which you can override:

    type
      TDBFilterEdit = class(TEdit)
        ...
      protected
        ...
        procedure Change; override;
        ...
      end;

    procedure TDBFilterEdit.Change;
    begin
      inherited;
      // use new Text value as needed...
      Filter := Text;
    end;
Sign up to request clarification or add additional context in comments.

4 Comments

Thank you for your answer, it helped me get further to my goal. I chose method 1 and made a CMTextChanged-message-Handler, however it won't fire. I tried a CM_MOUSEENTER-message Handler, which DID fire though, but the Text property remained empty. Do I need to implement/override the other procedures like Notification, StateChange and WndProc? Also, assigning the Datasource works, but for some strange reason, the DataSet of my DataSource remains nil
I found out that the correct message-handler I'm looking for is CN_COMMAND, now I can get the procedure to fire for the first letter I enter into the edit-field. any idea how to invoke this on every button press?
I figured it out, the dataset only executes the OnFilterRecord when it switches from filtered:=false to filtered:=true. since I didn't reset the filtered-property anywhere, it's value was always true after I entered the first letter into my edit-field. now the component behaves almost as intended, all that is left is researching the mysterious behaviour of the "nil-Dataset" when assigning the Datasource (maybe I'll just use a Dataset directly instead of a datasource) and extensive testing of my FilterRecords-method.
Sorry, my bad about forgetting the EN_CHANGE notification. I have updated my answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.