Jump to content
dormky

How do I know if the click is an actual user click ?

Recommended Posts

I have checkboxes that get initialized in FormCreate. This means that the OnClick callback gets called (SetChecked does that). How can I prevent this ? Either by detecting whether the callbakc is triggered by an actual click or by setting the value without triggering the callback.

I'm using checkboxes in this instance but this is of course a general question for other controls too.

Thanks !

Edited by dormky

Share this post


Link to post

It affects all TButtonControl descendants of which TCheckBox is just one.

 

The easiest way to avoid that is to use actions. 

 

Another way would be to make use of the (protected) ClicksDisabled property, which skips the call to the event handler. That is basically the same as what the actions do.

  • Like 2

Share this post


Link to post

Several ways to do that.

1. A small procedure in your toolbox:

 

procedure CheckBoxPreset(ACheckBox: TCheckBox; AChecked: Boolean);
var
  LBckEvent: TNotifyEvent;
begin
  LBckEvent := ACheckBox.OnClick;
  ACheckBox.OnClick := nil;
  ACheckBox.Checked := AChecked;
  ACheckBox.OnClick := LBckEvent;
end;

 

2. Use a class helper

 

type
  TCustomCheckBoxHelper = class helper for TCustomCheckBox
    procedure ForceChecked();
    procedure ForceUnchecked();
    procedure Preset(AValue: Boolean);
  end;
Implementation 

  procedure TCustomCheckBoxHelper.ForceChecked();
  begin
    with Self do
      FState := cbChecked;
  end;

  procedure TCustomCheckBoxHelper.ForceUnchecked();
  begin
    with Self do
      FState := cbUnchecked;
  end;

  procedure TCustomCheckBoxHelper.Preset(AValue: Boolean);
  begin
    if AValue then
      ForceChecked()
    else
      ForceUnchecked();
  end;

Then access the helper methods like a regular method of checkbox:

 

  cbMyCheckbox.ForceChecked();
  cbMyCheckbox.ForceUnchecked();
  cbMyCheckbox.Preset(True);

 

Edited by Patric van de Pol

Share this post


Link to post

Simply setting FState doesn't reflect the change in the control while its handle is allocated. That may work for some initialization during FormCreate before the handle allocated, but is no general solution.

Share this post


Link to post
10 hours ago, dormky said:

I have checkboxes that get initialized in FormCreate. This means that the OnClick callback gets called (SetChecked does that). How can I prevent this ?

You can either:

  • not assign the OnClick handler at design-time, but at runtime instead after you are done initializing the control.
  • set a variable somewhere that the event handler can look at, and then clear that variable when finished.

Share this post


Link to post

I have a var in the form class named 'ignore_this' and set it to true before doing something that will fire an event automatically that I want ignored, then set it false afterwards, and I put "if ignore_this then Exit;" at the top of the event handler. 

 

Sometimes there's so much stuff like that in the OnFormCreate that I just have a 'creating_form' variable that's set True at the top and False at the bottom of OnFormCreate so event handlers can get bypassed. 

Share this post


Link to post

I have a loading flag variable that I set in the form create and then check in the event handlers - clear the flag when done initialising the controls. Crude, but simple.

Share this post


Link to post

One can also use the csUpdating flag in ComponentState, which is controlled by the Updating and Updated methods. It works for all TComponent descendants.

 

But now we are in a much wider realm than the original OnClick problem describes.

Share this post


Link to post

Sorry for the delay. Here's how it ended up, using @Uwe Raabe's proposition of setting ClicksDisabled to True :

uses
  Vcl.StdCtrls;

type
  THelperCheckBox = class helper for TCheckBox
    procedure SetValue(const value: Boolean);
  end;

implementation

procedure THelperCheckBox.SetValue(const value: Boolean);
begin
  ClicksDisabled := True;
  try
    Checked := value;
  finally
    ClicksDisabled := False;
  end;
end;

For now I've set it for checkboxes only, but will probably copy/extend it to other components or just TButtonControl. Maybe with runtime checks of the actual class of the instance the method is called on.

 

Thanks everyone.

Share this post


Link to post
On 8/4/2023 at 9:48 AM, dormky said:

I have checkboxes that get initialized in FormCreate. This means that the OnClick callback gets called

That doesn't apply to checkboxes alone. Radio buttons, radio groups, checked menu items, actions, OnResize events, you name it. Some forms have plenty of details to initialize. We have been using update counters for that, since more than 20 years. Like, every TForm that does some initialization which may auto-trigger events, has an IsUpdating property. The setter increases/decreases the value, the getter returns a boolean. If IsUpdating returns true (> 0), the checkboxes do not execute their OnChange event, manually arranged panels do not update, etc.

constructor TForm1.Create(AOwner: TObject);
begin
  inherited;
  FIsUpdating := 0;
  IsUpdating := true; //increases FIsUpdating
  try
    //do your init here
  finally
    IsUpdating := false;
  end;
end;

procedure TForm1.SetIsUpdating(value: boolean);
begin
  if value then
    inc(FIsUpdating)
  else 
    dec(FIsUpdating);
end;

function TForm1.GetIsUpdating: boolean;
begin
  result := FIsUpdating > 0;
end;

A counter is more flexible and error-tolerant than using the csUpdating flag in ComponentState. It can be used to update corresponding controls in an OnChange event as well, without triggering their OnChange. The counter enables nested IsUpdating without worries. The only thing to remember is to consequently use try-finally.

Edited by Alexander Halser

Share this post


Link to post
8 hours ago, Alexander Halser said:

A counter is more flexible and error-tolerant than using the csUpdating flag in ComponentState.

csUpdating is for use within components, and it's not just used for initialization. Using it to manage initialization in forms is ill-advised.

 

There are three "levels" of initialization in forms: FormCreate, FormShow, and FormActivate. They occur at different times, and afford a lot of control over timing issues. 

 

If you want to see some really unpredictable timing issues with events, take a look at TTreeView and TListView controls. If you put some trace logic in the event handlers, you'll notice that several are called twice for every change. That can cause really strange things to happen if the handler does anything other than update a spot in the control.

  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×