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 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.
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.
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.
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;
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.
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.
Related
Delphi XE7 - When using custom styles (Project, Options, appearance) OnDestroy never gets called. Using default-native-windows skin-theme, form destroy is called as expected, is this normal?
If so, what are other alternatives besides OnClose?
Blank project, OnDestroy():
procedure TForm1.FormDestroy(Sender: TObject);
begin
ShowMessage('destroy called only when not using styles');
end;
Solution and advice:
When using styles (see #andreas advice): onDestroy() is not a good place to put code, since application termination will not wait for all code to finish, some code might be executed but there is a chance not all.
The OnDestroy event is called irrespective of whether styles are used or not, when the form is in the process of being destroyed. You can confirm it by putting a break point on the ShowMessage() line (works of course only when running under the debugger) or by adding a call to Beep() (assuming your sound system is OK).
When the main form is destroyed, the program starts to prepare for termination. The call to ShowMessage() makes a furious attempt to show the message box but the process is already going down. The message box can even be seen briefly as a flash, in the case of not succeeding in staying visible. It is close to a miracle that the message box shows up in some OS's under any conditions.
Anyway, The best place to show any messages at the end, is in either OnCloseQuery() or OnClose() events. The OnDestroy() event is only meant to clean up any resources aquired in the OnCreate() event.
Core problem: I don't like the way how TcxGrid delete (that is initiated by the default Ctrl+Del shortcut key for the TcxGrid or by the Delete button of the cx-navigator) is working: it is simple and direct call to the delete of the underlying dataset, there is no TcxGrid(/DataController).OnDelete(Sender: TObject, var AHandled: Boolean) event for the cxGrid or cxGridDataView. And from https://www.devexpress.com/Support/Center/Question/Details/Q308755/tcxgrid-deleting-record I understand that I can only configure the confirmation message, but I have no control over the Delete itself, i.e., I can not implement custom Delete and therefore I don't even try to ask for the straight solution of this problem. My question is about workaround.
That is why I am refusing to use cx-navigator and I opt to introduce TAction that handles Ctrl+Del shortcut key for the entire form. I have multiple grids on the form each with its own problematic Delete procedure that should be handled in non-standard way by the custom procedure.
That is why my DeleteAction determines the ActiveControl (TcxGridSite, TcxGrid) initially and then calls relevant Delete procedure afterwards. All that is fine. But some other components (e. g. TcxDBTextEdit) have default Ctrl+Del handling as well and that handling is very, very good. But if shortcut key Ctrl+Del is handled by action (and it is marked as handled even in the case when no active grid can not be found and therefor even in the case when ActionExecute has not done anything useful) then further propagation is stopped. That can be observed empirically and that can be seen theoretically from http://edn.embarcadero.com/article/38447 .
But maybe still there is some workaround how the shortcut key that has been processed by the ActionExecute can still be propagated further to the default components if they are Active controls.
I know that TAction is for (form-wide global) menu functionality but TcxGrid is not extensible enough and that is why should try to stretch the Delphi TAction design as well.
Probably there would be various ways to achieve the processing you require, there are quite a few places you can intervene during key processing, as you've already read from Peter Below's article linked in your question.
A place to intervene that I can think of which would contain all the pieces together is the form's IsShortCut method. One probable disadvantage of such a method could be that you'd have to implement it on all the forms that you want their key handling behavior changed.
Normally key processing continues only if it's not handled by shortcuts. Below solution reports the shortcut to be not processed for that reason, but then has to cause the execution of the action manually. I tested the code with a regular edit as can be seen, but I guess it shouldn't make any difference.
function TForm1.IsShortCut(var Message: TWMKey): Boolean;
begin
if (ActiveControl is TEdit) and (Message.CharCode = VK_DELETE) and
(KeyDataToShiftState(Message.KeyData) = [ssCtrl]) then begin
Result := False; // if you don't require the action to be executed, just exist here
ActionList1.IsShortCut(Message); // executes the action, ignore result
// returning false will result in main form's shortcut handler to be called
// below is only required if this is the main form
if Application.MainForm = Self then
Message.CharCode := 0;
end else
Result := inherited IsShortCut(Message);
end;
Is there a failsafe way of freeing a Delphi control?
I have a TStringGrid descendent which I am "embedding" a custom control in it for an inplace editor. When the user navigates within the cells of the grid via the tab key or arrow keys, I need to create a dynamic control if the cell is editable. I have hooked the needed events and am utilizing the OnKeyDown event of my custom control to pass the navigation keys back to the parent TStringGrid.
Previously, the TStringGrid descendent would simply call FreeAndNil on the embedded control, but under some circumstances this would cause access violations inside of UpdateUIState/GetParentForm. Looking at the call stack, it appears that sometimes after the control was free'd, a WM_KEYDOWN (TWinControl.WMKeyDown) message was still happening.
I've all ready looked at and implemented the changes discussed in How to free control inside its event handler?. This appears to have resolved the issue, but I am wondering if there are any other cavet's to this approach.
In effect, this workaround has simply delayed the destruction of the control until after all the existing messages on the queue at the time the CM_RELEASE message was posted.
Would it not be possible that after the CM_RELEASE was posted, another WM_KEY* or similar message could all ready have been posted to the message queue?
My current CM_RELEASE handler looks like:
procedure TMyCustomControl.HandleRelease(var Msg: TMessage);
begin
Free;
end;
So, will this be safe in all instances or should I do something to clear any other messages from the queue? ( SendMessage(Self.Handle, WM_DESTROY, 0, 0) comes to mind )
In general you should not destroy a control in an event-handler of that control.
But since your function is a plain non virtual message handler which is never called from internal code in that control you should be ok. I don't like too too much from a style point of view, but I think it's ok for your use-case.
But a custom message might be cleaner.
Would it not be possible that after the CM_RELEASE was posted, another WM_KEY* or similar message could all ready have been posted to the message queue?
If messages in queue would cause big problems you could never safely destroy a control since messages can be posted from other threads and applications. Just make sure the correct functioning of your application doesn't depends on those messages being handles in every case.
SendMessage sends the message and wait for it to return, that's why you can not use it safely in the event handler of the control you are freeing.
PostMessage in the other hand, will send the message and will be processed after the event exits (if there are no more code in the event).
in my app I have a main form with a button.
Clicking this button, a form (not auto-created in dpr) is created and displayed; on this form, I placed a TApplicationEvents component and I defined its OnIdle event handler.
This event handler doesn't get called! May this depend because I derived this second form not from
TForm but from an other class, TChartBasicForm (by means of VFI)?
Thank you very much for replies.
Massimo.
Hooking an application's idle event can lead to a lot of debugging woes and other maintenance headaches, especially on a form other than the main form. I realize this may not answer your specific question (which is hard to do at this point given the vagueness), but are you sure you can't accomplish what you're trying to do with a TTimer or TThread instead?
Thanks for interest to all people.
"It doesn't work" means that it's not called at all.
Instead, the event OnShowHint works!
Ooops!
Perhaps I have understood the bad behaviour!
In the main form I defined a procedure like this one:
procedure IdleHandler(Sender: TObject; var Done: Boolean);
and in the FormCreate:
Application.OnIdle := IdleHandler;
This probably inhibits TApplicationEvents.OnIdle, even if
in IdleHandler, at the end of the procedure, I put:
Application.OnIdle := nil;
because the code is usefull to try a connection only at the begin
of the application.
I beg your pardon: my face is red......
I am just wondering if I'm doign something that might be bad, although it seems a very practical solution to me...
I have two forms which the user will have to walk through. The user clicks on a button and form1 pops up. The user presses OK and the second one pops up. The user click OK again and the screens are gone. Or the user clicks on Retry and the screen goes back to the first one. The two screens are completely different sizes with different information.
So I came up with this code:
Form1 := TForm1.Create(SharedData);
Form2 := TForm2.Create(SharedData);
repeat
ModalResult := Form1.ShowModal;
if (ModalResult = mrOK) then ModalResult := Form2.ShowModal;
until (ModalResult <> mrRetry);
Form1.Release;
Form2.Release;
I've tested this code and it seems to work like a charm. In this code, SharedData is an object that contains data that's manipulated by both forms. I create this object before the two forms are created and when ModalResult==mrOK I just write the data back to the database.
Problem is, while I think this is a clean solution to handle flipping between two forms, I can't remember ever seeing something like this construction before. Of course, I'm a Genius. (At least, me Ego tells me I am.) But would there be something against using this piece of code or is it just fine?
If the code does what it's supposed to, I think it's fine. I've not seen anything quite like it before either, but it's pretty obvious what it's doing... which in my book is far better than just blindly following other people's work. :-)
I don't see anything wrong with the code. I do question, however, the passing of SharedData to the constructor of both forms. Unless you've overridden the constructor (using reintroduce as well), the argument to TForm.Create() accepts an owner; that owner is responsible for freeing the objects it owns. Since you're freeing them yourself, there's no need to assign an owner; just pass nil to the call to Create() to avoid the overhead of saving the reference, accessing the list of owned objects when one is freed to remove the reference, etc.
Also, Release is designed to be called from within the event handler of the control itself, such as in a button click event. It makes sure that all pending messages are handled before the control is actually freed, in order to avoid AVs. Using it the way you are again adds unnecessary overhead, as you're not using them inside an event handler. You can safely just use Form1.Free; instead.
To clarify use of Release, it's for use in code of the Form itself. For instance, if you have a button on the form, and you want that button's click to cause the form to be freed, you use Release:
procedure TForm1.Button1Click(Sender: TObject);
begin
Self.Release;
Close;
end;
This allows the button click to free the form, but makes sure that the subsequent call to Close runs first. If you use Free instead of Release above, you could end up calling Close on a non-existent Form1. (Chances are it would still be OK, because of the silliness of the code above; the form would still be resident in memory. Still a bad idea, though.)