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

自定义弹出窗口的自动关闭行为

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.07/5 (6投票s)

2009年11月23日

CPOL

3分钟阅读

viewsIcon

59841

downloadIcon

1692

使用附加属性自定义弹出窗口关闭的条件

引言

本文讨论了WPF Popup控件的一个问题,这个问题有时会非常烦人。我指的是StaysOpen属性背后的行为逻辑。如果您曾经尝试在控件模板中使用弹出窗口,您可能遇到过弹出窗口在您希望它消失时拒绝消失,或者它消失得太快的问题。本文讨论了一个特定的需求,即一旦在内部执行单击操作,弹出窗口就会关闭。默认行为会导致弹出窗口仅在您单击其区域外部时关闭。

此外,当您的键盘焦点不在弹出窗口区域之外时,按下按键时弹出窗口会拒绝关闭。本文中描述的解决方案使弹出窗口在任何按键上都关闭。

实现

此解决方案的实现使用一个行为类,该行为类具有一个名为ClosesOnInput的附加属性。当设置为true时,此附加属性会注册弹出窗口的PreviewMouseUp事件,并在事件处理程序中,将其IsOpen属性赋值为false,从而关闭它。请注意,我们只需要注册弹出窗口自己的PreviewMouseUp事件,因为StaysOpen属性处理它外部的鼠标事件。

这是类和附加属性的定义;请注意,我们在附加属性的元数据中指定一个回调(在下面的代码段中突出显示),以便检测其更改。

public class PopupBehavior
{
    public static bool GetClosesOnInput(DependencyObject obj)
    {
        return (bool)obj.GetValue(ClosesOnInputProperty);
    }

    public static void SetClosesOnInput(DependencyObject obj, bool value)
    {
        obj.SetValue(ClosesOnInputProperty, value);
    }

    // Using a DependencyProperty as the backing store for ClosesOnInput.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ClosesOnInputProperty =
        DependencyProperty.RegisterAttached(
            "ClosesOnInput", 
            typeof(bool), 
            typeof(PopupBehavior), 
            new UIPropertyMetadata(false, OnClosesOnInputChanged));

这是目前的回调

static void OnClosesOnInputChanged(
  DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
    if (depObj is Popup == false) return;
    Popup popup = (Popup)depObj;
    bool value = (bool)e.NewValue;
    bool oldValue = (bool)e.OldValue;
    if (value && !oldValue)
    {
        popup.PreviewMouseUp += new MouseButtonEventHandler(Popup_PreviewMouseUp);
    }
    else if(!value && oldValue)
    {
        popup.PreviewMouseUp -= Popup_PreviewMouseUp;
    }
}

static void Popup_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
    Popup popup = (Popup)sender;
    popup.IsOpen = false;
}

实现的第二部分涉及处理键盘事件。这稍微不那么简单,因为我们需要注册整个窗口的PreviewKeyUp事件,而不是弹出窗口的。我们通过向上遍历可视树到最顶层的FrameworkElement来做到这一点,并注册到它的PreviewKeyUp事件。

这是找到最顶层FrameworkElement的方法

private static FrameworkElement FindTopLevelElement(Popup popup)
{
    FrameworkElement iterator, nextUp = popup;
    do
    {
        iterator = nextUp;
        nextUp = VisualTreeHelper.GetParent(iterator) as FrameworkElement;
    } while (nextUp != null);
    return iterator;
}

这是用于注册和取消注册相应事件的代码片段

// Registering
var topLevelElement = FindTopLevelElement(popup);
topLevelElement.PreviewKeyUp += new KeyEventHandler(TopLevelElement_PreviewKeyUp);

// Unregistering
topLevelElement.PreviewKeyUp -= TopLevelElement_PreviewKeyUp;

然而,棘手的部分来了,注册很容易,但是当需要取消注册时,我们如何记住我们注册了哪个元素?当然,我们可以再次向上遍历树,但是如果控件被重新父化了呢?这并不少见。为了解决这个问题,我们将弹出窗口和我们使用的元素之间的关联存储在一个字典中。此外,我们创建一个上下文捕获对象,它会记住它,并且包含注册和取消注册事件的方法。

这些是字典和上下文类的定义

static Dictionary<Popup, PopupTopLevelContext> TopLevelPopupAssociations = 
				new Dictionary<Popup, PopupTopLevelContext>();

class PopupTopLevelContext
{
    private Popup popup;
    private FrameworkElement topLevelElement;

    internal PopupTopLevelContext(Popup Popup, FrameworkElement TopLevelElement)
    {
        popup = Popup;
        topLevelElement = TopLevelElement;
        TopLevelElement.PreviewKeyUp += Popup_PreviewKeyUp;
    }

    internal void Popup_PreviewKeyUp(object sender, KeyEventArgs e)
    {
        popup.IsOpen = false;
    }

    internal void Release()
    {
        topLevelElement.PreviewKeyUp -= Popup_PreviewKeyUp;
    }
}

这是包含所有内容的回调函数的完整版本

static void OnClosesOnInputChanged(
  DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
    if (depObj is Popup == false) return;
    Popup popup = (Popup)depObj;
    bool value = (bool)e.NewValue;
    bool oldValue = (bool)e.OldValue;
    if (value && !oldValue)
    {
        // Register for the popup's PreviewMouseUp event.
        popup.PreviewMouseUp += new MouseButtonEventHandler(Popup_PreviewMouseUp);
        
        // Obtain the top level element and register to its PreviewKeyUp event
        // using a context object.
        var topLevelElement = FindTopLevelElement(popup);
        PopupTopLevelContext cp = new PopupTopLevelContext(popup, topLevelElement);
        
        // Associate the popup with the context object, for the unregistering operation.
        TopLevelPopupAssociations[popup] = cp;
    }
    else if(!value && oldValue)
    {
        // Unregister from the popup's PreviewMouseUp event.
        popup.PreviewMouseUp -= Popup_PreviewMouseUp;
        
        // Tell the context object to unregister from the PreviewKeyUp event
        // of the appropriate element.
        TopLevelPopupAssociations[popup].Release();
        
        // Disassociate the popup from the context object. The context object
        // is now unreferenced and may be garbage collected.
        TopLevelPopupAssociations.Remove(popup);
    }
}

Using the Code

现在,使用代码就像在 XAML 中引用适当的 CLR 命名空间,并为附加属性赋值一样简单。以下示例显示了一个模板化的复选框,它具有绑定到它的自动关闭弹出窗口。这种类型的控件也称为弹出按钮或菜单按钮。重要的部分已突出显示。请原谅水平滚动条,XAML 绑定可能非常冗长。

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WPFPopupBehavior"
    x:Class="WPFPopupBehavior.Window1"
    x:Name="Window"
    Title="Window1"
    Width="300" Height="200">

    <Window.Resources>
        <Style x:Key="RBStyle1" TargetType="RadioButton">
            <Setter Property="Margin" Value="3,3,15,3"/>
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="GroupName" Value="MyGroup"/>
        </Style>
        <Style x:Key="CBStyle1" TargetType="{x:Type CheckBox}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type CheckBox}">
                        <Grid>
                            <ContentPresenter 
                                HorizontalAlignment=
				"{TemplateBinding HorizontalContentAlignment}" 
                                Margin="{TemplateBinding Padding}" 
                                VerticalAlignment=
				"{TemplateBinding VerticalContentAlignment}" />
                            <Popup 
                                IsOpen="{Binding RelativeSource=
				{RelativeSource TemplatedParent}, 
				Path=IsChecked, Mode=TwoWay}" 
                                StaysOpen="False" 
                                PopupAnimation="Slide" 
                                local:PopupBehavior.ClosesOnInput="True">
                                <StackPanel>
                                    <RadioButton Content="Option 1" 
				IsChecked="True" Style="{StaticResource RBStyle1}"/>
                                    <RadioButton Content="Option 2" 
				Style="{StaticResource RBStyle1}"/>
                                    <RadioButton Content="Option 3" 
				Style="{StaticResource RBStyle1}"/>
                                    <RadioButton Content="Option 4" 
				Style="{StaticResource RBStyle1}"/>
                                </StackPanel>
                            </Popup>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <Grid x:Name="LayoutRoot">
        <CheckBox Margin="30,30,0,0" Style="{DynamicResource CBStyle1}" 
			VerticalAlignment="Top" HorizontalAlignment="Left">
            <Ellipse Fill="Red" Width="50" Height="50"/>
        </CheckBox>
    </Grid>
</Window>

关注点

有时您会感觉全世界(以及微软的 WPF 设计师)都在与您作对,并且他们强迫您编写您不想编写的东西!例如直接进入可视树并手动处理控件。然而,真正开明的 WPF 程序员知道,在 99% 的情况下,这没有必要,并且可以通过使用附加属性、数据绑定、模板等来解决此类情况。

就这样,尽情享受吧。

历史

  • 2009 年 11 月 23 日:首次发布
© . All rights reserved.