WPF检测滚动父控件

时间:2010-02-22 09:19:00

标签: wpf scroll custom-controls scrollviewer

想象一下您打开WPF Popup的情况(例如通过 ButtonClick )。 您在ListBox中直接使用Popup包含一些项目,因此您必须能够滚动。 想象一下,这是您的Custom Control,它位于ScrollViewer

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

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

3 个答案:

答案 0 :(得分:10)

我们可以编写一个触发器,用于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添加到您的项目中。

答案 1 :(得分:1)

我不完全了解您的控件是如何进行的,但是您不能在Focus事件的基础上打开/关闭控件吗?如果失去焦点,关闭弹出窗口?  也许我理解错了,你可以发一个代码片段吗?  丹尼尔

答案 2 :(得分:0)

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

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

我所做的更改主要涉及3个项目。第一个原因是我没有主动滚动时(其他显然将其关闭),我看到ScrollViewer_ScrollChanged甚至在触发。接下来是当我卸载控件时,ScrollViewer_ScrollChanged并未与ScrollViewer分离,因此,如果我添加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中添加了逻辑,以查看偏移值是否实际更改。我在触发器上添加了一个依赖项属性,以允许您在需要时绕过该逻辑。第二个更改是,我向关联的对象添加了一个卸载事件,因此,如果控件被删除,它将删除与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's,并确保您的绑定是它们(根据您的操作方式,可能不需要绑定的ElementName部分)。有很多方法可以做到这一点,如果您不知道,只需使用Google“ mvvm数据绑定INotifyPropertyChanged”,您就可以轻松地找到它。

作为旁注,我也使用Prism,所以我使用的是DelegateCommand,但是您可以使用所需的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);
    }


    ...
}

然后,我将有一个弹出窗口,该弹出窗口将在任何实际发生更改的滚动触发器上关闭,没有任何不需要的调用,并且还允许用户修改是否打开

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