none
WrapControlで子要素が初めて画面に表示されたとき RRS feed

  • 質問

  • こんにちは。

    <ScrollViewer HorizontalScrollBarVisibility="Disabled" Grid.Row="1" >
        <WrapPanel Name="Wrp" Orientation="Horizontal" />
    </ScrollViewer>

    例えばこんなXAMLを用意してコードビハインドで

    For i As Integer = 0 To 50
        Dim a As New Rectangle
        a.Width = 50
        a.Height = 50
        a.Margin = New Thickness(10, 10, 0, 0)
        a.Fill = New System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Blue)
        Wrp.Children.Add(a)
    Next

    適当にRectangleを並べたとします。

    Windowサイズが小さいと下の方のRectangleは、画面に表示されません。

    ここからが、質問なのですがこのRectangleが「画面内に表示されたとき」というイベントを

    発生させることは可能でしょうか?

    実際に考えているアプリとしてはRectangleではなく画像を数千枚表示させようと

    考えているので、初期は画面に表示された分だけ画像を読込み、Scrollされるたびに

    徐々に画像を読み込むことでUIの反応速度を上げたいと考えています。

    ご存知方がいらっしゃいましたら、ご教示ください。

    2016年12月6日 0:09

回答

  • 直接の回答ではありませんが、目的に合うものとしては以下が参考になるのではないかと思います。

    ListView で表示用データを仮想化する
    http://d.hatena.ne.jp/tt_clown/20130327/listview_virtualizing

    WPFでコレクションの表示を高速化するオプションとか
    http://yuuxxxx.hatenablog.com/entry/2014/02/01/232320


    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/

    2016年12月6日 2:07
    モデレータ
  • 要素の大きさが固定であるなら、ScrollViewerの表示領域に表示されている要素は計算から調べられます。

    <Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">
        <DockPanel>
            <Button Content="TEST" DockPanel.Dock="Right" Click="Button_Click" />
            <UniformGrid Columns="2">
                <ScrollViewer HorizontalScrollBarVisibility="Disabled" Grid.Row="1" >
                    <WrapPanel x:Name="Wrp" Orientation="Horizontal" Loaded="Wrp_Loaded"/>
                </ScrollViewer>
    
                <ListBox x:Name="listBox1" ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                    <ListBox.ItemsPanel>
                        <ItemsPanelTemplate>
                            <app:WrapPanelEx Orientation="Horizontal" />
                        </ItemsPanelTemplate>
                    </ListBox.ItemsPanel>
                    <ListBox.ItemTemplate>
                        <DataTemplate >
                            <Border Width="50" Height="50" Margin="5" >
                                <Image />
                            </Border>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </UniformGrid>
        </DockPanel>
    
    </Window>
    Imports System.Collections.ObjectModel
    Class MainWindow
    
        Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
    
            For i As Integer = 0 To 50
                Dim b As New Border
                b.Width = 50
                b.Height = 50
                b.Margin = New Thickness(10, 10, 0, 0)
                Dim img As New Image
                b.Child = img
                '読み込ませたいURIを覚えさせておく
                b.DataContext = New Uri("https://i1.social.s-msft.com/profile/u/avatar.jpg?displayname=quarterdeck")
    
                Wrp.Children.Add(b)
            Next
    
            Me.listBox1.ItemsSource = collection
        End Sub
    
        Private Sub Wrp_Loaded(sender As Object, e As RoutedEventArgs)
            WrapPanelEx.OnLoaded(sender, e)
        End Sub
    
    
        'コレクションで増減に反応させる
        Dim collection As New ObservableCollection(Of Uri)
        Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
            For i As Integer = 0 To 10
                collection.Add(New Uri("https://i1.social.s-msft.com/profile/u/avatar.jpg?displayname=quarterdeck"))
            Next
        End Sub
    
    
    End Class
    
    
    Class WrapPanelEx
        Inherits WrapPanel
    
     
        Public Shared Sub OnLoaded(sender As Object, e As RoutedEventArgs)
            Dim wrap As WrapPanel = CType(sender, WrapPanel)
            Dim d As DependencyObject = wrap
            While (d IsNot Nothing)
                Dim sv As ScrollViewer = TryCast(d, ScrollViewer)
                If (sv IsNot Nothing) Then
                    sv.Tag = wrap
                    wrap.Tag = sv
                    AddHandler sv.ScrollChanged, AddressOf ScrollViewer_ScrollChanged
    
    
                    Return
                End If
                d = VisualTreeHelper.GetParent(d)
            End While
        End Sub
    
        Private Shared Sub ScrollViewer_ScrollChanged(sender As Object, e As ScrollChangedEventArgs)
            Changed(CType(sender, ScrollViewer))
        End Sub
    
        Private Shared Sub Changed(ByVal sv As ScrollViewer)
            Dim wrap As WrapPanel = CType(sv.Tag, WrapPanel)
            If (wrap.Children.Count = 0) Then
                Return
            End If
    
            'ScrollViewerの表示領域に表示される要素を調べる
    
            Dim w As Double = sv.ExtentWidth 'ScrollViewerの最大スクロール幅
            Dim h As Double = sv.ExtentHeight 'ScrollViewerの最大スクロール高さ
    
            Dim child As FrameworkElement = wrap.Children(0)
            Dim childWidth As Double = (child.ActualWidth + child.Margin.Left + child.Margin.Right) '固定サイズの要素の幅(マージン含む)
            Dim childHeight As Double = (child.ActualHeight + child.Margin.Top + child.Margin.Bottom) '固定サイズの要素の高さ(マージン含む)
            Dim columnCount = sv.ViewportWidth \ childWidth '1行に表示される列数
    
            'ScrollViewerの左上の要素のインデックス
            Dim visibleStartIndex As Integer = CInt(Math.Floor(sv.VerticalOffset / childHeight)) * columnCount
    
            'ScrollViewerの最後に表示されている要素のインデックス(見切れ含む)
            Dim visibleEndIndex As Integer = CInt(Math.Floor((sv.VerticalOffset + sv.ViewportHeight) / childHeight)) * columnCount + (columnCount - 1)
    
            visibleStartIndex = Math.Min(wrap.Children.Count - 1, visibleStartIndex)
            visibleEndIndex = Math.Min(wrap.Children.Count - 1, visibleEndIndex)
    
            OnScroll(wrap, visibleStartIndex, visibleEndIndex)
        End Sub
    
    
        Private Shared Sub OnScroll(ByVal wrp As WrapPanel, ByVal startIndex As Integer, ByVal endIndex As Integer)
            If (startIndex >= 0) Then
                For index As Integer = startIndex To endIndex
                    Dim child As FrameworkElement = wrp.Children(index)
                    If (child.Tag Is Nothing AndAlso TypeOf child.DataContext Is Uri) Then
                        child.Tag = True
                        Dim img As Image = FindChild(Of Image)(child)
                        If (img IsNot Nothing AndAlso img.Source Is Nothing) Then
                            'まだ読み込んでないなら読み込むImageをつくる
                            Dim bitmap As New BitmapImage()
                            Dim bnd As New Binding("DataContext")
                            bnd.Source = child
                            img.SetBinding(Image.SourceProperty, bnd)
                        End If
                    End If
                Next
            End If
        End Sub
    
    
        Private Shared Function FindChild(Of T As {DependencyObject})(ByVal p As DependencyObject) As T
            Dim count = VisualTreeHelper.GetChildrenCount(p)
            For i As Integer = 0 To count - 1
                Dim child = VisualTreeHelper.GetChild(p, i)
                If (TypeOf child Is T) Then
                    Return CType(child, T)
                End If
                child = FindChild(Of T)(child)
                If child IsNot Nothing Then
                    Return CType(child, T)
                End If
            Next
            Return Nothing
        End Function
    
    
    #Region "要素の追加削除に反応するように"
    
        Sub New()
            AddHandler Me.Loaded, AddressOf OnLoaded
        End Sub
    
    
        Private flag As Boolean
        Private Sub OnVisualChildrenChangedDelay()
            flag = False
            Dim sv As ScrollViewer = TryCast(Me.Tag, ScrollViewer)
            If (sv IsNot Nothing) Then
                Changed(sv)
            End If
        End Sub
    
        Protected Overrides Sub OnVisualChildrenChanged(visualAdded As DependencyObject, visualRemoved As DependencyObject)
            MyBase.OnVisualChildrenChanged(visualAdded, visualRemoved)
    
            If (Not flag) Then
                flag = True
                Dispatcher.BeginInvoke(New Action(AddressOf OnVisualChildrenChangedDelay), System.Windows.Threading.DispatcherPriority.Loaded)
            End If
        End Sub
    
    #End Region
    End Class
    
    #Wrappanelの仮想化は大変なのです…

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2016年12月6日 3:56
  • WrapPanel と似たような挙動をし、仮想化に対応している高速なコントロールでは、Infragistics 社が開発したxamDataCards というコントロールがあります。

    サードパーティー製で有償になりますが、多彩な表現力を有した優秀なコンポーネント群を提供してます。カスタマイズすれは、お望みのUIが得られるかもしれません。Infragisitics Ultimate の無料トライアルをダウンロード・インストールして、WPF サンプル ブラウザ―を起動すれば、挙動を確認できます。


    本フォーラムは、ユーザー(開発者)同士で情報交換を行うためのコミュニティです。初めて利用される方は、以下のアナウンスをご覧ください。 https://social.msdn.microsoft.com/Forums/ja-JP/ca9ecfb7-4407-4fcb-b8bd-207d68257e68?


    2016年12月12日 7:44
    モデレータ

すべての返信

  • 直接の回答ではありませんが、目的に合うものとしては以下が参考になるのではないかと思います。

    ListView で表示用データを仮想化する
    http://d.hatena.ne.jp/tt_clown/20130327/listview_virtualizing

    WPFでコレクションの表示を高速化するオプションとか
    http://yuuxxxx.hatenablog.com/entry/2014/02/01/232320


    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/

    2016年12月6日 2:07
    モデレータ
  • 要素の大きさが固定であるなら、ScrollViewerの表示領域に表示されている要素は計算から調べられます。

    <Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">
        <DockPanel>
            <Button Content="TEST" DockPanel.Dock="Right" Click="Button_Click" />
            <UniformGrid Columns="2">
                <ScrollViewer HorizontalScrollBarVisibility="Disabled" Grid.Row="1" >
                    <WrapPanel x:Name="Wrp" Orientation="Horizontal" Loaded="Wrp_Loaded"/>
                </ScrollViewer>
    
                <ListBox x:Name="listBox1" ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                    <ListBox.ItemsPanel>
                        <ItemsPanelTemplate>
                            <app:WrapPanelEx Orientation="Horizontal" />
                        </ItemsPanelTemplate>
                    </ListBox.ItemsPanel>
                    <ListBox.ItemTemplate>
                        <DataTemplate >
                            <Border Width="50" Height="50" Margin="5" >
                                <Image />
                            </Border>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </UniformGrid>
        </DockPanel>
    
    </Window>
    Imports System.Collections.ObjectModel
    Class MainWindow
    
        Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
    
            For i As Integer = 0 To 50
                Dim b As New Border
                b.Width = 50
                b.Height = 50
                b.Margin = New Thickness(10, 10, 0, 0)
                Dim img As New Image
                b.Child = img
                '読み込ませたいURIを覚えさせておく
                b.DataContext = New Uri("https://i1.social.s-msft.com/profile/u/avatar.jpg?displayname=quarterdeck")
    
                Wrp.Children.Add(b)
            Next
    
            Me.listBox1.ItemsSource = collection
        End Sub
    
        Private Sub Wrp_Loaded(sender As Object, e As RoutedEventArgs)
            WrapPanelEx.OnLoaded(sender, e)
        End Sub
    
    
        'コレクションで増減に反応させる
        Dim collection As New ObservableCollection(Of Uri)
        Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
            For i As Integer = 0 To 10
                collection.Add(New Uri("https://i1.social.s-msft.com/profile/u/avatar.jpg?displayname=quarterdeck"))
            Next
        End Sub
    
    
    End Class
    
    
    Class WrapPanelEx
        Inherits WrapPanel
    
     
        Public Shared Sub OnLoaded(sender As Object, e As RoutedEventArgs)
            Dim wrap As WrapPanel = CType(sender, WrapPanel)
            Dim d As DependencyObject = wrap
            While (d IsNot Nothing)
                Dim sv As ScrollViewer = TryCast(d, ScrollViewer)
                If (sv IsNot Nothing) Then
                    sv.Tag = wrap
                    wrap.Tag = sv
                    AddHandler sv.ScrollChanged, AddressOf ScrollViewer_ScrollChanged
    
    
                    Return
                End If
                d = VisualTreeHelper.GetParent(d)
            End While
        End Sub
    
        Private Shared Sub ScrollViewer_ScrollChanged(sender As Object, e As ScrollChangedEventArgs)
            Changed(CType(sender, ScrollViewer))
        End Sub
    
        Private Shared Sub Changed(ByVal sv As ScrollViewer)
            Dim wrap As WrapPanel = CType(sv.Tag, WrapPanel)
            If (wrap.Children.Count = 0) Then
                Return
            End If
    
            'ScrollViewerの表示領域に表示される要素を調べる
    
            Dim w As Double = sv.ExtentWidth 'ScrollViewerの最大スクロール幅
            Dim h As Double = sv.ExtentHeight 'ScrollViewerの最大スクロール高さ
    
            Dim child As FrameworkElement = wrap.Children(0)
            Dim childWidth As Double = (child.ActualWidth + child.Margin.Left + child.Margin.Right) '固定サイズの要素の幅(マージン含む)
            Dim childHeight As Double = (child.ActualHeight + child.Margin.Top + child.Margin.Bottom) '固定サイズの要素の高さ(マージン含む)
            Dim columnCount = sv.ViewportWidth \ childWidth '1行に表示される列数
    
            'ScrollViewerの左上の要素のインデックス
            Dim visibleStartIndex As Integer = CInt(Math.Floor(sv.VerticalOffset / childHeight)) * columnCount
    
            'ScrollViewerの最後に表示されている要素のインデックス(見切れ含む)
            Dim visibleEndIndex As Integer = CInt(Math.Floor((sv.VerticalOffset + sv.ViewportHeight) / childHeight)) * columnCount + (columnCount - 1)
    
            visibleStartIndex = Math.Min(wrap.Children.Count - 1, visibleStartIndex)
            visibleEndIndex = Math.Min(wrap.Children.Count - 1, visibleEndIndex)
    
            OnScroll(wrap, visibleStartIndex, visibleEndIndex)
        End Sub
    
    
        Private Shared Sub OnScroll(ByVal wrp As WrapPanel, ByVal startIndex As Integer, ByVal endIndex As Integer)
            If (startIndex >= 0) Then
                For index As Integer = startIndex To endIndex
                    Dim child As FrameworkElement = wrp.Children(index)
                    If (child.Tag Is Nothing AndAlso TypeOf child.DataContext Is Uri) Then
                        child.Tag = True
                        Dim img As Image = FindChild(Of Image)(child)
                        If (img IsNot Nothing AndAlso img.Source Is Nothing) Then
                            'まだ読み込んでないなら読み込むImageをつくる
                            Dim bitmap As New BitmapImage()
                            Dim bnd As New Binding("DataContext")
                            bnd.Source = child
                            img.SetBinding(Image.SourceProperty, bnd)
                        End If
                    End If
                Next
            End If
        End Sub
    
    
        Private Shared Function FindChild(Of T As {DependencyObject})(ByVal p As DependencyObject) As T
            Dim count = VisualTreeHelper.GetChildrenCount(p)
            For i As Integer = 0 To count - 1
                Dim child = VisualTreeHelper.GetChild(p, i)
                If (TypeOf child Is T) Then
                    Return CType(child, T)
                End If
                child = FindChild(Of T)(child)
                If child IsNot Nothing Then
                    Return CType(child, T)
                End If
            Next
            Return Nothing
        End Function
    
    
    #Region "要素の追加削除に反応するように"
    
        Sub New()
            AddHandler Me.Loaded, AddressOf OnLoaded
        End Sub
    
    
        Private flag As Boolean
        Private Sub OnVisualChildrenChangedDelay()
            flag = False
            Dim sv As ScrollViewer = TryCast(Me.Tag, ScrollViewer)
            If (sv IsNot Nothing) Then
                Changed(sv)
            End If
        End Sub
    
        Protected Overrides Sub OnVisualChildrenChanged(visualAdded As DependencyObject, visualRemoved As DependencyObject)
            MyBase.OnVisualChildrenChanged(visualAdded, visualRemoved)
    
            If (Not flag) Then
                flag = True
                Dispatcher.BeginInvoke(New Action(AddressOf OnVisualChildrenChangedDelay), System.Windows.Threading.DispatcherPriority.Loaded)
            End If
        End Sub
    
    #End Region
    End Class
    
    #Wrappanelの仮想化は大変なのです…

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2016年12月6日 3:56
  • 皆様ありがとうございます。

    紹介して頂いたサイト等を見ながらテストを繰り返しましたが

    WrapPanelの仮想化はかなり難しそうなので、ScrollViewerの

    VerticalOffsetを使って徐々にアイテムを追加する方向で開発を

    してみるつもりです。

    2016年12月12日 6:16
  • WrapPanel と似たような挙動をし、仮想化に対応している高速なコントロールでは、Infragistics 社が開発したxamDataCards というコントロールがあります。

    サードパーティー製で有償になりますが、多彩な表現力を有した優秀なコンポーネント群を提供してます。カスタマイズすれは、お望みのUIが得られるかもしれません。Infragisitics Ultimate の無料トライアルをダウンロード・インストールして、WPF サンプル ブラウザ―を起動すれば、挙動を確認できます。


    本フォーラムは、ユーザー(開発者)同士で情報交換を行うためのコミュニティです。初めて利用される方は、以下のアナウンスをご覧ください。 https://social.msdn.microsoft.com/Forums/ja-JP/ca9ecfb7-4407-4fcb-b8bd-207d68257e68?


    2016年12月12日 7:44
    モデレータ
  • 今回の要件で一番大きな要素が異なるアスペクト比の画像を出来る限りたくさん並べる事なので、

    同サイズのカードが並ぶこのようなSDKは使えないのです。

    実際に画像を読み込まないとアスペクト比もわかならないので、かなり苦戦しています。

    情報ありがとうございました。

    2016年12月16日 5:21