none
ArrangeOverride で子要素を再配置しても前に描画した内容が残ったままになっている RRS feed

  • 質問

  • .NET Framework 4.5、VS2013、C#+WPF で開発しています。

    次の画像のように独自にカスタマイズした VirtualCanvas というコントロール上に矩形をいくつか配置し、
    ScrollBar コントロールでその表示領域をスクロールさせることをしています。

    独自コントロールを使用した子要素の描画

    このとき、ScrollBar コントロールをゆっくり動かすと問題ないのですが、
    素早く動かすと表示範囲にないはずの矩形が一部描画されたままになってしまいます。
    また、その状態で矩形が表示範囲に入らない程度に ScrollBar コントロールを動かしても
    そのまま矩形の描画が消えずに残ったままになってしまいます。

    このため、例えば ScrollBar コントロールを右下に合わせることで
    すべての子要素は表示されないようになるはずですが、
    次の画像のように子要素の描画が残ってしまう現象が発生しています。

    表示領域を右下にすると何も表示されないはずなのに子要素の描画が残っている

    ScrollBar を動かすことで VirtualCanvas コントロールの ArrangeOverride メソッドがコールされていることは確認できているのですが、
    それでも子要素の描画が残ったままになってしまう理由がわかりません。
    原因や解決方法をどなたかご存知ではないでしょうか。
    よろしくお願いします。

    以下ソースコードです。

    <Window x:Class="VirtualCanvasTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:VirtualCanvasTest"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
    
            <local:VirtualCanvas x:Name="canvas">
                <Rectangle local:VirtualCanvas.Left="-50" local:VirtualCanvas.Top="-50" Fill="Orange" Width="50" Height="50" />
                <Rectangle local:VirtualCanvas.Left="-50" local:VirtualCanvas.Top="50" Fill="Orange" Width="50" Height="50" />
                <Rectangle local:VirtualCanvas.Left="-50" local:VirtualCanvas.Top="150" Fill="Orange" Width="50" Height="50" />
    
                <Rectangle local:VirtualCanvas.Left="0" local:VirtualCanvas.Top="0" Fill="Orange" Width="50" Height="50" />
                <Rectangle local:VirtualCanvas.Left="0" local:VirtualCanvas.Top="100" Fill="Orange" Width="50" Height="50" />
    
                <Rectangle local:VirtualCanvas.Left="50" local:VirtualCanvas.Top="-50" Fill="Orange" Width="50" Height="50" />
                <Rectangle local:VirtualCanvas.Left="50" local:VirtualCanvas.Top="50" Fill="Orange" Width="50" Height="50" />
                <Rectangle local:VirtualCanvas.Left="50" local:VirtualCanvas.Top="150" Fill="Orange" Width="50" Height="50" />
            </local:VirtualCanvas>
    
            <ScrollBar x:Name="horizontalScrollBar" Grid.Row="1" Orientation="Horizontal" Minimum="{Binding ViewPort.Left, ElementName=canvas}" Maximum="{Binding ViewPort.Right, ElementName=canvas}" LargeChange="10" ValueChanged="ScrollBar_ValueChanged" />
            <ScrollBar x:Name="verticalScrollBar" Grid.Column="1" Orientation="Vertical" Minimum="{Binding ViewPort.Top, ElementName=canvas}" Maximum="{Binding ViewPort.Bottom, ElementName=canvas}" LargeChange="10" ValueChanged="ScrollBar_ValueChanged" />
    
        </Grid>
    </Window>

    MainWindow.xaml.cs

    namespace VirtualCanvasTest
    {
        using System.Windows;
    
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
    
            private void ScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
            {
                var size = this.canvas.RenderSize;
                var rect = new Rect(new Point(this.horizontalScrollBar.Value, this.verticalScrollBar.Value), size);
                this.canvas.CurrentViewPort = rect;
            }
        }
    }

    VirtualCanvas.cs

    namespace VirtualCanvasTest
    {
        using System.Collections.ObjectModel;
        using System.Collections.Specialized;
        using System.ComponentModel;
        using System.Linq;
        using System.Windows;
        using System.Windows.Controls;
        using System.Windows.Markup;
        using System.Windows.Media;
    
        [ContentProperty("Children")]
        internal class VirtualCanvas : FrameworkElement
        {
            #region ViewPort 依存関係プロパティ
    
            /// <summary>
            /// ViewPort 依存関係プロパティのキー定義
            /// </summary>
            public static readonly DependencyPropertyKey ViewPortPropertyKey = DependencyProperty.RegisterReadOnly("ViewPort", typeof(Rect), typeof(VirtualCanvas), new FrameworkPropertyMetadata(default(Rect), FrameworkPropertyMetadataOptions.AffectsArrange));
    
            /// <summary>
            /// ViewPort 依存関係プロパティの定義
            /// </summary>
            public static readonly DependencyProperty ViewPortProperty = ViewPortPropertyKey.DependencyProperty;
    
            /// <summary>
            /// このパネルの仮想領域を取得します。
            /// </summary>
            public Rect ViewPort
            {
                get { return (Rect)GetValue(ViewPortProperty); }
                private set { SetValue(ViewPortPropertyKey, value); }
            }
    
            #endregion ViewPort 依存関係プロパティ
    
            #region CurrentViewPort 依存関係プロパティ
    
            /// <summary>
            /// CurrentViewPort 依存関係プロパティの定義
            /// </summary>
            public static readonly DependencyProperty CurrentViewPortProperty = DependencyProperty.Register("CurrentViewPort", typeof(Rect), typeof(VirtualCanvas), new FrameworkPropertyMetadata(default(Rect), FrameworkPropertyMetadataOptions.AffectsMeasure));
    
            /// <summary>
            /// このパネルの仮想領域に対する現在の表示領域を取得または設定します。
            /// </summary>
            public Rect CurrentViewPort
            {
                get { return (Rect)GetValue(CurrentViewPortProperty); }
                set { SetValue(CurrentViewPortProperty, value); }
            }
    
            #endregion CurrentViewPort 依存関係プロパティ
    
            #region Left 添付プロパティ
    
            /// <summary>
            /// Left 添付プロパティの定義
            /// </summary>
            public static readonly DependencyProperty LeftProperty = DependencyProperty.RegisterAttached("Left", typeof(double), typeof(VirtualCanvas), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsParentArrange));
    
            /// <summary>
            /// Left 添付プロパティを取得します。
            /// </summary>
            /// <param name="target">対象とする DependencyObject を指定します。</param>
            /// <returns>取得した値を返します。</returns>
            public static double GetLeft(DependencyObject target)
            {
                return (double)target.GetValue(LeftProperty);
            }
    
            /// <summary>
            /// Left 添付プロパティを設定します。
            /// </summary>
            /// <param name="target">対象とする DependencyObject を指定します。</param>
            /// <param name="value">設定する値を指定します。</param>
            public static void SetLeft(DependencyObject target, double value)
            {
                target.SetValue(LeftProperty, value);
            }
    
            #endregion Left 添付プロパティ
    
            #region Top 添付プロパティ
    
            /// <summary>
            /// Top 添付プロパティの定義
            /// </summary>
            public static readonly DependencyProperty TopProperty = DependencyProperty.RegisterAttached("Top", typeof(double), typeof(VirtualCanvas), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsParentArrange));
    
            /// <summary>
            /// Top 添付プロパティを取得します。
            /// </summary>
            /// <param name="target">対象とする DependencyObject を指定します。</param>
            /// <returns>取得した値を返します。</returns>
            public static double GetTop(DependencyObject target)
            {
                return (double)target.GetValue(TopProperty);
            }
    
            /// <summary>
            /// Top 添付プロパティを設定します。
            /// </summary>
            /// <param name="target">対象とする DependencyObject を指定します。</param>
            /// <param name="value">設定する値を指定します。</param>
            public static void SetTop(DependencyObject target, double value)
            {
                target.SetValue(TopProperty, value);
            }
    
            #endregion Top 添付プロパティ
    
            /// <summary>
            /// 新しいインスタンスを生成します。
            /// </summary>
            public VirtualCanvas()
            {
                this.Children = new ObservableCollection<FrameworkElement>();
                this.Children.CollectionChanged += OnChildrenCollectionChanged;
            }
    
            private void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                if (e.OldItems != null)
                {
                    foreach (FrameworkElement item in e.OldItems)
                    {
                        RemoveVisualChild(item);
                    }
                }
    
                if (e.NewItems != null)
                {
                    foreach (FrameworkElement item in e.NewItems)
                    {
                        AddVisualChild(item);
                    }
                }
    
                this.InvalidateArrange();
            }
    
            protected override Visual GetVisualChild(int index)
            {
                return this.Children[index];
            }
    
            protected override int VisualChildrenCount
            {
                get
                {
                    return this.Children.Count;
                }
            }
    
            [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
            public ObservableCollection<FrameworkElement> Children { get; private set; }
    
            static int measureCount;
            static int arrangeCount;
    
            protected override Size MeasureOverride(Size availableSize)
            {
                System.Diagnostics.Debug.WriteLine("MeasureOverride : " + (measureCount++).ToString());
    
                var size = new Size(double.PositiveInfinity, double.PositiveInfinity);
                var left = 0.0;
                var top = 0.0;
                var right = 0.0;
                var bottom = 0.0;
                foreach (var element in this.Children)
                {
                    element.Measure(size);
                    var eLeft = GetLeft(element);
                    var eTop = GetTop(element);
                    var eRight = eLeft + element.DesiredSize.Width;
                    var eBottom = eTop + element.DesiredSize.Height;
                    if (eLeft < left) left = eLeft;
                    if (eTop < top) top = eTop;
                    if (right < eRight) right = eRight;
                    if (bottom < eBottom) bottom = eBottom;
                }
                this.ViewPort = new Rect(new Point(left, top), new Point(right, bottom));
    
                return base.MeasureOverride(availableSize);
            }
    
            protected override Size ArrangeOverride(Size finalSize)
            {
                System.Diagnostics.Debug.WriteLine("ArrangeOverride : " + (arrangeCount++).ToString());
    
                foreach (var element in this.Children)
                {
                    var topLeft = new Point(GetLeft(element), GetTop(element));
                    var rect = new Rect(topLeft, element.DesiredSize);
                    if (this.CurrentViewPort.IntersectsWith(rect))
                    {
                        topLeft.Offset(-this.CurrentViewPort.Left, -this.CurrentViewPort.Top);
                        element.Arrange(new Rect(topLeft, element.DesiredSize));
                    }
                }
    
                return finalSize;
            }
        }
    }


    • 編集済み Yujiro15 2016年10月7日 0:11
    2016年10月7日 0:04

回答

  • AddVisualChild/RemoveVisualChildで見える要素の親子関係を適切に処理して、VisualChildrenCountは見える要素の数を返して、GetVisualChildrenは見える要素だけを返してみる。

    <Window x:Class="WpfApplication1.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="400" Width="500">
        <DockPanel>
            <StackPanel DockPanel.Dock="Bottom" Margin="0,20,0,0" TextElement.FontSize="20">
                <!-- 範囲外で消えてるか確認するならClipToBounds=falseに -->
                <CheckBox Content="枠内の要素のみ" IsChecked="{Binding ElementName=virtualCanvas,Path=ClipToBounds}"/>
            </StackPanel>
            <Border BorderBrush="Black" BorderThickness="1" Margin="100">
                <Grid >
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
    
                    <app:VirtualCanvas x:Name="virtualCanvas" Background="Transparent"/>
    
                    <ScrollBar Grid.Row="1" Grid.Column="0" Height="20" Orientation="Horizontal" HorizontalAlignment="Stretch" 
                       DataContext="{Binding ElementName=virtualCanvas}"
                       Minimum="{Binding Path=Extent.Left}"
                       Maximum="{Binding Path=Extent.Right}"
                       ViewportSize="{Binding Path=Viewport.Width}"
                       Value="{Binding Path=HorizontalOffset}" />
    
                    <ScrollBar Grid.Row="0" Grid.Column="1" Width="20" Orientation="Vertical" VerticalAlignment="Stretch" 
                       DataContext="{Binding ElementName=virtualCanvas}"
                       Minimum="{Binding Path=Extent.Top}"
                       Maximum="{Binding Path=Extent.Bottom}"
                       ViewportSize="{Binding Path=Viewport.Height}"
                       Value="{Binding Path=VerticalOffset}" />
                </Grid>
            </Border>
        </DockPanel>
    </Window>

    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;
    using System.Windows.Controls.Primitives;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                byte r = 0;
                byte g = 0;
                byte b = 0;
                for (int iy = 0, y = -500; y <= 2000; y += 50, iy++)
                {
                    b = 0;
                    for (int ix = 0, x = -500; x <= 2000; x += 50, ix++)
                    {
                        if (((ix ^ iy) & 1) == 1)
                        {
                            continue;
                        }
                        Button btn = new Button();
                        btn.Content = string.Format("{0},{1}", x, y);
                        btn.Width = 50;
                        btn.Height = 50;
                        Canvas.SetLeft(btn, x);//位置指定としてCanvas.LeftとTopを使用
                        Canvas.SetTop(btn, y);
                        if (x == 0 && y == 0)
                        {
                            btn.Background = System.Windows.Media.Brushes.LightPink;
                        }
                        else
                        {
                            btn.Background = new SolidColorBrush(Color.FromArgb(0x4f, r, g, b));
                        }
                        b = (byte)(b + 20);
                        this.virtualCanvas.Children.Add(btn);
                    }
                    g = (byte)(g + 20);
                }
            }
        }
    
        class VirtualCanvas : Panel, IScrollInfo
        {
            public VirtualCanvas()
            {
                this.ClipToBounds = true;
            }
    
            protected override int VisualChildrenCount
            {
                get
                {
                    //論理的な子供の数では無く描画する要素の数を返すこと
                    return visibleList.Count;
                }
            }
    
            protected override Visual GetVisualChild(int index)
            {
                //AddVisualChildで親子関係が登録されている要素を返すこと
                return visibleList[index];
            }
    
            //表示する要素の一覧
            private List<UIElement> visibleList = new List<UIElement>();
    
            /// <summary>配置計算</summary>>
            protected override Size MeasureOverride(Size availableSize)
            {
                //現在表示されている要素の親子関係を解除
                foreach (UIElement ui in Children)
                {
                    RemoveVisualChild(ui);
                }
                visibleList.Clear();
    
                Size size8 = new Size(double.PositiveInfinity, double.PositiveInfinity);
                double minX = double.MaxValue;
                double minY = double.MaxValue;
                double maxX = double.MinValue;
                double maxY = double.MinValue;
    
                Rect viewport = new Rect(HorizontalOffset, VerticalOffset, availableSize.Width, availableSize.Height);
                foreach (UIElement ui in Children)
                {
                    if (ui.Visibility == System.Windows.Visibility.Collapsed)
                    {
                        SetRect(ui, null);
                    }
                    else
                    {
                        AddVisualChild(ui);//DesiredSizeを得るためにVisualTreeに登録
                        ui.Measure(size8);//要素の大きさ計算
    
                        double l = Canvas.GetLeft(ui);
                        double t = Canvas.GetTop(ui);
                        if (double.IsNaN(l)) l = 0;
                        if (double.IsNaN(t)) t = 0;
                        Rect r = new Rect(l, t, ui.DesiredSize.Width, ui.DesiredSize.Height);
                        if (viewport.IntersectsWith(r))
                        {
                            //見える範囲に入っている
                            r.Offset(-HorizontalOffset, -VerticalOffset);
                            visibleList.Add(ui);
    
                            SetRect(ui, r);
    
                        }
                        else
                        {
                            //見えないので親子関係は解除
                            RemoveVisualChild(ui);
                            SetRect(ui, null);
                        }
    
                        //スクロール可能範囲最大を計算
                        minX = Math.Min(minX, l);
                        maxX = Math.Max(maxX, l + ui.DesiredSize.Width);
                        minY = Math.Min(minY, t);
                        maxY = Math.Max(maxY, t + ui.DesiredSize.Height);
                    }
                }
    
    
    
                //見える範囲とスクロール可能な範囲を更新
                this.Viewport = viewport;
                this.Extent = new Rect(minX, minY, Math.Max(0, maxX - minX - availableSize.Width), Math.Max(0, maxY - minY - availableSize.Height));
    
                //オフセットが表示可能範囲外にあったら範囲内になるようにオフセット変更
                bool isOuter = false;
                if (HorizontalOffset < Extent.Left) { HorizontalOffset = Extent.Left; isOuter = true; }
                if (HorizontalOffset > Extent.Right) { HorizontalOffset = Extent.Right; isOuter = true; }
                if (VerticalOffset < Extent.Top) { VerticalOffset = Extent.Top; isOuter = true; }
                if (VerticalOffset > Extent.Bottom) { VerticalOffset = Extent.Bottom; isOuter = true; }
                if (isOuter) { MeasureOverride(availableSize); }
    
                return availableSize;
            }
    
            protected override Size ArrangeOverride(Size finalSize)
            {
                //表示対象の要素の計算済みの描画位置に配置
                foreach (UIElement ui in visibleList)
                {
                    var nr = GetRect(ui);
                    if (nr.HasValue)
                    {
                        ui.Arrange(nr.Value);
                    }
                }
                return finalSize;
            }
    
            #region 描画位置を覚えておくための添付プロパティ
            private static Rect? GetRect(DependencyObject obj)
            {
                return (Rect?)obj.GetValue(RectProperty);
            }
    
            private static void SetRect(DependencyObject obj, Rect? value)
            {
                obj.SetValue(RectProperty, value);
            }
    
            public static readonly DependencyProperty RectProperty =
                DependencyProperty.RegisterAttached("Rect", typeof(Rect?), typeof(VirtualCanvas), new PropertyMetadata(null));
            #endregion
    
            #region 見える範囲の左上の位置
            public double HorizontalOffset
            {
                get { return (double)GetValue(HorizontalOffsetProperty); }
                set { SetValue(HorizontalOffsetProperty, value); }
            }
            public static readonly DependencyProperty HorizontalOffsetProperty
                = DependencyProperty.Register("HorizontalOffset", typeof(double), typeof(VirtualCanvas), new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    
            public double VerticalOffset
            {
                get { return (double)GetValue(VerticalOffsetProperty); }
                set { SetValue(VerticalOffsetProperty, value); }
            }
            public static readonly DependencyProperty VerticalOffsetProperty
                = DependencyProperty.Register("VerticalOffset", typeof(double), typeof(VirtualCanvas), new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
            #endregion
    
            #region 見える範囲
            public Rect Viewport
            {
                get { return (Rect)GetValue(ViewportProperty); }
                set { SetValue(ViewportPropertyKey, value); }
            }
            private static readonly DependencyPropertyKey ViewportPropertyKey =
                DependencyProperty.RegisterReadOnly("Viewport", typeof(Rect), typeof(VirtualCanvas), new PropertyMetadata(default(Rect)));
            public static readonly DependencyProperty ViewportProperty = ViewportPropertyKey.DependencyProperty;
            #endregion
    
            #region スクロール可能な範囲
            public Rect Extent
            {
                get { return (Rect)GetValue(ExtentProperty); }
                set { SetValue(ExtentPropertyKey, value); }
            }
            private static readonly DependencyPropertyKey ExtentPropertyKey =
                DependencyProperty.RegisterReadOnly("Extent", typeof(Rect), typeof(VirtualCanvas), new PropertyMetadata(default(Rect)));
            public static readonly DependencyProperty ExtentProperty = ExtentPropertyKey.DependencyProperty;
            #endregion
    
    
            public void ScrollIntoView(UIElement element)
            {
                var parent = VisualTreeHelper.GetParent(element);
                if (this.Children.OfType<UIElement>().Contains(element))
                {
                    double l = Canvas.GetLeft(element);
                    double t = Canvas.GetTop(element);
                    this.InvalidateMeasure();
                    this.HorizontalOffset = l;
                    this.VerticalOffset = t;
                }
            }
    
            #region IScrollInfo
    
            public bool CanHorizontallyScroll { get { return true; } set { } }
            public bool CanVerticallyScroll { get { return true; } set { } }
    
            public double ExtentHeight { get { return Extent.Height; } }
            public double ExtentWidth { get { return Extent.Width; } }
    
            public Rect MakeVisible(Visual visual, Rect rectangle)
            {
                DependencyObject d = visual;
                DependencyObject parent;
                while (d != null)
                {
                    parent = VisualTreeHelper.GetParent(d);
                    if (parent == null || parent == this)
                    {
                        foreach (DependencyObject child in this.Children)
                        {
                            if (child == d)
                            {
                                this.ScrollIntoView((UIElement)d);
                                return rectangle;
                            }
                        }
                    }
                    d = parent;
                }
                return rectangle;
            }
    
            public void LineDown() { VerticalOffset -= Viewport.Width / 10; }
            public void LineLeft() { HorizontalOffset += Viewport.Height / 10; }
            public void LineRight() { HorizontalOffset -= Viewport.Height / 10; }
            public void LineUp() { VerticalOffset += Viewport.Width / 10; }
            public void MouseWheelDown() { LineUp(); }
            public void MouseWheelLeft() { LineRight(); }
            public void MouseWheelRight() { LineLeft(); }
            public void MouseWheelUp() { LineDown(); }
    
            protected override void OnMouseWheel(System.Windows.Input.MouseWheelEventArgs e)
            {
                if (e.MiddleButton == System.Windows.Input.MouseButtonState.Pressed)
                {
                    if (e.Delta > 0) { LineLeft(); } else { LineRight(); }
                }
                else
                {
                    if (e.Delta > 0) { LineDown(); } else { LineUp(); }
                }
            }
    
            public void PageDown() { this.VerticalOffset = Math.Min(this.Extent.Bottom, this.VerticalOffset + this.Viewport.Height); }
            public void PageLeft() { this.HorizontalOffset = Math.Max(this.Extent.Left, this.HorizontalOffset - this.Viewport.Width); }
            public void PageRight() { this.HorizontalOffset = Math.Min(this.Extent.Right, this.HorizontalOffset + this.Viewport.Width); }
            public void PageUp() { this.VerticalOffset = Math.Max(this.Extent.Top, this.VerticalOffset - this.Viewport.Height); }
    
            public ScrollViewer ScrollOwner { get; set; }
    
            public void SetHorizontalOffset(double offset) { this.HorizontalOffset = Math.Min(Math.Max(Extent.Left, offset), Extent.Right); }
            public void SetVerticalOffset(double offset) { this.VerticalOffset = Math.Min(Math.Max(Extent.Top, offset), Extent.Bottom); }
    
            public double ViewportHeight { get { return Viewport.Height; } }
            public double ViewportWidth { get { return Viewport.Width; } }
    
            #endregion
        }
    }


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

    • 編集済み gekkaMVP 2016年10月8日 6:07 余計なXAMLの消し忘れ
    • 回答としてマーク Yujiro15 2016年10月10日 23:14
    2016年10月8日 1:06

すべての返信

  • コードを実行して試したわけではありませんが、VirtualCanvasとやらのWidthとHeightを明示的に指定していないことが原因な気がします。
    それと、もし仮想化でパフォーマンスを向上させるのが最終目的なのであれば、配置よりも描画のほうをオーバーライドして、自前でレンダリングしたほうが良いのではないでしょうか。Shapeを使うとオーバーヘッドが大きいですし。
    • 編集済み sygh 2016年10月7日 1:31
    2016年10月7日 1:14
  • 返信ありがとうございます。

    MainWindow.xaml で VirtualCanvas に対して
    Width と Height を直接指定してみましたが、
    現象は変わりませんでした。

    <local:VirtualCanvas x:Name="canvas" Width="492" Height="295">
    掲載したコードでは単純に矩形を表示していますが、
    コンテンツを表示するために ContentControl のようなものを配置する予定ですので、
    描画ではなく配置のほうをオーバーライドしています。


    • 編集済み Yujiro15 2016年10月7日 6:26
    2016年10月7日 6:25
  • Visual Studio 2015 Update 3、.NET 4.5.2、Windows 10 x64上で調べてみましたが、

    if (this.CurrentViewPort.IntersectsWith(rect))

    が原因となっているようです。この条件文をコメントアウトすると現象は発生しなくなります。

    詳しい理由はまだ解析していませんが、「交差しなかった場合はArrangeを一切しない」というロジックだと、更新されなかったものに関しては前回のArrange結果が使われてしまうので、急激にスクロールした場合にずれが顕著となって現れるのだと思います。

    2016年10月7日 17:53
  • AddVisualChild/RemoveVisualChildで見える要素の親子関係を適切に処理して、VisualChildrenCountは見える要素の数を返して、GetVisualChildrenは見える要素だけを返してみる。

    <Window x:Class="WpfApplication1.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="400" Width="500">
        <DockPanel>
            <StackPanel DockPanel.Dock="Bottom" Margin="0,20,0,0" TextElement.FontSize="20">
                <!-- 範囲外で消えてるか確認するならClipToBounds=falseに -->
                <CheckBox Content="枠内の要素のみ" IsChecked="{Binding ElementName=virtualCanvas,Path=ClipToBounds}"/>
            </StackPanel>
            <Border BorderBrush="Black" BorderThickness="1" Margin="100">
                <Grid >
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
    
                    <app:VirtualCanvas x:Name="virtualCanvas" Background="Transparent"/>
    
                    <ScrollBar Grid.Row="1" Grid.Column="0" Height="20" Orientation="Horizontal" HorizontalAlignment="Stretch" 
                       DataContext="{Binding ElementName=virtualCanvas}"
                       Minimum="{Binding Path=Extent.Left}"
                       Maximum="{Binding Path=Extent.Right}"
                       ViewportSize="{Binding Path=Viewport.Width}"
                       Value="{Binding Path=HorizontalOffset}" />
    
                    <ScrollBar Grid.Row="0" Grid.Column="1" Width="20" Orientation="Vertical" VerticalAlignment="Stretch" 
                       DataContext="{Binding ElementName=virtualCanvas}"
                       Minimum="{Binding Path=Extent.Top}"
                       Maximum="{Binding Path=Extent.Bottom}"
                       ViewportSize="{Binding Path=Viewport.Height}"
                       Value="{Binding Path=VerticalOffset}" />
                </Grid>
            </Border>
        </DockPanel>
    </Window>

    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;
    using System.Windows.Controls.Primitives;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                byte r = 0;
                byte g = 0;
                byte b = 0;
                for (int iy = 0, y = -500; y <= 2000; y += 50, iy++)
                {
                    b = 0;
                    for (int ix = 0, x = -500; x <= 2000; x += 50, ix++)
                    {
                        if (((ix ^ iy) & 1) == 1)
                        {
                            continue;
                        }
                        Button btn = new Button();
                        btn.Content = string.Format("{0},{1}", x, y);
                        btn.Width = 50;
                        btn.Height = 50;
                        Canvas.SetLeft(btn, x);//位置指定としてCanvas.LeftとTopを使用
                        Canvas.SetTop(btn, y);
                        if (x == 0 && y == 0)
                        {
                            btn.Background = System.Windows.Media.Brushes.LightPink;
                        }
                        else
                        {
                            btn.Background = new SolidColorBrush(Color.FromArgb(0x4f, r, g, b));
                        }
                        b = (byte)(b + 20);
                        this.virtualCanvas.Children.Add(btn);
                    }
                    g = (byte)(g + 20);
                }
            }
        }
    
        class VirtualCanvas : Panel, IScrollInfo
        {
            public VirtualCanvas()
            {
                this.ClipToBounds = true;
            }
    
            protected override int VisualChildrenCount
            {
                get
                {
                    //論理的な子供の数では無く描画する要素の数を返すこと
                    return visibleList.Count;
                }
            }
    
            protected override Visual GetVisualChild(int index)
            {
                //AddVisualChildで親子関係が登録されている要素を返すこと
                return visibleList[index];
            }
    
            //表示する要素の一覧
            private List<UIElement> visibleList = new List<UIElement>();
    
            /// <summary>配置計算</summary>>
            protected override Size MeasureOverride(Size availableSize)
            {
                //現在表示されている要素の親子関係を解除
                foreach (UIElement ui in Children)
                {
                    RemoveVisualChild(ui);
                }
                visibleList.Clear();
    
                Size size8 = new Size(double.PositiveInfinity, double.PositiveInfinity);
                double minX = double.MaxValue;
                double minY = double.MaxValue;
                double maxX = double.MinValue;
                double maxY = double.MinValue;
    
                Rect viewport = new Rect(HorizontalOffset, VerticalOffset, availableSize.Width, availableSize.Height);
                foreach (UIElement ui in Children)
                {
                    if (ui.Visibility == System.Windows.Visibility.Collapsed)
                    {
                        SetRect(ui, null);
                    }
                    else
                    {
                        AddVisualChild(ui);//DesiredSizeを得るためにVisualTreeに登録
                        ui.Measure(size8);//要素の大きさ計算
    
                        double l = Canvas.GetLeft(ui);
                        double t = Canvas.GetTop(ui);
                        if (double.IsNaN(l)) l = 0;
                        if (double.IsNaN(t)) t = 0;
                        Rect r = new Rect(l, t, ui.DesiredSize.Width, ui.DesiredSize.Height);
                        if (viewport.IntersectsWith(r))
                        {
                            //見える範囲に入っている
                            r.Offset(-HorizontalOffset, -VerticalOffset);
                            visibleList.Add(ui);
    
                            SetRect(ui, r);
    
                        }
                        else
                        {
                            //見えないので親子関係は解除
                            RemoveVisualChild(ui);
                            SetRect(ui, null);
                        }
    
                        //スクロール可能範囲最大を計算
                        minX = Math.Min(minX, l);
                        maxX = Math.Max(maxX, l + ui.DesiredSize.Width);
                        minY = Math.Min(minY, t);
                        maxY = Math.Max(maxY, t + ui.DesiredSize.Height);
                    }
                }
    
    
    
                //見える範囲とスクロール可能な範囲を更新
                this.Viewport = viewport;
                this.Extent = new Rect(minX, minY, Math.Max(0, maxX - minX - availableSize.Width), Math.Max(0, maxY - minY - availableSize.Height));
    
                //オフセットが表示可能範囲外にあったら範囲内になるようにオフセット変更
                bool isOuter = false;
                if (HorizontalOffset < Extent.Left) { HorizontalOffset = Extent.Left; isOuter = true; }
                if (HorizontalOffset > Extent.Right) { HorizontalOffset = Extent.Right; isOuter = true; }
                if (VerticalOffset < Extent.Top) { VerticalOffset = Extent.Top; isOuter = true; }
                if (VerticalOffset > Extent.Bottom) { VerticalOffset = Extent.Bottom; isOuter = true; }
                if (isOuter) { MeasureOverride(availableSize); }
    
                return availableSize;
            }
    
            protected override Size ArrangeOverride(Size finalSize)
            {
                //表示対象の要素の計算済みの描画位置に配置
                foreach (UIElement ui in visibleList)
                {
                    var nr = GetRect(ui);
                    if (nr.HasValue)
                    {
                        ui.Arrange(nr.Value);
                    }
                }
                return finalSize;
            }
    
            #region 描画位置を覚えておくための添付プロパティ
            private static Rect? GetRect(DependencyObject obj)
            {
                return (Rect?)obj.GetValue(RectProperty);
            }
    
            private static void SetRect(DependencyObject obj, Rect? value)
            {
                obj.SetValue(RectProperty, value);
            }
    
            public static readonly DependencyProperty RectProperty =
                DependencyProperty.RegisterAttached("Rect", typeof(Rect?), typeof(VirtualCanvas), new PropertyMetadata(null));
            #endregion
    
            #region 見える範囲の左上の位置
            public double HorizontalOffset
            {
                get { return (double)GetValue(HorizontalOffsetProperty); }
                set { SetValue(HorizontalOffsetProperty, value); }
            }
            public static readonly DependencyProperty HorizontalOffsetProperty
                = DependencyProperty.Register("HorizontalOffset", typeof(double), typeof(VirtualCanvas), new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    
            public double VerticalOffset
            {
                get { return (double)GetValue(VerticalOffsetProperty); }
                set { SetValue(VerticalOffsetProperty, value); }
            }
            public static readonly DependencyProperty VerticalOffsetProperty
                = DependencyProperty.Register("VerticalOffset", typeof(double), typeof(VirtualCanvas), new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
            #endregion
    
            #region 見える範囲
            public Rect Viewport
            {
                get { return (Rect)GetValue(ViewportProperty); }
                set { SetValue(ViewportPropertyKey, value); }
            }
            private static readonly DependencyPropertyKey ViewportPropertyKey =
                DependencyProperty.RegisterReadOnly("Viewport", typeof(Rect), typeof(VirtualCanvas), new PropertyMetadata(default(Rect)));
            public static readonly DependencyProperty ViewportProperty = ViewportPropertyKey.DependencyProperty;
            #endregion
    
            #region スクロール可能な範囲
            public Rect Extent
            {
                get { return (Rect)GetValue(ExtentProperty); }
                set { SetValue(ExtentPropertyKey, value); }
            }
            private static readonly DependencyPropertyKey ExtentPropertyKey =
                DependencyProperty.RegisterReadOnly("Extent", typeof(Rect), typeof(VirtualCanvas), new PropertyMetadata(default(Rect)));
            public static readonly DependencyProperty ExtentProperty = ExtentPropertyKey.DependencyProperty;
            #endregion
    
    
            public void ScrollIntoView(UIElement element)
            {
                var parent = VisualTreeHelper.GetParent(element);
                if (this.Children.OfType<UIElement>().Contains(element))
                {
                    double l = Canvas.GetLeft(element);
                    double t = Canvas.GetTop(element);
                    this.InvalidateMeasure();
                    this.HorizontalOffset = l;
                    this.VerticalOffset = t;
                }
            }
    
            #region IScrollInfo
    
            public bool CanHorizontallyScroll { get { return true; } set { } }
            public bool CanVerticallyScroll { get { return true; } set { } }
    
            public double ExtentHeight { get { return Extent.Height; } }
            public double ExtentWidth { get { return Extent.Width; } }
    
            public Rect MakeVisible(Visual visual, Rect rectangle)
            {
                DependencyObject d = visual;
                DependencyObject parent;
                while (d != null)
                {
                    parent = VisualTreeHelper.GetParent(d);
                    if (parent == null || parent == this)
                    {
                        foreach (DependencyObject child in this.Children)
                        {
                            if (child == d)
                            {
                                this.ScrollIntoView((UIElement)d);
                                return rectangle;
                            }
                        }
                    }
                    d = parent;
                }
                return rectangle;
            }
    
            public void LineDown() { VerticalOffset -= Viewport.Width / 10; }
            public void LineLeft() { HorizontalOffset += Viewport.Height / 10; }
            public void LineRight() { HorizontalOffset -= Viewport.Height / 10; }
            public void LineUp() { VerticalOffset += Viewport.Width / 10; }
            public void MouseWheelDown() { LineUp(); }
            public void MouseWheelLeft() { LineRight(); }
            public void MouseWheelRight() { LineLeft(); }
            public void MouseWheelUp() { LineDown(); }
    
            protected override void OnMouseWheel(System.Windows.Input.MouseWheelEventArgs e)
            {
                if (e.MiddleButton == System.Windows.Input.MouseButtonState.Pressed)
                {
                    if (e.Delta > 0) { LineLeft(); } else { LineRight(); }
                }
                else
                {
                    if (e.Delta > 0) { LineDown(); } else { LineUp(); }
                }
            }
    
            public void PageDown() { this.VerticalOffset = Math.Min(this.Extent.Bottom, this.VerticalOffset + this.Viewport.Height); }
            public void PageLeft() { this.HorizontalOffset = Math.Max(this.Extent.Left, this.HorizontalOffset - this.Viewport.Width); }
            public void PageRight() { this.HorizontalOffset = Math.Min(this.Extent.Right, this.HorizontalOffset + this.Viewport.Width); }
            public void PageUp() { this.VerticalOffset = Math.Max(this.Extent.Top, this.VerticalOffset - this.Viewport.Height); }
    
            public ScrollViewer ScrollOwner { get; set; }
    
            public void SetHorizontalOffset(double offset) { this.HorizontalOffset = Math.Min(Math.Max(Extent.Left, offset), Extent.Right); }
            public void SetVerticalOffset(double offset) { this.VerticalOffset = Math.Min(Math.Max(Extent.Top, offset), Extent.Bottom); }
    
            public double ViewportHeight { get { return Viewport.Height; } }
            public double ViewportWidth { get { return Viewport.Width; } }
    
            #endregion
        }
    }


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

    • 編集済み gekkaMVP 2016年10月8日 6:07 余計なXAMLの消し忘れ
    • 回答としてマーク Yujiro15 2016年10月10日 23:14
    2016年10月8日 1:06
  • 確かにそのようですね。
    理由は Gekka さんの回答のように、
    AddVisualChild と RemoveVisualChild を適切にしていなかったためと思われます。
    Gekka さんの回答でなんとか解決できました。
    調査していただきありがとうございました。
    2016年10月10日 23:11
  • 詳細なコード例まで掲載していただきありがとうございます。
    visibleList のように描画するものだけを集めたコレクションを作ることまでは試したことがあったのですが、
    AddVisualChild() や RemoveVisualChild() をしたことはありませんでした。
    これら両方を適切に実行することで不必要な子要素の描画が残ることがなくなりました。

    また、IScrollInfo インターフェースの実装についても
    ヒントを掲載していただきありがとうございます。
    参考にさせていただきます。

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

    2016年10月10日 23:14