How to show a modal dialog from a modeless form?
Asked Answered
C

1

9

I have two "modeless" forms:

  • one is the special MainForm
  • the other is a modeless form

enter image description here

You can see:

  • both exist on the taskbar
  • both have a taskbar button
  • both can be independantly minimized
  • both can be independantly restored
  • neither is always on top (owned) by the other

Now show a modal form

From this modeless form, i want to show a modal one:

enter image description here

The Modal form is being constructed as:

var
    frmExchangeConfirm: TfrmExchangeConfirm;
begin
    frmExchangeConfirm := TfrmExchangeConfirm.Create(Application);
    try
        //Setting popupMode and popupParent still makes the MainForm disabled
//      frmExchangeConfirm.PopupMode := pmExplicit;
//      frmExchangeConfirm.PopupParent := Self; //owned by us

        frmExchangeConfirm.OwnerForm := Self; //tell the form which owner to use
        frmExchangeConfirm.ShowModal;
    finally
        frmExchangeConfirm.Free;
    end;

The modal form is told which owner to use through a new OwnerForm property:

protected
   procedure SetOwnerForm(const Value: TForm);
public
   property OwnerForm: TForm read GetOwnerForm write SetOwnerForm;
end;

which forces an handle recreation:

procedure TfrmExchangeConfirm.SetOwnerForm(const Value: TForm);
begin
    FOwnerForm := Value;

    if Self.HandleAllocated then
        Self.RecreateWnd;
end;

and is then the second time through CreateParams:

procedure TfrmExchangeConfirm.CreateParams(var Params: TCreateParams);
begin
    inherited;

    if FOwnerForm <> nil then
        Params.WndParent := FOwnerForm.Handle;
end;

The problem is:

  • once this owned modal form is shown, i cannot interact with the MainForm
  • i cannot minimize the MainForm using the taskbar button
  • i cannot minimize the Modal, or its owning parent, using the taskbar button
  • if i minimize the modal form using the Minimize button, the MainForm disappears
  • i can activate the MainForm using its taskbar button; but i cannot interact with it

I've asked this question about 7 times over the last decade. The last time i was promised that making the main form the MainForm would solve everything.

Bonus: WinForms has handled this correctly since .NET 1.0.

There is a lot of confusion about what a modal dialog is. A dialog is modal when you must interact with it before you can continue to use its owner. From the Windows Interface Design Guidelines:

Dialog boxes have two fundamental types:

  • Modal dialog boxes require users to complete and close before continuing with the owner window. These dialog boxes are best used for critical or infrequent, one-off tasks that require completion before continuing.
  • Modeless dialog boxes allow users to switch between the dialog box and the owner window as desired. These dialog boxes are best used for frequent, repetitive, on-going tasks.

Windows has the concept of an "owner". When a window is "owned" that will will always appear on top of its owner. When a window is "modal", it means that the owner is disabled until the modal task is complete.

You an see this effect in the ProgressDialog API:

HRESULT StartProgressDialog(
  [in] HWND     hwndParent,
       IUnknown *punkEnableModless,
       DWORD    dwFlags,
       LPCVOID  pvReserved
);

hwndParent [in]
Type: HWND
A handle to the dialog box's parent window.

dwFlags
Type: DWORD
PROGDLG_MODAL
The progress dialog box will be modal to the window specified by hwndParent. By default, a progress dialog box is modeless.

Sure, you could be mean, and disable all other windows

  • in the thread
  • the process
  • or the system

But i want to have the correct behavior. I want to do:

  • what Windows does
  • what Office applications do
  • what Beyond Compare does
  • what WinForms does
  • what WPF does
  • what every application i've ever used does
  • and what any user would expect

I've wanted this in my Delphi apps since 1998; when realized Delphi 3 didn't properly support Windows 95 and the taskbar.

Canaille answered 29/7, 2015 at 16:14 Comment(21)
When the modal window is shown, does the framework disable the main form? That's what you need to stop happening.Inwards
I guess you start by looking at how DisableTaskWindows operatesInwards
That's what modal means, it disables interaction with all the forms of the same thread but the modal one. I guess there was a misunderstanding between you and whoever promised that this setup would work through modality. Anyway, don't use showmodal, just disable the window that you want to be disabled.Sherleysherline
@SertacAkyuz The point of a modal window isn't to disable all other windows in the process, it's to disable the window's ownerCanaille
@DavidHeffernan Yes, calling ShowModal also disables the main form.Canaille
@IanBoyd: In fact, ShowModal() disables all visible windows that belong to the calling thread. This is by design. It is just not the design you want. You need to use Show() instead of ShowModal() in this case, disabling only the OwnerForm when the confirm window is shown, and re-enabling the OwnerForm when the confirm window is closed.Holmium
@RemyLebeau I realize ShowModal disables all windows in the application. But as a Windows developer, creating a Windows application, i am trying to show a window modally. On Windows A modal window is an owned window that requires the user to interact with it before they can return to operating the owner window.. The built-in ShowModal of the VCL doesn't so that; so i presume i have to write it myself?Canaille
@IanBoyd: Yes, you have to write it yourself.Holmium
@Ian - You're picking one of the steps that leads to the definition of a modal window in perspective of a customer who required to determine if a particular window is modal. If you continue to read on the document you linked, you'll find the author refers to this definition (the final definition, not the one you quoted) as the right definition for the customer's needs....Sherleysherline
... The document refers to a previous document which includes author's own definition: "From the end-users' point of view, modality occurs when the users are locked into completing a task once it is begun, with the only escape being to cancel the entire operation."Sherleysherline
@ProgrammerNotFound Yes, WoW! It was one of three icons that i found in My Documents for the demo app. Light be with you.Canaille
@Ian Regarding your edit, consider this scenario. Main form shows modeless window. Modeless window shows modal form. How many forms should be disabled? I wonder what happens when you call MessageBox in this scenario, passing the modeless window as the owner.Inwards
How many forms should be disabled?: Just the owner hWnd. What happens if i pass a disabled window as the owner of a modal dialog box? I tested it by calling MessageBox once during a click, and the other 5 seconds later from a timer. You have two modal windows, both owned by the same owner, both interactiable. Only once the first modal dialog is dismissed does the owner become enabled. It seems Windows checks only enables the owner if it was the one to disable it. And it only disables it if it isn't already disabled. Which is a good note to add to my .ShowDialog. Thanks!Canaille
Here are some other correct definitions/examples for you: WinForms: A modal form or dialog box must be closed or hidden before you can continue working with the rest of the application. WPF: *ShowDialog shows the window, disables all other windows in the application, and returns only when the window is closed. This type of window is known as a modal window." ...Sherleysherline
... Office: Float navigation pane in Word, open a modal dialog. Main window and the pane is disabled. Windows: Start wordpad, launch find dialog. Open any modal dialog. Main window and find dialog are disabled. Rest of your items are quite subjective and doesn't match my experience/expectations. It should be apparent by now that the api does not specify modal behavior. It specifies what a modal dialog is, which you come up with now and again. But if that specification is all you have, then the VCL have got it right. It is just mean....Sherleysherline
I wish you stopped trying to impose your correct modal behavior through your question which is completely unrelated to the technical matter. Nobody said that you shouldn't have an enabled window when there is a modal one. Just start considering that this can be not what everybody expects. In my opinion, majority... But evidently this is debatable.Sherleysherline
@David - With the default MB_APPLMODAL, main window remains enabled. If you use MB_TASKMODAL while passing 0 for hwnd, all thread windows are disabled.Sherleysherline
@Ian Surely you can see that different scenarios call for different designs. There is no single standard here.Inwards
It's standard enough throughout Windows. WinForms, WPF, TaskDialog, ProgressDialog, SHFileOperation, IFileOperation. Lets call this preferred behavior "the pit of success", and Delphi's inability to do it "the pit of failure". I would argue there is one standard here. But even we did allow that the 99.9% case means 0.01% of the time i couldn't do it - Delphi can't do the 99.9% case.Canaille
Quite astounding how you deny statements from the developer teams of the products and examples from the programs and systems that you exemplify. I don't really care my causing a fair question becoming an unnecessary wall of false assertions as I trust programmers, in general, are an intelligent kind.Sherleysherline
@SertacAkyuz Those example are owned modeless dialogs. Which we are, obviously, not talking about (as we're talking about unowned modeless dialogs). Of course owned windows are disabled when the owner is disabled. My problem is that unowned windows are disabled when an unrelated unowned window shows a dialog.Canaille
I
7

ShowModal disables all other top level windows in the same thread. That includes your main form.

You'll have to finesse the showing of this form to make it behave the way you want. Do the following:

  1. Disable the modeless owner form.
  2. Show the "modal" form by calling Show.
  3. When the "modal" form is closed, enable the modeless owner. Make sure the owner is enabled before the "modal" form's window is destroyed, as explained below.

You could potentially run your own modal message loop in between steps 2 and 3, as ShowModal does but this might be overkill. I'd just show the form modeless but disable its owner to make it "modal" with respect to that owner.

This process is a little delicate. Look to the source of ShowModal for inspiration. Also, Raymond's epic series of articles on modality is essential reading. I link to it all here: Why does a MessageBox not block the Application on a synchronized thread?

And even more from Raymond: The correct order for disabling and enabling windows:

When you destroy the modal dialog, you are destroying the window with foreground activation. The window manager now needs to find somebody else to give activation to. It tries to give it to the dialog's owner, but the owner is still disabled, so the window manager skips it and looks for some other window, somebody who is not disabled.

That's why you get the weird interloper window.

The correct order for destroying a modal dialog is

  • Re-enable the owner.
  • Destroy the modal dialog.

This time, when the modal dialog is destroyed, the window manager looks to the owner and hey this time it's enabled, so it inherits activation.

No flicker. No interloper.

Inwards answered 29/7, 2015 at 18:26 Comment(7)
Raymond's old article; and especially the issue of being careful which order to disable and enable in, was going through my mind when i commented to Sertac what a modal UI is. I guess fundamentally Delphi doesn't do the Windows concept of a modal dialog. I'd really hate to have to implement all of this myself; i just assumed that with the great Delphi 2009 transition all of these bugs were solved. :(Canaille
@Ian I don't think there's one single standard here. It's plausible that one app would want to disable just the modeless window, but another would need to disable both modeless and main window. That's a choice to be made by the app. The VCL chooses the latter for us but it can't read our minds. A more flexible design would allow us to choose which windows to disable. It would be easy enough to write a better ShowModal that allowed you to pass in windows that should not be disabled. You could readily do that I think.Inwards
Writing my own ShowModal is the task i'm faced with; but i'm experienced enough to know i'll get it wrong. Expecially when it comes to things like the special case needed if you receive a WM_QUIT. Has anyone else solved this in Delphi? Calling @ZoëPeterson!Canaille
Comment out DisableTaskWindows/EnableTaskWindows and hope for the best?Canaille
Write a class helper. Pass in an open array of windows to be disabled. Use the same code as ShowModal. Replace DisableTaskWindows/EnableTaskWindows with code to disable/enable the supplied windows. Job done.Inwards
Look at how DTW/ETW works and replace their enumeration with enumeration of the supplied windows.Inwards
I was going to model it after Windows/WinForms: .ShowModal(OwnerWnd: HWND). Then just be sure to fiddle the disabling and enabling correctly.Canaille

© 2022 - 2024 — McMap. All rights reserved.