使 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 的补充说明。


