Можно ли использовать ShowDialog, не блокируя все формы? - PullRequest
47 голосов
/ 09 января 2009

Я надеюсь, что смогу объяснить это достаточно ясно. У меня есть основная форма (A), и она открывает 1 дочернюю форму (B) с помощью form.Show () и вторую дочернюю форму (C) с помощью form.Show (). Теперь я хочу, чтобы дочерняя форма B открывала форму (D) с помощью form.ShowDialog (). Когда я делаю это, он также блокирует форму А и форму С. Есть ли способ открыть модальное диалоговое окно и заблокировать только форму, открывшую его?

Ответы [ 10 ]

82 голосов
/ 09 января 2009

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

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

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

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

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

  4. Когда форма отображается в виде диалога, установка ее свойства DialogResult автоматически закрывает ее. Это свойство устанавливается всякий раз, когда нажимается кнопка со свойством DialogResult, отличным от None. Форма, показанная с 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;
    }
}
10 голосов
/ 09 января 2009

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

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

9 голосов
/ 09 января 2009

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

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

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

6 голосов
/ 20 января 2015

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

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

А теперь краткий обзор возможных решений:

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

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

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

  2. Эмулировать ShowDialog() Поведение

    Возможно подражать поведению этого метода. Снова рекомендую прочитать P Ответ папы .

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

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

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

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

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

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

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

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

    d) Использовать несколько потоков GUI . Ответ от TheSmurf .

4 голосов
/ 23 мая 2012

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

Запустить FormB в новой теме в FormA:

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

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

3 голосов
/ 29 марта 2012

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

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

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

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

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

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

2 голосов
/ 16 марта 2017

Вот помощник, который я использую в 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
}
0 голосов
/ 22 марта 2015

Используя пример:

(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));
    }
}
0 голосов
/ 29 декабря 2012

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

...