Generic, bindable, uniform-stretch layout - wpf

Excuse the length, trying to ensure all info's contained!
I need a view of cells (a view with corresponding VM) almost in a circle like so:
********
**********
************
**************
**************
**************
**************
************
**********
********
A few of the complications:
Before we fire up I don't know how many cells to display. All cells must be equal in size. The entire view must be scalable.
In my simplified world, my window creates 20 RowViewModel objects and passes each constructor the number of columns it should create.
My main view:
<Viewbox Stretch="Uniform">
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type Window}},Path=Rows}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="{Binding Path=Rows.Count}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Viewbox>
And my RowView:
<Grid>
<ItemsControl ItemsSource="{Binding Columns}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding Columns.Count}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
My cell view:
<Border >
<Viewbox Stretch="Uniform" >
<Label FontSize="10" Content="{Binding Notice}" Foreground="White" />
</Viewbox>
</Border>
As it stands, all rows are the same width so rows with fewer cells have wider cells. If I tell each row there are 20 cells, each cell is the same size but everything aligns left despite HorizontalAlignment settings, presumably because it appends blank cells. I'm assuming I could insert blank cells where I want them but this feels like a fudge of the data to make the display right, which I'm sure you'll agree is B-A-D.
I've tried squillions of approaches and think this is the closest so far but I'm missing it. Can you help please?
Thanks for your patience.

Well, maybe it's impossible? I ended up adding a property to the ViewModel that builds the grid and assigns the appropriate row/column during the generation, then databinding a contentcontrol to that grid.
It remains unsolved - how to do it in XAML?
The view:
<Viewbox Stretch="Uniform">
<ContentControl Content="{Binding PrettyGrid}" />
</Viewbox>
In the ViewModel:
public Grid PrettyGrid
{
get
{
return BuildGrid(data);
}
}
and the BuildGrid snippets:
private static Grid BuildGrid(List<objects> cellData)
{
var localGrid = new Grid();
// ...
localGrid.RowDefintions.Add(...);
localGrid.ColumnDefinitions.Add(...);
cellData.ForEach(cell =>
{
ContentControl ctl = new ContentControl();
ctl.Content = cell;
Grid.SetRow(ctl, cell.Row);
Grid.SetColumn(ctl, cell.Column);
localGrid.Children.Add(ctl);
});
return localGrid;
}

Related

How to setup a vertical WPF WrapPanel to use as much columns as possible

I have an ItemsControl with WrapPanel as ItemsPanel. The ItemsControl is inside a ScrollViewer.
Depending on how wide the window is, the ItemsControl/WrapPanel shall have more columns to make more use of the screen (and that the user has to scroll less).
If I set the WrapPanel to Orientation="Horizontal" it works as expected. But if I set the Orientation to "Vertical" the ItemsControl/WrapPanel only show one column.
The difference is, that I like the columns to be like newspaper columns, so like:
A E I
B F J
C G
D H
But if the WrapPanel is set to Horizontal the columns are like:
A B C
D E F
G H I
J
How can I setup the WrapPanel to behave like that?
I could of course limit the height. Of course I could realize this with some clever logic, that takes the number of items from the source into account as well as the width of the window. But this would be complicated and probably a mess, that no one else would ever understand (or even myself, If I review this in a couple of years).
I hope that something like this already exists in WPF.
Here is the Pseudo-XAML-Code
<ScrollViewer>
<StackPanel>
<!-- Some other stuff -->
<ItemsControl ItemsSource="{Binding … }">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Width="300"><!-- … --></Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Some more other stuff -->
</StackPanel>
</ScrollViewer>
It works with Orientation="Horizontal" (but then cells are in wrong order), but only shows one column, when Orientation="Vertical".
I believe Orientation="Vertical" is the option you are looking for. It will attempt to fill a column before wrapping to a new column.
A __D __G
| | | | |
B | E | .
| | | | .
C_| F_| .->
The trouble you are having is that the WrapPanel is being allowed an infinite amount of vertical layout space by the StackPanel and, as a result, does not have to wrap.
To fix this issue, change your StackPanel to a WrapPanel or another control that does not allow infinite layout space.
Example
<WrapPanel>
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<Rectangle Height="50" Width="50" Fill="#F000"/>
<Rectangle Height="50" Width="50" Fill="#F111"/>
<Rectangle Height="50" Width="50" Fill="#F222"/>
<!- ... ->
<Rectangle Height="50" Width="50" Fill="#F111"/>
<Rectangle Height="50" Width="50" Fill="#F000"/>
</ItemsControl>
</WrapPanel>
Result
Ok, I came up with a solution, that is pure XAML.
So basically I have a Grid. With two rows. Inside the grid are two almost identical ItemsControls with bindings and WrapPanels.
The first has a WrapPanel with Orientation="Horizontal". It is only there to calculate the boundaries for the second ItemsControl. Therefor its Visibility="Hidden". Also it only spans over the first row.
The second ItemsControl has the WrapPanel with Orientation="Vertical". Its Height is bound to the ActualHeight of the first ItemsControl. Therefor it is forced to use multiple Columns, because the height can't get higher then of the height of ItemsControl #1.
The second ItemsControl spans over both rows (this is important, otherwise the height can get stuck, wenn resizing the window*).
It's not a very nice solution. More like Brute-force. But of course it works. The first ItemsControl calculates the outer size. I force this size to the second one, which than can distribute the items in a nice pattern.
<ScrollViewer>
<StackPanel>
<!-- Some other stuff -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<!-- first ItemsControl, to calculate the outer boundaries with -->
<ItemsControl ItemsSource="{Binding … }"
Name="Calculating_Size"
Visibility="Hidden">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Width="300"><!-- … --></Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Second ItemsControl with fixed size for Vertical WrapPanel -->
<ItemsControl ItemsSource="{Binding … }"
Height="{Binding ActualHeight, ElementName=Calculating_Size}"
Grid.RowSpan="2">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Width="300"><!-- … --></Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Some more other stuff -->
</StackPanel>
</ScrollViewer>
* The reason for the second row is, that if the first WrapPanel reorganizing, because the width increases, it still fixed with the height because the row cannot get smaller, that the second ItemsControl. But, if the second ItemsControl spans over two rows, the first row can get smaller independent of the second ItemsControl. The second gets smaller immediteately afterwards. But this seems to be an extra step.

How to make the items in a WPF ListBox wrap horizontally and vertically

I want to show a list of thumbnails, and allow the user to choose one. A ListBox seemed an obvious choice, setting the template to include the thumbnail and description.
The following code does this correctly...
<ListBox Margin="5"
Name="BackgroundsLb">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Margin="5"
BorderBrush="Black"
BorderThickness="1">
<StackPanel Margin="5">
<Image Source="{Binding Path=Data, Converter={StaticResource BytesToImageVC}}"
Width="150"
HorizontalAlignment="Center" />
<TextBlock Text="{Binding Path=Description}"
HorizontalAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
However, this displays the thumbnails vertically, one per row, as is normal for a ListBox. Given that the thumbnails are only 150 pixels wide, I would like to show them in something more like a grid, but (ideally) in a way so that the number of columns adapts to the size of the window.
I tried replacing the ListBox with an ItemsControl, adding in the following...
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
...and it displayed exactly how I wanted it, but the ItemsControl does not allow selection, so is no use for my purpose.
Is there a way to achieve a flexible, selectable display that fills the horizontal space, and breaks onto a new row according to the size of the window?
Thanks
You can use an ItemsPanelTemplate in a ListBox just the same as you are using one in the ItemsControl. The difference I think you're seeing is that ListBox uses scroll bars by default rather than wrapping the content. Basically the content is allowed to grow forever, so you never get the wrap. Instead you get a scrollbar. The good news is you can disable this behavior. The following should give you a horizontal wrap, where new rows are created as needed.
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>

Looking for a WPF multicolumn list with scaling

I would like to create a multi-column list of checkboxes, but here's the catch - as I resize the window I would like everything to scale accordingly, including text size. I've been trying to make use of a WrapPanel and ViewBox but can't get the XAML right. Are these controls the best option or should I be using a ListBox (note I don't need selection functionality or scrollbars)? Any suggestions or examples on how I could achieve this would be much appreciated. I'm using MVVM and the list will be data bound, if that makes a difference.
BTW since starting WPF I've been struggling to understand which controls size to their children and which size to their parent. Are there any good sites, cheat sheets, or whatever summarising the behaviour of each control?
If you have a variable number of child elements, you could put a UniformGrid into a ViewBox.
If the child elements have to be visualized by a DataTemplate, you would have to use an ItemsControl with the ItemsPanel property set to such a UniformGrid:
<Viewbox Stretch="Uniform">
<ItemsControl ItemsSource="{Binding Items}" Width="400" Height="200">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="AliceBlue">
<CheckBox Content="{Binding Label}" IsChecked="{Binding IsChecked}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Viewbox>

Proper usage of Viewbox for WPF kiosk touch screen applications

Is there some kind of "best practices" manual for creating proper GUI for kiosk touch screens? These applications need to have consistent look and feel across different screen resolutions and more importantly screen ratios (since everything is rendered as vectors so screen resolution and DPI shouldn't be an issue with WPF).
Take for example this screenshot where I tried to create simple keyboard for touch screens. I've used UniformGrid so that each button gets cell of equal size:
Here is the code for this:
<TabItem Header="Test 1">
<ItemsControl ItemsSource="{Binding KeyboardItems}" SnapsToDevicePixels="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Viewbox Margin="5">
<Button Content="{Binding}"></Button>
</Viewbox>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</TabItem>
Notice that all buttons are sized to content which makes them non-stretchable so each button has its own size... This is how Viewbox scales its content and of course this kind of GUI is out of question. This is not the keyboard I want to use on some kiosk application, so the next better version is following:
<TabItem Header="Test 2">
<ItemsControl ItemsSource="{Binding KeyboardItems}" SnapsToDevicePixels="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Margin="5">
<Viewbox>
<TextBlock Text="{Binding}" />
</Viewbox>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</TabItem>
Now this is a bit better as Viewbox is now wrapping the content rather than the whole button. Notice however that because we are now wrapping the button's content rather the whole button the button's border is now not scaled. I want this to be scaled too, not just the content. In the first example we had this but the overall look of the GUI was horrible.
Also notice that in this version I've set Margin to the Button and in the first version on the Viewbox. This means that in the first version margin will scale too (I want that!) while in the second version it will be constant for any screen size. So for really big screens the white space between buttons will become relatively smaller though they are absolutely of constant size (not what I want!).
Here is the code for generating keyboard buttons:
public partial class MainWindow : Window
{
public List<string> KeyboardItems { get; set; }
public MainWindow()
{
KeyboardItems = new List<string>();
for (char c = 'A'; c <= 'Z'; c++)
{
KeyboardItems.Add(c.ToString());
}
KeyboardItems.Add("Space");
DataContext = this;
InitializeComponent();
}
}
Problems like this are all around development of WPF touch screen kiosks so I'd like to hear some ideas and solutions you came about while dealing with scaling issues.
You didn't show us Test 3, which I thought might be this:
<TabItem Header="Test 3">
<Viewbox>
<ItemsControl ItemsSource="{Binding KeyboardItems}" SnapsToDevicePixels="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Margin="5">
<TextBlock Text="{Binding}" />
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Viewbox>
</TabItem>
Does that have the desired effect?

WPF ListView with horizontal arrangement of items?

I want to lay out items in a ListView in a similar manner to the WinForms ListView in List mode. That is, where items are laid out not just vertically but horizontally in the ListView as well.
I don't mind if the items are laid out like this:
1 4 7
2 5 8
3 6 9
Or like this:
1 2 3
4 5 6
7 8 9
As long as they are presented both vertically and horizontally in order to maximize the use of available space.
The closest I could find was this question:
How do I make WPF ListView items repeat horizontally, like a horizontal scrollbar?
Which only lays out the items only horizontally.
It sounds like what you are looking for is a WrapPannel, which will lay the items out horizontally until there is no more room, and then move to the next line, like this:
(MSDN)
alt text http://i.msdn.microsoft.com/Cc295081.b1c415fb-9a32-4a18-aa0b-308fca994ac9(en-us,Expression.10).png
You also could use a UniformGrid, which will lay the items out in a set number of rows or columns.
The way we get the items to arange using these other panels in a ListView, ListBox, or any form of ItemsControl is by changing the ItemsPanel property. By setting the ItemsPanel you can change it from the default StackPanel that is used by ItemsControls. With the WrapPanel we also should set the widths as shown here.
<ListView>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Width="{Binding (FrameworkElement.ActualWidth),
RelativeSource={RelativeSource AncestorType=ScrollContentPresenter}}"
ItemWidth="{Binding (ListView.View).ItemWidth,
RelativeSource={RelativeSource AncestorType=ListView}}"
MinWidth="{Binding ItemWidth, RelativeSource={RelativeSource Self}}"
ItemHeight="{Binding (ListView.View).ItemHeight,
RelativeSource={RelativeSource AncestorType=ListView}}" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
...
</ListView>
I recently research how to achieve this in WPF and found a good solution. What I wanted was to the replicate the List mode in Windows Explorer, i.e. top-to-bottom, then left-to-right.
Basically what you want to do override the ListBox.ItemsPanel property to use a WrapPanel with it's orientation set to Vertical.
<ListBox>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
However this WILL be slow when loading a large data set as it the wrap panel is not virtualised. This is important. So this task now becomes a little more as now you need to write your own VirtualizedWrapPanel by extending VirtualizedPanel and implementing IScrollInfo.
public class VirtualizedWrapPanel : VirtualizedPanel, IScrollInfo
{
// ...
}
This is as far as I got in my research before having to go off to another task. If you want more information or examples, please comment.
UPDATE. Ben Constable's has a great series on how to implement IScrollInfo.
There are 4 articles in total. A really good read.
I have since implemented a virtualized wrap panel, it is not an easy task even with the help of the above series of articles.
In my case, the best option was to use:
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical"
MaxHeight="{Binding (FrameworkElement.ActualHeight), RelativeSource={RelativeSource AncestorType=ScrollContentPresenter}}"
ItemWidth="{Binding (ListView.View).ItemWidth, RelativeSource={RelativeSource AncestorType=ListView}}"
MinHeight="{Binding ItemHeight, RelativeSource={RelativeSource Self}}"
ItemHeight="{Binding (ListView.View).ItemHeight, RelativeSource={RelativeSource AncestorType=ListView}}"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
This gave me a decent analog to Windows Explorer's List option
for left to right then top to bottom use
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"
MaxWidth="{Binding ActualWidth, Mode=OneWay,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type er:MainWindow}}}"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
In addition to #Dennis's answer, about the WrapPanel losing Virtualization, I have found a nice class that correctly implements this. While the suggested post by Ben Constable (Part 1, Part 2, Part 3, Part 4) is a nice introduction, I couldn't quite complete the task for a Wrap Panel.
Here is an implementation:
https://virtualwrappanel.codeplex.com/
I've tested it with total of 3.300 video's and photo's, loading the list itself is of course a bit long, but eventually it is correctly virtualizing the list, no scroll lag whatsoever.
There are some issues to this code, see the issues tab on the page above.
After adding the source code to your project, example source code:
<!--in your <Window> or <UserControl> tag -->
<UserControl
xmlns:hw="clr-namespace:Project.Namespace.ToClassFile" >
<!--...-->
<ListView x:Name="lvImages" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Margin="10" Height="auto"
ItemsSource="{Binding ListImages}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" >
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<hw:VirtualizingWrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Margin="5" MaxHeight="150">
<TextBlock Text="{Binding title}" FontWeight="Bold"/>
<Image Source="{Binding path, IsAsync=True}" Height="100"/>
<TextBlock Text="{Binding createDate, StringFormat=dd-MM-yyyy}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
MVVM style back-end, so this is inside the ViewModel:
public ObservableCollection<Media> ListImages
{
get
{
return listImages;
}
set { listImages = value; OnPropertyChanged(); }
}
//Just load the images however you do it, then assign it to above list.
//Below is the class defined that I have used.
public class Media
{
private static int nextMediaId = 1;
public int mediaId { get; }
public string title { get; set; }
public string path { get; set; }
public DateTime createDate { get; set; }
public bool isSelected { get; set; }
public Media()
{
mediaId = nextMediaId;
nextMediaId++;
}
}

Resources