J'espère pouvoir expliquer cela assez clairement. J'ai mon formulaire principal (A) et il ouvre 1 formulaire enfant (B) en utilisant form.Show () et un deuxième formulaire enfant (C) en utilisant form.Show (). Maintenant, je veux que le formulaire enfant B ouvre un formulaire (D) à l'aide de form.ShowDialog (). Lorsque je fais cela, il bloque également la forme A et la forme C. Existe-t-il un moyen d'ouvrir une boîte de dialogue modale et de la faire bloquer uniquement le formulaire qui l'a ouverte?
Si vous exécutez le formulaire B sur un thread distinct de A et C, l'appel ShowDialog bloquera uniquement ce thread. De toute évidence, ce n'est pas un investissement trivial de travail bien sûr.
Vous pouvez faire en sorte que la boîte de dialogue ne bloque aucun thread en exécutant simplement l'appel ShowDialog de Form D sur un thread séparé. Cela nécessite le même type de travail, mais beaucoup moins, car vous n'aurez qu'un seul formulaire en cours d'exécution sur le thread principal de votre application.
L'utilisation de plusieurs threads GUI est une entreprise délicate, et je vous déconseille, si c'est votre seule motivation pour le faire.
Une approche beaucoup plus appropriée consiste à utiliser Show()
au lieu de ShowDialog()
, et de désactiver le formulaire propriétaire jusqu'à ce que le formulaire popup revienne. Il n'y a que quatre considérations:
Lorsque ShowDialog(owner)
est utilisé, le formulaire contextuel reste au-dessus de son propriétaire. Il en va de même lorsque vous utilisez Show(owner)
. Vous pouvez également définir la propriété Owner
de manière explicite, avec le même effet.
Si vous définissez la propriété Enabled
du formulaire propriétaire sur false
, le formulaire affiche un état désactivé (les contrôles enfants sont "grisés"), tandis que lorsque ShowDialog
est utilisé, le propriétaire le formulaire est toujours désactivé, mais n'affiche pas un état désactivé.
Lorsque vous appelez ShowDialog
, le formulaire propriétaire est désactivé dans le code Win32 — son bit de style WS_DISABLED
Est défini. Cela lui fait perdre la capacité de gagner le focus et de "Ding" lorsqu'il est cliqué, mais ne le fait pas se dessiner en gris.
Lorsque vous définissez la propriété Enabled
d'un formulaire sur false
, un indicateur supplémentaire est défini (dans le cadre, pas le sous-système Win32 sous-jacent) que certains contrôles vérifient lorsqu'ils se dessinent eux-mêmes. Ce drapeau indique aux contrôles de se dessiner dans un état désactivé.
Donc, pour émuler ce qui se passerait avec ShowDialog
, nous devrions définir le bit de style natif WS_DISABLED
Directement, au lieu de définir la propriété Enabled
du formulaire sur false
. Ceci est accompli avec un tout petit peu d'interopérabilité:
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));
}
L'appel ShowDialog()
ne revient pas tant que la boîte de dialogue n'est pas fermée. C'est pratique, car vous pouvez suspendre la logique dans votre formulaire de propriétaire jusqu'à ce que la boîte de dialogue ait fait son travail. L'appel Show()
, nécessairement, ne se comporte pas de cette façon. Par conséquent, si vous allez utiliser Show()
au lieu de ShowDialog()
, vous devrez diviser votre logique en deux parties. Le code qui doit s'exécuter après la fermeture de la boîte de dialogue (ce qui comprend la réactivation du formulaire propriétaire), doit être exécuté par un gestionnaire d'événements Closed
.
Lorsqu'un formulaire est affiché sous forme de boîte de dialogue, la définition de sa propriété DialogResult
le ferme automatiquement. Cette propriété est définie chaque fois qu'un bouton avec une propriété DialogResult
autre que None
est cliqué. Un formulaire affiché avec Show
ne se fermera pas automatiquement comme ceci, nous devons donc le fermer explicitement lorsque l'un de ses boutons de fermeture est cliqué. Notez cependant que la propriété DialogResult
est toujours définie de manière appropriée par le bouton.
En implémentant ces quatre choses, votre code devient quelque chose comme:
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;
}
}
Vous pouvez utiliser un thread séparé (comme ci-dessous), mais cela pénètre en territoire dangereux - vous ne devriez vous approcher de cette option que si vous comprenez les implications du thread (synchronisation, accès inter-thread, 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);
}
(Évidemment, vous ne structureriez pas réellement le code comme ci-dessus - c'est juste le moyen le plus court de montrer le comportement; dans le vrai code, vous auriez une classe par formulaire, etc.)
Je voudrais résumer les solutions possibles et ajouter une nouvelle alternative (3a et 3b). Mais je veux d'abord clarifier de quoi nous parlons:
Nous avons une application qui a plusieurs formes. Il est nécessaire d'afficher une boîte de dialogue modale qui bloquerait uniquement certains sous-ensembles de nos formulaires mais pas les autres. Les boîtes de dialogue modales peuvent être affichées uniquement dans un sous-ensemble (scénario A) ou plusieurs sous-ensembles (scénario B).
Et maintenant résumé des solutions possibles:
N'utilisez pas du tout de formes modales affichées via ShowDialog()
Pensez à la conception de votre application. Avez-vous vraiment besoin d'utiliser la méthode ShowDialog()
? Si vous n'avez pas besoin d'avoir une forme modale, c'est le moyen le plus simple et le plus propre.
Bien entendu, cette solution n'est pas toujours adaptée. Il y a quelques fonctionnalités que ShowDialog()
nous donne. Le plus notable est qu'il désactive le propriétaire (mais ne grise pas) et que l'utilisateur ne peut pas interagir avec lui. La réponse très épuisante fournie P papa .
Émuler le comportement de ShowDialog()
Il est possible d'émuler le comportement de cette méthode. Encore une fois, je recommande la lecture réponse de papa P .
a) Utilisez la combinaison de Enabled
propriété sur Form
et montrant la forme comme non modale via Show()
. En conséquence, le formulaire désactivé sera grisé. Mais c'est une solution entièrement gérée sans aucune interopérabilité.
b) Vous n'aimez pas que le formulaire parent soit grisé? Référencez quelques méthodes natives et désactivez le bit WS_DISABLED
Sur le formulaire parent (encore une fois - voir la réponse de P Daddy =).
Ces deux solutions nécessitent que vous ayez un contrôle complet sur toutes les boîtes de dialogue que vous devez gérer. Vous devez utiliser une construction spéciale pour afficher la "boîte de dialogue de blocage partiel" et ne pas l'oublier. Vous devez ajuster votre logique car Show()
n'est pas bloquant et ShowDialog()
est bloquant. Le traitement des boîtes de dialogue système (sélecteurs de fichiers, sélecteurs de couleurs, etc.) peut poser problème. En revanche, vous n'avez pas besoin de code supplémentaire sur les formulaires qui ne sera pas bloqué par la boîte de dialogue.
Surmonter les limitations de ShowDialog()
Notez qu'il y a Application.EnterThreadModal
et Application.LeaveThreadModal
événements. Cet événement est déclenché chaque fois qu'une boîte de dialogue modale est affichée. Sachez que les événements sont en fait à l'échelle du thread, et non à l'échelle de l'application.
a) Écoutez l'événement Application.EnterThreadModal
sous des formes qui ne doivent pas être bloquées par la boîte de dialogue et tournez sur WS_DISABLED
bit dans ces formulaires. Il vous suffit d'ajuster les formulaires qui ne doivent pas être bloqués par les boîtes de dialogue modales. Vous devrez peut-être également inspecter la chaîne parent du formulaire modal affiché et basculer WS_DISABLED
En fonction de cette condition (dans votre exemple, si vous avez également besoin d'ouvrir des boîtes de dialogue par les formulaires A et C mais pas de bloquer les formulaires B et D).
b) Masquer et afficher à nouveau les formulaires qui ne doivent pas être bloqués . Notez que lorsque vous affichez un nouveau formulaire après l'affichage de la boîte de dialogue modale, il n'est pas bloqué. Profitez-en et lorsque la boîte de dialogue modale s'affiche, masquez et affichez à nouveau les formulaires souhaités afin qu'ils ne soient pas bloqués. Cependant, cette approche peut apporter un certain scintillement. Il pourrait être théoriquement corrigé en activant/désactivant la repeinture des formulaires dans Win API, mais je ne le garantis pas.
c) Définissez la propriété Owner
sur le formulaire de dialogue sur les formulaires qui ne doivent pas être bloqués lorsque le dialogue est affiché. Je n'ai pas testé cela.
d) Utilisez plusieurs threads GUI . Réponse de TheSmurf .
Démarrez FormB dans un nouveau thread dans FormA:
(new System.Threading.Thread(()=> {
(new FormB()).Show();
})).Start();
Maintenant, tous les formulaires ouverts dans le nouveau thread à l'aide de ShowDialog () ne bloqueront que FormB et NON FormA ou FormC
Je voulais juste ajouter ma solution ici car elle semble bien fonctionner pour moi et peut être encapsulée dans une méthode d'extension simple. La seule chose que je dois faire est de gérer le clignotement comme @nightcoder a commenté la réponse 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);
}
J'étais confronté à un problème similaire dans une application que j'écrivais. Mon interface utilisateur principale était un formulaire fonctionnant sur le thread principal. J'avais une boîte de dialogue d'aide que je voulais exécuter en tant que boîte de dialogue non modale. Cela a été facile à mettre en œuvre, même au point de garantir que je n'ai jamais eu qu'une seule instance de la boîte de dialogue d'aide en cours d'exécution. Malheureusement, toutes les boîtes de dialogue modales que j'ai utilisées ont également fait perdre la focalisation à la boîte de dialogue d'aide - lorsque c'est pendant que certaines de ces boîtes de dialogue modales étaient en cours d'exécution que la boîte de dialogue d'aide y serait la plus utile.
En utilisant les idées mentionnées ici et ailleurs, j'ai réussi à surmonter ce bogue.
J'ai déclaré un thread dans mon interface utilisateur principale.
Thread helpThread;
Le code suivant traite de l'événement déclenché pour ouvrir la boîte de dialogue d'aide.
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();
}
}
J'ai également eu besoin de l'initialisation du thread ajouté dans mon constructeur pour m'assurer que je ne faisais pas référence à un objet nul la première fois que ce code est exécuté.
public MainWindow()
{
...
helpThread = new Thread(new ThreadStart(startHelpThread));
helpThread.SetApartmentState(ApartmentState.STA);
...
}
Cela garantit que le thread n'a qu'une seule instance à un moment donné. Le thread lui-même exécute la boîte de dialogue et s'arrête une fois la boîte de dialogue fermée. Puisqu'il s'exécute sur un thread séparé, la création d'une boîte de dialogue modale à partir de l'interface utilisateur principale ne provoque pas le blocage de la boîte de dialogue d'aide. J'avais besoin d'ajouter
helpDialog.Abort();
à l'événement de fermeture de formulaire de mon interface utilisateur principale pour vous assurer que la boîte de dialogue d'aide se ferme lorsque l'application est terminée.
J'ai maintenant une boîte de dialogue d'aide non modale qui n'est affectée par aucune boîte de dialogue modale générée à partir de mon interface utilisateur principale, ce qui est exactement ce que je voulais. Ceci est sûr car aucune communication n'est nécessaire entre l'interface utilisateur principale et la boîte de dialogue d'aide.
Voici l'aide que j'utilise dans WPF pour empêcher la boîte de dialogue de bloquer les fenêtres autres que celles basées sur certaines réponses à cette 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
}
En utilisant l'exemple:
(new NoneBlockingDialog((new frmDialog()))).ShowDialogNoneBlock(this);
Code source:
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));
}
}
Peut-être qu'une fenêtre enfant (voir ChildWindow pour plus de détails) serait une solution plus élégante et éviterait tous les problèmes avec threads séparés pour l'interface graphique.