Setting focus on TWinControl after an Exception?

129 Views Asked by At

I have the following procedure in Delphi 6:

procedure TfrmUserCRUD.ShowErrorWithFocus(Message: string; FocusedField: TWinControl);
begin
  try
    raise Exception.Create(Message);
  except
    on E : Exception do
    begin
      ShowMessage(E.Message);
      FocusedField.SetFocus;
      Abort;
    end;
  end;
end;

This is how I'm calling the function:

procedure TfrmUserCRUD.ValidateFields();
begin
  if not(chkListProducts.Checked) and (chkUsesProductValue.Checked) then
    ShowErrorWithFocus('You need to use the product list if you wanna use the product value.', chkListProducts);

  if (chkUsesProductValue.Checked) and (edtProductValue.KeyValue  = nil) then
    ShowErrorWithFocus('The field "value of product" needs to be filled', edtProductValue);   
end;

The objective of this procedure is to receive an error message and one of the fields from the form (CheckBox, Grid, Edit, etc) and whenever the function is called, it's supposed to show the message on screen with the error, then focus the field that it received like the system was highlighting it, like in those pictures:

image

image

I've been trying to use different methods so it could focus on the field, but if I use raise, the program loses the field variable, if I use Abort, the program doesn't do anything. I've also tried different ways of focusing the TField, like using Perform() and event updates, but it didn't worked as intended.

Any ideas of what I could do to resolve this problem?

1

There are 1 best solutions below

3
Remy Lebeau On

It makes absolutely no sense to raise an exception just to immediately catch it. You don't need the try..except at all:

procedure TfrmUserCRUD.ShowErrorWithFocus(Message: string; FocusedField: TWinControl);
begin
  ShowMessage(Message);
  FocusedField.SetFocus;
  Abort;
end;

That being said, depending on the context in which ShowErrorWithFocus() is being called (which you did not show), it may not be valid/prudent to set focus right away (the OS/VCL can be really finicky about focus changes done out of order), so you may need to delay the focus until after the message queue has settled down first. PostMessage() will work fine for that, ie you could post a message to the Form, where you pass the FocusedField pointer in the message's lParam, and then the message handler can call SetFocus() on that object, eg:

const
  WM_APP_SETFOCUS = WM_APP + 1;

procedure TfrmUserCRUD.ShowErrorWithFocus(Message: string; FocusedField: TWinControl);
begin
  ShowMessage(Message);
  //FocusedField.SetFocus;
  PostMessage(Handle, WM_APP_SETFOCUS, 0, LPARAM(FocusedField));
end;

procedure TfrmUserCRUD.WndProc(var Message: TMessage);
begin
  if Message.Msg = WM_APP_SETFOCUS then
    TWinControl(Message.LParam).SetFocus
  else
    inherited;
end;

UPDATE:

You didn't show where you are calling ValidateFields() from, exactly. Calling SetFocus() while input focus is already in progress of being changed can cause UI problems. But since you mentioned in a comment that you are calling it in a Button OnClick event, then in that context you can simply SetFocus() the desired control first and then raise (and not catch!) the error message. Let the VCL display the error message to the user for you, and then the focused control will regain input focus after the error dialog is dismissed, eg:

procedure TfrmUserCRUD.SomeButtonClick(Sender: TObject);
begin
  ValidateFields;
end;

procedure TfrmUserCRUD.ValidateFields;
begin
  ...
  ShowErrorWithFocus(...);
  ...
end;

procedure TfrmUserCRUD.ShowErrorWithFocus(Message: string; FocusedField: TWinControl);
begin
  FocusedField.SetFocus;
  raise Exception.Create(Message);
end;