65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF - 非模态窗口管理器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2021年3月1日

CPOL

6分钟阅读

viewsIcon

7841

downloadIcon

216

在需要对无模式窗口的创建/显示时进行更多控制的情况下

引言

在工作中,我最近不得不开发一个创建多个无模式窗口的工具。这些无模式窗口用于展示个人的培训数据、个人的测试指标以及可用的课程数据。虽然无模式窗口本身并不新颖或令人兴奋,但我还需要考虑一个额外的因素:不允许用户重复选择同一个用户并打开两个不同的窗口(数据是静态的,因此没有理由让两个窗口显示相同的数据)。这个要求使得简单地显示无模式窗口变得不可能,迫使我提出了一个无模式窗口管理器,这也是本文的主题。

本文按以下顺序呈现信息:引言、用法和代码描述。这对注意力不集中的人很有益,他们不想知道代码是如何工作的,但又想要代码,而且无需忍受技术知识的负担和枯燥。

Modeless window manager screen shot
 

特点

ModelessWindowMgr 类实现了以下功能

  • 如果窗口的 Owner 属性没有被特别设置,则使用 MainWindow 作为所有者。
     
  • 自动处理 Owner_Closing 事件,以便在所有者窗口关闭时,由该窗口创建的所有无模式窗口也将自动关闭。Owner_Closing 事件处理程序是虚拟的,因此您可以在需要时重写它。
     
  • 窗口管理器可以配置为替换具有相同标识属性值的现有窗口。
     
  • 窗口管理器可以配置为允许具有相同标识属性值的窗口的重复实例。
     
  • 您可以使用 IModelessWindow 接口中提供的 id 属性,或者 Window 属性中的许多可能提供唯一标识给定窗口的方法(Title, Name, Tag 等)。
     
  • 您可以阻止给定无模式窗口被用作模态窗口。

用法

以下是最小化实现 ModelessWindowMgr 的步骤。下面提供的示例代码仅用于说明需要做什么,并且是示例代码中内容的简化版本。除了创建无模式窗口之外,实际实现中没有 XAML,但即使是它们也不使用任何窗口管理器功能。

0) 以任何适合您项目的方式将 ModelessWindowMgr 文件添加到您的项目中。在我的例子中,我有一个 WpfCommon 程序集,我所有的通用 WPF 代码都在其中。

1) 当您创建一个打算放入窗口管理器的窗口时,必须继承并实现 IModelessWindow 接口。在下面的示例中,我还有一个基类窗口,它继承/实现了 INotifyPropertyChangedNotifiableWindow),因此我继承自该窗口类以及 IModelessWindow 接口。请注意 HandleModeless() 方法 - 该方法由窗口管理器调用,以便为您添加 Owner_Closing 事件处理程序。

我**强烈**建议您采用类似的基类策略。这将使您基本上可以忘记特定于窗口的实现,而只需继续您的编码。

public class ModelessWindowBase : NotifiableWindow, IModelessWindow
{
    #region IModelessWindow implementation

    // the two properties that must be included in your window class
    public string IDString { get; set; }
    public int    IDInt    { get; set; }
    public bool IsModal()
    {
        return ((bool)(typeof(Window).GetField("_showingAsDialog", 
               BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this)));
    }

    public void HandleModeless(Window owner)
    {
        // we can't user the Window.Owner property because doing so causes the 
        // modeless window to always be on top of its owner. This is a "bad thing" 
        // (TM). Therefore, it's up to the developer to supply the owner window 
        // when he adds the modeless window to the manager.
        owner.Closing += this.Owner_Closing;
    }

    public virtual void Owner_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        if (!this.IsModal())
        {
            this.Close();
        }
    }

    #endregion IModelessWindow implementation

    public TestWindowBase():this(string.Empty, 0)
    {
    }

    public TestWindowBase(string idString, int idInt=0) : base()
    {
        // populate the id properties
        this.IDString = idString;
        this.IDInt    = idInt;
    }
}

2) 创建一个继承自基类的窗口

public partial class Window1 : ModelessWindowBase
{
    public Window1()
    {
        this.InitializeComponent();
    }

    public Window1(string id):base(id)
    {
        this.InitializeComponent();
    }
}

并相应地更改您的 xaml 以匹配基类

local:ModelessWindowBase x:Class="WpfModelessWindowMgr.Window1"
                         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                         ...
                         xmlns:local="clr-namespace:WpfModelessWindowMgr"
                         Title="Window1" Height="100" Width="600"
                         x:Name="ModelessWindow1" Tag="1">
    ...
</local:ModelessWindowBase>

3) 将类管理器添加到您的 MainWindow 对象。

public partial class MainWindow : NotifiableWindow
{
	private ModelessWindowMgr manager;
	public ModelessWindowMgr Manager 
    { get { return this.manager; } 
    set { if (value != this.manager) { this.manager = value; } } }

    private string Window1Guid { get; set; }
		
	public MainWindow()
	{
		this.InitializeComponent();
		this.DataContext = this;
        string idProperty = "IDString";
        bool replaceIfExists = false;
        bool allowDuplicates = false;
		this.Manager = new ModelessWindowMgr("IDString", 
                                             replaceIfExists, 
                                             allowDuplicates);
	}
}

4) 添加代码以创建您(在上面第 2 步中创建)的无模式窗口的实例。如果您不希望窗口立即显示,请将 false 作为 Add 方法的第二个参数。

Window1 form = new Window1(this.Window1Guid);
this.Manager.Add(form);

差不多就是这样了。您现在拥有一个功能齐全的无模式窗口管理器实现。由于继承自 IModelessWindow 的窗口会自动挂钩 Owner_Closing 事件,因此当您关闭主窗口时,任何(添加到窗口管理器的)仍然打开的无模式窗口都将自动清理。

代码

本节将只介绍 ModelessWindowMgr 类以及 IModelessWindow 接口的实现。

IModelessWindow 接口

IModelessWindow 接口支持两种类型的 id 属性,以及一些必要的方法,以确保给定窗口可以在窗口管理器维护的窗口列表中正常工作。id 属性允许使用通用方法来标识窗口列表中的给定窗口。我认为提供整数和字符串之间的选择是有意义的。此 id 值最好通过窗口构造函数参数,或窗口内部的某些其他机制(如窗口 XAML 中的 TitleNameTag 属性)来分配。

该接口的实际实现还涉及几个方法。建议的实现(包括下面的代码)可以在 IModelessWindow.cs 中的注释中找到。

/// <summary>
/// Determine if the window is modal
/// </summary>
/// <returns></returns>
public bool IsModal()
{
    // uses a private Window property to determine if this window is modal
    return ((bool)(typeof(Window).GetField("_showingAsDialog", 
            BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this)));
}

/// <summary>
/// Adds an event handler for the Owner window's Closing event.
/// </summary>
public void HandleModeless()
{
    // we can't user the Window.Owner property because doing so causes the modeless 
    // window to always be on top of its owner. This is a "bad thing" (TM). 
    // Therefore, it's up to the developer to supply the owner window when he adds 
    // the modeless window to the manager.
    owner.Closing += this.Owner_Closing;
}

/// <summary>
/// The closing event for this window's owner.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void Owner_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    if (!this.IsModal())
    {
        this.Close();
    }
}

接口实现的另一个方面是能够实际阻止窗口被用作模态窗口。默认情况下,PreventModal 属性设置为 false,但如果您需要/想要,它就在那里。

public new bool? ShowDialog()
{
    if (this.PreventModal)
    {
        throw new InvalidOperationException("Can't treat a this window like a modal window.");
    }
    return base.ShowDialog();
}

ModelessWindowManager 类

ModelessWindowMgr 的基本功能是添加、显示和移除窗口。

添加/显示窗口

窗口的添加和显示由一对方法处理。Add 方法允许您指定已实例化(但未显示)的无模式窗口,并且您可以选择指定是否立即显示窗口(默认是立即显示)。我将直接展示代码,而不是提供叙述和代码,因为我认为代码注释已足够详尽。

/// <summary>
/// Add/activate the specified window
/// </summary>
/// <param name="owner">The owner window (cannot be null)</param>
/// <param name="window">The IModelessWindow window (cannot be null)</param>
/// <param name="showNow">Show the window now, or simply add it to manager</param>
public void Add(IModelessWindow window, bool showNow=true)
{
    // sanity checks
    if (owner == null)
    {
        throw new ArgumentNullException("owner");
    }
    if (window == null)
    {
        throw new ArgumentNullException("window");
    }

    // first, make sure the "window" is really a window (to keep the developer from trying 
    // to catch us asleep at the wheel)
    if (window is Window)
    {
        IModelessWindow existingWindow = null;
        bool            addIt          = false;

        // if we're allowing duplicates, we can skip all this and just get down to 
        // showing the window.
        if (this.AllowDuplicates)
        {
            addIt = true;
        }
        else
        {
            // see if it's already in the list
            existingWindow       = this.Find(window);
            // if it is already in the list
            if (existingWindow != null)
            {
                // go ahead and close the new window and set it to null. There's 
                // no point in keeping it around.
                ((Window)window).Close();
                window = null;

                // if we need to replace it, eradicate the existing instance first
                if (this.ReplaceIfOpen)
                {
                    ((Window)(existingWindow)).Close();
                    this.Windows.Remove(existingWindow);
                    existingWindow = null;
                    addIt = true;
                }
            }
            else
            {
                addIt = true;
            }
        }
        // add or focus the window
        if (addIt)
        {
            // make sure we hook the Owner_Closing event
            window.HandleModeless(owner);
            // and add the window to the list
            this.Windows.Add(window);
            // if we want immediate gratification, show the window
            if (showNow)
            {
                ((Window)(window)).Show();
            }
        }
        else
        {
            Window wnd = ((Window)(existingWindow));
            if (wnd.Visibility == Visibility.Collapsed)
            {
                wnd.Show();
            }
            if (wnd.WindowState == WindowState.Minimized)
            {
                wnd.WindowState = WindowState.Normal;
            }
            wnd.Focus();
        }
    }
}

/// <summary>
/// Find the window in the manager's window list that matches <br/>
/// the ID property value of the specified window.
/// </summary>
/// <param name="window">The window to find</param>
/// <returns>The matching window if found, or null</returns>
protected IModelessWindow Find(IModelessWindow window)
{
    // "The relationship is new. Let's not sully it with an exchange of saliva..."
    // George Takei, Big Bang Theory, S4, E4

    Window foundAsWindow = null;
    Window windowAsWindow = ((Window)window);

    // find a window with the same id property value. We can check both the string and int 
    // id properties because we converted the value we're looking for into a string. We 
    // also check to make sure the window is actually modeless
    IModelessWindow found = null;
    try
    {
        // if the id property is one of the two defined in the IModelessWindow interface
        if (new string[] { "IDString", "IDInt" }.Contains(this.IDProperty))
        {
            // get the named property's value as a string
            string value = typeof(IModelessWindow).GetProperty(this.IDProperty).GetValue(window).ToString();

            // and find the first window that has the same value for the id property
            found = this.Windows.FirstOrDefault(x => x.IDString == value || x.IDInt.ToString() == value);
        }
        else
        {
            // get the value of the specified Window property, could be Title, Tag, or some 
            // other property that could be used to uniquely identify a window
            object windowPropertyValue = typeof(Window).GetProperty(IDProperty).GetValue(window);

            // and find the first window that has the same value for the specified property
            found = this.Windows.FirstOrDefault(x => typeof(Window).GetProperty(IDProperty).GetValue(x).Equals(windowPropertyValue));
        }
        foundAsWindow = ((Window)found);
    }
    catch (Exception ex)
    { 
        // avoid the compiler warning while allowing us to examine the exception in the 
        // debugger if necessary
        if (ex != null) { }
        // We don't want to do anything but eat the exception. This means if there's an 
        // exception, this method will return null, thus guaranteeing that the window 
        // will be shown.
    }
    // return the result (null or the found window)
    return found;
}

移除窗口

窗口可以按 id 属性值或底层窗口类型移除,并且可以一次移除一个(第一个或最后一个)或全部移除。所有这些方法都会获取一个要移除的窗口列表(即使只是移除一个窗口)。

/// <summary>
/// Removes all windows from the windows manager
/// </summary>
public void RemoveAll()

/// <summary>
/// Remove the first window with the specified id value from the windows manager
/// </summary>
/// <param name="id">The value of the IDProperty property</param>
public void RemoveFirstWithID(object id)

/// <summary>
/// Remove the last window with the specified id value from the windows manager
/// </summary>
/// <param name="id">The value of the IDProperty property</param>
public void RemoveLastWithID(object id)

/// <summary>
/// Remove the all windows with the specified id value from the windows manager
/// </summary>
/// <param name="id">The value of the IDProperty property</param>
public void RemoveAllWithID(object id)

/// <summary>
/// Remove the first window of the specified type from the windows manager
/// </summary>
/// <param name="windowType">The underlying window type</param>
public void RemoveFirstWindowOfType(Type windowType)

/// <summary>
/// Remove the first window of the specified type from the windows manager
/// </summary>
/// <param name="windowType">The underlying window type</param>
public void RemoveLastWindowOfType(Type windowType)

/// <summary>
/// Remove the first window of the specified type from the windows manager
/// </summary>
/// <param name="windowType">The underlying window type</param>
public void RemoveAllWindowsOfType(Type windowType)

这使得实际的移除代码存在于一个通用的方法中,从而更容易进行故障排除。该方法如下所示

/// <summary>
/// Remove specified windowswindows manager
/// </summary>
/// <param name="windows">The list of found windows to remove</param>
protected void RemoveThese(List<IModelessWindow> windows)
{
    while (windows.Count > 0)
    {
        IModelessWindow window = windows[0];
        ((Window)window).Close();
        this.Windows.Remove(window);
        windows.Remove(window);
    }
}

关注点

如果您希望在整个应用程序中都可以访问窗口管理器,我建议您使用您最喜欢的方法来实现。我通常会这样做:

  • 创建一个静态全局类,并在其中实例化它。
     
  • 创建一个代表 ModelessWindowMgr 对象的单例。
     

我还发现,如果您在 Window 上设置 Owner 属性,您正在创建的窗口将始终显示在所有者窗口的顶部 - 这不是一个理想的效果。这导致我在尝试将窗口添加到窗口管理器时,要求指定所有者窗口。

最后,将此代码转换为 .Net Core 应该非常简单。尽情享受吧。

标准 JSOP 免责声明

这是我关于解决实际编程问题文章系列的最新一篇。这里没有理论的空谈,没有“如果怎样”,也没有假设的废话。如果您正在寻找一些突破性的、颠覆性的甚至接近前沿的东西,我建议您另寻阅读材料(.Net Core 的粉丝们似乎为自己感到非常自豪)。我并不是那种为他人开辟道路的人(事实上,我应该作为榜样的唯一例子是**不**应该做什么),而且我绝不会声称我的方法是“唯一正确的方式”(除了关于邪恶的 Entity Framework)。如果这篇文章不合您的口味,请便,祝您旅途愉快,并且我以我德州灵魂最深处的爱与尊重(我能尽可能地表达),AMF。

历史

  • 2021.03.02 - 修正了 IModelessWindow 实现的代码片段(下载中的实际代码是正确的,但我忘记更新文章文本)。
     
  • 2021.03.01 - 初次发布。
     
© . All rights reserved.