自定义弹出窗口的自动关闭行为
使用附加属性自定义弹出窗口关闭的条件
引言
本文讨论了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 日:首次发布