WPF 检测滚动父控件

发布于 2024-08-22 05:56:43 字数 386 浏览 5 评论 0原文

想象一下您打开 WPF Popup 的情况(例如通过 ButtonClick)。 您在 Popup 中直接有一个 ListBox ,其中包含一些项目,因此您必须能够滚动。 想象一下,这是您的自定义控件,它位于ScrollViewer 中。

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

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

Imagine the situation you open a WPF Popup(e.g. through ButtonClick).
You have a ListBox directly in the Popup with some items, so you have to be able to scroll.
Imagine that this is your Custom Control and it's located in the ScrollViewer.

Now if you move with your mouse outside from the Popup surface and scroll, what happens?
You scroll up and down but with the Popup opened ! And that's the problem.

The question is, how to detect from inside the Control, that some other unknown Parent Control in the VisualTree has started to scroll ?
and consecutively set IsDropDownOpen = false?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

七色彩虹 2024-08-29 05:56:43

我们可以编写一个触发器来与 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。我们可以使用视觉树中任何附近的元素。另请注意,我们使用另一个触发器来打开弹出窗口,但如何打开它对于原始问题并不重要。

这是 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 添加到您的项目中。

We can write a trigger for use with elements contained within a ScrollViewer. Here is a complete sample application:

<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>

We have a button that opens a Popup and any scrolling in any parent ScrollViewer causes the ScrollTrigger actions to fire and then we can close the popup. Note that the trigger is attached to the Button and not the Popup. We can use any nearby element that is in the visual tree. Also note that we use another trigger to open the Popup but how it opens is not important to the original question.

Here is the 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;
    }
}

The ScrollTrigger is very simple, it just attaches to all parent ScrollChanged events and fires any contained actions. In the sample we use the ChangePropertyAction to close the Popup.

If you are not familiar with behaviors, install the Expression Blend 4 SDK and add these namespaces:

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

and add System.Windows.Interactivity and Microsoft.Expression.Interactions to your project.

多情出卖 2024-08-29 05:56:43

我不太清楚您的控件是如何的,但是您不能根据 Focus 事件来打开/关闭控件吗?如果失去焦点,要关闭弹出窗口吗?
也许我理解错了,可以贴一下代码片段吗?
丹尼尔

I don't quite picture how your controls are, but can't you base your opening/closing of a control on the Focus event? And if it loses focus, to close the popup?
Maybe I understand wrong, can you post a code snippet?
Daniel

鸠书 2024-08-29 05:56:43

警告:这是一条很长的评论,它基本上只是解释我对 @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 中添加了逻辑,以查看偏移值是否实际更改。我在触发器上添加了一个依赖属性,以便您可以根据需要绕过该逻辑。第二个更改是我向关联对象添加了 unloaded 事件,这样如果控件被删除,它就会删除 ScrollViewers 的相关操作,从而减少 ScrollViewer_ScrollChanged< 的次数动态添加和删除控件时进行了 /code> 调用。

考虑到这些变化以及我希望能够允许我的控件的使用者决定弹出窗口的显示方式,我的 .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 部分,具体取决于您的操作方式)。有很多方法可以做到这一点,如果你不知道,只需谷歌“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);
    }


    ...
}

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

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

Warning: This is a long comment, and it is basically just explaining my changes to @Rick Sladkey's response. It was a great starting point, but i did notice a few changes that I made with some things that I saw happening.

While doing my custom controls, I wanted something similar to this (I wanted to close a popup on a scroll), and found the answer to be something very similar to that of Rick Sladkey's answer with just a few minor changes to help improve some items.

The changes I made were mainly in regards to 3 items. The first being that i was seeing that the ScrollViewer_ScrollChanged even was firing when i wasn't activly scrolling (other things set it off apparently). Next was that when I was unloading my controls, ScrollViewer_ScrollChanged wasn't detached from the ScrollViewers, so if I added 3 and then removed 1 and scrolled, it would still fire 3 times instead of 2. Finally, I wanted to be able to add the functionality of allowing the consumer of my control to set the IsOpen property dynamically as well.

With that, my modified version of the ScrollTrigger class looks something like:

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;
    }
}

The first change here is that I added logic in ScrollViewer_ScrollChanged to see if the offset values actually changed or not. I added a dependency property on the trigger to allow you to bypass that logic if you wish to. The second change that I added an unloaded event to the associated object, so that if the control was removed, it would remove the related actions to the ScrollViewers, reducing the amount of times the ScrollViewer_ScrollChanged call was made when adding and removing my controls dynamically.

With those changes in mind and the fact that I want to be able to allow consumers of my control to dictate how the popup is displayed, my .xaml looked something like:

<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>

Now I needed something to bind to, and because i'm creating a custom control, I created some dependency properties and some other items in the code-behind. If you are using this approach with MVVM, you will want to write 'INotifyProperty's and make sure your bindings are them (might not need the ElementName parts of the binding depending on how you do it). There are many ways to do that, and if you don't know, just google "mvvm data binding INotifyPropertyChanged" and you will easily find it out.

As a side note, I am also using Prism, so I am using DelegateCommands, but you can use whatever implementation of ICommand you want. With that, my code-behind looked something like:

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);
    }


    ...
}

And with that, I am able to have a popup that will close on any scroll trigger that actually has a change, doens't have any un-needed calls, and will also allow a user to modify whether is open or not.

If you have made it this far, thank you. I appreciate your dedication, and hopefully this helps out a little bit.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文