I hope I can explain this clearly enough. I have my main form (A) and it opens 1 child form (B) using form.Show() and a second child form (C) using form.Show(). Now I want child form B to open a form (D) using form.ShowDialog(). When I do this, it blocks form A and form C as well. Is there a way to open a modal dialog and only have it block the form that opened it?
If you run Form B on a separate thread from A and C, the ShowDialog call will only block that thread. Clearly, that's not a trivial investment of work of course.
You can have the dialog not block any threads at all by simply running Form D's ShowDialog call on a separate thread. This requires the same kind of work, but much less of it, as you'll only have one form running off of your app's main thread.
Using multiple GUI threads is tricky business, and I would advise against it, if this is your only motivation for doing so.
A much more suitable approach is to use Show()
instead of ShowDialog()
, and disable the owner form until the popup form returns. There are just four considerations:
When
ShowDialog(owner)
is used, the popup form stays on top of its owner. The same is true when you useShow(owner)
. Alternatively, you can set theOwner
property explicitly, with the same effect.If you set the owner form's
Enabled
property tofalse
, the form shows a disabled state (child controls are "grayed out"), whereas whenShowDialog
is used, the owner form still gets disabled, but doesn't show a disabled state.When you call
ShowDialog
, the owner form gets disabled in Win32 code—itsWS_DISABLED
style bit gets set. This causes it to lose the ability to gain the focus and to "ding" when clicked, but doesn't make it draw itself gray.When you set a form's
Enabled
property tofalse
, an additional flag is set (in the framework, not the underlying Win32 subsystem) that certain controls check when they draw themselves. This flag is what tells controls to draw themselves in a disabled state.So to emulate what would happen with
ShowDialog
, we should set the nativeWS_DISABLED
style bit directly, instead of setting the form'sEnabled
property tofalse
. This is accomplished with a tiny bit of interop:const int GWL_STYLE = -16; const int WS_DISABLED = 0x08000000; [DllImport("user32.dll")] static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32.dll")] static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); void SetNativeEnabled(bool enabled){ SetWindowLong(Handle, GWL_STYLE, GetWindowLong(Handle, GWL_STYLE) & ~WS_DISABLED | (enabled ? 0 : WS_DISABLED)); }
The
ShowDialog()
call doesn't return until the dialog is dismissed. This is handy, because you can suspend the logic in your owner form until the dialog has done its business. TheShow()
call, necessarily, does not behave this way. Therefore, if you're going to useShow()
instead ofShowDialog()
, you'll need to break your logic into two parts. The code that should run after the dialog is dismissed (which would include re-enabling the owner form), should be run by aClosed
event handler.When a form is shown as a dialog, setting its
DialogResult
property automatically closes it. This property gets set whenever a button with aDialogResult
property other thanNone
is clicked. A form shown withShow
will not automatically close like this, so we must explicitly close it when one of its dismissal buttons is clicked. Note, however, that theDialogResult
property still gets set appropriately by the button.
Implementing these four things, your code becomes something like:
class FormB : Form{
void Foo(){
SetNativeEnabled(false); // defined above
FormD f = new FormD();
f.Closed += (s, e)=>{
switch(f.DialogResult){
case DialogResult.OK:
// Do OK logic
break;
case DialogResult.Cancel:
// Do Cancel logic
break;
}
SetNativeEnabled(true);
};
f.Show(this);
// function Foo returns now, as soon as FormD is shown
}
}
class FormD : Form{
public FormD(){
Button btnOK = new Button();
btnOK.DialogResult = DialogResult.OK;
btnOK.Text = "OK";
btnOK.Click += (s, e)=>Close();
btnOK.Parent = this;
Button btnCancel = new Button();
btnCancel.DialogResult = DialogResult.Cancel;
btnCancel.Text = "Cancel";
btnCancel.Click += (s, e)=>Close();
btnCancel.Parent = this;
AcceptButton = btnOK;
CancelButton = btnCancel;
}
}
You can use a separate thread (as below), but this is getting into dangerous territory - you should only go near this option if you understand the implications of threading (synchronization, cross-thread access, etc.):
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Button loadB, loadC;
Form formA = new Form {
Text = "Form A",
Controls = {
(loadC = new Button { Text = "Load C", Dock = DockStyle.Top}),
(loadB = new Button { Text = "Load B", Dock = DockStyle.Top})
}
};
loadC.Click += delegate {
Form formC = new Form { Text = "Form C" };
formC.Show(formA);
};
loadB.Click += delegate {
Thread thread = new Thread(() => {
Button loadD;
Form formB = new Form {
Text = "Form B",
Controls = {
(loadD = new Button { Text = "Load D",
Dock = DockStyle.Top})
}
};
loadD.Click += delegate {
Form formD = new Form { Text = "Form D"};
formD.ShowDialog(formB);
};
formB.ShowDialog(); // No owner; ShowDialog to prevent exit
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
};
Application.Run(formA);
}
(Obviously, you wouldn't actually structure the code like the above - this is just the shortest way of showing the behavior; in real code you'd have a class per form, etc.)
I would like to summarize possible solutions and add one new alternatives (3a and 3b). But first I want to clarify what we are talking about:
We have an application which have multiple forms. There is a requirement to show modal dialog which would block only certain subset of our forms but not the others. Modal dialogs may be shown only in one subset (scenario A) or multiple subsets (scenario B).
And now summary of possible solutions:
Don't use modal forms shown via
ShowDialog()
at allThink about design of your application. Do you really need to use
ShowDialog()
method? If you don't need having modal form it's the easiest and the cleanest way to go.Of course this solution is not always suitable. There are some features which
ShowDialog()
gives us. The most notable is that it disables the owner (but do not grays out) and user cannot interact with it. The very exhausting answer provided P Daddy.Emulate
ShowDialog()
behaviorIt is possible to emulate behavior of that mathod. Again I recommend reading P Daddy's answer.
a) Use combination of
Enabled
property onForm
and showing form as non-modal viaShow()
. As a result disabled form will be grayed out. But it's completely managed solution without any interop needed.b) Don't like the parent form being grayed out? Reference few native methods and turn off
WS_DISABLED
bit on parent form (once again - see answer from P Daddy).Those two solutions require that you have complete control on all the dialog boxes you need to handle. You have to use special construct to show "partially blocking dialog" and must not forget about it. You need to adjust your logic because
Show()
is non-blocking andShowDialog()
is blocking. Dealing with system dialogs (file choosers, color pickers, etc.) could be problem. On the other hand you do not need any extra code on the forms which shall not be blocked by dialog.Overcome limitations of
ShowDialog()
Note that there are
Application.EnterThreadModal
andApplication.LeaveThreadModal
events. This event is raised whenever modal dialog is shown. Beware that events are actually thread-wide, not application-wide.a) Listen to the
Application.EnterThreadModal
event in forms which shall not be blocked by dialog and turn onWS_DISABLED
bit in those forms. You only need to adjust forms which should not be blocked by modal dialogs. You may also need to inspect the parent-chain of the modal form being shown and switchWS_DISABLED
based on this condition (in your example if you also needed to open dialogs by forms A and C but not to block forms B and D).b) Hide and re-show forms which should not be blocked. Note that when you show new form after modal dialog is shown it is not blocked. Take advantage of that and when modal dialog is shown, hide and show again desired forms so that they are not blocked. However this approach may bring some flickering. It could be theoretically fixed by enabling/disabling repaint of forms in Win API but I do not guarantee that.
c) Set
Owner
property to dialog form on forms which should not be blocked when dialog is shown. I did not test this.d) Use multiple GUI threads. Answer from TheSmurf.
Start FormB in a new thread in FormA:
(new System.Threading.Thread(()=> {
(new FormB()).Show();
})).Start();
Now, any forms opened in the new thread using ShowDialog() will only block FormB and NOT FormA or FormC
I just wanted to add my solution here as it seems to work well for me, and can be encapsulated into a simple extension method. The only thing I need to do is deal with the flashing as @nightcoder commented on @PDaddy's answer.
public static void ShowWithParentFormLock(this Form childForm, Form parentForm)
{
childForm.ShowWithParentFormLock(parentForm, null);
}
public static void ShowWithParentFormLock(this Form childForm, Form parentForm, Action actionAfterClose)
{
if (childForm == null)
throw new ArgumentNullException("childForm");
if (parentForm == null)
throw new ArgumentNullException("parentForm");
EventHandler activatedDelegate = (object sender, EventArgs e) =>
{
childForm.Focus();
//To Do: Add ability to flash form to notify user that focus changed
};
childForm.FormClosed += (sender, closedEventArgs) =>
{
try
{
parentForm.Focus();
if(actionAfterClose != null)
actionAfterClose();
}
finally
{
try
{
parentForm.Activated -= activatedDelegate;
if (!childForm.IsDisposed || !childForm.Disposing)
childForm.Dispose();
}
catch { }
}
};
parentForm.Activated += activatedDelegate;
childForm.Show(parentForm);
}
I was facing a similar problem in an application I was writing. My main UI was a form running on the main thread. I had a help dialog that I wanted to run as a modeless dialog. This was easy to implement, even to the point of ensuring that I only ever had one instance of the help dialog running. Unfortunately, any modal dialogs I used caused the help dialog to lose focus as well - when it was while some of these modal dialogs were running that having the help dialog there would be most useful.
Using ideas mentioned here, and in other places, I managed to overcome this bug.
I declared a thread inside my main UI.
Thread helpThread;
The following code deals with the event fired to open the help dialog.
private void Help(object sender, EventArgs e)
{
//if help dialog is still open then thread is still running
//if not, we need to recreate the thread and start it again
if (helpThread.ThreadState != ThreadState.Running)
{
helpThread = new Thread(new ThreadStart(startHelpThread));
helpThread.SetApartmentState(ApartmentState.STA);
helpThread.Start();
}
}
void startHelpThread()
{
using (HelpDialog newHelp = new HelpDialog(resources))
{
newHelp.ShowDialog();
}
}
I also needed the initialization of the thread added into my constructor to make sure that I was not referencing a null object the first time this code is run.
public MainWindow()
{
...
helpThread = new Thread(new ThreadStart(startHelpThread));
helpThread.SetApartmentState(ApartmentState.STA);
...
}
This makes sure that the thread has only one instance at any given time. The thread itself runs the dialog, and stops once the dialog is closed. Since it runs on a separate thread, creating a modal dialog from within the main UI does not cause the help dialog to hang. I did need to add
helpDialog.Abort();
to the form closing event of my main UI to make sure that the help dialog closes when the application is terminated.
I now have a modeless help dialog which is not affected by any modal dialogs spawned from within my main UI, which is exactly what I wanted. This is safe since there is no communication needed between the main UI and the help dialog.
Here is helper I'm using in WPF to prevent dialog from blocking non dialog windows based on some answers to this question:
public static class WindowHelper
{
public static bool? ShowDialogNonBlocking(this Window window)
{
var frame = new DispatcherFrame();
void closeHandler(object sender, EventArgs args)
{
frame.Continue = false;
}
try
{
window.Owner.SetNativeEnabled(false);
window.Closed += closeHandler;
window.Show();
Dispatcher.PushFrame(frame);
}
finally
{
window.Closed -= closeHandler;
window.Owner.SetNativeEnabled(true);
}
return window.DialogResult;
}
const int GWL_STYLE = -16;
const int WS_DISABLED = 0x08000000;
[DllImport("user32.dll")]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
static void SetNativeEnabled(this Window window, bool enabled)
{
var handle = new WindowInteropHelper(window).Handle;
SetWindowLong(handle, GWL_STYLE, GetWindowLong(handle, GWL_STYLE) &
~WS_DISABLED | (enabled ? 0 : WS_DISABLED));
}
}
Usage:
if(true == window.ShowDialogNonBlocking())
{
// Dialog result has correct value
}
Using Example:
(new NoneBlockingDialog((new frmDialog()))).ShowDialogNoneBlock(this);
Source code:
class NoneBlockingDialog
{
Form dialog;
Form Owner;
public NoneBlockingDialog(Form f)
{
this.dialog = f;
this.dialog.FormClosing += new FormClosingEventHandler(f_FormClosing);
}
void f_FormClosing(object sender, FormClosingEventArgs e)
{
if(! e.Cancel)
PUtils.SetNativeEnabled(this.Owner.Handle, true);
}
public void ShowDialogNoneBlock(Form owner)
{
this.Owner = owner;
PUtils.SetNativeEnabled(owner.Handle, false);
this.dialog.Show(owner);
}
}
partial class PUtils
{
const int GWL_STYLE = -16;
const int WS_DISABLED = 0x08000000;
[DllImport("user32.dll")]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
static public void SetNativeEnabled(IntPtr hWnd, bool enabled)
{
SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) & ~WS_DISABLED | (enabled ? 0 : WS_DISABLED));
}
}
Maybe a child window (see ChildWindow for details) would be a more elegant solution, and it would avoid all the problems with separate threads for the GUI.
来源:https://stackoverflow.com/questions/428494/is-it-possible-to-use-showdialog-without-blocking-all-forms