I have a customized vertical scrollbar which displays markers for selected items in a DataGrid.
The problem I'm facing is, when there are a great number of items (e.g. could be 5000 to 50000) there is a lag while it is rendering the markers.
With the following code it basically renders as per the selected items index, number of items and height of the track. Obviously this is inefficient and am looking for other solutions.
This is my customized vertical scrollbar
<helpers:MarkerPositionConverter x:Key="MarkerPositionConverter"/>
<ControlTemplate x:Key="VerticalScrollBar" TargetType="{x:Type ScrollBar}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition MaxHeight="18" />
<RowDefinition Height="0.00001*" />
<RowDefinition MaxHeight="18" />
</Grid.RowDefinitions>
<Border Grid.RowSpan="3"
CornerRadius="2"
Background="#F0F0F0" />
<RepeatButton Grid.Row="0"
Style="{StaticResource ScrollBarLineButton}"
Height="18"
Command="ScrollBar.LineUpCommand"
Content="M 0 4 L 8 4 L 4 0 Z" />
<!--START-->
<ItemsControl VerticalAlignment="Stretch" x:Name="ItemsSelected"
ItemsSource="{Binding ElementName=GenericDataGrid, Path=SelectedItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Fill="SlateGray" Width="9" Height="4">
<Rectangle.RenderTransform>
<TranslateTransform>
<TranslateTransform.Y>
<MultiBinding Converter="{StaticResource MarkerPositionConverter}" FallbackValue="-1000">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type DataGrid}}" />
<Binding Path="ActualHeight" ElementName="ItemsSelected"/>
<Binding Path="Items.Count" ElementName="GenericDataGrid"/>
</MultiBinding>
</TranslateTransform.Y>
</TranslateTransform>
</Rectangle.RenderTransform>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas ClipToBounds="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!--END-->
<Track x:Name="PART_Track" Grid.Row="1" IsDirectionReversed="true">
<Track.DecreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollBarPageButton}"
Command="ScrollBar.PageUpCommand" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumb}" Margin="1,0,1,0">
<Thumb.BorderBrush>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="{DynamicResource BorderLightColor}" Offset="0.0" />
<GradientStop Color="{DynamicResource BorderDarkColor}" Offset="1.0" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Thumb.BorderBrush>
<Thumb.Background>
<LinearGradientBrush StartPoint="0,0"
EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="{DynamicResource ControlLightColor}" Offset="0.0" />
<GradientStop Color="{DynamicResource ControlMediumColor}" Offset="1.0" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Thumb.Background>
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollBarPageButton}" Command="ScrollBar.PageDownCommand" />
</Track.IncreaseRepeatButton>
</Track>
<RepeatButton Grid.Row="3" Style="{StaticResource ScrollBarLineButton}" Height="18" Command="ScrollBar.LineDownCommand" Content="M 0 0 L 4 4 L 8 0 Z" />
</Grid>
</ControlTemplate>
This is my converter that transforms the Y position and scales accordingly if the DataGrid height changes.
public class MarkerPositionConverter: IMultiValueConverter
{
//Performs the index to translate conversion
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
//calculated the transform values based on the following
object o = (object)values[0];
DataGrid dg = (DataGrid)values[1];
double itemIndex = dg.Items.IndexOf(o);
double trackHeight = (double)values[2];
int itemCount = (int)values[3];
double translateDelta = trackHeight / itemCount;
return itemIndex * translateDelta;
}
catch (Exception ex)
{
Console.WriteLine("MarkerPositionConverter error : " + ex.Message);
return false;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
[RE-EDIT] I have tried to create a separate class for a marker canvas, for use with ObservableCollection's. Note that at present, this does not work.
XAML still the same as yesterday:
<helpers:MarkerCollectionCanvas
x:Name="SearchMarkerCanvas"
Grid.Row="1"
Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
MarkerCollection="{Binding Source={x:Static helpers:MyClass.Instance}, Path=SearchMarkers}"/>
Canvas class, ObservableCollection changed to use object instead of double, there is a console.writeline in MarkerCollectionCanvas_CollectionChanged that never gets called:
class MarkerCollectionCanvas : Canvas
{
public DataGrid Grid
{
get { return (DataGrid)GetValue(GridProperty); }
set { SetValue(GridProperty, value); }
}
public static readonly DependencyProperty GridProperty =
DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCollectionCanvas), new PropertyMetadata(null));
public ObservableCollection<object> MarkerCollection
{
get { return (ObservableCollection<object>)GetValue(MarkerCollectionProperty); }
set { SetValue(MarkerCollectionProperty, value); }
}
public static readonly DependencyProperty MarkerCollectionProperty =
DependencyProperty.Register("MarkerCollection", typeof(ObservableCollection<object>), typeof(MarkerCollectionCanvas), new PropertyMetadata(null, OnCollectionChanged));
private static void OnCollectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MarkerCollectionCanvas canvas = d as MarkerCollectionCanvas;
if (e.NewValue != null)
{
(e.NewValue as ObservableCollection<object>).CollectionChanged += canvas.MarkerCollectionCanvas_CollectionChanged;
}
if (e.OldValue != null)
{
(e.NewValue as ObservableCollection<object>).CollectionChanged -= canvas.MarkerCollectionCanvas_CollectionChanged;
}
}
void MarkerCollectionCanvas_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
Console.WriteLine("InvalidateVisual");
InvalidateVisual();
}
public Brush MarkerBrush
{
get { return (Brush)GetValue(MarkerBrushProperty); }
set { SetValue(MarkerBrushProperty, value); }
}
public static readonly DependencyProperty MarkerBrushProperty =
DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCollectionCanvas), new PropertyMetadata(Brushes.DarkOrange));
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || MarkerCollection == null)
return;
//Get all items
object[] items = new object[Grid.Items.Count];
Grid.Items.CopyTo(items, 0);
//Get all selected items
object[] selection = new object[MarkerCollection.Count];
MarkerCollection.CopyTo(selection, 0);
Dictionary<object, int> indexes = new Dictionary<object, int>();
for (int i = 0; i < selection.Length; i++)
{
indexes.Add(selection[i], 0);
}
int itemCounter = 0;
for (int i = 0; i < items.Length; i++)
{
object item = items[i];
if (indexes.ContainsKey(item))
{
indexes[item] = i;
itemCounter++;
}
if (itemCounter >= selection.Length)
break;
}
double translateDelta = ActualHeight / (double)items.Length;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);
foreach (int itemIndex in sortedIndex)
{
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
}
This is my singleton class with SearchMarkers in it:
public class MyClass : INotifyPropertyChanged
{
public static ObservableCollection<object> m_searchMarkers = new ObservableCollection<object>();
public ObservableCollection<object> SearchMarkers
{
get
{
return m_searchMarkers;
}
set
{
m_searchMarkers = value;
NotifyPropertyChanged();
}
}
private static MyClass m_Instance;
public static MyClass Instance
{
get
{
if (m_Instance == null)
{
m_Instance = new MyClass();
}
return m_Instance;
}
}
private MyClass()
{
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And this is a textbox text changed behavior. This is where the ObservableCollection SearchMarkers gets populated.
public class FindTextChangedBehavior : Behavior<TextBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.TextChanged += OnTextChanged;
}
protected override void OnDetaching()
{
AssociatedObject.TextChanged -= OnTextChanged;
base.OnDetaching();
}
private void OnTextChanged(object sender, TextChangedEventArgs args)
{
var textBox = (sender as TextBox);
if (textBox != null)
{
DataGrid dg = DataGridObject as DataGrid;
string searchValue = textBox.Text;
if (dg.Items.Count > 0)
{
var columnBoundProperties = new List<KeyValuePair<int, string>>();
IEnumerable<DataGridColumn> visibleColumns = dg.Columns.Where(c => c.Visibility == System.Windows.Visibility.Visible);
foreach (var col in visibleColumns)
{
if (col is DataGridTextColumn)
{
var binding = (col as DataGridBoundColumn).Binding as Binding;
columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
}
else if (col is DataGridComboBoxColumn)
{
DataGridComboBoxColumn dgcbc = (DataGridComboBoxColumn)col;
var binding = dgcbc.SelectedItemBinding as Binding;
columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
}
}
Type itemType = dg.Items[0].GetType();
if (columnBoundProperties.Count > 0)
{
ObservableCollection<Object> tempItems = new ObservableCollection<Object>();
var itemsSource = dg.Items as IEnumerable;
Task.Factory.StartNew(() =>
{
ClassPropTextSearch.init(itemType, columnBoundProperties);
if (itemsSource != null)
{
foreach (object o in itemsSource)
{
if (ClassPropTextSearch.Match(o, searchValue))
{
tempItems.Add(o);
}
}
}
})
.ContinueWith(t =>
{
Application.Current.Dispatcher.Invoke(new Action(() => MyClass.Instance.SearchMarkers = tempItems));
});
}
}
}
}
public static readonly DependencyProperty DataGridObjectProperty =
DependencyProperty.RegisterAttached("DataGridObject", typeof(DataGrid), typeof(FindTextChangedBehavior), new UIPropertyMetadata(null));
public object DataGridObject
{
get { return (object)GetValue(DataGridObjectProperty); }
set { SetValue(DataGridObjectProperty, value); }
}
}
Here you go, I tried to attempt it for you.
Created a class MarkerCanvas deriving Canvas with a property to bind with the data grid
Attached SelectionChanged to listen to any change and requested the canvas to redraw itself by InvalidateVisual
overrided the method OnRender to take control of drawing and did the necessary check and calculation
finally rendered the rectangle on the calculated coordinates using the given brush
MarkerCanvas class
class MarkerCanvas : Canvas
{
public DataGrid Grid
{
get { return (DataGrid)GetValue(GridProperty); }
set { SetValue(GridProperty, value); }
}
// Using a DependencyProperty as the backing store for Grid. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GridProperty =
DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCanvas), new PropertyMetadata(null, OnGridChanged));
private static void OnGridChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MarkerCanvas canvas = d as MarkerCanvas;
if (e.NewValue != null)
{
(e.NewValue as DataGrid).SelectionChanged += canvas.MarkerCanvas_SelectionChanged;
}
if (e.OldValue != null)
{
(e.NewValue as DataGrid).SelectionChanged -= canvas.MarkerCanvas_SelectionChanged;
}
}
void MarkerCanvas_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
InvalidateVisual();
}
public Brush MarkerBrush
{
get { return (Brush)GetValue(MarkerBrushProperty); }
set { SetValue(MarkerBrushProperty, value); }
}
// Using a DependencyProperty as the backing store for MarkerBrush. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MarkerBrushProperty =
DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCanvas), new PropertyMetadata(Brushes.SlateGray));
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid==null || Grid.SelectedItems == null)
return;
object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
for (int i = 0; i < markers.Length; i++)
{
double itemIndex = Grid.Items.IndexOf(markers[i]);
double top = itemIndex * translateDelta;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
}
}
}
I have also adjusted the height of marker so it grows when there are less items, you can choose to fix it to specific value as per your needs
in XAML replace your items control with the new marker canvas with binding to the grid
<helpers:MarkerCanvas Grid.Row="1" Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
helpers: is referring to WpfAppDataGrid.Helpers where I create the class, you can choose your own namespace
also you can bind the MarkerBrush property for your desired effet, which defaulted to SlateGray
rendering is pretty fast now, perhaps could make it more fast by doing some work on indexof method.
Also to skip some of the overlapping rectangles to be rendered you can change the method like this. little buggy as of now
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid==null || Grid.SelectedItems == null)
return;
object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
for (int i = 0; i < markers.Length; i++)
{
double itemIndex = Grid.Items.IndexOf(markers[i]);
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
Performance optimization
I tried to optimize the performance using a slight different approach, specially for the select all button
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || Grid.SelectedItems == null)
return;
object[] items = new object[Grid.Items.Count];
Grid.Items.CopyTo(items, 0);
object[] selection = new object[Grid.SelectedItems.Count];
Grid.SelectedItems.CopyTo(selection, 0);
Dictionary<object, int> indexes = new Dictionary<object, int>();
for (int i = 0; i < selection.Length; i++)
{
indexes.Add(selection[i], 0);
}
int itemCounter = 0;
for (int i = 0; i < items.Length; i++)
{
object item = items[i];
if (indexes.ContainsKey(item))
{
indexes[item] = i;
itemCounter++;
}
if (itemCounter >= selection.Length)
break;
}
double translateDelta = ActualHeight / (double)items.Length;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);
foreach (int itemIndex in sortedIndex)
{
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
Reflection approach
in this I have tried to get the underlying selection list and attempted to retrieve the selected indexes from the same, also added even more optimization when doing select all
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || Grid.SelectedItems == null)
return;
List<int> indexes = new List<int>();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double height = Math.Max(translateDelta, 4);
int itemInOneRect = (int)Math.Floor(height / translateDelta);
itemInOneRect -= (int)(itemInOneRect * 0.2);
if (Grid.SelectedItems.Count == Grid.Items.Count)
{
for (int i = 0; i < Grid.Items.Count; i += itemInOneRect)
{
indexes.Add(i);
}
}
else
{
FieldInfo fi = Grid.GetType().GetField("_selectedItems", BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
IEnumerable<object> internalSelectionList = fi.GetValue(Grid) as IEnumerable<object>;
PropertyInfo pi = null;
int lastIndex = int.MinValue;
foreach (var item in internalSelectionList)
{
if (pi == null)
{
pi = item.GetType().GetProperty("Index", BindingFlags.Instance | BindingFlags.NonPublic);
}
int newIndex = (int)pi.GetValue(item);
if (newIndex > (lastIndex + itemInOneRect))
{
indexes.Add(newIndex);
lastIndex = newIndex;
}
}
indexes.Sort();
}
double width = ActualWidth;
Brush dBrush = MarkerBrush;
foreach (int itemIndex in indexes)
{
double top = itemIndex * translateDelta;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
}
}
Related
I'm trying to get my GridView to automatically adjust the appearance of its items depending on screen width but every time I snap my app's window, the GridView items keep getting bunched too close together. How can I ensure that each GridView item will appear on its own line and highlight the entire row like a ListView item when hovered over when the window is snapped to 1 side or less than a certain width (say 720 - which is often used for the MasterDetailsView - CompactModeThresholdWidth="720")?
Wide window
Snapped window
Snapped window (expected result)
MainPage.xaml
<Page
x:Class="MyApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:data="using:MyApp.Models"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Margin="0,0,0,20" Grid.Row="0">
<TextBlock Text="Hello World"/>
</StackPanel>
<GridView Grid.Row="1"
IsItemClickEnabled="True"
ItemsSource="{x:Bind GridItems}"
ItemClick="GridView_ItemClick">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<data:MyGridViewPanel/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemTemplate>
<DataTemplate x:DataType="data:GridItemMain">
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Column="0"
Text="{Binding Icon}"
Style="{StaticResource TitleTextBlockStyle}"
FontFamily="Segoe MDL2 Assets"/>
<TextBlock Grid.Column="1"
Text="{Binding Title}"
Style="{StaticResource TitleTextBlockStyle}"/>
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</Grid>
</Page>
MainPage.xaml.cs
public sealed partial class MainPage : Page
{
public List<GridItemMain> GridItems;
public MainPage()
{
this.InitializeComponent();
GridItems = GridItemManager.GetGridItems();
}
}
MainGridItem.cs
public class MainGridItem
{
public string Icon { get; set; }
public string Title { get; set; }
}
public class GridItemManager
{
public static List<MainGridItem> GetGridItems()
{
var gridItems = new List<MainGridItem>();
gridItems.Add(new MainGridItem { Icon = "\uE770", Title = "System" });
gridItems.Add(new MainGridItem { Icon = "\uE772", Title = "Devices" });
gridItems.Add(new MainGridItem { Icon = "\uEC75", Title = "Phone" });
gridItems.Add(new MainGridItem { Icon = "\uE774", Title = "Network & Internet" });
gridItems.Add(new MainGridItem { Icon = "\uE771", Title = "Personalisation" });
return gridItems;
}
}
MyGridViewPanel.cs
public class MyGridViewPanel : Panel
{
private double _maxWidth;
private double _maxHeight;
protected override Size ArrangeOverride(Size finalSize)
{
var x = 0.0;
var y = 0.0;
double width = Window.Current.Bounds.Width;
if (width <= 720)
{
foreach (var child in Children)
{
var newpos = new Rect(0, y, width, _maxHeight);
child.Arrange(newpos);
y += _maxHeight;
}
return finalSize;
}
else {
foreach (var child in Children)
{
if ((_maxWidth + x) > finalSize.Width)
{
x = 0;
y += _maxHeight;
}
var newpos = new Rect(x, y, _maxWidth, _maxHeight);
child.Arrange(newpos);
x += _maxWidth;
}
return finalSize;
}
}
protected override Size MeasureOverride(Size availableSize)
{
double width = Window.Current.Bounds.Width;
if (width <= 720)
{
foreach (var child in Children)
{
child.Measure(new Size(width, availableSize.Height));
var desiredheight = child.DesiredSize.Height;
if (desiredheight > _maxHeight)
_maxHeight = desiredheight;
}
return new Size(width, _maxHeight * Children.Count);
}
else {
foreach (var child in Children)
{
child.Measure(availableSize);
var desirtedwidth = child.DesiredSize.Width;
if (desirtedwidth > _maxWidth)
_maxWidth = desirtedwidth;
var desiredheight = child.DesiredSize.Height;
if (desiredheight > _maxHeight)
_maxHeight = desiredheight;
}
var itemperrow = Math.Floor(availableSize.Width / _maxWidth);
var rows = Math.Ceiling(Children.Count / itemperrow);
return new Size(itemperrow * _maxWidth, _maxHeight * rows);
}
}
}
Update
If you want to display an item on each line when the window is less than 720px, you could get the current window's width in the ArrangeOverride and MeasureOverride method, then resize the item and re-layout each item to show them in each row. For example:
MyGridViewPanel.cs
public class MyGridViewPanel : Panel
{
private double _maxWidth;
private double _maxHeight;
protected override Size ArrangeOverride(Size finalSize)
{
var x = 0.0;
var y = 0.0;
double width = Window.Current.Bounds.Width;
if (width <= 720)
{
foreach (var child in Children)
{
var newpos = new Rect(0, y, width, _maxHeight);
child.Arrange(newpos);
y += _maxHeight;
}
return finalSize;
}
else {
foreach (var child in Children)
{
if ((_maxWidth + x) > finalSize.Width)
{
x = 0;
y += _maxHeight;
}
var newpos = new Rect(x, y, _maxWidth, _maxHeight);
child.Arrange(newpos);
x += _maxWidth;
}
return finalSize;
}
}
protected override Size MeasureOverride(Size availableSize)
{
double width = Window.Current.Bounds.Width;
if (width <= 720)
{
foreach (var child in Children)
{
child.Measure(new Size(width, availableSize.Height));
var desiredheight = child.DesiredSize.Height;
if (desiredheight > _maxHeight)
_maxHeight = desiredheight;
}
return new Size(width, _maxHeight * Children.Count);
}
else {
foreach (var child in Children)
{
child.Measure(availableSize);
var desirtedwidth = child.DesiredSize.Width;
if (desirtedwidth > _maxWidth)
_maxWidth = desirtedwidth;
var desiredheight = child.DesiredSize.Height;
if (desiredheight > _maxHeight)
_maxHeight = desiredheight;
}
var itemperrow = Math.Floor(availableSize.Width / _maxWidth);
var rows = Math.Ceiling(Children.Count / itemperrow);
return new Size(itemperrow * _maxWidth, _maxHeight * rows);
}
}
}
I am using the Radial Progress Bar from the UWP ToolKit. I want to use this progress in a game that I creating. I have an image that I want to fill the foreground of the progress bar with. No matter what I try I cannot get the image to fill in the progress bar, in the manner a progress bar fills in.
I am currently trying to scale the image in the foreground but that isn't quite the effect I am going for.
XAML:
<toolKit:RadialProgressBar Width="316" Height="316" Minimum="0" Maximum="100" Value="{Binding Value}" Thickness="36"
BorderBrush="Transparent" >
<toolKit:RadialProgressBar.Foreground>
<ImageBrush ImageSource="ms-appx:///Assets/progress2.png" >
<ImageBrush.Transform>
<ScaleTransform CenterX="0" CenterY="0" ScaleX="{Binding Scale}" ScaleY="{Binding Scale}" />
</ImageBrush.Transform>
</ImageBrush>
</toolKit:RadialProgressBar.Foreground>
</toolKit:RadialProgressBar>
C#:
private int _value;
public int Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
RaisePropertyChanged(nameof(Value));
Scale = 1.0-(100.0 - (316.0 * (Convert.ToDouble(value) / 100.0) / 316.0 * 100.0))/100.0;
}
}
}
private double _scale;
public double Scale
{
get => _scale;
set
{
if (_scale != value)
{
_scale = value;
RaisePropertyChanged(nameof(Scale));
}
}
}
Below is the effect that the above code creates:
Is there a way to fill a progress bar with an image?
How about custom one using Win2d?
XAML:
<Grid Width="500" Height="500">
<xaml:CanvasControl x:Name="CanvasControl" Draw="CanvasControl_OnDraw"/>
<Slider Width="500" VerticalAlignment="Center" ValueChanged="RangeBase_OnValueChanged"/>
</Grid>
and behind it:
private static Vector2 _centerPoint = new Vector2(250);
private static float _radius = 200;
private int _currentValue;
private readonly List<Vector2> _points = new List<Vector2>();
private readonly CanvasStrokeStyle _strokeStyle = new CanvasStrokeStyle
{
StartCap = CanvasCapStyle.Round,
EndCap = CanvasCapStyle.Round
};
public MainPage()
{
InitializeComponent();
GetPoints(100);
}
private void GetPoints(int i)
{
var radFactor = Math.PI / 180;
var angleStep = 360f / i;
for (int j = 0; j < i + 1; j++)
{
_points.Add(new Vector2(_centerPoint.X + _radius * (float)Math.Cos((angleStep * j + 90) * radFactor),
_centerPoint.Y + _radius * (float)Math.Sin((angleStep * j + 90) * radFactor)));
}
}
private void CanvasControl_OnDraw(CanvasControl sender, CanvasDrawEventArgs args)
{
for (int i = 0; i < _currentValue; i++)
{
args.DrawingSession.DrawLine(_points[i], _points[i + 1], Colors.LimeGreen, 24, _strokeStyle);
}
}
private void RangeBase_OnValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
_currentValue = (int) e.NewValue;
CanvasControl.Invalidate();
}
and it should look like this:
I have decided to go away from using a radial progress bar. I have learned about Image cliping. I am using the image clip along with a Rotation transformation to get the desired effect.
XAML:
<Image Source="ms-appx:///Assets/Progress.png" Width="{Binding ImageWidth}" Height="{Binding ImageHeight}" >
<Image.Clip>
<RectangleGeometry Rect="{Binding Rect}" >
<RectangleGeometry.Transform>
<RotateTransform CenterX="{Binding CenterXY}" CenterY="{Binding CenterXY}" Angle="{Binding Angle}" />
</RectangleGeometry.Transform>
</RectangleGeometry>
</Image.Clip>
</Image>
C#:
private int _value;
public int Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
RaisePropertyChanged(nameof(Value));
Angle = 144 - 1.44 * value;
}
}
}
I have a model called "ListItem", and I'm printing a list of them. I used to just use a ListView, but I wanted to print them horizontally, so I created a custom WrapLayout.
The ListView had simple ways to perform actions when the items were tapped or long-pressed. When a MenuItem was tapped, PutBack was called, and the ListItem was passed through as parameters. When a MenuItem was long-pressed, a button reading "delete" appeared in the toolbar, and when that was tapped, DeleteListItem was called and the ListItem was passed through as parameters.
Old Xaml:
<ListView x:Name="inactiveList" ItemTapped="PutBack" >
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Name}">
<TextCell.ContextActions>
<MenuItem Command="{Binding Source={x:Reference ListPage}, Path=DeleteListItem}"
CommandParameter="{Binding .}" Text="delete" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I'm having trouble recreating this functionality with the buttons generated by my custom WrapLayout. Is this possible to do? I tried adding a Clicked property to the buttons, but VS tells me I can only set Clicked with += or -=, which doesn't make any sense to me.
New Xaml:
<local:WrapLayout x:Name="inactiveList" Spacing="5" />
New Code-Behind:
namespace Myapp
{
public partial class ListPage
{
public Command DeleteListItem { get; set; }
public ListPage()
{
InitializeComponent();
ObservableCollection<ListItem> inactiveItems =
new ObservableCollection<ListItem>(
App.ListItemRepo.GetInactiveListItems());
inactiveList.ItemsSource = inactiveItems;
DeleteListItem = new Command(async (parameter) => {
ListItem item = (ListItem)parameter as ListItem;
App.ListItemRepo.DeleteListItemAsync(item);
ObservableCollection<ListItem> deleteInactiveItems =
new ObservableCollection<ListItem>(
await App.ListItemRepo.GetInactiveListItemsAsync());
inactiveList.ItemsSource = deleteInactiveItems;
});
}
public async void PutBack(object sender, ItemTappedEventArgs e)
{
var selectedListItem = e.Item as ListItem;
await App.ListItemRepo.SetListItemActive(selectedListItem);
ObservableCollection<ListItem> inactiveItems =
new ObservableCollection<ListItem>(
await App.ListItemRepo.GetInactiveListItemsAsync());
inactiveList.ItemsSource = inactiveItems;
}
}
public class WrapLayout : Layout<View>
{
public ObservableCollection<ListItem> ItemsSource
{
get { return (ObservableCollection<ListItem>)GetValue(ItemSourceProperty); }
set { SetValue(ItemSourceProperty, value); }
}
public static readonly BindableProperty ItemSourceProperty =
BindableProperty.Create
(
"ItemsSource",
typeof(ObservableCollection<ListItem>),
typeof(WrapLayout),
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).AddViews()
);
// THIS IS WHERE THE BUTTONS ARE SET
void AddViews()
{
Children.Clear();
foreach (ListItem s in ItemsSource)
{
Button button = new Button();
button.Text = s.Name;
Children.Add(button);
}
}
public static readonly BindableProperty SpacingProperty =
BindableProperty.Create
(
"Spacing",
typeof(double),
typeof(WrapLayout),
10.0,
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).OnSizeChanged()
);
public double Spacing
{
get { return (double)GetValue(SpacingProperty); }
set { SetValue(SpacingProperty, value); }
}
private void OnSizeChanged()
{
this.ForceLayout();
}
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
if (WidthRequest > 0)
widthConstraint = Math.Min(widthConstraint, WidthRequest);
if (HeightRequest > 0)
heightConstraint = Math.Min(heightConstraint, HeightRequest);
double internalWidth = double.IsPositiveInfinity(widthConstraint) ? double.PositiveInfinity : Math.Max(0, widthConstraint);
double internalHeight = double.IsPositiveInfinity(heightConstraint) ? double.PositiveInfinity : Math.Max(0, heightConstraint);
return DoHorizontalMeasure(internalWidth, internalHeight);
}
private SizeRequest DoHorizontalMeasure(double widthConstraint, double heightConstraint)
{
int rowCount = 1;
double width = 0;
double height = 0;
double minWidth = 0;
double minHeight = 0;
double widthUsed = 0;
foreach (var item in Children)
{
var size = item.Measure(widthConstraint, heightConstraint);
height = Math.Max(height, size.Request.Height);
var newWidth = width + size.Request.Width + Spacing;
if (newWidth > widthConstraint)
{
rowCount++;
widthUsed = Math.Max(width, widthUsed);
width = size.Request.Width;
}
else
width = newWidth;
minHeight = Math.Max(minHeight, size.Minimum.Height);
minWidth = Math.Max(minWidth, size.Minimum.Width);
}
if (rowCount > 1)
{
width = Math.Max(width, widthUsed);
height = (height + Spacing) * rowCount - Spacing; // via MitchMilam
}
return new SizeRequest(new Size(width, height), new Size(minWidth, minHeight));
}
protected override void LayoutChildren(double x, double y, double width, double height)
{
double rowHeight = 0;
double yPos = y, xPos = x;
foreach (var child in Children.Where(c => c.IsVisible))
{
var request = child.Measure(width, height);
double childWidth = request.Request.Width;
double childHeight = request.Request.Height;
rowHeight = Math.Max(rowHeight, childHeight);
if (xPos + childWidth > width)
{
xPos = x;
yPos += rowHeight + Spacing;
rowHeight = 0;
}
var region = new Rectangle(xPos, yPos, childWidth, childHeight);
LayoutChildIntoBoundingRegion(child, region);
xPos += region.Width + Spacing;
}
}
}
}
I'm working with RibbonControlsLibrary, WPF and VS2015, I need to perform an action on a selection of multi RibbonGallery. Following the images to facilitate understanding.
What I've done
As I want you to
Follows the code as was done:
<r:RibbonWindow.Resources>
<DataTemplate
x:Key="tableRectTemplate">
<DockPanel
Margin="-2,-1,-2,-1">
<Rectangle
Width="14"
Height="14"
Stroke="Gray"
ToolTip="{Binding}"
MouseEnter="Rectangle_MouseEnter">
</Rectangle>
</DockPanel>
</DataTemplate>
<x:Array
Type="sys:String"
x:Key="tablePickerRowColumn">
...items..
...items..
...items..
</x:Array>
</r:RibbonWindow.Resources>
<r:RibbonGallery
Name="_rgInsertTable"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
Command="{StaticResource InserirTabelaHandler}"
MouseLeave="_rgInsertTable_MouseLeave">
<r:RibbonGalleryCategory
Name="_rgcInsertTable"
Header="Inserir tabela..."
MinColumnCount="10"
MaxColumnCount="10"
ItemTemplate="{StaticResource tableRectTemplate}"
ItemsSource="{Binding Source={StaticResource tablePickerRowColumn}, Path=SyncRoot}">
</r:RibbonGalleryCategory>
<r:RibbonSeparator/>
<r:RibbonMenuItem
Header="Inserir tabela"
ImageSource="/Images/Small_32bit/table.png"
Command="{StaticResource InserirTabelaHandler}"
MouseEnter="RibbonMenuItem_MouseEnter"/>
</r:RibbonGallery>
You should use a custom control,
Demo:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:MyTableControl x:Name="MyTableControl"></local:MyTableControl>
</Grid>
</Window>
Control (XAML):
<UserControl x:Class="WpfApplication1.MyTableControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UniformGrid x:Name="root">
</UniformGrid>
</UserControl>
Control (code):
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
namespace WpfApplication1
{
public partial class MyTableControl
{
private const int MinColumns = 1;
private const int MaxColumns = 10;
private const int MinRows = 1;
private const int MaxRows = 10;
public static readonly DependencyProperty RowsProperty = DependencyProperty.Register(
"Rows", typeof (int), typeof (MyTableControl), new PropertyMetadata(MinRows, OnRowsChanged, OnRowsCoerce));
public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register(
"Columns", typeof (int), typeof (MyTableControl),
new PropertyMetadata(MinColumns, OnColumnsChanged, OnColumnsCoerce));
private bool _pressed;
public MyTableControl()
{
InitializeComponent();
Columns = 5;
Rows = 5;
}
public int Rows
{
get { return (int) GetValue(RowsProperty); }
set { SetValue(RowsProperty, value); }
}
public int Columns
{
get { return (int) GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
private static object OnRowsCoerce(DependencyObject d, object basevalue)
{
var i = (int) basevalue;
return i < MinRows ? MinRows : i > MaxRows ? MaxRows : i;
}
private static object OnColumnsCoerce(DependencyObject d, object basevalue)
{
var i = (int) basevalue;
return i < MinColumns ? MinColumns : i > MaxColumns ? MaxColumns : i;
}
private static void OnRowsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control1 = (MyTableControl) d;
control1.Populate();
}
private static void OnColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control1 = (MyTableControl) d;
control1.Populate();
}
private void Populate()
{
root.Children.Clear();
root.Columns = Columns;
root.Rows = Rows;
for (var y = 0; y < Rows; y++)
{
for (var x = 0; x < Columns; x++)
{
var toggleButton = new ToggleButton {Tag = new Point(x, y)};
toggleButton.MouseEnter += ToggleButton_MouseEnter;
toggleButton.Click += ToggleButton_Click;
root.Children.Add(toggleButton);
}
}
}
private void ToggleButton_Click(object sender, RoutedEventArgs e)
{
_pressed = true; // stops selection
// little bug here, button will be unchecked since it is a toggle button
// but since you'll use images instead, this behavior will vanish
}
private void ToggleButton_MouseEnter(object sender, MouseEventArgs e)
{
if (_pressed)
{
return;
}
var button = (ToggleButton) sender;
var point = (Point) button.Tag;
var x = (int) point.X;
var y = (int) point.Y;
for (var i = 0; i < Columns*Rows; i++)
{
var element = (ToggleButton) root.Children[i];
var tag = (Point) element.Tag;
var x1 = (int) tag.X;
var y1 = (int) tag.Y;
element.IsChecked = x1 <= x && y1 <= y;
}
}
}
}
I used a ToggleButton for simplicity, you could further improve by using VisualTreeHelper instead of MouseEnter, etc ...
It might not be the most elegant solution, but I solved the problem this way.
XAML:
<r:RibbonMenuButton
LargeImageSource="/Images/Large_32bit/table.png"
Label="Tabela"
ToolTip="Tabela"
ToolTipDescription="Insere uma tabela no documento">
<r:RibbonGallery
ScrollViewer.VerticalScrollBarVisibility="Auto">
<r:RibbonGalleryCategory
Name="_rgcInsertTable"
Header="Inserir tabela">
<Canvas
Name="InsertTblCellMnuItemContainer"
Background="AliceBlue"
MouseMove="InsertTblCellMnuItemContainer_MouseMove"
MouseLeave="InsertTblCellMnuItemContainer_MouseLeave"/>
</r:RibbonGalleryCategory>
<r:RibbonSeparator/>
<r:RibbonMenuItem
Header="Inserir tabela"
ImageSource="/Images/Small_32bit/table.png"
Command="{StaticResource InserirTabelaHandler}"/>
</r:RibbonGallery>
</r:RibbonMenuButton>
Code:
public MainWindow()
{
InitializeComponent();
PopulateTableInsertMenuItem(10, 8, 15, 15, 2);
}
private void ResetTableCellRectangles()
{
foreach (Rectangle r in InsertTblCellMnuItemContainer.Children)
{
r.Fill = Brushes.AliceBlue;
r.Stroke = Brushes.Black;
}
}
private void InsertTblCellMnuItemContainer_MouseMove(object sender, MouseEventArgs e)
{
Point pos = e.GetPosition(InsertTblCellMnuItemContainer);
foreach (Rectangle r in InsertTblCellMnuItemContainer.Children)
{
Vector posRect = VisualTreeHelper.GetOffset(r);
if ((posRect.X <= pos.X) && (posRect.Y <= pos.Y))
{
r.Fill = Brushes.LightYellow;
r.Stroke = Brushes.Orange;
}
else
{
r.Fill = Brushes.AliceBlue;
r.Stroke = Brushes.Black;
}
}
}
private void InsertTblCellMnuItemContainer_MouseLeave(object sender, MouseEventArgs e)
{
ResetTableCellRectangles();
_rgcInsertTable.Header = "Inserir tabela";
}
private void PopulateTableInsertMenuItem(int width, int height, int rectWidth, int rectHeight, int margin)
{
Rectangle r;
InsertTblCellMnuItemContainer.Width = (rectWidth + margin) * width;
InsertTblCellMnuItemContainer.Height = (rectHeight + margin) * height;
for (int j = 0; j < height; ++j)
{
for (int i = 0; i < width; ++i)
{
// Create new rectangle
r = new Rectangle
{
Width = rectWidth,
Height = rectHeight,
Stroke = Brushes.Black,
Fill = Brushes.AliceBlue,
Tag = new Point(i + 1, j + 1), // Remember rectangle's position in grid somehow
};
r.MouseLeftButtonDown += new MouseButtonEventHandler(TableInsertRectangle_MouseLeftButtonDown);
r.MouseEnter += new MouseEventHandler(TableInsertRectangle_MouseEnter);
// Set position in canvas
Canvas.SetLeft(r, (i * margin) + (i * rectWidth));
Canvas.SetTop(r, (j * margin) + (j * rectHeight));
// Add rectangle to canvas
InsertTblCellMnuItemContainer.Children.Add(r);
}
}
}
void TableInsertRectangle_MouseEnter(object sender, MouseEventArgs e)
{
Point rectKoords = (Point)((Rectangle)sender).Tag;
_rgcInsertTable.Header = rectKoords.X.ToString() + "x" + rectKoords.Y.ToString() + " tabela";
}
void TableInsertRectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point rectKoords = (Point)((Rectangle)sender).Tag;
_textControl.Tables.Add((int)rectKoords.Y, (int)rectKoords.X);
}
Any improvement is welcome.
I want to create a data-binded horizontal layout ItemsControl where for each item there would be a Button. When I add new items to the collection the ItemsControl should grow, relative to the Window it is in, until it reaches it's MaxWidth property. Then all buttons should shrink equally to fit inside MaxWidth. Something similar to the tabs of a Chrome browser.
Tabs with space:
Tabs with no empty space:
So far I've gotten to this:
<ItemsControl Name="ButtonsControl" MaxWidth="400">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type dataclasses:TextNote}">
<Button Content="{Binding Title}" MinWidth="80"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
When adding items the expansion of the StackPanel and Window are fine, but when MaxWidth is reached the items just start to disappear.
I don't think it is possible to produce that behaviour using any combination of the standard WPF controls, but this custom StackPanel control should do the job:
public class SqueezeStackPanel : Panel
{
private const double Tolerance = 0.001;
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register
("Orientation", typeof (Orientation), typeof (SqueezeStackPanel),
new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure,
OnOrientationChanged));
private readonly Dictionary<UIElement, Size> _childToConstraint = new Dictionary<UIElement, Size>();
private bool _isMeasureDirty;
private bool _isHorizontal = true;
private List<UIElement> _orderedSequence;
private Child[] _children;
static SqueezeStackPanel()
{
DefaultStyleKeyProperty.OverrideMetadata
(typeof (SqueezeStackPanel),
new FrameworkPropertyMetadata(typeof (SqueezeStackPanel)));
}
protected override bool HasLogicalOrientation
{
get { return true; }
}
protected override Orientation LogicalOrientation
{
get { return Orientation; }
}
public Orientation Orientation
{
get { return (Orientation) GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
protected override Size ArrangeOverride(Size finalSize)
{
var size = new Size(_isHorizontal ? 0 : finalSize.Width, !_isHorizontal ? 0 : finalSize.Height);
var childrenCount = Children.Count;
var rc = new Rect();
for (var index = 0; index < childrenCount; index++)
{
var child = _orderedSequence[index];
var childVal = _children[index].Val;
if (_isHorizontal)
{
rc.Width = double.IsInfinity(childVal) ? child.DesiredSize.Width : childVal;
rc.Height = Math.Max(finalSize.Height, child.DesiredSize.Height);
size.Width += rc.Width;
size.Height = Math.Max(size.Height, rc.Height);
child.Arrange(rc);
rc.X += rc.Width;
}
else
{
rc.Width = Math.Max(finalSize.Width, child.DesiredSize.Width);
rc.Height = double.IsInfinity(childVal) ? child.DesiredSize.Height : childVal;
size.Width = Math.Max(size.Width, rc.Width);
size.Height += rc.Height;
child.Arrange(rc);
rc.Y += rc.Height;
}
}
return new Size(Math.Max(finalSize.Width, size.Width), Math.Max(finalSize.Height, size.Height));
}
protected override Size MeasureOverride(Size availableSize)
{
for (var i = 0; i < 3; i++)
{
_isMeasureDirty = false;
var childrenDesiredSize = new Size();
var childrenCount = Children.Count;
if (childrenCount == 0)
return childrenDesiredSize;
var childConstraint = GetChildrenConstraint(availableSize);
_children = new Child[childrenCount];
_orderedSequence = Children.Cast<UIElement>().ToList();
for (var index = 0; index < childrenCount; index++)
{
if (_isMeasureDirty)
break;
var child = _orderedSequence[index];
const double minLength = 0.0;
const double maxLength = double.PositiveInfinity;
MeasureChild(child, childConstraint);
if (_isHorizontal)
{
childrenDesiredSize.Width += child.DesiredSize.Width;
_children[index] = new Child(minLength, maxLength, child.DesiredSize.Width);
childrenDesiredSize.Height = Math.Max(childrenDesiredSize.Height, child.DesiredSize.Height);
}
else
{
childrenDesiredSize.Height += child.DesiredSize.Height;
_children[index] = new Child(minLength, maxLength, child.DesiredSize.Height);
childrenDesiredSize.Width = Math.Max(childrenDesiredSize.Width, child.DesiredSize.Width);
}
}
if (_isMeasureDirty)
continue;
var current = _children.Sum(s => s.Val);
var target = GetSizePart(availableSize);
var finalSize = new Size
(Math.Min(availableSize.Width, _isHorizontal ? current : childrenDesiredSize.Width),
Math.Min(availableSize.Height, _isHorizontal ? childrenDesiredSize.Height : current));
if (double.IsInfinity(target))
return finalSize;
RecalcChilds(current, target);
current = 0.0;
for (var index = 0; index < childrenCount; index++)
{
var child = _children[index];
if (IsGreater(current + child.Val, target, Tolerance) &&
IsGreater(target, current, Tolerance))
{
var rest = IsGreater(target, current, Tolerance) ? target - current : 0.0;
if (IsGreater(rest, child.Min, Tolerance))
child.Val = rest;
}
current += child.Val;
}
RemeasureChildren(finalSize);
finalSize = new Size
(Math.Min(availableSize.Width, _isHorizontal ? target : childrenDesiredSize.Width),
Math.Min(availableSize.Height, _isHorizontal ? childrenDesiredSize.Height : target));
if (_isMeasureDirty)
continue;
return finalSize;
}
return new Size();
}
public static double GetHeight(Thickness thickness)
{
return thickness.Top + thickness.Bottom;
}
public static double GetWidth(Thickness thickness)
{
return thickness.Left + thickness.Right;
}
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
var removedUiElement = visualRemoved as UIElement;
if (removedUiElement != null)
_childToConstraint.Remove(removedUiElement);
}
private Size GetChildrenConstraint(Size availableSize)
{
return new Size
(_isHorizontal ? double.PositiveInfinity : availableSize.Width,
!_isHorizontal ? double.PositiveInfinity : availableSize.Height);
}
private double GetSizePart(Size size)
{
return _isHorizontal ? size.Width : size.Height;
}
private static bool IsGreater(double a, double b, double tolerance)
{
return a - b > tolerance;
}
private void MeasureChild(UIElement child, Size childConstraint)
{
Size lastConstraint;
if ((child.IsMeasureValid && _childToConstraint.TryGetValue(child, out lastConstraint) &&
lastConstraint.Equals(childConstraint))) return;
child.Measure(childConstraint);
_childToConstraint[child] = childConstraint;
}
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var panel = (SqueezeStackPanel) d;
panel._isHorizontal = panel.Orientation == Orientation.Horizontal;
}
private void RecalcChilds(double current, double target)
{
var shouldShrink = IsGreater(current, target, Tolerance);
if (shouldShrink)
ShrinkChildren(_children, target);
}
private void RemeasureChildren(Size availableSize)
{
var childrenCount = Children.Count;
if (childrenCount == 0)
return;
var childConstraint = GetChildrenConstraint(availableSize);
for (var index = 0; index < childrenCount; index++)
{
var child = _orderedSequence[index];
if (Math.Abs(GetSizePart(child.DesiredSize) - _children[index].Val) > Tolerance)
MeasureChild(child, new Size(_isHorizontal ? _children[index].Val : childConstraint.Width,
!_isHorizontal ? _children[index].Val : childConstraint.Height));
}
}
private static void ShrinkChildren(IEnumerable<Child> children, double target)
{
var sortedChilds = children.OrderBy(v => v.Val).ToList();
var minValidTarget = sortedChilds.Sum(s => s.Min);
if (minValidTarget > target)
{
foreach (var child in sortedChilds)
child.Val = child.Min;
return;
}
do
{
var tmpTarget = target;
for (var iChild = 0; iChild < sortedChilds.Count; iChild++)
{
var child = sortedChilds[iChild];
if (child.Val*(sortedChilds.Count - iChild) >= tmpTarget)
{
var avg = tmpTarget/(sortedChilds.Count - iChild);
var success = true;
for (var jChild = iChild; jChild < sortedChilds.Count; jChild++)
{
var tChild = sortedChilds[jChild];
tChild.Val = Math.Max(tChild.Min, avg);
// Min constraint skip success expand on this iteration
if (Math.Abs(avg - tChild.Val) <= Tolerance) continue;
target -= tChild.Val;
success = false;
sortedChilds.RemoveAt(jChild);
jChild--;
}
if (success)
return;
break;
}
tmpTarget -= child.Val;
}
} while (sortedChilds.Count > 0);
}
private class Child
{
public readonly double Min;
public double Val;
public Child(double min, double max, double val)
{
Min = min;
Val = val;
Val = Math.Max(min, val);
Val = Math.Min(max, Val);
}
}
}
Try using it as your ItemsPanelTemplate:
<ItemsControl Name="ButtonsControl" MaxWidth="400">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:SqueezeStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type dataclasses:TextNote}">
<Button Content="{Binding Title}" MinWidth="80"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I can't be sure based on the code that you have supplied, but I think you will have better layout results by removing your MaxWidth on the ItemsControl.
You can achieve something like this using a UniformGrid with Rows="1". The problem is that you can either have it stretched or not and neither of these options will do exactly what you want:
If it's stretched, then your "tabs" will always fill the whole available width. So, if you only have 1, it will be stretched across the whole width. If you set MaxWidth for the "tab", then if you have 2 they will not be adjacent but floating each in the middle of its column.
If it's left-aligned, then it will be difficult to get any padding/margin in your control, because when it shrinks, the padding will stay, making the actual content invisible.
So basically you need a control that has a "preferred" width:
When it has more space available than this preferred width, it sets itself to the preferred width.
When it has less space, it just takes up all the space it has.
This cannot be achieved using XAML (as far as I can tell), but it's not too difficult to do in code-behind. Let's create a custom control for the "tab" (namespaces omitted):
<ContentControl x:Class="WpfApplication1.UserControl1">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Border BorderBrush="Black" BorderThickness="1" Padding="0,5">
<ContentPresenter HorizontalAlignment="Center" Content="{TemplateBinding Content}"></ContentPresenter>
</Border>
</ControlTemplate>
</ContentControl.Template>
Code behind:
public partial class UserControl1 : ContentControl
{
public double DefaultWidth
{
get { return (double)GetValue(DefaultWidthProperty); }
set { SetValue(DefaultWidthProperty, value); }
}
public static readonly DependencyProperty DefaultWidthProperty =
DependencyProperty.Register("DefaultWidth", typeof(double), typeof(UserControl1), new PropertyMetadata(200.0));
public UserControl1()
{
InitializeComponent();
}
protected override Size MeasureOverride(Size constraint)
{
Size baseSize = base.MeasureOverride(constraint);
baseSize.Width = Math.Min(DefaultWidth, constraint.Width);
return baseSize;
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
Size baseBounds = base.ArrangeOverride(arrangeBounds);
baseBounds.Width = Math.Min(DefaultWidth, arrangeBounds.Width);
return baseBounds;
}
}
Then, you can create your ItemsControl, using a UniformGrid as the container:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:UserControl1 Content="{Binding}" Margin="0,0,5,0" DefaultWidth="150"></local:UserControl1>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1" HorizontalAlignment="Left"></UniformGrid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Here's a screenshot of the result with 3 items and many items (don't feel like counting them :)