views:

605

answers:

6

We have a Delphi 6 application that uses a non modal form with in-grid editing. Within the FormClose event we check that the entries are square and prevent closure if they're not.

However, if the user clicks on the main form behind then the original form disappears behind (as you'd expect) but this allows the user to move to a new record on the main screen, without their changes in the grid having been validated.

I've tried the FormDeactivate event, which does fire but doesn't seem to have any mechanism to prevent deactivation (unlike the FormClose events Action parameter). I tried the OnExit from the grid, but it doesn't fire on deactivation. I tried trapping the WM_ACTIVATE message and setting Msg.Result = 1 but this has no effect (possibly because another WM_ACTIVATE message is being sent to the main form?).

So, I'm looking for ideas on how to (conditionally) prevent the deactivation of a form when the user clicks on another form. (PS I don't want to change the form style to fsStayOnTop)

Thanks

A: 

You may also introduce a state in your model that tracks if the window needs the focus as you describe it here and use onFocus handlers on the other forms that programmatically set the focus back to your grid window.

[Edit] Copy of my Comment:

You could register for the onShow Event of the forms with the grid. (If you implement it be sure to make it somehow configurable to minimize the grid dependency on the present layout of the application. Maybe by providing a method that is called by the forms which in turn triggers the event registration of the grid at the calling form for the onShow event)

To elaborate on the event registration:

You can attach event handlers programatically. There are plenty howtos in the net about this. I have no Delphi available here, so I can't copy working code now.

Pseudocode for programatically attaching the event!

myform.onShow=myGrid.formOnShowHandler;

formOnShowHandler has the same signature as the functions that are generated by the IDE for onShow Events. It has a parameter that you can use to figure out which form has called the handler so you can reuse the function and simply put the form in the background and show your gridform (which will be for example the parent of the grid) again.

Patrick Cornelissen
Thanks for the suggestion Patrick.I tried this in a test app by adding a public boolean "Editing" into my grid form (Form2), then added this to the main form's OnActivate event:if Form2.Editing then Form2.Show;It seems to work, but I'm hoping to control it in the grid form, to keep it neat, and to save checking on each FormActivate of the main form.
David Carle
You could register for the onShow Event of the forms with the grid. (If you implement it be sure to make it somehow configurable to minimize the grid dependency on the present layout of the application. Maybe by providing a method that is called by the forms which in turn triggers the event registration of the grid at the calling form for the onShow event)
Patrick Cornelissen
Not sure that I follow you. Would you mind expanding on how this would work, and how to register for an event. Thanks
David Carle
A: 

Delphi 2006 introduced OnMouseActivate events. The main form's OnMouseActivate would let you prevent the main form from being activated if the other form is visible.

This doesn't work with D6 of course.

Giel
+1  A: 

When you call ShowModal, all the forms except the one being shown get disabled. They're re-enabled just before ShowModal returns.

Display your editing form nonmodally, and when data starts being edited, have the form make itself modal by disabling the other form. Enable the other form when editing is complete. Apparently, disabling windows isn't always quite as simple as setting the Enabled property. I'd suggest using DisableTaskWindows, but it would disable all windows, including your editing form. Nonetheless, take a look at how it's implemented in Forms.pas. It keeps a list of all the windows it disables so that only they get re-enabled afterward.

Rob Kennedy
Thanks for the suggestion Rob.The problem is that the component prices are edited individually in a grid, so the total can only be checked when the user closes the form (which is what we do). However, we can't trap (or more specifically prevent) the FormDeactivate event when the user clicks away. I suppose I could display a new modal form to edit the prices and then validate them, but I was hoping there was a way to prevent the form being deactivated. Maybe not?
David Carle
By disabling the other targets of "clicking away," you effectively prevent the `FormDeactivate` event, at least within the context of your application. (Users can click away to another program, but you should never prevent that anyway — it's extremely rude and is liable to send business to your competitors.) I suggested enabling the other form when editing is complete. If editing can only be complete when you close the edit form, then logically that means I suggested to enable the other form when you close the editing form. Is that a problem?
Rob Kennedy
+1  A: 

A classic rule in Windows is that you can't change the focus during a focus-changing event. The OnDeactivate event occurs during a focus-changing event. Your form is being told that it is being deactivated — the OS is not asking permission — and at the same time, the other form is being told that it is being activated. Neither window has any say in the matter, and attempting to change the focus while these events are going on will only get all the windows confused. Symptoms include having two windows painting themselves as though they have focus, and having keyboard messages go nowhere despite the input cursor blinking. MSDN is even more dire, although I've never witnessed anything that bad:

While processing this message [WM_KILLFOCUS], do not make any function calls that display or activate a window. This causes the thread to yield control and can cause the application to stop responding to messages. For more information, see Message Deadlocks.

Since you can't deny a focus change after it's already started, the thing to do is to delay handling of the event until after things have settled down. When your editing form gets deactivated and the data on it isn't valid yet, post the form a message. Posting puts the message on the end of the message queue, so it won't get handled until all previous messages — the focus-changing notifications in particular — have already been handled. When the message arrives, indicate that data is invalid and set focus back to the editing form:

const
  efm_InvalidData = wm_User + 1;

type
  TEditForm = class(TForm)
  ...
  private
    procedure EFMInvalidData(var Msg: TMessage); message efm_InvalidData;
  end;

procedure TEditForm.FormDeactivate(Sender: TObject);
begin
  if DataNotValid then
    PostMessage(Handle, efm_InvalidData, 0, 0);
end;

procedure TEditForm.EFMInvalidData(var Msg: TMessage);
begin
  Self.SetFocus;
  ShowMessage('Invalid data');
end;

I should point out that this answer doesn't technically answer your question since it does nothing to prevent form deactivation, but you rejected my other answer that really does prevent deactivation.

Rob Kennedy
A: 

It not a helpful answer David, but I think I would have to agree with other respondents that this is not the correct approach. There are so many ways that it can all go wrong that stopping and taking a look at a different way to solve your problem might be better.

Even if you did manage to find the event/method/message that does what you want, you would still need to be able to deal with the scenario where the electricity goes off.

On a slightly more useful note, have you tried disabling your mainform until ready? You could put all controls on a panel and then just do

panel1.enabled := false;
Toby Allen
If the electricity goes off, there is no danger of the user manipulating the main form with inconsistent, uncommitted data in the edit form.
Rob Kennedy
A: 

Thanks to everyone for their help and suggestions.

Here's the solution I went with: In the 'Grid Form' (eg Form2) ...

public  
    PricesNotSquare: boolean;

In the FormDeactivate event, set PricesNotSquare to true if they don't match.

In the OnActivate event of the Main Form, ...

  if Assigned(Form2) and (Form2.PricesNotSquare) then  
  begin  
      ShowMessage( 'Please ensure the total Prices match before leaving the form' );  
      Form2.Show;  
      exit;  
  end;  
  // other form activate stuff here

Turned out to be a simple solution, just took a while to get it.

Seems to work fine, but if it has issues then I'll incorporate the idea of sending a message.

David Carle