WPF:窗口关闭后无法重复使用

62

我正在尝试保留一个Window的实例,并在需要时调用ShowDialog。这在Winforms中工作正常,但在WPF中我收到以下异常:

System.InvalidOperationException: Cannot set Visibility or call Show, ShowDialog, or WindowInteropHelper.EnsureHandle after a Window has closed.

在WPF中有没有办法做类似的事情?

MyWindow.Instance.ShowDialog();

public class MyWindow : Window
{
    private static MyWindow _instance;

    public static MyWindow Instance
    {
        if( _instance == null )
        {
            _instance = new Window();
        }
        return _instance();
    }
}

2
有没有特定的原因,为什么你不能每次实例化一个新的对象?在我看来,这样更安全、更好。 - Alex Paven
@Alex,问题的根源在于我正在使用的第三方控件。当加入Prism和Unity时,情况变得更加复杂。实际上,我认为像Winform时代那样的单例表单会更容易实现。当尝试在非模态对话框上进行Show/Hide操作时,性能非常出色。然而,要求规定对话框必须是模态的。 - Jerod Houghtelling
对话框的 Show 方法是否接受参数?我找到了这个网址:http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/f3565f01-f972-4aaf-80cc-986488d25261 ,或许可以帮助你。 - Alex Paven
+1 正是我所需要的。从 WinForms 到 WPF 的转变令人惊讶。 - Gishu
有很多情况下保留窗口是有用/重要的。在我的情况下,窗口包含我希望保留的编辑文本。这是从WinForms中出现的奇怪行为变化。 - A. Vieira
9个回答

54

如果我没记错的话,你可以取消那个窗口的关闭事件,改为将其可见性设置为隐藏

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        e.Cancel = true;
        this.Visibility = Visibility.Hidden;
    } 

这个答案和Martin Harris的一样,只是加了代码。 - evanb
1
如果有人遇到类似的问题,我不得不在我的窗口标签中的XAML中添加Closing="Window_Closing"。 - nivekgnay
.Show()方法会使窗口的Visibility属性变为Visible吗? - gghuffer
这个解决方案是可以接受的,但我觉得它不够美观,你可以有很多开放性能消耗的优势!!!你可能应该防止窗口关闭,并在“show”方法完成后重新关闭它 - undefined

52

如果您改变窗口的可见性而不是关闭它,那么您可以做到。您需要在Closing()事件中执行此操作,然后取消关闭。如果允许关闭发生,您肯定无法重新打开已关闭的窗口-来自这里:

 

如果未取消关闭事件,则会发生以下情况:  

...  

由Window创建的非托管资源将被处理。

之后,该窗口将再也无效了。

不过,我认为这不值得努力-每次制作新窗口确实不会对性能造成太大影响,并且您很少会引入难以调试的错误/内存泄漏。 (另外,您需要确保它在应用程序关闭时关闭并释放其资源)


刚刚读到您正在使用ShowDialog(),这将使窗口模态并且仅隐藏它不会将控件返回给父窗口。我怀疑使用模态窗口根本不可能做到这一点。


12
实际上,创建一个新的窗口是一个相当昂贵的操作,因为需要考虑布局、初始化等所有成本。对于一些比较复杂的窗口,这样做可以明显提高性能 - 我已经尝试过了 ;-). - Eamon Nerbonne

7

试试这个:

protected override void OnClosing(CancelEventArgs e)
{
    this.Visibility = Visibility.Hidden;
    e.Cancel = true;
}

这是最好的答案。 - James Westgate

3
当我们试图展示一个已经关闭的窗口时,会出现以下异常信息:“Cannot set Visibility or call Show, ShowDialog, or WindowInteropHelper.EnsureHandle after a Window has closed.” 因此,为了处理这种情况,最好使用窗口的Visibility选项。我们需要将窗口的可见性设置为Hidden或Collapsed而不是直接关闭它。
若要隐藏窗口,可以使用以下代码:this.Visibility = System.Windows.Visibility.Collapsed或Hidden;
如果我们想再次显示该窗口,只需将其可见性设置为Visible即可,如下所示:this.Visibility = System.Windows.Visibility.Visible;

2
如果您取消关闭事件并设置visibility = hidden,则可以覆盖此问题。
Private Sub ChildWindow_Closing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) Handles Me.Closing
        e.Cancel = True
        Me.Visibility = Windows.Visibility.Hidden
End Sub

1
public class MyWindow : Window

public MyWindow ()
    {
        InitializeComponent();            
        Closed += new System.EventHandler(MyWindow_Closed);
    }

private static MyWindow _instance;

public static MyWindow Instance
{
    if( _instance == null )
    {
        _instance = new Window();
    }
    return _instance();
}
void MyWindow_Closed(object sender, System.EventArgs e)
    {
         _instance = null;
    }

1

这可能是我不理解的逻辑,但关闭窗口是不可逆的。

如果你想要“关闭”窗口并通过按钮重新打开它,你实际上可以将其隐藏起来,像这样:

 private MyWindow myWindow;
 private void OpenMyWindow_OnClick(object sender, RoutedEventArgs e)
 {
     if (myWindow == null)
     {
         myWindow = new MyWindow();
     }

     if(!myWindow.IsVisible)
     {
         myWindow.Show();
     }
     else
     {
         myWindow.Hide();
     }
 }

如果您的窗口可以关闭,我建议您使用 Closed 事件进行处理。 (这是我使用的解决方案)
private MyWindow myWindow;
private void OpenMyWindow_OnClick(object sender, RoutedEventArgs e)
{
    if (myWindow == null)
    {
        myWindow = new MyWindow();
        myWindow.Closed += OnMyWindowClosed;
    }

    if(!myWindow.IsVisible)
    {
        myWindow.Show();
    }
    else
    {
        myWindow.Hide();
    }
}

private void OnMyWindowClosed(object obj, EventArgs e)
{
    myWindow = null;
}

我希望我能帮助到某个人


1

以下是我的处理方式:

public partial class MainWindow 
{
    bool IsAboutWindowOpen = false;

    private void Help_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        if (!IsAboutWindowOpen)
        {
            var aboutWindow = new About();
            aboutWindow.Closed += new EventHandler(aboutWindow_Closed);
            aboutWindow.Show();
            IsAboutWindowOpen = true;
        }
    }

    void aboutWindow_Closed(object sender, EventArgs e)
    {
        IsAboutWindowOpen = false;
    }
}

0

我遇到了一个类似的问题。有一个模态对话框,但在该对话框中有一个按钮“选择”,需要切换到主表单(最好不关闭模态对话框),从那里选择一些区域,然后返回到带有选择信息的模态对话框。我试图用无模式对话框/显示/隐藏来解决这个问题,并且在找不到任何好的(易于编码)解决方案之后,编写了一些有点繁琐的方法,使用Win32本地函数调用。在测试过后,它可以很好地适用于Winforms和XAML。

问题本身也不是一个容易的问题-用户按下“选择”键,然后他可能会忘记自己正在选择某些内容,并返回到相同或不同的选择对话框,这可能导致两个或多个相同对话框的实例。

我正在尝试使用静态变量(instance / parent)来解决这个问题-如果您具有纯Winforms或纯WPF技术,您可以从instance.Parent或instance.Owner中获取parent。

public partial class MeasureModalDialog : Window
{
    //  Dialog has "Select area" button, need special launch mechanism. (showDialog / SwitchParentChildWindows)
    public static MeasureModalDialog instance = null;
    public static object parent = null;

    static public void showDialog(object _parent)
    {
        parent = _parent;
        if (instance == null)
        {
            instance = new MeasureModalDialog();

            // Parent is winforms, child is xaml, this is just glue to get correct window owner to child dialog.
            if (parent != null && parent is System.Windows.Forms.IWin32Window)
                new System.Windows.Interop.WindowInteropHelper(instance).Owner = (parent as System.Windows.Forms.IWin32Window).Handle;

            // Enable parent window if it was disabled.
            instance.Closed += (_sender, _e) => { instance.SwitchParentChildWindows(true); };
            instance.ShowDialog();

            instance = null;
            parent = null;
        }
        else
        {
            // Try to switch to child dialog.
            instance.SwitchParentChildWindows(false);
        }
    } //showDialog

    public void SwitchParentChildWindows( bool bParentActive )
    {
        View3d.SwitchParentChildWindows(bParentActive, parent, this);
    }


    public void AreaSelected( String selectedAreaInfo )
    {
        if( selectedAreaInfo != null )     // Not cancelled
            textAreaInfo.Text = selectedAreaInfo;

        SwitchParentChildWindows(false);
    }

    private void buttonAreaSelect_Click(object sender, RoutedEventArgs e)
    {
        SwitchParentChildWindows(true);
        View3d.SelectArea(AreaSelected);
    }

    ...

public static class View3d
{

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool EnableWindow(IntPtr hWnd, bool bEnable);

    [DllImport("user32.dll")]
    static extern bool SetForegroundWindow(IntPtr hWnd);

    [DllImport("user32.dll", SetLastError = true)]
    static extern bool BringWindowToTop(IntPtr hWnd);

    [DllImport("user32.dll", SetLastError = true)]
    static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool IsWindowEnabled(IntPtr hWnd);

    /// <summary>
    /// Extracts window handle in technology independent wise.
    /// </summary>
    /// <param name="formOrWindow">form or window</param>
    /// <returns>window handle</returns>
    static public IntPtr getHandle( object formOrWindow )
    {
        System.Windows.Window window = formOrWindow as System.Windows.Window;
        if( window != null )
            return new System.Windows.Interop.WindowInteropHelper(window).Handle;

        System.Windows.Forms.IWin32Window form = formOrWindow as System.Windows.Forms.IWin32Window;
        if (form != null)
            return form.Handle;

        return IntPtr.Zero;
    }

    /// <summary>
    /// Switches between modal sub dialog and parent form, when sub dialog does not needs to be destroyed (e.g. selecting 
    /// something from parent form)
    /// </summary>
    /// <param name="bParentActive">true to set parent form active, false - child dialog.</param>
    /// <param name="parent">parent form or window</param>
    /// <param name="dlg">sub dialog form or window</param>
    static public void SwitchParentChildWindows(bool bParentActive, object parent, object dlg)
    {
        if( parent == null || dlg == null )
            return;

        IntPtr hParent = getHandle(parent);
        IntPtr hDlg = getHandle(dlg);

        if( !bParentActive )
        {
            //
            // Prevent recursive loops which can be triggered from UI. (Main form => sub dialog => select (sub dialog hidden) => sub dialog in again.
            // We try to end measuring here - if parent window becomes inactive - 
            // means that we returned to dialog from where we launched measuring. Meaning nothing special needs to be done.
            //
            bool bEnabled = IsWindowEnabled(hParent);
            View3d.EndMeasuring(true);   // Potentially can trigger SwitchParentChildWindows(false,...) call.
            bool bEnabled2 = IsWindowEnabled(hParent);

            if( bEnabled != bEnabled2 )
                return;
        }

        if( bParentActive )
        {
            EnableWindow(hDlg, false);      // Disable so won't eat parent keyboard presses.
            ShowWindow(hDlg, 0);  //SW_HIDE
        }

        EnableWindow(hParent, bParentActive);

        if( bParentActive )
        {
            SetForegroundWindow(hParent);
            BringWindowToTop(hParent);
        } else {
            ShowWindow(hDlg, 5 );  //SW_SHOW
            EnableWindow(hDlg, true);
            SetForegroundWindow(hDlg);
        }
    } //SwitchParentChildWindows

    ...

相同的范式可能会在模态对话框中出现问题,因为每个选择函数调用链都会消耗堆栈,最终可能会导致堆栈溢出,或者您也可能会在管理父窗口状态(启用/禁用)时遇到问题。

因此,我认为这是解决问题的相当轻量级的解决方案,即使它看起来相当复杂。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接