¿Es posible usar ShowDialog sin bloquear todas las formas?

Espero poder explicar esto con suficiente claridad. Tengo mi formulario principal (A) y abre 1 formulario hijo (B) usando form.Show () y un segundo formulario secundario (C) usando form.Show (). Ahora quiero que el formulario hijo B abra un formulario (D) usando form.ShowDialog (). Cuando hago esto, bloquea la forma A y la forma C también. ¿Hay alguna manera de abrir un cuadro de diálogo modal y bloquear el formulario que lo abrió?

Si ejecuta el Formulario B en un hilo separado de A y C, la llamada ShowDialog solo bloqueará ese hilo. Claramente, esa no es una inversión trivial de trabajo, por supuesto.

Puede hacer que el diálogo no bloquee ningún subproceso simplemente ejecutando la llamada ShowDialog de Form D’s en un subproceso separado. Esto requiere el mismo tipo de trabajo, pero mucho menos, ya que solo tendrá un formulario ejecutándose fuera del hilo principal de su aplicación.

El uso de múltiples hilos de GUI es un asunto complicado, y aconsejaría que no lo haga, si esta es su única motivación para hacerlo.

Un enfoque mucho más adecuado es usar Show() lugar de ShowDialog() , y deshabilitar el formulario de propietario hasta que el formulario emergente regrese. Solo hay cuatro consideraciones:

  1. Cuando se ShowDialog(owner) , el formulario emergente se queda en la parte superior de su propietario. Lo mismo es cierto cuando usa Show(owner) . Alternativamente, puede establecer la propiedad Owner explícita, con el mismo efecto.

  2. Si configura la propiedad Enabled del formulario del propietario en false , el formulario muestra un estado deshabilitado (los controles secundarios están “deshabilitados”), mientras que cuando se utiliza ShowDialog , el formulario de propietario sigue deshabilitado, pero no muestra un estado deshabilitado.

    Cuando llama a ShowDialog , el formulario de propietario se deshabilita en el código de Win32; se establece su WS_DISABLED estilo WS_DISABLED . Esto hace que pierda la capacidad de obtener el foco y de “cantar” cuando se hace clic, pero no hace que se dibuje en gris.

    Cuando establece la propiedad Enabled un formulario en false , se establece un indicador adicional (en el marco, no en el subsistema Win32 subyacente) que ciertos controles verifican cuando se dibujan ellos mismos. Este indicador es lo que les dice a los controles que se dibujen en un estado deshabilitado.

    Entonces, para emular lo que sucedería con ShowDialog , deberíamos establecer el WS_DISABLED estilo nativo WS_DISABLED directamente, en lugar de establecer la propiedad Enabled del formulario en false . Esto se logra con un poco de interoperabilidad:

     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)); } 
  3. La llamada a ShowDialog() no regresa hasta que se ShowDialog() el diálogo. Esto es útil, porque puede suspender la lógica en su forma de propietario hasta que el diálogo haya hecho su trabajo. La llamada Show() , necesariamente, no se comporta de esta manera. Por lo tanto, si va a usar Show() lugar de ShowDialog() , tendrá que dividir su lógica en dos partes. El código que debe ejecutarse después de que se desestime el diálogo (que incluiría volver a habilitar el formulario de propietario), debe ser ejecutado por un controlador de eventos Closed .

  4. Cuando se muestra un formulario como un cuadro de diálogo, establecer su propiedad DialogResult cierra automáticamente. Esta propiedad se establece cuando se hace clic en un botón con una propiedad DialogResult distinta de None . Un formulario que se muestra con Show no se cerrará automáticamente de esta manera, por lo que debemos cerrarlo explícitamente cuando se haga clic en uno de sus botones de descarte. Sin embargo, tenga en cuenta que la propiedad DialogResult aún se configura correctamente con el botón.

Implementando estas cuatro cosas, su código se convierte en algo así como:

 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; } } 

Puede usar un hilo separado (como se muestra a continuación), pero esto se está metiendo en un territorio peligroso: solo debe acercarse a esta opción si comprende las implicaciones del enhebrado (sincronización, acceso a hilos cruzados, 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); } 

(Obviamente, en realidad no estructuraría el código como el anterior: esta es la forma más corta de mostrar el comportamiento, en el código real tendría una clase por formulario, etc.)

Me gustaría resumir posibles soluciones y agregar una nueva alternativa (3a y 3b). Pero primero quiero aclarar de qué estamos hablando:

Tenemos una aplicación que tiene múltiples formas. Existe un requisito para mostrar un diálogo modal que bloquearía solo cierto subconjunto de nuestros formularios, pero no los otros. Los diálogos modales pueden mostrarse solo en un subconjunto (escenario A) o múltiples subconjuntos (escenario B).

Y ahora resumen de posibles soluciones:

  1. No use formularios modales mostrados a través de ShowDialog() en absoluto

    Piense en el diseño de su aplicación. ¿De verdad necesitas usar el método ShowDialog() ? Si no necesita una forma modal, es la manera más fácil y más limpia de hacerlo.

    Por supuesto, esta solución no siempre es adecuada. Hay algunas características que ShowDialog() nos brinda. Lo más notable es que desactiva al propietario (pero no se atenúa) y el usuario no puede interactuar con él. La muy agotadora respuesta le dio a P Daddy .

  2. Emular el comportamiento de ShowDialog()

    Es posible emular el comportamiento de ese mathod. De nuevo, recomiendo leer la respuesta de P Daddy .

    a) Utilice la combinación de propiedad Enabled en el Form y muestre el formulario como no modal a través de Show() . Como resultado, la forma desactivada se atenuará. Pero es una solución completamente administrada sin necesidad de interoperabilidad alguna.

    b) ¿No te gusta que el formulario principal esté en gris? WS_DISABLED referencia a algunos métodos nativos y apague el bit WS_DISABLED en la forma principal (una vez más, vea la respuesta de P Daddy ).

    Esas dos soluciones requieren que tenga control total sobre todos los cuadros de diálogo que necesita manejar. Tienes que usar una construcción especial para mostrar “diálogo de locking parcial” y no debes olvidarlo. ShowDialog() ajustar su lógica porque Show() no bloquea y ShowDialog() está bloqueando. Tratar con los diálogos del sistema (selectores de archivos, selectores de color, etc.) podría ser un problema. Por otro lado, no necesita ningún código adicional en los formularios que no se bloquearán mediante el diálogo.

  3. Superar las limitaciones de ShowDialog()

    Tenga en cuenta que hay eventos Application.EnterThreadModal y Application.LeaveThreadModal . Este evento se plantea cada vez que se muestra un cuadro de diálogo modal. Tenga en cuenta que los eventos son en realidad en todo el subproceso, no en toda la aplicación.

    a) Escuche el evento Application.EnterThreadModal en formularios que no deben ser bloqueados por el diálogo y active el bit WS_DISABLED en esos formularios. Solo necesita ajustar formularios que no deberían ser bloqueados por diálogos modales. También es posible que deba inspeccionar la cadena primaria del formulario modal que se muestra y cambiar WS_DISABLED función de esta condición (en su ejemplo, si también necesitó abrir cuadros de diálogo mediante los formularios A y C, pero no para bloquear los formularios B y D).

    b) Ocultar y volver a mostrar los formularios que no deben bloquearse . Tenga en cuenta que cuando muestra un nuevo formulario después de mostrar el diálogo modal, no está bloqueado. Aproveche eso y cuando se muestre el cuadro de diálogo modal, oculte y muestre nuevamente los formularios deseados para que no se bloqueen. Sin embargo, este enfoque puede traer cierto parpadeo. Podría ser corregido teóricamente habilitando / deshabilitando el repintado de formularios en Win API, pero no lo garantizo.

    c) Establezca la propiedad Owner en el formulario de diálogo en formularios que no deben bloquearse cuando se muestra el cuadro de diálogo. No probé esto.

    d) Usar múltiples hilos de GUI . Respuesta de TheSmurf .

Solo quería agregar mi solución aquí, ya que parece funcionar bien para mí, y se puede encapsular en un método de extensión simple. Lo único que tengo que hacer es ocuparme de los flashes ya que @nightcoder comentó la respuesta de @PDaddy.

 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); } 

Inicie FormB en un nuevo hilo en FormA:

  (new System.Threading.Thread(()=> { (new FormB()).Show(); })).Start(); 

Ahora, cualquier formulario abierto en el nuevo subproceso que use ShowDialog () solo bloqueará FormB y NO FormA o FormC

Estaba enfrentando un problema similar en una aplicación que estaba escribiendo. Mi interfaz de usuario principal era un formulario que se ejecuta en el hilo principal. Tenía un diálogo de ayuda que quería ejecutar como un diálogo no modal. Esto fue fácil de implementar, incluso hasta el punto de garantizar que solo haya ejecutado una instancia del cuadro de diálogo de ayuda. Desafortunadamente, cualquier cuadro de diálogo modal que utilicé provocó que el diálogo de ayuda también perdiera enfoque, cuando estaba mientras se ejecutaban algunos de estos diálogos modales que el diálogo de ayuda allí sería más útil.

Usando las ideas mencionadas aquí, y en otros lugares, logré superar este error.

Decidí un hilo dentro de mi IU principal.

 Thread helpThread; 

El siguiente código trata sobre el evento disparado para abrir el diálogo de ayuda.

 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(); } } 

También necesitaba la inicialización del hilo agregado en mi constructor para asegurarme de que no estaba haciendo referencia a un objeto nulo la primera vez que se ejecuta este código.

 public MainWindow() { ... helpThread = new Thread(new ThreadStart(startHelpThread)); helpThread.SetApartmentState(ApartmentState.STA); ... } 

Esto asegura que el hilo tenga solo una instancia en un momento dado. El hilo ejecuta el diálogo y se detiene una vez que se cierra el diálogo. Dado que se ejecuta en un subproceso separado, la creación de un cuadro de diálogo modal desde dentro de la IU principal no hace que cuelgue el cuadro de diálogo de ayuda. Necesité agregar

 helpDialog.Abort(); 

al evento de cierre de formulario de mi interfaz de usuario principal para asegurarme de que el cuadro de diálogo de ayuda se cierra cuando finaliza la aplicación.

Ahora tengo un diálogo de ayuda no modal que no se ve afectado por ningún cuadro de diálogo modal engendrado desde mi interfaz de usuario principal, que es exactamente lo que quería. Esto es seguro ya que no se necesita comunicación entre la interfaz de usuario principal y el diálogo de ayuda.

Aquí está el helper que estoy usando en WPF para evitar que el diálogo bloquee las ventanas que no son de diálogo en función de algunas respuestas a esta pregunta:

 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)); } } 

Uso:

 if(true == window.ShowDialogNonBlocking()) { // Dialog result has correct value } 

Usando el ejemplo:

 (new NoneBlockingDialog((new frmDialog()))).ShowDialogNoneBlock(this); 

Código fuente:

 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)); } } 

Tal vez una ventana secundaria (vea ChildWindow para más detalles) sería una solución más elegante, y evitaría todos los problemas con hilos separados para la GUI.