Подтвердить что ты не робот

Включение ScrollViewer HorizontalSnapPoints с возможностью объединения

Я пытаюсь создать аналогичный опыт, как в ScrollViewerSample из образцов SDK Windows 8, чтобы иметь возможность привязываться к элементам внутри ScrollViewer при прокрутке влево и вправо. Реализация из образца (который работает) выглядит следующим образом:

<ScrollViewer x:Name="scrollViewer" Width="480" Height="270"
              HorizontalAlignment="Left" VerticalAlignment="Top"
              VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto" 
              ZoomMode="Disabled" HorizontalSnapPointsType="Mandatory">
    <StackPanel Orientation="Horizontal">
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a cliff" Source="images/cliff.jpg" Stretch="None"  HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of Grapes" Source="images/grapes.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of Mount Rainier" Source="images/Rainier.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a sunset" Source="images/sunset.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a valley" Source="images/valley.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
    </StackPanel>
</ScrollViewer>

Единственная разница с моей желаемой реализацией заключается в том, что я не хочу, чтобы StackPanel с элементами внутри, но с чем-то я могу привязываться. Я пытаюсь выполнить это с помощью ItemsControl, но по какой-то причине поведение Snap не срабатывает:

<ScrollViewer x:Name="scrollViewer" Width="480" Height="270"
              HorizontalAlignment="Left" VerticalAlignment="Top"
              VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto" 
              ZoomMode="Disabled" HorizontalSnapPointsType="Mandatory">
    <ItemsControl>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a cliff" Source="images/cliff.jpg" Stretch="None"  HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of Grapes" Source="images/grapes.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of Mount Rainier" Source="images/Rainier.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a sunset" Source="images/sunset.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a valley" Source="images/valley.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
    </ItemsControl>
</ScrollViewer>

Предложения будут очень благодарны!


Благодаря Денису, я закончил использование следующего стиля в ItemsControl и полностью удалил ScrollViewer и встроенный ItemPanelTemplate:

<Style x:Key="ItemsControlStyle" TargetType="ItemsControl">
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ItemsControl">
                <ScrollViewer Style="{StaticResource HorizontalScrollViewerStyle}" HorizontalSnapPointsType="Mandatory">
                    <ItemsPresenter />
                </ScrollViewer>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
4b9b3361

Ответ 1

Получение привязок для работы с привязанными коллекциями может быть сложным. Для моментальных точек для работы с немедленным выпуском ScrollViewer должен реализоваться интерфейс IScrollSnapPointsInfo. ItemsControl не реализует IScrollSnapPointsInfo, и поэтому вы не увидите поведение привязки.

Чтобы обойти эту проблему, у вас есть несколько вариантов:

  • Создайте собственный класс, полученный из ItemsControl, и реализуйте интерфейс IScrollSnapPointsInfo.
  • Создайте собственный стиль для управления элементами и установите свойство HorizontalSnapPointsType в ScrollViewer внутри стиля.

Я реализовал прежний подход и могу подтвердить, что он работает, но в вашем случае пользовательский стиль может быть лучшим выбором.

Ответ 2

Хорошо, вот простейший (и автономный) пример для горизонтального ListView с привязанными элементами и правильной обработки привязки (см. комментарии в следующем коде).

XAML

    <ListView x:Name="YourListView"
              ItemsSource="{x:Bind Path=Items}"
              Loaded="YourListView_OnLoaded">
        <!--Set items panel to horizontal-->
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <ItemsStackPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
        <!--Some item template-->
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding}"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

код фона:

    private void YourListView_OnLoaded(object sender, RoutedEventArgs e)
    {
        //get ListView
        var yourList = sender as ListView;

        //*** yourList style-based changes ***
        //see Style here https://msdn.microsoft.com/en-us/library/windows/apps/mt299137.aspx

        //** Change orientation of scrollviewer (name in the Style "ScrollViewer") **
        //1. get scrollviewer (child element of yourList)
        var sv = GetFirstChildDependencyObjectOfType<ScrollViewer>(yourList);

        //2. enable ScrollViewer horizontal scrolling
        sv.HorizontalScrollMode =ScrollMode.Auto;
        sv.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
        sv.IsHorizontalRailEnabled = true;

        //3. disable ScrollViewer vertical scrolling
        sv.VerticalScrollMode = ScrollMode.Disabled;
        sv.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
        sv.IsVerticalRailEnabled = false;
        // //no we have horizontally scrolling ListView


        //** Enable snapping **
        sv.HorizontalSnapPointsType = SnapPointsType.MandatorySingle; //or you can use SnapPointsType.Mandatory
        sv.HorizontalSnapPointsAlignment = SnapPointsAlignment.Near; //example works only for Near case, for other there should be some changes
        // //no we have horizontally scrolling ListView with snapping and "scroll last item into view" bug (about bug see here http://stackoverflow.com/info/11084493/snapping-scrollviewer-in-windows-8-metro-in-wide-screens-not-snapping-to-the-las)

        //** fix "scroll last item into view" bug **
        //1. Get items presenter (child element of yourList)
        var ip = GetFirstChildDependencyObjectOfType<ItemsPresenter>(yourList);
        //  or   var ip = GetFirstChildDependencyObjectOfType<ItemsPresenter>(sv); //also will work here

        //2. Subscribe to its SizeChanged event
        ip.SizeChanged += ip_SizeChanged;

        //3. see the continuation in: private void ip_SizeChanged(object sender, SizeChangedEventArgs e)
    }


    public static T GetFirstChildDependencyObjectOfType<T>(DependencyObject depObj) where T : DependencyObject
    {
        if (depObj is T) return depObj as T;

        for (var i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            var child = VisualTreeHelper.GetChild(depObj, i);

            var result = GetFirstChildDependencyObjectOfType<T>(child);
            if (result != null) return result;
        }
        return null;
    }

    private void ip_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        //3.0 if rev size is same as new - do nothing
        //here should be one more condition added by && but it is a little bit complicated and rare, so it is omitted.
        //The condition is: yourList.Items.Last() must be equal to (yourList.Items.Last() used on previous call of ip_SizeChanged)
        if (e.PreviousSize.Equals(e.NewSize)) return;

        //3.1 get sender as our ItemsPresenter
        var ip = sender as ItemsPresenter;

        //3.2 get the ItemsPresenter parent to get "viewable" width of ItemsPresenter that is ActualWidth of the Scrollviewer (it is scrollviewer actually, but we need just its ActualWidth so - as FrameworkElement is used)
        var sv = ip.Parent as FrameworkElement;

        //3.3 get parent ListView to be able to get elements Containers
        var yourList = GetParent<ListView>(ip);

        //3.4 get last item ActualWidth
        var lastItem = yourList.Items.Last();
        var lastItemContainerObject = yourList.ContainerFromItem(lastItem);
        var lastItemContainer = lastItemContainerObject as FrameworkElement;
        if (lastItemContainer == null)
        {
            //NO lastItemContainer YET, wait for next call
            return;
        }
        var lastItemWidth = lastItemContainer.ActualWidth;

        //3.5 get margin fix value
        var rightMarginFixValue = sv.ActualWidth - lastItemWidth;

        //3.6. fix  "scroll last item into view" bug
        ip.Margin = new Thickness(ip.Margin.Left, 
            ip.Margin.Top, 
            ip.Margin.Right + rightMarginFixValue, //APPLY FIX
            ip.Margin.Bottom);
    }

    public static T GetParent<T>(DependencyObject reference) where T : class
    {
        var depObj = VisualTreeHelper.GetParent(reference);
        if (depObj == null) return (T)null;
        while (true)
        {
            var depClass = depObj as T;
            if (depClass != null) return depClass;
            depObj = VisualTreeHelper.GetParent(depObj);
            if (depObj == null) return (T)null;
        }
    }

Об этом примере.

  • Большая часть проверок и обработки ошибок опущена.

  • Если вы переопределяете стиль/шаблон ListView, необходимо соответствующим образом изменить части поиска VisualTree.

  • Я бы предпочел создать унаследованный элемент управления ListView с помощью этой логики, чем использовать приведенный пример as-is в реальном коде.
  • Тот же код работает для случая Вертикаль (или обоих) с небольшими изменениями.
  • Упомянутая ошибка привязки - ошибка ScrollViewer обработки SnapPointsType.MandatorySingle и SnapPointsType.Mandatory cases. Он отображается для элементов с нефиксированными размерами

.