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;
}
}
Related
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;
}
I tried out snapToGrid which I thought might be just the simple solution one wants for lists with side-swipeable entries.
But if I disable tensileDragEnabled then there is no animation when the row snaps to the grid.
Is there a way to disable the tensile scrolling on the X-axis whilst having snapToGrid still animated?
Here's the code:
public class FormScrollingXY extends Form {
public FormScrollingXY() {
setTitle("FormScrollingXY");
setScrollable(false);
setScrollableY(true);
setLayout(new BoxLayout(BoxLayout.Y_AXIS));
for (int rowIndex = 0; rowIndex < 50; rowIndex++) {
Container containerRow = new Container(new BoxLayout(BoxLayout.X_AXIS));
containerRow.setSnapToGrid(true);
containerRow.setScrollableX(true);
containerRow.setTensileDragEnabled(false); // bad behaviour
for (int columnIndex = 0; columnIndex < 3; columnIndex++) {
Container containerColumn = new Container(new FlowLayout()) {
#Override
protected Dimension calcPreferredSize() {
Dimension dimension = new Dimension(super.calcPreferredSize());
dimension.setWidth(containerRow.getWidth());
return dimension;
}
};
containerColumn.add(new Label((rowIndex + 1) + "/" + (columnIndex + 1)));
containerRow.add(containerColumn);
}
add(containerRow);
}
}
}
This is a guess but try doing this to see if it works around this:
Container containerRow = new Container(new BoxLayout(BoxLayout.X_AXIS)) {
public boolean isScrollableY() {
return false;
}
};
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;
}
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);
}
}
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
}