Можно ли использовать ShowDialog без блокировки всех форм?

надеюсь, я смогу объяснить это достаточно ясно. У меня есть моя основная форма (A), и она открывает 1 дочернюю форму (B) с помощью формы.Show () и вторая дочерняя форма (C) с использованием формы.Шоу.)( Теперь я хочу, чтобы дочерняя форма B открыла форму (D) с помощью формы.Метода showdialog(). Когда я это делаю, он блокирует форму A и форму C. Есть ли способ открыть модальный диалог и только заблокировать форму, которая его открыла?

10 ответов


Если вы запустите форму B в отдельном потоке из A и C, вызов ShowDialog заблокирует только этот поток. Понятно, что это не тривиальная инвестиция в работу, конечно.

вы можете иметь диалог не блокировать любые потоки вообще, просто запустив вызов ShowDialog формы D в отдельном потоке. Это требует такой же работы, но гораздо меньше, так как у вас будет только одна форма, работающая с основным потоком вашего приложения.


использование нескольких потоков GUI-сложный бизнес, и я бы не советовал, если это ваша единственная мотивация для этого.

гораздо более подходящим подходом является использование Show() вместо ShowDialog() и отключите форму владельца, пока не вернется всплывающая форма. Есть только четыре соображения:

  1. , когда ShowDialog(owner) используется, всплывающая форма остается поверх своего владельца. То же самое верно, когда вы используете Show(owner). Кроме того, вы можете установить Owner свойство явно, с тем же эффектом.

  2. если вы установите форму владельца Enabled свойство false, форма показывает отключенное состояние (дочерние элементы управления "выделены серым цветом"), тогда как когда ShowDialog используется, форма владельца по-прежнему отключается, но не показывает отключенное состояние.

    когда вы называете ShowDialog, форма владельца отключается в коде Win32-its WS_DISABLED бит стиля устанавливается. Это приводит к тому, что он теряет способность фокусироваться и "Динь" при нажатии, но не делает его рисовать себя серым.

    при создании формы Enabled свойство false, устанавливается дополнительный флаг (в рамках, а не в базовой подсистеме Win32), который некоторые элементы управления проверяют, когда они рисуют себя. Этот флаг указывает элементам управления рисовать себя в отключенном состоянии.

    Итак, чтобы подражать тому, что произойдет с ShowDialog, мы должны установить уроженца WS_DISABLED бит стиля напрямую, вместо установки формы Enabled свойство false. Это достигается с помощью крошечного взаимодействия:

    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. на ShowDialog() вызов не возвращается, пока диалоговое окно не будет отклонено. Это удобно, потому что вы можете приостановить логику в форме владельца, пока диалоговое окно не завершит свою работу. The Show() call, обязательно, ведет себя не так. Поэтому, если вы собираетесь использовать Show() вместо ShowDialog(), вам нужно будет разбить свою логику на две части. Код, который должен выполняться после того, как диалог будет отклонен (который будет включать повторное включение формы владельца), должен быть запущен Closed обработчик событий.

  4. когда форма отображается в виде диалога, установите ее DialogResult собственность автоматически закрывает его. Это свойство устанавливается всякий раз, когда кнопка с DialogResult собственность, кроме это. Форма, показанная с Show не будет автоматически закрываться, поэтому мы должны явно закрыть его при нажатии одной из кнопок увольнения. Обратите внимание, однако, что DialogResult свойство по-прежнему устанавливается соответствующим образом кнопкой.

реализуя эти четыре вещи, ваш код становится чем-то вроде:

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

вы можете использовать отдельный поток (как показано ниже), но это становится опасной территорией - вы должны только приблизиться к этой опции, если понимаете последствия потоковой передачи (синхронизация, кросс-потоковый доступ и т. д.):

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

(очевидно, вы бы на самом деле не структурировали код, как указано выше - это самый короткий способ показать поведение; в реальном коде у вас был бы класс для каждой формы и т. д.)


я хотел бы обобщить возможные решения и добавить одну новую альтернативу (3a и 3b). Но сначала я хочу уточнить, о чем мы говорим:

у нас есть приложение, которое имеет несколько форм. Существует требование показать модальный диалог, который будет блокировать только определенное подмножество наших форм, но не другие. Модальные диалоги могут отображаться только в одном подмножестве (сценарий A) или нескольких подмножествах (сценарий B).

и теперь резюме можно решения:

  1. не используйте модальные формы, показанные через ShowDialog() на всех

    подумайте о дизайне вашего приложения. Вам действительно нужно использовать ShowDialog() способ? Если вам не нужна модальная форма, это самый простой и чистый способ пойти.

    конечно, это решение не всегда подходит. Есть некоторые особенности, которые ShowDialog() дает нам. Самое примечательное, что он отключает владельца (но не серые) и пользователя не может взаимодействовать с ним. Очень утомительный ответ предоставил P Папа.

  2. повторить ShowDialog() поведение

    можно эмулировать поведение этого mathod. Еще раз рекомендую прочитать P Папин ответ.

    a) использовать сочетание Enabled собственности на Form и отображение формы как немодальной через Show(). В результате отключенная форма будет выделена серым цветом. Но это полностью управляемое решение без какого-либо взаимодействия.

    b) не нравится, что родительская форма выделена серым цветом? Ссылка на несколько собственных методов и выключить WS_DISABLED бит в родительской форме (еще раз - см. ответ от P Папа).

    эти два решения требуют, чтобы у вас был полный контроль над всеми диалоговыми окнами, которые вам нужно обработать. Вы должны использовать специальную конструкцию для отображения "частично блокирующего диалога" и должны не забывайте об этом. Вам нужно настроить свою логику, потому что Show() не блокирует и ShowDialog() блокирует. Работа с системными диалогами (выбор файлов, выбор цветов и т. д.) может быть проблема. С другой стороны, вам не нужен дополнительный код на формах, которые не должны быть заблокированы диалогом.

  3. преодолеть ограничения ShowDialog()

    обратите внимание, что есть Application.EnterThreadModal и Application.LeaveThreadModal событий. Этот событие возникает всякий раз, когда отображается модальный диалог. Помните, что события на самом деле являются потоковыми, а не прикладными.

    a) прослушайте Application.EnterThreadModal событие в формах, которые не должны быть заблокированы диалог и поворот на WS_DISABLED немного в этих формах. Вам нужно только настроить формы, которые не должны быть заблокированы модальными диалогами. Вам также может потребоваться проверить родительскую цепочку показываемой модальной формы и переключиться WS_DISABLED на основе этого условия (в вашем пример, если вам также нужно открыть диалоги по формам A и C, но не блокировать формы B и D).

    b) скрыть и повторно показать формы, которые не должны быть заблокированы. Обратите внимание, что при отображении новой формы после отображения модального диалога она не блокируется. Воспользуйтесь этим, и когда модальный диалог отображается, скрыть и показать снова нужные формы, чтобы они не были заблокированы. Однако такой подход может вызвать некоторое мерцание. Теоретически это можно исправить, включив / отключив перекраску формы в win API, но я не гарантирую, что.

    c) Set Owner свойство диалоговой формы на формах, которые не должны быть заблокированы при отображении диалога. Я не проверял это.

    d) используйте несколько потоков GUI. ответ от TheSmurf.


Я просто хотел добавить свое решение здесь, поскольку оно, похоже, хорошо работает для меня и может быть инкапсулировано в простой метод расширения. Единственное, что мне нужно сделать, это разобраться с миганием, поскольку @nightcoder прокомментировал ответ @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);
}

запустить FormB в новом потоке в FormA:

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

теперь любые формы, открытые в новом потоке с помощью ShowDialog (), будут блокировать только FormB, а не FormA или FormC


я столкнулся с аналогичной проблемой в приложении, которое я писал. Мой основной пользовательский интерфейс был формой, запущенной в основном потоке. У меня был диалог справки, который я хотел запустить как немодальный диалог. Это было легко реализовать, даже до такой степени, что у меня был только один экземпляр диалогового окна справки. К сожалению, любые модальные диалоги, которые я использовал, привели к тому, что диалог справки также потерял фокус - когда некоторые из этих модальных диалогов были запущены, чтобы иметь диалог справки наиболее полезный.

используя идеи, высказанные здесь, и в других местах, мне удалось преодолеть эту ошибку.

Я объявил поток внутри моего основного пользовательского интерфейса.

Thread helpThread;

следующий код имеет дело с событием, запущенным, чтобы открыть диалоговое окно справки.

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

мне также нужна была инициализация потока, добавленного в мой конструктор, чтобы убедиться, что я не ссылаюсь на нулевой объект при первом запуске этого кода.

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

это гарантирует что поток имеет только один экземпляр в любой момент времени. Поток сам запускает диалог и останавливается после закрытия диалога. Поскольку он работает в отдельном потоке, создание модального диалога из основного пользовательского интерфейса не вызывает зависания диалога справки. Мне нужно было добавить

helpDialog.Abort();

к событию закрытия формы моего основного пользовательского интерфейса, чтобы убедиться, что диалоговое окно справки закрывается при завершении приложения.

теперь у меня есть диалог modeless help, который не зависит от каких-либо модальные диалоги возникли из моего основного пользовательского интерфейса, что именно то, что я хотел. Это безопасно, так как нет никакой связи между основным пользовательским интерфейсом и диалогом справки.


вот помощник, который я использую в WPF, чтобы предотвратить блокировку диалоговых окон на основе некоторых ответов на этот вопрос:

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

использование:

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

Пример Использования:

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

исходный код:

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

возможно, дочернее окно (см. детское окно для деталей) было бы более элегантным решением, и это позволило бы избежать всех проблем с отдельными потоками для GUI.