【问题标题】:WPF detect scrolling parent ControlWPF检测滚动父控件
【发布时间】:2010-02-22 09:19:00
【问题描述】:

想象一下您打开 WPF Popup(例如通过 ButtonClick)的情况。 你有一个ListBox 直接在Popup 有一些项目,所以你必须能够滚动。 想象一下,这是您的Custom Control,它位于ScrollViewer

现在,如果您用鼠标从Popup 表面向外移动并滚动,会发生什么? 您上下滚动,但 Popup 打开!这就是问题所在。

问题是,如何从控件内部检测到 VisualTree 中其他一些未知的父控件已经开始滚动? 并连续设置IsDropDownOpen = false?

【问题讨论】:

  • 我有同样的问题和问题。我滚动我的网格,我的自定义操作弹出窗口保持在同一个地方!我需要用网格滚动弹出窗口!

标签: wpf scroll custom-controls scrollviewer


【解决方案1】:

我们可以编写一个触发器用于ScrollViewer 中包含的元素。这是一个完整的示例应用程序:

<Grid>
    <ScrollViewer VerticalAlignment="Top" Height="200">
        <StackPanel HorizontalAlignment="Left">
            <Button Name="button" Content="Open">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <ei:ChangePropertyAction TargetObject="{Binding ElementName=popup}" PropertyName="IsOpen" Value="True"/>
                    </i:EventTrigger>
                    <local:ScrollTrigger>
                        <ei:ChangePropertyAction TargetObject="{Binding ElementName=popup}" PropertyName="IsOpen" Value="False"/>
                    </local:ScrollTrigger>
                </i:Interaction.Triggers>
            </Button>
            <Popup Name="popup" PlacementTarget="{Binding ElementName=button}">
                <TextBlock Background="White" Text="Sample text"/>
            </Popup>
            <Rectangle Width="100" Height="100" Fill="Red"/>
            <Rectangle Width="100" Height="100" Fill="Green"/>
            <Rectangle Width="100" Height="100" Fill="Blue"/>
            <Rectangle Width="100" Height="100" Fill="Yellow"/>
        </StackPanel>
    </ScrollViewer>
</Grid>

我们有一个打开Popup 的按钮,任何父ScrollViewer 中的任何滚动都会触发ScrollTrigger 操作,然后我们可以关闭弹出窗口。请注意,触发器附加到Button 而不是Popup。我们可以使用视觉树中的任何附近元素。另请注意,我们使用另一个触发器打开Popup,但它如何打开对于原始问题并不重要。

这里是ScrollTrigger

class ScrollTrigger : TriggerBase<FrameworkElement>
{
    protected override void OnAttached()
    {
        AssociatedObject.Loaded += new RoutedEventHandler(AssociatedObject_Loaded);
    }

    void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        foreach (var scrollViewer in GetScrollViewers())
            scrollViewer.ScrollChanged += new ScrollChangedEventHandler(scrollViewer_ScrollChanged);
    }

    void scrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        InvokeActions(e.OriginalSource);
    }

    IEnumerable<ScrollViewer> GetScrollViewers()
    {
        for (DependencyObject element = AssociatedObject; element != null; element = VisualTreeHelper.GetParent(element))
            if (element is ScrollViewer) yield return element as ScrollViewer;
    }
}

ScrollTrigger 非常简单,它只是附加到所有父 ScrollChanged 事件并触发任何包含的操作。在示例中,我们使用ChangePropertyAction 来关闭Popup

如果您不熟悉行为,请安装 Expression Blend 4 SDK 并添加以下命名空间:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"

并将System.Windows.InteractivityMicrosoft.Expression.Interactions 添加到您的项目中。

【讨论】:

    【解决方案2】:

    我不太清楚您的控件是怎样的,但是您不能将控件的打开/关闭基于 Focus 事件吗?如果它失去焦点,关闭弹出窗口? 也许我理解错了,你能发布一个代码sn-p吗? 丹尼尔

    【讨论】:

      【解决方案3】:

      警告:这是一个很长的评论,它基本上只是解释我对@Rick Sladkey的回复所做的更改。这是一个很好的起点,但我确实注意到我对一些我看到的正在发生的事情进行了一些更改。

      在执行我的自定义控件时,我想要类似的东西(我想关闭滚动上的弹出窗口),并发现答案与 Rick Sladkey 的答案非常相似,只是进行了一些小改动以提供帮助改进一些项目。

      我所做的更改主要涉及 3 个项目。第一个是我看到ScrollViewer_ScrollChanged 甚至在我没有主动滚动时正在触发(其他事情显然将其关闭)。接下来是当我卸载我的控件时,ScrollViewer_ScrollChanged 没有与ScrollViewers 分离,所以如果我添加 3,然后删除 1 并滚动,它仍然会触发 3 次而不是 2。最后,我想要能够添加允许我的控件的使用者也可以动态设置 IsOpen 属性的功能。

      这样,我修改后的ScrollTrigger 类看起来像:

      public class ScrollTrigger : TriggerBase<FrameworkElement>
      {
          public bool TriggerOnNoChange
          {
              get
              {
                  var val = GetValue(TriggerOnNoChangeProperty);
                  if (val is bool b)
                  {
                      return b;
                  }
      
                  return false;
              }
              set => SetValue(TriggerOnNoChangeProperty, value);
          }
      
          public static readonly DependencyProperty TriggerOnNoChangeProperty =
              DependencyProperty.Register(
                  "TriggerOnNoChange", 
                  typeof(bool), 
                  typeof(ScrollTrigger), 
                  new FrameworkPropertyMetadata(
                      false, 
                      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
      
          protected override void OnAttached()
          {
              AssociatedObject.Loaded += AssociatedObject_Loaded;
              AssociatedObject.Unloaded += AssociatedObject_Unloaded;
          }
      
          private void AssociatedObject_Loaded(
              object sender, 
              RoutedEventArgs e)
          {
              foreach (var scrollViewer in GetScrollViewers())
                  scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
          }
      
          private void AssociatedObject_Unloaded(
              object sender, 
              RoutedEventArgs e)
          {
              foreach (var scrollViewer in GetScrollViewers())
                  scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
          }
      
          private void ScrollViewer_ScrollChanged(
              object sender,
              ScrollChangedEventArgs e)
          {
              if(TriggerOnNoChange ||
                 Math.Abs(e.VerticalChange) > 0 || 
                 Math.Abs(e.HorizontalChange) > 0)
                  InvokeActions(e.OriginalSource);
          }
      
          private IEnumerable<ScrollViewer> GetScrollViewers()
          {
              for (DependencyObject element = AssociatedObject; 
                   element != null; 
                   element = VisualTreeHelper.GetParent(element))
                  if (element is ScrollViewer viewer) yield return viewer;
          }
      }
      

      这里的第一个更改是我在ScrollViewer_ScrollChanged 中添加了逻辑,以查看偏移值是否实际更改。我在触发器上添加了一个依赖属性,如果您愿意,可以绕过该逻辑。第二个更改是我在关联对象中添加了一个 unloaded 事件,这样如果控件被移除,它会将相关操作移除到ScrollViewers,从而减少了在添加和移除时进行ScrollViewer_ScrollChanged 调用的次数我的动态控制。

      考虑到这些变化以及我希望能够让我的控件的使用者决定弹出窗口的显示方式这一事实,我的 .xaml 看起来像:

      <UserControl ...
                   xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
                   xmlns:tgrs="clr-namespace:NameSpace.To.ScrollTrigger.class.Namespace"
                   x:Name="MyControlNameRoot"
                   .../>
          <i:Interaction.Triggers>
              <tgrs:ScrollTrigger TriggerOnNoChange="False">
                  <i:InvokeCommandAction Command="{Binding ElementName=MyCommandNameRoot, Path=ClosePopupCommand}"/>
              </tgrs:ScrollTrigger>
          </i:Interaction.Triggers>
          ...
          <Popup ...
                 IsOpen="{Binding ElementName=MyControlNameRoot, Path=IsPopupOpen, Mode=OneWay}"
                 .../>
              ...
          </Popup>
          ...
      </UserControl>
      

      现在我需要绑定一些东西,因为我正在创建一个自定义控件,所以我在代码隐藏中创建了一些依赖项属性和一些其他项。如果您在 MVVM 中使用这种方法,您将需要编写 'INotifyProperty' 并确保您的绑定是它们(可能不需要绑定的 ElementName 部分,具体取决于您的操作方式)。有很多方法可以做到这一点,如果您不知道,只需 google “mvvm 数据绑定 INotifyPropertyChanged” 即可轻松找到。

      作为旁注,我也在使用 Prism,所以我使用 DelegateCommands,但你可以使用任何你想要的 ICommand 实现。有了这个,我的代码隐藏看起来像:

      public partial class MyUserControl : UserControl
      {
          public MyUserControl()
          {
               ClosePopupCommand = new DelegateCommand(OnPopupCommand);
      
              InitializeComponent();
          }
          ...
          public ICommand ClosePopupCommand { get; }
          private OnClosePopupCommand ()
          {
              IsPopupOpen = false;
          }
      
          public static readonly DependencyProperty IsPopupOpenProperty =
              DependencyProperty.Register(
                  "IsPopupOpen", 
                  typeof(bool), 
                  typeof(MyUserControl), 
                  new FrameworkPropertyMetadata(
                      false,
                      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
      
          public bool IsPopupOpen
          {
              get
              {
                  var val = GetValue(IsPopupOpenProperty);
                  if (val is bool b)
                  {
                      return b;
                  }
      
                  return false;
              }
              set => SetValue(IsPopupOpenProperty, value);
          }
      
      
          ...
      }
      

      有了这个,我可以有一个弹出窗口,它会在任何实际发生变化的滚动触发器上关闭,没有任何不需要的调用,并且还允许用户修改是否打开.

      如果你已经做到了,谢谢。感谢您的奉献精神,希望这对您有所帮助。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-07
        • 2017-05-19
        • 2012-01-31
        • 2017-06-03
        • 1970-01-01
        相关资源
        最近更新 更多