使 UIElement 在 MouseDown 时可见的行为





5.00/5 (11投票s)
本文介绍了一种行为,可以在触发其附加的 UIElement 的鼠标按下事件时使另一个 UIElement 可见。该行为已扩展为直接支持 Popup 控件的淡入淡出。
引言
很多时候,我们可能希望在另一个 UIElement
被点击时使某个 UIElement
可见。在我的案例中,我想显示一组叠加在图像上的按钮,并且不希望它们一直可见,因为它们会弄乱窗口并干扰图像的查看。我最初是在代码隐藏中创建的,计划将代码封装成一个行为。由于弹出窗口旨在包含按钮,因此单击面板会将隐藏控件的 Visibility
设置为 Collapsed
。我必须承认,这种功能使得这个弹出行为在许多应用程序中的用途非常有限,也许如果我发现有保留控件打开的需求,我会增强设计。
背景
一旦代码被封装成行为,我对其进行了一些改进。我想要的功能之一是在设定的时间(在本例中为 10 秒)后消失按钮。当然,如果鼠标悬停在先前隐藏的 UIElement 上,我不希望它们立即消失,因此我为 UIElement
附加了 MouseEnterEvent
和 MouseLeaveEvent
事件。基本上,只要鼠标在 UIElement
内,就停止计时器,然后在鼠标离开 UIElement
时重置计时器。此外,再次单击原始 UIElement
将隐藏先前隐藏的 UIElement
。然后我添加了一个 Storyboard
,创建了一个 2 秒的淡出效果,这样按钮就不会突然消失。
行为
实际的 MouseDownUiElementVisibilityBehavior
其实相当简单,因为大部分功能都在辅助类 PopupControlTimer
中。有一个公共的 DependencyProperty
用于隐藏的控件,还有一个私有的 DependencyProperty
用于关联的 PopupControlTimer
。隐藏控件的 DependencyProperty
有一个事件处理程序,用于将事件处理程序与 MouseLeftButtonDown
事件关联,并初始化 PopupControlTimer
类的实例。
MouseLeftButtonDown
事件处理程序基本上只是更改隐藏控件的 Visibility
,如果 Visibility
是 Visible
,则启动 PopupControlTimer
,否则停止 PopupControlTimer
。
PopupControlTimer
是大部分智能所在的地方。它监视 MouseEnter
和 MouseLeave
事件—当 MouseEnter
事件发生时,计时器停止,当 MouseLeave
事件发生时,计时器从零开始重新启动。有两个公共方法可以启动和停止计时器,以及在计时器到期时使用 Storyboard
启动隐藏控件淡出效果的代码,并在淡出完成后进行清理。
由于最初它是用在一个非 Button
的 Control
上,我实现了它基于 MouseLeftButtonDown
事件,而不是点击事件。这更灵活,因为它可用于没有 Click
事件的控件。后来,我开始使用 MouseDownUiElementVisibilityBehavior
用于 PopUp
Control
的 PopupControlTimer
(为什么创建一个如此相似的类),这需要对 PopupControlTimer
进行一些小的修改,因为需要确保在计时器到期时关闭 Popup
,并且淡出效果必须在 Popup
的 Child
上进行,而不是直接在 Popup
上。
public class MouseDownUiElementVisibilityBehavior
{
public static readonly DependencyProperty HiddenUiElementProperty =
DependencyProperty.RegisterAttached("HiddenUiElement",
typeof(UIElement), typeof(MouseDownUiElementVisibilityBehavior),
new PropertyMetadata(null, OnHiddenUiElementChanged));
public static UIElement GetHiddenUiElement(UIElement uiElement)
{
return (UIElement)uiElement.GetValue(HiddenUiElementProperty);
}
public static void SetHiddenUiElement(UIElement uiElement, UIElement value)
{
uiElement.Visibility = Visibility.Collapsed;
uiElement.SetValue(HiddenUiElementProperty, value);
}
private static void OnHiddenUiElementChanged(object sender,
DependencyPropertyChangedEventArgs e)
{
var uiElement = (UIElement)sender;
if (e.OldValue != null) GetVisibilityTimer((UIElement)e.OldValue).Dispose();
if (e.NewValue == null)
{
uiElement.MouseLeftButtonDown -= MouseDownEvent;
SetVisibilityTimer(uiElement, null);
}
else if (e.OldValue == null)
{
var hiddenUiElement = (UIElement)e.NewValue;
hiddenUiElement.Visibility = Visibility.Collapsed;
uiElement.MouseLeftButtonDown += MouseDownEvent;
var visibilityTimer = new PopupControlTimer(hiddenUiElement);
SetVisibilityTimer(uiElement, visibilityTimer);
}
}
private static void MouseDownEvent(object sender, RoutedEventArgs e)
{
var hiddenUiElement = GetHiddenUiElement((UIElement)sender);
hiddenUiElement.Visibility = (hiddenUiElement.Visibility == Visibility.Collapsed)
? Visibility.Visible : Visibility.Collapsed;
if (hiddenUiElement.Visibility == Visibility.Visible)
GetVisibilityTimer((UIElement)sender).Start();
else GetVisibilityTimer((UIElement)sender).Stop();
}
private static readonly DependencyProperty VisibilityTimerProperty =
DependencyProperty.RegisterAttached("VisibilityTimer",
typeof(PopupControlTimer), typeof(MouseDownUiElementVisibilityBehavior),
new PropertyMetadata(null));
private static PopupControlTimer GetVisibilityTimer(UIElement uiElement)
{
return (PopupControlTimer)uiElement.GetValue(VisibilityTimerProperty);
}
private static void SetVisibilityTimer(UIElement uiElement, PopupControlTimer value)
{
uiElement.SetValue(VisibilityTimerProperty, value);
}
}
HiddenUiElement
是唯一可见的 DependencyProperty
。该行为附加到将接收点击以显示隐藏 UIElement
的控件。当 HiddenUiElement
的值发生变化时,会创建一个与 MouseLeftButtonDown
事件关联的事件处理程序和一个新的 PopupControlTimer
实例,用于隐藏的 UIElement
。PopupControlTimer
需要对隐藏的 UIElement
的引用,因为它将监视 MouseEnter
和 MouseLeave
事件,以确保只要鼠标在隐藏的 UIElement
上,该 UIElement
就不会消失。实例保存在与最初单击的 UIElement 关联的 DependencyProperty
中,以便在隐藏的 UIElement
更改时可以将其处置。当 UIElement 更改时发生的另一件事是删除 MouseLeftButtonDown
的事件处理程序。
捕获 MouseLeftButtonDown
用于隐藏 Popup
控件。如果需要不同的行为,删除此事件处理程序即可实现。
Popup 控件类
internal class PopupControlTimer : IDisposable
{
private readonly UIElement _hiddenUiElement;
private readonly DispatcherTimer _timer = new DispatcherTimer();
public PopupControlTimer(UIElement hiddenUiElement)
{
_hiddenUiElement = hiddenUiElement;
_timer.Interval = new TimeSpan(0, 0, 10);
_timer.Tick += (s, arg) =>
{
FadeOutStoryBoard(_hiddenUiElement);
_timer.Stop();
};
hiddenUiElement.MouseEnter += HiddenUiElementMouseEnter;
hiddenUiElement.MouseLeave += HiddenUiElementMouseLeave;
}
public void Start()
{
_timer.Start();
_hiddenUiElement.Visibility = Visibility.Visible;
}
public void Stop()
{
_timer.Stop();
}
private void HiddenUiElementMouseLeave(object sender, MouseEventArgs e) { Start(); }
private void HiddenUiElementMouseEnter(object sender, MouseEventArgs e) { Stop(); }
public void Dispose()
{
_hiddenUiElement.MouseEnter += HiddenUiElementMouseEnter;
_hiddenUiElement.MouseLeave += HiddenUiElementMouseLeave;
}
private void FadeOutStoryBoard(UIElement hiddenUiElement)
{
if (_fadeOutStoryboard == null)
{
// Create the fade out storyboard
_fadeOutStoryboard = new Storyboard();
_fadeOutStoryboard.Completed += (s, e) =>
{
if (!(hiddenUiElement is Popup))
hiddenUiElement.Visibility = Visibility.Collapsed;
else
((Popup) hiddenUiElement).IsOpen = false;
_fadeOutStoryboard.Stop();
};
var fadeOutAnimation = new DoubleAnimation(1.0F, 0.0F,
new Duration(TimeSpan.FromSeconds(2)));
Storyboard.SetTarget(fadeOutAnimation, (hiddenUiElement is Popup)
? ((Popup)hiddenUiElement).Child : hiddenUiElement);
Storyboard.SetTargetProperty(fadeOutAnimation,
new PropertyPath(UIElement.OpacityProperty));
_fadeOutStoryboard.Children.Add(fadeOutAnimation);
}
hiddenUiElement.Dispatcher.BeginInvoke(new Action(_fadeOutStoryboard.Begin),
DispatcherPriority.Render, null);
}
private Storyboard _fadeOutStoryboard;
}
PopupControlTimer
主要负责在 10 秒时间过去后将 UIElement
的 Visibility
设置为 Collapsed
,但它这样做的前提是先通过动画在 2 秒内将 UIElement
的 Opacity
更改为零。一旦 2 秒过去,Visibility
才会被设置为 Collapsed
。
还有一个 FadeOutStoryBoard
方法用于创建动画 Storyboard
。Storyboard
只创建一次然后重复使用,并且包含动画完成时的代码。
PopupControlTimer
还监视 MouseEnter 和 MouseLeave 事件,在 MouseEnter
时禁用计时器,在 MouseLeave
事件时重置并重新启动计时器。
Popup 控件类行为
上面的代码处理了使用 MouseDownUiElementVisibilityBehavior
所需的一切,但为了支持 Popup 的淡入淡出而进行了增强。所需的一部分包含在上面的代码中,包括识别附加的 Control
是一个 Popup
,并在淡出计时器结束后将 Popup
的 IsOpen
属性设置为 false
,并且对 Popup
的 Child
进行淡出,因为无法对 Popup
进行淡出。还有一个添加的 DependencyProperty
,当 Popup
打开后,该属性将被设置为 true
以启用淡出行为。
当 UseWithPopup
DependencyProperty
设置为 true
时,会附加一个事件处理程序到 Popup
的 Opened
事件,并将 PopupControlTimer
的一个实例保存在与 Popup 关联的 DependencyProperty
中。然后 Popup_Opened
事件处理程序将获取此计时器实例并执行计时器的 Start
方法。
public static readonly DependencyProperty UseWithPopupProperty =
DependencyProperty.RegisterAttached("UseWithPopup",
typeof(bool), typeof(PopupControlTimer),
new PropertyMetadata(false, OnUseWithPopupChanged));
public static bool GetUseWithPopup(DependencyObject uiElement)
{
return (bool)uiElement.GetValue(UseWithPopupProperty);
}
public static void SetUseWithPopup(DependencyObject uiElement, bool value)
{
uiElement.SetValue(UseWithPopupProperty, value);
}
private static void OnUseWithPopupChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var popup = (Popup)d;
if ((bool)e.NewValue)
{
popup.Opened += Popup_Opened;
SetPopupControlTimer(popup, new PopupControlTimer(popup));
}
else
{
popup.Opened += Popup_Opened;
SetPopupControlTimer(popup, null);
}
}
private static void Popup_Opened(object sender, EventArgs e)
{
var timer = GetPopupControlTimer((UIElement)sender);
timer.Start();
}
private static readonly DependencyProperty PopupControlTimerProperty =
DependencyProperty.RegisterAttached("PopupControlTimer",
typeof(PopupControlTimer), typeof(DependencyObject),
new PropertyMetadata(null));
private static PopupControlTimer GetPopupControlTimer(DependencyObject uiElement)
{
return (PopupControlTimer)uiElement.GetValue(PopupControlTimerProperty);
}
private static void SetPopupControlTimer(DependencyObject uiElement, PopupControlTimer value)
{
uiElement.SetValue(PopupControlTimerProperty, value);
}
示例
应用程序启动时
应用程序在点击顶部控件(Border)后,显示隐藏的 Border 及其包含的控件
应用程序在 10 秒计时器到期后,隐藏的 Border 正在淡出
应用程序在按下 ToggleButton
将 IsChecked
设置为 true
后,导致 Popup
控件出现
示例包含一个被点击以弹出包含 TextBox
的 Border
的 Border
。再次点击 Border
将关闭包含 TextBox
的 Border
。可以点击 TextBox
并输入内容,
使用计时器
使用带 Popup 的计时器
要在 Popup
控件上使用此 PopupControlTimer
,只需将 PopupControlTimer
的 UseWithPopup
属性设置为 true
,如下面的 Popup
Control
示例 XAML 所示。
<Popup Name="Popup"
HorizontalAlignment="Center"
VerticalAlignment="Center"
fadingPopupMouseDown:PopupControlTimer.UseWithPopup="True"
IsOpen="{Binding IsChecked,
ElementName=ToggleButton}">
<StackPanel Margin="2"
Background="Aqua"
Orientation="Vertical">
<TextBlock Width="100"
Margin="2"
Text="Pop-up" />
<TextBox Width="200"
Margin="2"
Text="You can change this text" />
</StackPanel>
</Popup>
使用带通用控件的计时器
要在通用 UIElement
上使用 PopupControlTimer
,只需在 UIElement
上将 MouseDownUiElementVisibilityBehavior
的 HiddenUiElementproperty
属性设置为要点击的那个,以显示隐藏的 UIElement
。
<Border Width="100"
Height="100"
VerticalAlignment="Top"
Background="AliceBlue"
fadingPopupMouseDown:MouseDownUiElementVisibilityBehavior.HiddenUiElement
="{Binding ElementName=Panel}">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Click Here to show stack panel"
TextAlignment="Center"
TextWrapping="Wrap" />
</Border>
需要注意的是,在 MouseDownUiElementVisibilityBehanvior
的 Binding
中只指定了 ElementName
。这就是 Binding
到另一个 UIElement
的方式。
历史
- 2016/02/16: 初始版本。
- 2016/02/19: 更新代码,包含使用带 Popup 的计时器。
- 2016/02/23: 对 Popup 的补充说明。