WPF обнаруживает прокрутку родительского элемента управления - PullRequest
8 голосов
/ 22 февраля 2010

Представьте себе ситуацию, когда вы открываете WPF Popup (например, через ButtonClick ). У вас есть ListBox прямо в Popup с некоторыми предметами, поэтому вы должны иметь возможность прокрутки. Представьте, что это ваш Custom Control и он находится в ScrollViewer.

Теперь, если вы двигаете мышью снаружи от поверхности Popup и прокручиваете, что произойдет? Вы прокручиваете вверх и вниз, но с открытым Popup! И это проблема.

Вопрос в том, как изнутри обнаружить, что какой-то другой неизвестный родительский элемент управления в VisualTree начал прокручиваться? и последовательно установить IsDropDownOpen = false?

Ответы [ 3 ]

10 голосов
/ 25 января 2011

Мы можем написать триггер для использования с элементами, содержащимися в 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.Interactivity и Microsoft.Expression.Interactions к вашему проекту.

1 голос
/ 23 февраля 2010

Я не совсем представляю, как работают ваши элементы управления, но разве вы не можете основывать свое открытие / закрытие элемента управления на событии Focus? А если он потеряет фокус, чтобы закрыть всплывающее окно? Может я неправильно понял, можешь выложить фрагмент кода? Daniel

0 голосов
/ 09 ноября 2018

Предупреждение: это длинный комментарий, в основном он объясняет мои изменения в ответе * Рика Сладки . Это была отличная отправная точка, но я заметил несколько изменений, которые я сделал с некоторыми вещами, которые я видел происходящими.

Выполняя свои собственные элементы управления, я хотел что-то похожее на это (я хотел закрыть всплывающее окно на свитке), и обнаружил, что ответ очень похож на ответ в ответе Рика Слэдки с несколькими незначительными изменениями, чтобы помочь улучшить некоторые предметы.

Внесенные мною изменения касались в основном трех пунктов. Во-первых, я видел, что ScrollViewer_ScrollChanged даже стрелял, когда я активно не прокручивал (очевидно, другие вещи это вызывали). Затем было то, что когда я выгружал свои элементы управления, ScrollViewer_ScrollChanged не был отсоединен от ScrollViewer s, поэтому, если бы я добавил 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, поэтому я использую DelegateCommand s, но вы можете использовать любую реализацию 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);
    }


    ...
}

И с этим я могу иметь всплывающее окно, которое будет закрываться при любом триггере прокрутки, который действительно имеет изменение, не имеет никаких ненужных вызовов, а также позволит пользователю изменять, открыт или нет .

Если вы сделали это далеко, спасибо. Я ценю вашу преданность, и, надеюсь, это немного поможет.

...