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

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;
}

Related

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;
}
}

Alternative of VirtualizingStackPanel.CacheSize in .Net 4.0

What are the factors that virtualizingstackpanel consider when it decides visualizing items around the visible item?
Ex: Having a listview, when viewing item 7, item 6& 8 will be visualized too although they are not seen.
How can limit visualized items, so that ONLY visible item is visualized?
I have solved the problem by overriding MeasureOverride function, we already purchased the book Sheridan has mentioned in his answer, depending on virtualization chapter, here is what I did to my class that extends VirtualizingStackPanel : and it worked!
private ItemsControl ItemsOwner { get; set; }
private int StartIndex { get; set; }
private int EndIndex { get; set; }
protected override void OnInitialized(EventArgs e)
{
ItemsOwner = ItemsControl.GetItemsOwner(this) as ItemsControl;
}
protected override Size MeasureOverride(Size availableSize)
{
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
// we can set StartIndex& EndIndex to mimic
// VirtualizingStackPanel.CacheSize in .Net 4.5
// for my problem, I just fixed them at the index of item to be shown
StartIndex = PagesView.Instance.SelectedIndex;
EndIndex = StartIndex;
// Virtualize items
IItemContainerGenerator generator = ItemsOwner.ItemContainerGenerator;
GeneratorPosition startPos = generator.GeneratorPositionFromIndex(StartIndex);
int childIndex = startPos.Offset == 0 ? startPos.Index : startPos.Index + 1;
using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
{
for (int i = StartIndex; i <= EndIndex; i++, childIndex++)
{
bool isNewlyRealized;
UIElement child = generator.GenerateNext(out isNewlyRealized) as UIElement;
if (isNewlyRealized)
{
if (childIndex >= InternalChildren.Count)
{
AddInternalChild(child);
}
else
{
InsertInternalChild(childIndex, child);
}
generator.PrepareItemContainer(child);
}
}
}
//DumpGeneratorContent();
// Measure
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
}
// Clean up
CleanupItems();
return availableSize;
}
private void CleanupItems()
{
IItemContainerGenerator generator = ItemsOwner.ItemContainerGenerator;
for (int i = InternalChildren.Count - 1; i >= 0; i--)
{
GeneratorPosition position = new GeneratorPosition(i, 0);
int itemIndex = generator.IndexFromGeneratorPosition(position);
if (itemIndex < StartIndex || itemIndex > EndIndex)
{
generator.Remove(position, 1);
RemoveInternalChildRange(i, 1);
}
}
}
Virtualization is quite a complicated subject, but I have a book that describes it well. Furthermore, this book also shows how to implement custom virtualization, which you may need to implement to achieve your goal. Luckily for you, I found that someone has posted a PDF of the book online and you can find it by clicking on this link.
The virtualization section starts on page 129, but it is well worth reading the rest of it too as it has some very interesting stuff in it.

Resizing a form introduces visual artifacts on bottom docked controls

I am working on a "bottom-right docked" popup form.
The layout of the form follows this structure:
Header
Content (text message)
Footer
This form should resize according to its content.
I am using SetBounds to make it grow to the top instead of the bottom (remember that the window is docked bottom-right).
However, when the animation occurs, the footer redraws itself in a very bad way, as its form-relative location is continuously updated.
I provide a sample to give you an idea:
using System.Windows.Forms;
namespace AnimatorTest
{
public class Form3 : Form
{
Timer timer = new Timer();
public Form3()
{
timer.Interval = 30;
timer.Tick += timer_Tick;
// Create 3 test buttons
for (int i = 0; i < 3; i++)
{
Button b = new Button() { Dock = DockStyle.Bottom };
b.Click += (s, e) => timer.Start();
b.Text = "Click and watch how ugly I am during the animation.";
Controls.Add(b);
}
Height = 100;
StartPosition = FormStartPosition.CenterScreen;
}
void timer_Tick(object sender, System.EventArgs e)
{
int desiredHeight = 500;
int difference = desiredHeight - Height;
int change = difference / 6;
if (System.Math.Abs(change) < 1)
{
SetBounds(Left, Top - difference, Width, Height + difference);
timer.Stop();
}
else
{
SetBounds(Left, Top - change, Width, Height + change);
}
}
}
}
I don't really have any idea to work around this.
Thanks.

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);
}
}

Add WPF control at runtime

I've written a WPF UserControl, and want to add one or more of it to my Window at runtime when I click a button. How can I do that?
Edit: Further specification
I want to add the usercontrols to a Canvas, and put in a absolute position. The canvas is a drawing of the floors in my house, and each usercontrol has properties to indicate where in the house it is positioned. So I want all the controls to be positioned in the correct position on the canvas.
I'm thinking something like this
var light = new LightUserControl(2);
HouseCanvas.Children.Add(light); // this should be positioned in a specific place
After you add the your control to the Canvas you need to specify the top and left co-ordinates using the Canvas.Top and Canvas.Left attached properties as follows.
var light = new LightUserControl(2);
HouseCanvas.Children.Add(light);
Canvas.SetLeft(light, 20);
Canvas.SetTop(light, 20);
In case you want to add the control to a Grid instead of a Canvas you can specify all the Grid properties through the Grid static class as follows:
Label newLabel = new Label();
newLabel.Content = "The New Element";
Main.Children.Add(newLabel);
Grid.SetColumn(newLabel, 0);
Grid.SetRow(newLabel, 0);
Add a StackPanel to the window and on each button click,
_stackPanel.Children.Add(new YourControl());
You can do this in many ways.
My solution:
for (i = 1; i <= buttoncount; i++)
{
Button mybutton = new Button();
Grid1.Children.Add(mybutton);
mybutton.Height = 100;
mybutton.Width = 100;
mybutton.Name = "button" + i;
mybutton.Content = mybutton.Name;
}
public static void AddChild(this Visual parent, UIElement child)
{
if (InternalAddChild(parent, child))
{
return;
}
throw new NotSupportedException();
}
private static bool InternalAddChild(Visual parent, UIElement child)
{
Panel panel = parent as Panel;
if (panel != null)
{
panel.Children.Add(child);
return true;
}
for (int i = VisualTreeHelper.GetChildrenCount(parent) - 1; i != -1; i--)
{
Visual target = VisualTreeHelper.GetChild(parent, i) as Visual;
if (target != null && InternalAddChild(target, child))
{
return true;
}
}
return false;
}
Just complementing the answer:
for (i = 1; i <= buttoncount; i++)
{
Button mybutton = new Button();
Grid1.Children.Add(mybutton);
mybutton.Height = 100;
mybutton.Width = 100;
mybutton.Name = "button" + i;
mybutton.Content = mybutton.Name;
mybutton.Click += button_Click;
}
private void button_Click(object sender, RoutedEventArgs e)
{
// do something
}

Resources