How to balance contrls using ArrangeOverride and MeasureOverride - wpf

I have created a custom panel to help understand the MeasureOverride and ArrangeOverride methods. I want the odd items to show up on the left and the even numbered items to show up on the right.
1 - 0
3 - 2
5 - 3
7 - 4
8
public class MyPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
var mySize = new Size();
foreach (UIElement child in this.InternalChildren)
{
child.Measure(availableSize);
mySize.Width = child.DesiredSize.Width * 2;
var itemCount = Math.Ceiling(InternalChildren.Count / 2f);
mySize.Height = itemCount * child.DesiredSize.Height;
}
return mySize;
}
protected override Size ArrangeOverride(Size finalSize)
{
var odd = InternalChildren.OfType<UIElement>()
.Where((c, i) => i % 2 != 0);
var even = InternalChildren.OfType<UIElement>()
.Where((c, i) => i % 2 == 0);
var location = new Point();
foreach (var child in even)
{
child.Arrange(new Rect(location, child.DesiredSize));
location.X = child.DesiredSize.Width;
location.Y += child.DesiredSize.Height;
Debug.WriteLine(location);
}
location = new Point();
foreach (var child in odd)
{
child.Arrange(new Rect(location, child.DesiredSize));
location.Y += child.DesiredSize.Height;
Debug.WriteLine(location);
}
return finalSize;
}
}
Here is the markup for the panel.
<Canvas>
<local:MyPanel x:Name="MyPanel1" Canvas.Left="500" Canvas.Top="200"
Background="#FFE84B4B">
<Button Content="0"></Button>
<Button Content="1"></Button>
<Button Content="2"></Button>
<Button Content="3"></Button>
<Button Content="4"></Button>
<Button Content="5"></Button>
<Button Content="6"></Button>
<Button Content="7"></Button>
<Button Content="8"></Button>
</local:MyPanel>
</Canvas>
and here is the result
How can I get item 0 to show up and line up with the 1 column?

I'd first arrange the items on the left (the 'odds') because they define the horizontal offset. Then use that offset for the 'evens'.
Of course this is just kind of demo code. DesiredSize isn't RenderSize. As soon as the DP-system calculates a diff between the two the fixed positioning proofs too rigid.
protected override Size ArrangeOverride(Size finalSize)
{
var odd = InternalChildren.OfType<UIElement>()
.Where((c, i) => i % 2 != 0);
var even = InternalChildren.OfType<UIElement>()
.Where((c, i) => i % 2 == 0);
var location = new Point();
foreach (var child in odd)
{
child.Arrange(new Rect(location, child.DesiredSize));
location.Y += child.DesiredSize.Height;
Debug.WriteLine(location);
}
var evenHorzOffset = odd.First().DesiredSize.Width;
location = new Point(evenHorzOffset, 0);
foreach (var child in even)
{
child.Arrange(new Rect(location, child.DesiredSize));
location.Y += child.DesiredSize.Height;
Debug.WriteLine(location);
}
return finalSize;
}

Related

TextBox doesn't stretch inside a Stackpanel

Please see my attached example code:
<StackPanel Width="300">
<StackPanel Orientation="Horizontal">
<Label Content="Label" Width="100" />
<TextBox Text="Content" />
</StackPanel>
</StackPanel>
How can I make the TextBox stretch to fill all the remaining space horizontally? HorizontalAlignment is not working the way I expect it.
The inner StackPanel will be a UserControl, so I can't just replace both StackPanels with a Grid.
Instead of using the inner StackPanel, you can use a DockPanel.
<StackPanel Width="300">
<DockPanel>
<Label Content="Label" Width="100" />
<TextBox Text="Content" />
</DockPanel>
</StackPanel>
I would recommend using DockPanel as the root control for your UserControl so that you could get the behavior you are looking for in its Child Controls. However if you absolutely need it to be based on a StackPanel then just use some code behind to set the textbox width dynamically.
Xaml:
<StackPanel Name="stpnlOuterControl" Width="300" SizeChanged="StackPanel_SizeChanged">
<StackPanel Orientation="Horizontal">
<Label Content="Test" Width="100"/>
<TextBox Name="txtbxTest" Text="Content"/>
</StackPanel>
</StackPanel>
Code Behind:
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
txtbxTest.Width = stpnlOuterControl.ActualWidth - 100
End Sub
Private Sub StackPanel_SizeChanged(sender As Object, e As SizeChangedEventArgs)
txtbxTest.Width = stpnlOuterControl.ActualWidth - 100
End Sub
Alternative way is to use custom StackPanel
https://github.com/SpicyTaco/SpicyTaco.AutoGrid/blob/master/src/AutoGrid/StackPanel.cs
MainWindow
<Window x:Class="WpfApplication4.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:System="clr-namespace:System;assembly=mscorlib"
xmlns:VM="clr-namespace:WpfApplication4" >
<StackPanel Background="Red" Width="300">
<VM:StackPanel Orientation="Horizontal">
<Label Background="Blue" Content="Label" Width="100" />
<TextBox VM:StackPanel.Fill="Fill" Text="Content" />
</VM:StackPanel>
</StackPanel>
</Window>
Code for Custom Stack panel from SpicyTaco
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication4
{
public class StackPanel : Panel
{
protected override Size MeasureOverride(Size constraint)
{
UIElementCollection children = InternalChildren;
double parentWidth = 0;
double parentHeight = 0;
double accumulatedWidth = 0;
double accumulatedHeight = 0;
var isHorizontal = Orientation == Orientation.Horizontal;
var totalMarginToAdd = CalculateTotalMarginToAdd(children, MarginBetweenChildren);
for (int i = 0; i < children.Count; i++)
{
UIElement child = children[i];
if (child == null) { continue; }
// Handle only the Auto's first to calculate remaining space for Fill's
if (GetFill(child) != StackPanelFill.Auto) { continue; }
// Child constraint is the remaining size; this is total size minus size consumed by previous children.
var childConstraint = new Size(Math.Max(0.0, constraint.Width - accumulatedWidth),
Math.Max(0.0, constraint.Height - accumulatedHeight));
// Measure child.
child.Measure(childConstraint);
var childDesiredSize = child.DesiredSize;
if (isHorizontal)
{
accumulatedWidth += childDesiredSize.Width;
parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height);
}
else
{
parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width);
accumulatedHeight += childDesiredSize.Height;
}
}
// Add all margin to accumulated size before calculating remaining space for
// Fill elements.
if (isHorizontal)
{
accumulatedWidth += totalMarginToAdd;
}
else
{
accumulatedHeight += totalMarginToAdd;
}
var totalCountOfFillTypes = children
.OfType<UIElement>()
.Count(x => GetFill(x) == StackPanelFill.Fill
&& x.Visibility != Visibility.Collapsed);
var availableSpaceRemaining = isHorizontal
? Math.Max(0, constraint.Width - accumulatedWidth)
: Math.Max(0, constraint.Height - accumulatedHeight);
var eachFillTypeSize = totalCountOfFillTypes > 0
? availableSpaceRemaining / totalCountOfFillTypes
: 0;
for (int i = 0; i < children.Count; i++)
{
UIElement child = children[i];
if (child == null) { continue; }
// Handle all the Fill's giving them a portion of the remaining space
if (GetFill(child) != StackPanelFill.Fill) { continue; }
// Child constraint is the remaining size; this is total size minus size consumed by previous children.
var childConstraint = isHorizontal
? new Size(eachFillTypeSize,
Math.Max(0.0, constraint.Height - accumulatedHeight))
: new Size(Math.Max(0.0, constraint.Width - accumulatedWidth),
eachFillTypeSize);
// Measure child.
child.Measure(childConstraint);
var childDesiredSize = child.DesiredSize;
if (isHorizontal)
{
accumulatedWidth += childDesiredSize.Width;
parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height);
}
else
{
parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width);
accumulatedHeight += childDesiredSize.Height;
}
}
// Make sure the final accumulated size is reflected in parentSize.
parentWidth = Math.Max(parentWidth, accumulatedWidth);
parentHeight = Math.Max(parentHeight, accumulatedHeight);
var parent = new Size(parentWidth, parentHeight);
return parent;
}
protected override Size ArrangeOverride(Size arrangeSize)
{
UIElementCollection children = InternalChildren;
int totalChildrenCount = children.Count;
double accumulatedLeft = 0;
double accumulatedTop = 0;
var isHorizontal = Orientation == Orientation.Horizontal;
var marginBetweenChildren = MarginBetweenChildren;
var totalMarginToAdd = CalculateTotalMarginToAdd(children, marginBetweenChildren);
double allAutoSizedSum = 0.0;
int countOfFillTypes = 0;
foreach (var child in children.OfType<UIElement>())
{
var fillType = GetFill(child);
if (fillType != StackPanelFill.Auto)
{
if (child.Visibility != Visibility.Collapsed && fillType != StackPanelFill.Ignored)
countOfFillTypes += 1;
}
else
{
var desiredSize = isHorizontal ? child.DesiredSize.Width : child.DesiredSize.Height;
allAutoSizedSum += desiredSize;
}
}
var remainingForFillTypes = isHorizontal
? Math.Max(0, arrangeSize.Width - allAutoSizedSum - totalMarginToAdd)
: Math.Max(0, arrangeSize.Height - allAutoSizedSum - totalMarginToAdd);
var fillTypeSize = remainingForFillTypes / countOfFillTypes;
for (int i = 0; i < totalChildrenCount; ++i)
{
UIElement child = children[i];
if (child == null) { continue; }
Size childDesiredSize = child.DesiredSize;
var fillType = GetFill(child);
var isCollapsed = child.Visibility == Visibility.Collapsed || fillType == StackPanelFill.Ignored;
var isLastChild = i == totalChildrenCount - 1;
var marginToAdd = isLastChild || isCollapsed ? 0 : marginBetweenChildren;
Rect rcChild = new Rect(
accumulatedLeft,
accumulatedTop,
Math.Max(0.0, arrangeSize.Width - accumulatedLeft),
Math.Max(0.0, arrangeSize.Height - accumulatedTop));
if (isHorizontal)
{
rcChild.Width = fillType == StackPanelFill.Auto || isCollapsed ? childDesiredSize.Width : fillTypeSize;
rcChild.Height = arrangeSize.Height;
accumulatedLeft += rcChild.Width + marginToAdd;
}
else
{
rcChild.Width = arrangeSize.Width;
rcChild.Height = fillType == StackPanelFill.Auto || isCollapsed ? childDesiredSize.Height : fillTypeSize;
accumulatedTop += rcChild.Height + marginToAdd;
}
child.Arrange(rcChild);
}
return arrangeSize;
}
static double CalculateTotalMarginToAdd(UIElementCollection children, double marginBetweenChildren)
{
var visibleChildrenCount = children
.OfType<UIElement>()
.Count(x => x.Visibility != Visibility.Collapsed && GetFill(x) != StackPanelFill.Ignored);
var marginMultiplier = Math.Max(visibleChildrenCount - 1, 0);
var totalMarginToAdd = marginBetweenChildren * marginMultiplier;
return totalMarginToAdd;
}
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
"Orientation", typeof(Orientation), typeof(StackPanel),
new FrameworkPropertyMetadata(
Orientation.Vertical,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure));
public Orientation Orientation
{
get { return (Orientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
public static readonly DependencyProperty MarginBetweenChildrenProperty = DependencyProperty.Register(
"MarginBetweenChildren", typeof(double), typeof(StackPanel),
new FrameworkPropertyMetadata(
0.0,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure));
public double MarginBetweenChildren
{
get { return (double)GetValue(MarginBetweenChildrenProperty); }
set { SetValue(MarginBetweenChildrenProperty, value); }
}
public static readonly DependencyProperty FillProperty = DependencyProperty.RegisterAttached(
"Fill", typeof(StackPanelFill), typeof(StackPanel),
new FrameworkPropertyMetadata(
StackPanelFill.Auto,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsParentArrange |
FrameworkPropertyMetadataOptions.AffectsParentMeasure));
public static void SetFill(DependencyObject element, StackPanelFill value)
{
element.SetValue(FillProperty, value);
}
public static StackPanelFill GetFill(DependencyObject element)
{
return (StackPanelFill)element.GetValue(FillProperty);
}
}
public enum StackPanelFill
{
Auto, Fill, Ignored
}
}

Cutom WPF WrapPanel with overridden OnVisualChildrenChanged won't work as expected

I am working on an app on Windows, and I want a Panel that layouts its children like this:
The closest I find in WPF panels is a WrapPanel with a vertical orientation which has a layout like this:
My solution so far has been creating a derived class from WrapPanel and rotating it and each of its children 180 degrees so that my goal is achieved:
public class NotificationWrapPanel : WrapPanel
{
public NotificationWrapPanel()
{
this.Orientation = Orientation.Vertical;
this.RenderTransformOrigin = new Point(.5, .5);
this.RenderTransform = new RotateTransform(180);
}
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
var addedChild = visualAdded as UIElement;
if (addedChild != null)
{
addedChild.RenderTransformOrigin = new Point(.5, .5);
addedChild.RenderTransform = new RotateTransform(180);
}
base.OnVisualChildrenChanged(addedChild, visualRemoved);
}
}
The problem is that the addedChild.RenderTransform = new RotateTransform(180) part in the overridden OnVisualChildrenChanged does not seem to work. Any solutions (even very unrelated to my approach) are welcomed.
UPDATE:
I re-examined my code and realized I am changing RenderTransform somewhere else, preventing this from working. So my problem is solved. However, I'd appreciate if you offer any solutions that may be more elegant, e.g. using MeasureOverride/ArrangeOverride.
Based on your requirements following is a custom panel that I think should get you what you're after. It arranges child elements in bottom-to-top then right-to-left manner, stacking up to MaxRows elements in a column (or all elements if it is null). All slots are of the same size. It also does take into account elements' Visibility value, i.e. if an item is Hidden, it leaves an empty slot, and if it is Collapsed, it is skipped and next element "jumps" into its place.
public class NotificationWrapPanel : Panel
{
public static readonly DependencyProperty MaxRowsProperty =
DependencyProperty.Register(
name: nameof(MaxRows),
propertyType: typeof(int?),
ownerType: typeof(NotificationWrapPanel),
typeMetadata: new FrameworkPropertyMetadata
{
AffectsArrange = true,
AffectsMeasure = true,
},
validateValueCallback: o => o == null || (int)o >= 1);
public int? MaxRows
{
get { return (int?)GetValue(MaxRowsProperty); }
set { SetValue(MaxRowsProperty, value); }
}
protected override Size MeasureOverride(Size constraint)
{
var children = InternalChildren
.Cast<UIElement>()
.Where(e => e.Visibility != Visibility.Collapsed)
.ToList();
if (children.Count == 0) return new Size();
var rows = MaxRows.HasValue ?
Math.Min(MaxRows.Value, children.Count) :
children.Count;
var columns = children.Count / rows +
Math.Sign(children.Count % rows);
var childConstraint = new Size
{
Width = constraint.Width / columns,
Height = constraint.Height / rows,
};
foreach (UIElement child in children)
child.Measure(childConstraint);
return new Size
{
Height = rows * children
.Cast<UIElement>()
.Max(e => e.DesiredSize.Height),
Width = columns * children
.Cast<UIElement>()
.Max(e => e.DesiredSize.Width),
};
}
protected override Size ArrangeOverride(Size finalSize)
{
var children = InternalChildren
.Cast<UIElement>()
.Where(e => e.Visibility != Visibility.Collapsed)
.ToList();
if (children.Count == 0) return finalSize;
var rows = MaxRows.HasValue ?
Math.Min(MaxRows.Value, children.Count) :
children.Count;
var columns = children.Count / rows +
Math.Sign(children.Count % rows);
var childSize = new Size
{
Width = finalSize.Width / columns,
Height = finalSize.Height / rows
};
for (int i = 0; i < children.Count; i++)
{
var row = i % rows; //rows are numbered bottom-to-top
var col = i / rows; //columns are numbered right-to-left
var location = new Point
{
X = finalSize.Width - (col + 1) * childSize.Width,
Y = finalSize.Height - (row + 1) * childSize.Height,
};
children[i].Arrange(new Rect(location, childSize));
}
return finalSize;
}
}

WPF/Metro-style: Making ListView show only complete items

In my Metro application, I have a data source containing a certain number of items (say 25). I have a ListView that presents those items. My problem is that the ListView have a size that allows it to display, say, 6.5 items, so that the last item it displays is cut in half. If the resolution changes, it might display 4 items, or 8.2 items, or whatever. What I'd like is that the ListView shows exactly the number of items that fits in the height of the control, instead of clipping the last item.
Right now, I see two possible half-solutions, none of which is optimal:
Set the height of the ListView to a fixed height that is a multiple of the item size. This does not scale with changes in resolution.
Limit the number of items in the data source. This does not scale either.
So my question is, how can I get the ListView to only display complete items (items where all edges are inside the viewport/listview), and hide the rest?
ListView inherits from ItemsControl,
so one more optimized solution consists in injecting custom panel (overriding measure by custom clipping display) in ItemsPanel
something like this(sorry, i did not try to compile):
protected override Size MeasureOverride(Size constraint)
{
if (this.VisualChildrenCount <= 0)
return base.MeasureOverride(constraint);
var size = ne Size(constraint.Width,0);
for(int i = 0; i < this.visualChildrenCount; i++)
{
child.Measure(size);
if(size.height + child.desiredSize > constraint.height)
break;
size.Height += child.DesiredSize;
}
return size;
}
My final solution was to combine the suggestions of #NovitchiS and #JesuX.
I created a stack panel override, and listened to the LayoutUpdated event. My final solution:
class HeightLimitedStackPanel : StackPanel
{
public HeightLimitedStackPanel() : base()
{
this.LayoutUpdated += OnLayoutUpdated;
}
double GetSizeOfVisibleChildren(double parentHeight)
{
double currentSize = 0;
bool hasBreaked = false;
for (int i = 0; i < Children.Count; i++)
{
var child = Children[i];
if (currentSize + child.DesiredSize.Height > parentHeight)
{
hasBreaked = true;
break;
}
currentSize += child.DesiredSize.Height;
}
if (hasBreaked) return currentSize;
return parentHeight;
}
double ParentHeight
{
get
{
ItemsPresenter parent = VisualTreeHelper.GetParent(this) as ItemsPresenter;
if (parent == null)
return 0;
return parent.ActualHeight;
}
}
double previousHeight = 0;
int previousChildCount = 0;
protected void OnLayoutUpdated(object sender, object e)
{
double height = ParentHeight;
if (height == previousHeight && previousChildCount == Children.Count) return;
previousHeight = height;
previousChildCount = Children.Count;
this.Height = GetSizeOfVisibleChildren(height);
}
}
The answer from #JesuX is the better approach -- if done correctly. The following ListView subclass works fine for me:
public sealed class IntegralItemsListView : ListView
{
protected override Size MeasureOverride(Size availableSize)
{
Size size = base.MeasureOverride(availableSize);
double height = 0;
if (Items != null)
{
for (int i = 0; i < Items.Count; ++i)
{
UIElement itemContainer = (UIElement)ContainerFromIndex(i);
if (itemContainer == null)
{
break;
}
itemContainer.Measure(availableSize);
double childHeight = itemContainer.DesiredSize.Height;
if (height + childHeight > size.Height)
{
break;
}
height += childHeight;
}
}
size.Height = height;
return size;
}
}
One caveat -- if you plop an IntegralItemsListView into a Grid, it will have
VerticalAlignment="Stretch"
by default, which defeats the purpose of this class.
Also: If the items are of uniform height, the method can obviously be simplified:
protected override Size MeasureOverride(Size availableSize)
{
Size size = base.MeasureOverride(availableSize);
size.Height = (int)(size.Height / ItemHeight) * ItemHeight;
return size;
}

How can we set the Wrap-point for the WrapPanel?

I have an ItemsControl and I want the data to be entered into two columns. When the User resizes to a width lesser than the second column, the items of the second column must wrap into the first column. Something like a UniformGrid but with wrapping.
I have managed to use a WrapPanel to do this. But I am forced to manipulate and hard code the ItemWidth and MaxWidth of the WrapPanel to achieve the two columns thing and the wrapping. This is not a good practice.
Is there anyway to set the Maximum no of columns or in other words allowing us to decide at what point the WrapPanel should start wrapping?
Some browsing on the internet revealed that the WrapGrid in Windows 8 Metro has this property. Does anyone have this implementation in WPF?
There's an UniformWrapPanel article on codeproject.com, which I modified to ensure the items in a wrap panel have uniform width and fit to the width of the window. You should easily be able to modify this code to have a maximum number of columns. Try changing the code near
var itemsPerRow = (int) (totalWidth/ItemWidth);
in order to specify the max columns.
Here's the code:
public enum ItemSize
{
None,
Uniform,
UniformStretchToFit
}
public class UniformWrapPanel : WrapPanel
{
public static readonly DependencyProperty ItemSizeProperty =
DependencyProperty.Register(
"ItemSize",
typeof (ItemSize),
typeof (UniformWrapPanel),
new FrameworkPropertyMetadata(
default(ItemSize),
FrameworkPropertyMetadataOptions.AffectsMeasure,
ItemSizeChanged));
private static void ItemSizeChanged(
DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var uniformWrapPanel = sender as UniformWrapPanel;
if (uniformWrapPanel != null)
{
if (uniformWrapPanel.Orientation == Orientation.Horizontal)
{
uniformWrapPanel.ItemWidth = double.NaN;
}
else
{
uniformWrapPanel.ItemHeight = double.NaN;
}
}
}
public ItemSize ItemSize
{
get { return (ItemSize) GetValue(ItemSizeProperty); }
set { SetValue(ItemSizeProperty, value); }
}
protected override Size MeasureOverride(Size availableSize)
{
var mode = ItemSize;
if (Children.Count > 0 && mode != ItemSize.None)
{
bool stretchToFit = mode == ItemSize.UniformStretchToFit;
if (Orientation == Orientation.Horizontal)
{
double totalWidth = availableSize.Width;
ItemWidth = 0.0;
foreach (UIElement el in Children)
{
el.Measure(availableSize);
Size next = el.DesiredSize;
if (!(Double.IsInfinity(next.Width) || Double.IsNaN(next.Width)))
{
ItemWidth = Math.Max(next.Width, ItemWidth);
}
}
if (stretchToFit)
{
if (!double.IsNaN(ItemWidth) && !double.IsInfinity(ItemWidth) && ItemWidth > 0)
{
var itemsPerRow = (int) (totalWidth/ItemWidth);
if (itemsPerRow > 0)
{
ItemWidth = totalWidth/itemsPerRow;
}
}
}
}
else
{
double totalHeight = availableSize.Height;
ItemHeight = 0.0;
foreach (UIElement el in Children)
{
el.Measure(availableSize);
Size next = el.DesiredSize;
if (!(Double.IsInfinity(next.Height) || Double.IsNaN(next.Height)))
{
ItemHeight = Math.Max(next.Height, ItemHeight);
}
}
if (stretchToFit)
{
if (!double.IsNaN(ItemHeight) && !double.IsInfinity(ItemHeight) && ItemHeight > 0)
{
var itemsPerColumn = (int) (totalHeight/ItemHeight);
if (itemsPerColumn > 0)
{
ItemHeight = totalHeight/itemsPerColumn;
}
}
}
}
}
return base.MeasureOverride(availableSize);
}
}

Virtualizing Wrap Panel WPF - free solution? [closed]

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 5 years ago.
Improve this question
I know this is a duplication of Is there a (good/free) VirtualizingWrapPanel available for WPF?, but the answer there does not work as I need it. Example: When I click on an item in partial view, it takes me to the item below it, or when I refresh my items to let changes take effect on the UI that I do behind the scenes, it scrolls to the top. So has anyone found a good, free (or cheap) solution since last year?
Now I know there is an option at http://www.binarymission.co.uk/Products/WPF_SL/virtualizingwrappanel_wpf_sl.htm and that works exactly how I need it, I just would really prefer not to spend a grand for 1 project. Even if it was like 200-300 I would have already bought it but $900 for that one control in one project (so far), I just can't justify.
Any suggestions would be much appreciated!
Anthony F Greco
Use this one: http://virtualwrappanel.codeplex.com/ .
Here's how you use it:
xmlns:local="clr-namespace:MyWinCollection"
...
<local:VirtualizingWrapPanel ..../>
The one on your original link just needs a bit of tweaking to be able to support ScrollIntoView on the parent ListView so that when you refresh you can put the selection/scroll position back where you want it.
Add this extra override to the class mentioned in the question (found here)
protected override void BringIndexIntoView(int index)
{
var currentVisibleMin = _offset.Y;
var currentVisibleMax = _offset.Y + _viewportSize.Height - ItemHeight;
var itemsPerLine = Math.Max((int)Math.Floor(_viewportSize.Width / ItemWidth), 1);
var verticalOffsetRequiredToPutItemAtTopRow = Math.Floor((double)index / itemsPerLine) * ItemHeight;
if (verticalOffsetRequiredToPutItemAtTopRow < currentVisibleMin) // if item is above visible area put it on the top row
SetVerticalOffset(verticalOffsetRequiredToPutItemAtTopRow);
else if (verticalOffsetRequiredToPutItemAtTopRow > currentVisibleMax) // if item is below visible area move to put it on bottom row
SetVerticalOffset(verticalOffsetRequiredToPutItemAtTopRow - _viewportSize.Height + ItemHeight);
}
While you are there the following couple of functions can be changed as below to improve performance
protected override Size MeasureOverride(Size availableSize)
{
if (_itemsControl == null)
{
return availableSize;
}
_isInMeasure = true;
_childLayouts.Clear();
var extentInfo = GetExtentInfo(availableSize, ItemHeight);
EnsureScrollOffsetIsWithinConstrains(extentInfo);
var layoutInfo = GetLayoutInfo(availableSize, ItemHeight, extentInfo);
RecycleItems(layoutInfo);
// Determine where the first item is in relation to previously realized items
var generatorStartPosition = _itemsGenerator.GeneratorPositionFromIndex(layoutInfo.FirstRealizedItemIndex);
var visualIndex = 0;
var currentX = layoutInfo.FirstRealizedItemLeft;
var currentY = layoutInfo.FirstRealizedLineTop;
using (_itemsGenerator.StartAt(generatorStartPosition, GeneratorDirection.Forward, true))
{
for (var itemIndex = layoutInfo.FirstRealizedItemIndex; itemIndex <= layoutInfo.LastRealizedItemIndex; itemIndex++, visualIndex++)
{
bool newlyRealized;
var child = (UIElement)_itemsGenerator.GenerateNext(out newlyRealized);
SetVirtualItemIndex(child, itemIndex);
if (newlyRealized)
{
InsertInternalChild(visualIndex, child);
}
else
{
// check if item needs to be moved into a new position in the Children collection
if (visualIndex < Children.Count)
{
if (Children[visualIndex] != child)
{
var childCurrentIndex = Children.IndexOf(child);
if (childCurrentIndex >= 0)
{
RemoveInternalChildRange(childCurrentIndex, 1);
Debug.WriteLine("Moving child from {0} to {1}", childCurrentIndex, visualIndex);
}
else
Debug.WriteLine("Inserting child {0}", visualIndex);
InsertInternalChild(visualIndex, child);
}
}
else
{
// we know that the child can't already be in the children collection
// because we've been inserting children in correct visualIndex order,
// and this child has a visualIndex greater than the Children.Count
AddInternalChild(child);
Debug.WriteLine("Adding child at {0}", Children.Count-1);
}
}
// only prepare the item once it has been added to the visual tree
_itemsGenerator.PrepareItemContainer(child);
if (newlyRealized)
child.Measure(new Size(ItemWidth, ItemHeight));
_childLayouts.Add(child, new Rect(currentX, currentY, ItemWidth, ItemHeight));
if (currentX + ItemWidth * 2 >= availableSize.Width)
{
// wrap to a new line
currentY += ItemHeight;
currentX = 0;
}
else
{
currentX += ItemWidth;
}
}
}
RemoveRedundantChildren();
UpdateScrollInfo(availableSize, extentInfo);
var desiredSize = new Size(double.IsInfinity(availableSize.Width) ? 0 : availableSize.Width,
double.IsInfinity(availableSize.Height) ? 0 : availableSize.Height);
_isInMeasure = false;
return desiredSize;
}
class RemoveRange
{
public int Start;
public int Length;
}
private void RecycleItems(ItemLayoutInfo layoutInfo)
{
Queue<RemoveRange> ranges = null;
int idx = 0;
RemoveRange nextRange = null;
foreach (UIElement child in Children)
{
var virtualItemIndex = GetVirtualItemIndex(child);
if (virtualItemIndex < layoutInfo.FirstRealizedItemIndex || virtualItemIndex > layoutInfo.LastRealizedItemIndex)
{
var generatorPosition = _itemsGenerator.GeneratorPositionFromIndex(virtualItemIndex);
if (generatorPosition.Index >= 0)
{
_itemsGenerator.Recycle(generatorPosition, 1);
}
if (nextRange == null)
nextRange = new RemoveRange() { Start = idx, Length = 1 };
else
nextRange.Length++;
}
else if (nextRange!= null)
{
if (ranges== null)
ranges = new Queue<RemoveRange>();
ranges.Enqueue(nextRange);
nextRange = null;
}
SetVirtualItemIndex(child, -1);
idx++;
}
if (nextRange != null)
{
if (ranges == null)
ranges = new Queue<RemoveRange>();
ranges.Enqueue(nextRange);
}
int removed = 0;
if (ranges != null)
{
foreach (var range in ranges)
{
RemoveInternalChildRange(range.Start-removed, range.Length);
removed +=range.Length;
}
}
}
After trying to fix the issues of the http://virtualwrappanel.codeplex.com/ control I ended up with the solution from https://stackoverflow.com/a/13560758/5887121. I also added the BringIntoView method and a DP to set the orientation.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace Wpf.Controls
{
// class from: https://github.com/samueldjack/VirtualCollection/blob/master/VirtualCollection/VirtualCollection/VirtualizingWrapPanel.cs
// MakeVisible() method from: http://www.switchonthecode.com/tutorials/wpf-tutorial-implementing-iscrollinfo
public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo
{
private const double ScrollLineAmount = 16.0;
private Size _extentSize;
private Size _viewportSize;
private Point _offset;
private ItemsControl _itemsControl;
private readonly Dictionary<UIElement, Rect> _childLayouts = new Dictionary<UIElement, Rect>();
public static readonly DependencyProperty ItemWidthProperty =
DependencyProperty.Register("ItemWidth", typeof(double), typeof(VirtualizingWrapPanel), new PropertyMetadata(1.0, HandleItemDimensionChanged));
public static readonly DependencyProperty ItemHeightProperty =
DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualizingWrapPanel), new PropertyMetadata(1.0, HandleItemDimensionChanged));
private static readonly DependencyProperty VirtualItemIndexProperty =
DependencyProperty.RegisterAttached("VirtualItemIndex", typeof(int), typeof(VirtualizingWrapPanel), new PropertyMetadata(-1));
private IRecyclingItemContainerGenerator _itemsGenerator;
private bool _isInMeasure;
private static int GetVirtualItemIndex(DependencyObject obj)
{
return (int)obj.GetValue(VirtualItemIndexProperty);
}
private static void SetVirtualItemIndex(DependencyObject obj, int value)
{
obj.SetValue(VirtualItemIndexProperty, value);
}
#region Orientation
/// <summary>
/// Gets and sets the orientation of the panel.
/// </summary>
/// <value>The orientation of the panel.</value>
public Orientation Orientation
{
get
{
return (Orientation)GetValue(OrientationProperty);
}
set
{
SetValue(OrientationProperty, value);
}
}
/// <summary>
/// Identifies the Orientation dependency property.
/// </summary>
/// <remarks>
/// Returns: The identifier for the Orientation dependency property.
/// </remarks>
public static readonly DependencyProperty OrientationProperty = StackPanel.OrientationProperty.AddOwner(typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(Orientation.Horizontal));
#endregion Orientation
public double ItemHeight
{
get
{
return (double)GetValue(ItemHeightProperty);
}
set
{
SetValue(ItemHeightProperty, value);
}
}
public double ItemWidth
{
get
{
return (double)GetValue(ItemWidthProperty);
}
set
{
SetValue(ItemWidthProperty, value);
}
}
public VirtualizingWrapPanel()
{
if ( !DesignerProperties.GetIsInDesignMode(this) )
{
Dispatcher.BeginInvoke((Action)Initialize);
}
}
private void Initialize()
{
_itemsControl = ItemsControl.GetItemsOwner(this);
_itemsGenerator = (IRecyclingItemContainerGenerator)ItemContainerGenerator;
InvalidateMeasure();
}
protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
{
base.OnItemsChanged(sender, args);
InvalidateMeasure();
}
protected override Size MeasureOverride(Size availableSize)
{
if ( _itemsControl == null )
{
return availableSize;
}
_isInMeasure = true;
_childLayouts.Clear();
var extentInfo = GetExtentInfo(availableSize);
EnsureScrollOffsetIsWithinConstrains(extentInfo);
var layoutInfo = GetLayoutInfo(availableSize, (Orientation == Orientation.Vertical) ? ItemHeight : ItemWidth, extentInfo);
RecycleItems(layoutInfo);
// Determine where the first item is in relation to previously realized items
var generatorStartPosition = _itemsGenerator.GeneratorPositionFromIndex(layoutInfo.FirstRealizedItemIndex);
var visualIndex = 0;
Double currentX = layoutInfo.FirstRealizedItemLeft;
Double currentY = layoutInfo.FirstRealizedLineTop;
using ( _itemsGenerator.StartAt(generatorStartPosition, GeneratorDirection.Forward, true) )
{
for ( var itemIndex = layoutInfo.FirstRealizedItemIndex ; itemIndex <= layoutInfo.LastRealizedItemIndex ; itemIndex++, visualIndex++ )
{
bool newlyRealized;
var child = (UIElement)_itemsGenerator.GenerateNext(out newlyRealized);
SetVirtualItemIndex(child, itemIndex);
if ( newlyRealized )
{
InsertInternalChild(visualIndex, child);
}
else
{
// check if item needs to be moved into a new position in the Children collection
if ( visualIndex < Children.Count )
{
if ( Children[visualIndex] != child )
{
var childCurrentIndex = Children.IndexOf(child);
if ( childCurrentIndex >= 0 )
{
RemoveInternalChildRange(childCurrentIndex, 1);
}
InsertInternalChild(visualIndex, child);
}
}
else
{
// we know that the child can't already be in the children collection
// because we've been inserting children in correct visualIndex order,
// and this child has a visualIndex greater than the Children.Count
AddInternalChild(child);
}
}
// only prepare the item once it has been added to the visual tree
_itemsGenerator.PrepareItemContainer(child);
child.Measure(new Size(ItemWidth, ItemHeight));
_childLayouts.Add(child, new Rect(currentX, currentY, ItemWidth, ItemHeight));
if ( Orientation == Orientation.Vertical )
{
if ( currentX + ItemWidth * 2 > availableSize.Width )
{
// wrap to a new line
currentY += ItemHeight;
currentX = 0;
}
else
{
currentX += ItemWidth;
}
}
else
{
if ( currentY + ItemHeight * 2 > availableSize.Height )
{
// wrap to a new column
currentX += ItemWidth;
currentY = 0;
}
else
{
currentY += ItemHeight;
}
}
}
}
RemoveRedundantChildren();
UpdateScrollInfo(availableSize, extentInfo);
var desiredSize = new Size(double.IsInfinity(availableSize.Width) ? 0 : availableSize.Width,
double.IsInfinity(availableSize.Height) ? 0 : availableSize.Height);
_isInMeasure = false;
return desiredSize;
}
private void EnsureScrollOffsetIsWithinConstrains(ExtentInfo extentInfo)
{
if ( Orientation == Orientation.Vertical )
{
_offset.Y = Clamp(_offset.Y, 0, extentInfo.MaxVerticalOffset);
}
else
{
_offset.X = Clamp(_offset.X, 0, extentInfo.MaxHorizontalOffset);
}
}
private void RecycleItems(ItemLayoutInfo layoutInfo)
{
foreach ( UIElement child in Children )
{
var virtualItemIndex = GetVirtualItemIndex(child);
if ( virtualItemIndex < layoutInfo.FirstRealizedItemIndex || virtualItemIndex > layoutInfo.LastRealizedItemIndex )
{
var generatorPosition = _itemsGenerator.GeneratorPositionFromIndex(virtualItemIndex);
if ( generatorPosition.Index >= 0 )
{
_itemsGenerator.Recycle(generatorPosition, 1);
}
}
SetVirtualItemIndex(child, -1);
}
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach ( UIElement child in Children )
{
child.Arrange(_childLayouts[child]);
}
return finalSize;
}
private void UpdateScrollInfo(Size availableSize, ExtentInfo extentInfo)
{
_viewportSize = availableSize;
if ( Orientation == Orientation.Vertical )
{
_extentSize = new Size(availableSize.Width, extentInfo.ExtentHeight);
}
else
{
_extentSize = new Size(extentInfo.ExtentWidth, availableSize.Height);
}
InvalidateScrollInfo();
}
private void RemoveRedundantChildren()
{
// iterate backwards through the child collection because we're going to be
// removing items from it
for ( var i = Children.Count - 1 ; i >= 0 ; i-- )
{
var child = Children[i];
// if the virtual item index is -1, this indicates
// it is a recycled item that hasn't been reused this time round
if ( GetVirtualItemIndex(child) == -1 )
{
RemoveInternalChildRange(i, 1);
}
}
}
private ItemLayoutInfo GetLayoutInfo(Size availableSize, double itemHeightOrWidth, ExtentInfo extentInfo)
{
if ( _itemsControl == null )
{
return new ItemLayoutInfo();
}
// we need to ensure that there is one realized item prior to the first visible item, and one after the last visible item,
// so that keyboard navigation works properly. For example, when focus is on the first visible item, and the user
// navigates up, the ListBox selects the previous item, and the scrolls that into view - and this triggers the loading of the rest of the items
// in that row
if ( Orientation == Orientation.Vertical )
{
var firstVisibleLine = (int)Math.Floor(VerticalOffset / itemHeightOrWidth);
var firstRealizedIndex = Math.Max(extentInfo.ItemsPerLine * firstVisibleLine - 1, 0);
var firstRealizedItemLeft = firstRealizedIndex % extentInfo.ItemsPerLine * ItemWidth - HorizontalOffset;
var firstRealizedItemTop = (firstRealizedIndex / extentInfo.ItemsPerLine) * itemHeightOrWidth - VerticalOffset;
var firstCompleteLineTop = (firstVisibleLine == 0 ? firstRealizedItemTop : firstRealizedItemTop + ItemHeight);
var completeRealizedLines = (int)Math.Ceiling((availableSize.Height - firstCompleteLineTop) / itemHeightOrWidth);
var lastRealizedIndex = Math.Min(firstRealizedIndex + completeRealizedLines * extentInfo.ItemsPerLine + 2, _itemsControl.Items.Count - 1);
return new ItemLayoutInfo
{
FirstRealizedItemIndex = firstRealizedIndex,
FirstRealizedItemLeft = firstRealizedItemLeft,
FirstRealizedLineTop = firstRealizedItemTop,
LastRealizedItemIndex = lastRealizedIndex,
};
}
else
{
var firstVisibleColumn = (int)Math.Floor(HorizontalOffset / itemHeightOrWidth);
var firstRealizedIndex = Math.Max(extentInfo.ItemsPerColumn * firstVisibleColumn - 1, 0);
var firstRealizedItemTop = firstRealizedIndex % extentInfo.ItemsPerColumn * ItemHeight - VerticalOffset;
var firstRealizedItemLeft = (firstRealizedIndex / extentInfo.ItemsPerColumn) * itemHeightOrWidth - HorizontalOffset;
var firstCompleteColumnLeft = (firstVisibleColumn == 0 ? firstRealizedItemLeft : firstRealizedItemLeft + ItemWidth);
var completeRealizedColumns = (int)Math.Ceiling((availableSize.Width - firstCompleteColumnLeft) / itemHeightOrWidth);
var lastRealizedIndex = Math.Min(firstRealizedIndex + completeRealizedColumns * extentInfo.ItemsPerColumn + 2, _itemsControl.Items.Count - 1);
return new ItemLayoutInfo
{
FirstRealizedItemIndex = firstRealizedIndex,
FirstRealizedItemLeft = firstRealizedItemLeft,
FirstRealizedLineTop = firstRealizedItemTop,
LastRealizedItemIndex = lastRealizedIndex,
};
}
}
private ExtentInfo GetExtentInfo(Size viewPortSize)
{
if ( _itemsControl == null )
{
return new ExtentInfo();
}
if ( Orientation == Orientation.Vertical )
{
var itemsPerLine = Math.Max((int)Math.Floor(viewPortSize.Width / ItemWidth), 1);
var totalLines = (int)Math.Ceiling((double)_itemsControl.Items.Count / itemsPerLine);
var extentHeight = Math.Max(totalLines * ItemHeight, viewPortSize.Height);
return new ExtentInfo
{
ItemsPerLine = itemsPerLine,
TotalLines = totalLines,
ExtentHeight = extentHeight,
MaxVerticalOffset = extentHeight - viewPortSize.Height,
};
}
else
{
var itemsPerColumn = Math.Max((int)Math.Floor(viewPortSize.Height / ItemHeight), 1);
var totalColumns = (int)Math.Ceiling((double)_itemsControl.Items.Count / itemsPerColumn);
var extentWidth = Math.Max(totalColumns * ItemWidth, viewPortSize.Width);
return new ExtentInfo
{
ItemsPerColumn = itemsPerColumn,
TotalColumns = totalColumns,
ExtentWidth = extentWidth,
MaxHorizontalOffset = extentWidth - viewPortSize.Width
};
}
}
public void LineUp()
{
SetVerticalOffset(VerticalOffset - ScrollLineAmount);
}
public void LineDown()
{
SetVerticalOffset(VerticalOffset + ScrollLineAmount);
}
public void LineLeft()
{
SetHorizontalOffset(HorizontalOffset - ScrollLineAmount);
}
public void LineRight()
{
SetHorizontalOffset(HorizontalOffset + ScrollLineAmount);
}
public void PageUp()
{
SetVerticalOffset(VerticalOffset - ViewportHeight);
}
public void PageDown()
{
SetVerticalOffset(VerticalOffset + ViewportHeight);
}
public void PageLeft()
{
SetHorizontalOffset(HorizontalOffset - ItemWidth);
}
public void PageRight()
{
SetHorizontalOffset(HorizontalOffset + ItemWidth);
}
public void MouseWheelUp()
{
if ( Orientation == Orientation.Vertical )
{
SetVerticalOffset(VerticalOffset - ScrollLineAmount * SystemParameters.WheelScrollLines);
}
else
{
MouseWheelLeft();
}
}
public void MouseWheelDown()
{
if ( Orientation == Orientation.Vertical )
{
SetVerticalOffset(VerticalOffset + ScrollLineAmount * SystemParameters.WheelScrollLines);
}
else
{
MouseWheelRight();
}
}
public void MouseWheelLeft()
{
SetHorizontalOffset(HorizontalOffset - ScrollLineAmount * SystemParameters.WheelScrollLines);
}
public void MouseWheelRight()
{
SetHorizontalOffset(HorizontalOffset + ScrollLineAmount * SystemParameters.WheelScrollLines);
}
public void SetHorizontalOffset(double offset)
{
if ( _isInMeasure )
{
return;
}
offset = Clamp(offset, 0, ExtentWidth - ViewportWidth);
_offset = new Point(offset, _offset.Y);
InvalidateScrollInfo();
InvalidateMeasure();
}
public void SetVerticalOffset(double offset)
{
if ( _isInMeasure )
{
return;
}
offset = Clamp(offset, 0, ExtentHeight - ViewportHeight);
_offset = new Point(_offset.X, offset);
InvalidateScrollInfo();
InvalidateMeasure();
}
public Rect MakeVisible(Visual visual, Rect rectangle)
{
if ( rectangle.IsEmpty ||
visual == null ||
visual == this ||
!IsAncestorOf(visual) )
{
return Rect.Empty;
}
rectangle = visual.TransformToAncestor(this).TransformBounds(rectangle);
var viewRect = new Rect(HorizontalOffset, VerticalOffset, ViewportWidth, ViewportHeight);
rectangle.X += viewRect.X;
rectangle.Y += viewRect.Y;
viewRect.X = CalculateNewScrollOffset(viewRect.Left, viewRect.Right, rectangle.Left, rectangle.Right);
viewRect.Y = CalculateNewScrollOffset(viewRect.Top, viewRect.Bottom, rectangle.Top, rectangle.Bottom);
SetHorizontalOffset(viewRect.X);
SetVerticalOffset(viewRect.Y);
rectangle.Intersect(viewRect);
rectangle.X -= viewRect.X;
rectangle.Y -= viewRect.Y;
return rectangle;
}
private static double CalculateNewScrollOffset(double topView, double bottomView, double topChild, double bottomChild)
{
var offBottom = topChild < topView && bottomChild < bottomView;
var offTop = bottomChild > bottomView && topChild > topView;
var tooLarge = (bottomChild - topChild) > (bottomView - topView);
if ( !offBottom && !offTop )
return topView;
if ( (offBottom && !tooLarge) || (offTop && tooLarge) )
return topChild;
return bottomChild - (bottomView - topView);
}
public ItemLayoutInfo GetVisibleItemsRange()
{
return GetLayoutInfo(_viewportSize, (Orientation == Orientation.Vertical) ? ItemHeight : ItemWidth, GetExtentInfo(_viewportSize));
}
protected override void BringIndexIntoView(int index)
{
if ( Orientation == Orientation.Vertical )
{
var currentVisibleMin = _offset.Y;
var currentVisibleMax = _offset.Y + _viewportSize.Height - ItemHeight;
var itemsPerLine = Math.Max((int)Math.Floor(_viewportSize.Width / ItemWidth), 1);
var verticalOffsetRequiredToPutItemAtTopRow = Math.Floor((double)index / itemsPerLine) * ItemHeight;
if ( verticalOffsetRequiredToPutItemAtTopRow < currentVisibleMin ) // if item is above visible area put it on the top row
SetVerticalOffset(verticalOffsetRequiredToPutItemAtTopRow);
else if ( verticalOffsetRequiredToPutItemAtTopRow > currentVisibleMax ) // if item is below visible area move to put it on bottom row
SetVerticalOffset(verticalOffsetRequiredToPutItemAtTopRow - _viewportSize.Height + ItemHeight);
}
else
{
var currentVisibleMin = _offset.X;
var currentVisibleMax = _offset.X + _viewportSize.Width - ItemWidth;
var itemsPerColumn = Math.Max((int)Math.Floor(_viewportSize.Height / ItemHeight), 1);
var horizontalOffsetRequiredToPutItemAtLeftRow = Math.Floor((double)index / itemsPerColumn) * ItemWidth;
if ( horizontalOffsetRequiredToPutItemAtLeftRow < currentVisibleMin ) // if item is left from the visible area put it at the left most column
SetHorizontalOffset(horizontalOffsetRequiredToPutItemAtLeftRow);
else if ( horizontalOffsetRequiredToPutItemAtLeftRow > currentVisibleMax ) // if item is right from the visible area put it at the right most column
SetHorizontalOffset(horizontalOffsetRequiredToPutItemAtLeftRow - _viewportSize.Width + ItemWidth);
}
}
public bool CanVerticallyScroll
{
get;
set;
}
public bool CanHorizontallyScroll
{
get;
set;
}
public double ExtentWidth
{
get
{
return _extentSize.Width;
}
}
public double ExtentHeight
{
get
{
return _extentSize.Height;
}
}
public double ViewportWidth
{
get
{
return _viewportSize.Width;
}
}
public double ViewportHeight
{
get
{
return _viewportSize.Height;
}
}
public double HorizontalOffset
{
get
{
return _offset.X;
}
}
public double VerticalOffset
{
get
{
return _offset.Y;
}
}
public ScrollViewer ScrollOwner
{
get;
set;
}
private void InvalidateScrollInfo()
{
if ( ScrollOwner != null )
{
ScrollOwner.InvalidateScrollInfo();
}
}
private static void HandleItemDimensionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var wrapPanel = (d as VirtualizingWrapPanel);
if ( wrapPanel != null )
wrapPanel.InvalidateMeasure();
}
private double Clamp(double value, double min, double max)
{
return Math.Min(Math.Max(value, min), max);
}
internal class ExtentInfo
{
public int ItemsPerLine;
public Int32 ItemsPerColumn;
public int TotalLines;
public Int32 TotalColumns;
public double ExtentHeight;
public double ExtentWidth;
public double MaxVerticalOffset;
public double MaxHorizontalOffset;
}
public class ItemLayoutInfo
{
public int FirstRealizedItemIndex;
public double FirstRealizedLineTop;
public double FirstRealizedItemLeft;
public int LastRealizedItemIndex;
}
}
}
I was searching a week for some solution and than I had idea how to fake VirtualizingWrapPanel.
If you know width of your Items, than you can create "rows" as StackPanel and these rows render in VirtualizingStackPanel.
XAML
<ItemsControl x:Name="Thumbs" VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingPanel.ScrollUnit="Pixel" ScrollViewer.CanContentScroll="True">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="{Binding ThumbWidth}" Height="{Binding ThumbHeight}"
BorderThickness="2" BorderBrush="Black" ClipToBounds="True">
<Image Source="{Binding FilePathCacheUri}" Stretch="Fill" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
C#
public List<BaseMediaItem> MediaItems { get; set; }
public List<List<BaseMediaItem>> SplitedMediaItems { get; set; }
public MainWindow() {
InitializeComponent();
DataContext = this;
MediaItems = new List<BaseMediaItem>();
SplitedMediaItems = new List<List<BaseMediaItem>>();
}
private void MainWindow_OnLoaded(object sender, RoutedEventArgs e) {
LoadMediaItems();
SplitMediaItems();
Thumbs.ItemsSource = SplitedMediaItems;
}
public void LoadMediaItems() {
var sizes = new List<Point> {new Point(320, 180), new Point(320, 240), new Point(180, 320), new Point(240, 320)};
MediaItems.Clear();
var random = new Random();
for (var i = 0; i < 5000; i++) {
var size = sizes[random.Next(sizes.Count)];
MediaItems.Add(new BaseMediaItem($"Item {i}") {
ThumbWidth = (int)size.X,
ThumbHeight = (int)size.Y
});
}
}
public void SplitMediaItems() {
foreach (var itemsGroup in SplitedMediaItems) {
itemsGroup.Clear();
}
SplitedMediaItems.Clear();
var groupMaxWidth = Thumbs.ActualWidth;
var groupWidth = 0;
const int itemOffset = 6; //border, margin, padding, ...
var row = new List<BaseMediaItem>();
foreach (var item in MediaItems) {
if (item.ThumbWidth + itemOffset <= groupMaxWidth - groupWidth) {
row.Add(item);
groupWidth += item.ThumbWidth + itemOffset;
}
else {
SplitedMediaItems.Add(row);
row = new List<BaseMediaItem>();
row.Add(item);
groupWidth = item.ThumbWidth + itemOffset;
}
}
SplitedMediaItems.Add(row);
}

Resources