10

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

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

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

4

3 回答 3

11

我们可以编写一个触发器用于包含在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>

我们有一个打开 a 的按钮,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.Interactivity并将and添加Microsoft.Expression.Interactions到您的项目中。

于 2011-01-25T03:36:02.903 回答
1

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

于 2010-02-23T15:43:26.367 回答
1

警告:这是一个很长的评论,它基本上只是解释我对@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 部分,具体取决于您的操作方式)。有很多方法可以做到这一点,如果你不知道,只需谷歌“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);
    }


    ...
}

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

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

于 2018-11-09T17:09:08.973 回答