How to recreate MenuItem ItemTapped and Command with a Button - c#

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

Related

Custom Selector control does not update after ItemsSource has been set?

I've put together a custom control in WPF in C# based on the Selector primitive:
public class PadControl : Selector
{
private const int padWidth = 28;
private const int padHeight = 18;
private const int padGap = 5;
private double _width;
private double _height;
static PadControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(PadControl), new FrameworkPropertyMetadata(typeof(PadControl),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));
}
public PadControl()
{
}
protected override Size MeasureOverride(Size constraint)
{
_width = constraint.Width;
_height = constraint.Height;
return base.MeasureOverride(constraint);
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
var numHorz = _width / (padWidth + padGap);
var numVert = Math.Min(_height / (padHeight + padGap), 16);
var pen = new Pen(Brushes.Black, 1.0);
var brush = new SolidColorBrush(Color.FromRgb(192, 192, 192));
for(int bar = 0; bar < numHorz; bar++)
{
for(int track = 0; track < numVert; track++)
{
var rect = GetPadRect(bar, track);
dc.DrawRectangle(brush, pen, rect);
}
}
if (Items == null)
return;
brush = new SolidColorBrush(Colors.LightYellow);
var typeface = new Typeface("Tahoma");
foreach(PadViewModel item in Items)
{
var rect = GetPadRect(item.Bar, item.Track);
dc.DrawRectangle(brush, pen, rect);
var formatted = new FormattedText(item.Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12, Brushes.Black, 1.0)
{
MaxTextWidth = padWidth,
TextAlignment = TextAlignment.Center
};
dc.DrawText(formatted, GetPadPoint(item.Bar, item.Track));
}
}
private Rect GetPadRect(int bar, int track)
{
var rect = new Rect(bar * (padWidth + padGap) + padGap, track * (padHeight + padGap) + padGap, padWidth, padHeight);
return rect;
}
private Point GetPadPoint(int bar, int track)
{
var point = new Point(bar * (padWidth + padGap) + padGap, track * (padHeight + padGap) + padGap);
return point;
}
}
This draws as I'd like it, but it doesn't draw the items until I resize the control.
When the control renders for the first time, it doesn't render any Items as that is empty. Items is populated indirectly with this:
<controls:PadControl Grid.Column="0" Grid.Row="0" ItemsSource="{Binding Pads}"/>
The problem is, I don't see an update come through after ItemsSource has been set. So the question is, how do I attach an event handler to catch this? Do I attach it to Items or to ItemsSource?
The answer is to override OnItemsSourceChanged as described by Keith Stein in the comments above.

Button with properties set in code-behind not firing click event

When setting the translationX and translationY of a button in the code-behind, its click events aren't fired: the button doesn't seem to be able to be clicked. The buttons are part of a StackLayout and I have used x:Name to access them from the code-behind!
The buttons properties except for the translations are set in the XAML, the formers are set like so:
element.TranslationX = array[i].xPos;
element.TranslationY = array[i].yPos;
UPDATE
Code-Behind
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
button.Clicked += Button_Clicked;
CircleLayout(132.0, Math.PI, 7, "menuGrid");
}
public void CircleLayout(double r, double theta, int nbElements, String currentMenu)
{
CircleObject[] array = new CircleObject[nbElements];
for (int i = 1; i < nbElements; i++)
{
if (i == 0)
{
array[i] = new CircleObject(r * Math.Sin(theta), r * Math.Cos(theta));
} else
{
theta += (Math.PI / 180) * 360 / nbElements;
array[i] = new CircleObject(r * Math.Sin(theta), r * Math.Cos(theta));
}
StackLayout element = this.FindByName<StackLayout>(currentMenu + i);
element.TranslationX = array[i].xPos;
element.TranslationY = array[i].yPos;
}
}
public class CircleObject
{
public double xPos;
public double yPos;
public CircleObject(double x, double y)
{
xPos = x;
yPos = y;
}
}
private void Button_Clicked(object sender, EventArgs e)
{
//do something
}
}
Example of a button in XAML
<StackLayout x:Name="menuGrid0">
<Button x:Name="button"
BackgroundColor="#FF6633"
CornerRadius="70"
HeightRequest="65"
WidthRequest="65"
HorizontalOptions="Center"/>
<Label x:Name="textDetection"
Text="D E T E C T I O N"
FontSize="9"
TextColor="#FF6633"
HeightRequest="10"
HorizontalOptions="Center"/>
</StackLayout>

Shrink ItemsControl items when visible space is filled

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 :)

WrapPanel with last-in-row fill

What I need: listbox with textboxes inside, textboxes wraps, and last in row fills remaining space:
|word 1||word 2___|
|word 3___________|
I'm trying to implement this behaviour using that advice. My xaml:
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{Binding Tags}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanelLastChildFill />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Text}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
MyWrapPanel (inherits form WrapPanel) code:
protected override Size MeasureOverride(Size constraint)
{
Size curLineSize = new Size();
Size panelSize = new Size(constraint.Width, 0);
UIElementCollection children = base.InternalChildren;
for (int i = 0; i < children.Count; i++)
{
UIElement child = children[i] as UIElement;
child.Measure(constraint);
Size sz = child.DesiredSize;
if (curLineSize.Width + sz.Width > constraint.Width) // new line
{
panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
panelSize.Height += curLineSize.Height;
if (i > 0)
{
// change width of prev control here
var lastChildInRow = children[i - 1] as Control;
lastChildInRow.Width = lastChildInRow.ActualWidth + panelSize.Width - curLineSize.Width;
}
curLineSize = sz;
}
else
{
curLineSize.Width += sz.Width;
curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
}
}
panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
panelSize.Height += curLineSize.Height;
return panelSize;
}
Thats work, but in one side only - textbox width never shrinks.
Any help appreciated.
I have done it recently.
You can see my code at: CodeProject
or use this class directly as shown:
Usage:
<TextBox MinWidth="120" wrapPanelWithFill:WrapPanelFill.UseToFill="True">*</TextBox>
Code:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
namespace WrapPanelWithFill
{
public class WrapPanelFill : WrapPanel
{
// ******************************************************************
public static readonly DependencyProperty UseToFillProperty = DependencyProperty.RegisterAttached("UseToFill", typeof(Boolean),
typeof(WrapPanelFill), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
// ******************************************************************
public static void SetUseToFill(UIElement element, Boolean value)
{
element.SetValue(UseToFillProperty, value);
}
// ******************************************************************
public static Boolean GetUseToFill(UIElement element)
{
return (Boolean)element.GetValue(UseToFillProperty);
}
// ******************************************************************
const double DBL_EPSILON = 2.2204460492503131e-016; /* smallest such that 1.0+DBL_EPSILON != 1.0 */
// ******************************************************************
private static bool DoubleAreClose(double value1, double value2)
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
// This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON
double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON;
double delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}
// ******************************************************************
private static bool DoubleGreaterThan(double value1, double value2)
{
return (value1 > value2) && !DoubleAreClose(value1, value2);
}
// ******************************************************************
private bool _atLeastOneElementCanHasItsWidthExpanded = false;
// ******************************************************************
/// <summary>
/// <see cref="FrameworkElement.MeasureOverride"/>
/// </summary>
protected override Size MeasureOverride(Size constraint)
{
UVSize curLineSize = new UVSize(Orientation);
UVSize panelSize = new UVSize(Orientation);
UVSize uvConstraint = new UVSize(Orientation, constraint.Width, constraint.Height);
double itemWidth = ItemWidth;
double itemHeight = ItemHeight;
bool itemWidthSet = !Double.IsNaN(itemWidth);
bool itemHeightSet = !Double.IsNaN(itemHeight);
Size childConstraint = new Size(
(itemWidthSet ? itemWidth : constraint.Width),
(itemHeightSet ? itemHeight : constraint.Height));
UIElementCollection children = InternalChildren;
// EO
LineInfo currentLineInfo = new LineInfo(); // EO, the way it works it is always like we are on the current line
_lineInfos.Clear();
_atLeastOneElementCanHasItsWidthExpanded = false;
for (int i = 0, count = children.Count; i < count; i++)
{
UIElement child = children[i] as UIElement;
if (child == null) continue;
//Flow passes its own constrint to children
child.Measure(childConstraint);
//this is the size of the child in UV space
UVSize sz = new UVSize(
Orientation,
(itemWidthSet ? itemWidth : child.DesiredSize.Width),
(itemHeightSet ? itemHeight : child.DesiredSize.Height));
if (DoubleGreaterThan(curLineSize.U + sz.U, uvConstraint.U)) //need to switch to another line
{
// EO
currentLineInfo.Size = curLineSize;
_lineInfos.Add(currentLineInfo);
panelSize.U = Math.Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
curLineSize = sz;
// EO
currentLineInfo = new LineInfo();
var feChild = child as FrameworkElement;
if (GetUseToFill(feChild))
{
currentLineInfo.ElementsWithNoWidthSet.Add(feChild);
_atLeastOneElementCanHasItsWidthExpanded = true;
}
if (DoubleGreaterThan(sz.U, uvConstraint.U)) //the element is wider then the constrint - give it a separate line
{
currentLineInfo = new LineInfo();
panelSize.U = Math.Max(sz.U, panelSize.U);
panelSize.V += sz.V;
curLineSize = new UVSize(Orientation);
}
}
else //continue to accumulate a line
{
curLineSize.U += sz.U;
curLineSize.V = Math.Max(sz.V, curLineSize.V);
// EO
var feChild = child as FrameworkElement;
if (GetUseToFill(feChild))
{
currentLineInfo.ElementsWithNoWidthSet.Add(feChild);
_atLeastOneElementCanHasItsWidthExpanded = true;
}
}
}
if (curLineSize.U > 0)
{
currentLineInfo.Size = curLineSize;
_lineInfos.Add(currentLineInfo);
}
//the last line size, if any should be added
panelSize.U = Math.Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
// EO
if (_atLeastOneElementCanHasItsWidthExpanded)
{
return new Size(constraint.Width, panelSize.Height);
}
//go from UV space to W/H space
return new Size(panelSize.Width, panelSize.Height);
}
// ************************************************************************
private struct UVSize
{
internal UVSize(Orientation orientation, double width, double height)
{
U = V = 0d;
_orientation = orientation;
Width = width;
Height = height;
}
internal UVSize(Orientation orientation)
{
U = V = 0d;
_orientation = orientation;
}
internal double U;
internal double V;
private Orientation _orientation;
internal double Width
{
get { return (_orientation == Orientation.Horizontal ? U : V); }
set { if (_orientation == Orientation.Horizontal) U = value; else V = value; }
}
internal double Height
{
get { return (_orientation == Orientation.Horizontal ? V : U); }
set { if (_orientation == Orientation.Horizontal) V = value; else U = value; }
}
}
// ************************************************************************
private class LineInfo
{
public List<UIElement> ElementsWithNoWidthSet = new List<UIElement>();
// public double SpaceLeft = 0;
// public double WidthCorrectionPerElement = 0;
public UVSize Size;
public double Correction = 0;
}
private List<LineInfo> _lineInfos = new List<LineInfo>();
// ************************************************************************
/// <summary>
/// <see cref="FrameworkElement.ArrangeOverride"/>
/// </summary>
protected override Size ArrangeOverride(Size finalSize)
{
int lineIndex = 0;
int firstInLine = 0;
double itemWidth = ItemWidth;
double itemHeight = ItemHeight;
double accumulatedV = 0;
double itemU = (Orientation == Orientation.Horizontal ? itemWidth : itemHeight);
UVSize curLineSize = new UVSize(Orientation);
UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height);
bool itemWidthSet = !Double.IsNaN(itemWidth);
bool itemHeightSet = !Double.IsNaN(itemHeight);
bool useItemU = (Orientation == Orientation.Horizontal ? itemWidthSet : itemHeightSet);
UIElementCollection children = InternalChildren;
for (int i = 0, count = children.Count; i < count; i++)
{
UIElement child = children[i] as UIElement;
if (child == null) continue;
UVSize sz = new UVSize(
Orientation,
(itemWidthSet ? itemWidth : child.DesiredSize.Width),
(itemHeightSet ? itemHeight : child.DesiredSize.Height));
if (DoubleGreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) //need to switch to another line
{
arrangeLine(lineIndex, accumulatedV, curLineSize.V, firstInLine, i, useItemU, itemU, uvFinalSize);
lineIndex++;
accumulatedV += curLineSize.V;
curLineSize = sz;
if (DoubleGreaterThan(sz.U, uvFinalSize.U)) //the element is wider then the constraint - give it a separate line
{
//switch to next line which only contain one element
arrangeLine(lineIndex, accumulatedV, sz.V, i, ++i, useItemU, itemU, uvFinalSize);
accumulatedV += sz.V;
curLineSize = new UVSize(Orientation);
}
firstInLine = i;
}
else //continue to accumulate a line
{
curLineSize.U += sz.U;
curLineSize.V = Math.Max(sz.V, curLineSize.V);
}
}
//arrange the last line, if any
if (firstInLine < children.Count)
{
arrangeLine(lineIndex, accumulatedV, curLineSize.V, firstInLine, children.Count, useItemU, itemU, uvFinalSize);
}
return finalSize;
}
// ************************************************************************
private void arrangeLine(int lineIndex, double v, double lineV, int start, int end, bool useItemU, double itemU, UVSize uvFinalSize)
{
double u = 0;
bool isHorizontal = (Orientation == Orientation.Horizontal);
Debug.Assert(lineIndex < _lineInfos.Count);
LineInfo lineInfo = _lineInfos[lineIndex];
double lineSpaceAvailableForCorrection = Math.Max(uvFinalSize.U - lineInfo.Size.U, 0);
double perControlCorrection = 0;
if (lineSpaceAvailableForCorrection > 0 && lineInfo.Size.U > 0)
{
perControlCorrection = lineSpaceAvailableForCorrection / lineInfo.ElementsWithNoWidthSet.Count;
if (double.IsInfinity(perControlCorrection))
{
perControlCorrection = 0;
}
}
int indexOfControlToAdjustSizeToFill = 0;
UIElement uIElementToAdjustNext = indexOfControlToAdjustSizeToFill < lineInfo.ElementsWithNoWidthSet.Count ? lineInfo.ElementsWithNoWidthSet[indexOfControlToAdjustSizeToFill] : null;
UIElementCollection children = InternalChildren;
for (int i = start; i < end; i++)
{
UIElement child = children[i] as UIElement;
if (child != null)
{
UVSize childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
double layoutSlotU = (useItemU ? itemU : childSize.U);
if (perControlCorrection > 0 && child == uIElementToAdjustNext)
{
layoutSlotU += perControlCorrection;
indexOfControlToAdjustSizeToFill++;
uIElementToAdjustNext = indexOfControlToAdjustSizeToFill < lineInfo.ElementsWithNoWidthSet.Count ? lineInfo.ElementsWithNoWidthSet[indexOfControlToAdjustSizeToFill] : null;
}
child.Arrange(new Rect(
(isHorizontal ? u : v),
(isHorizontal ? v : u),
(isHorizontal ? layoutSlotU : lineV),
(isHorizontal ? lineV : layoutSlotU)));
u += layoutSlotU;
}
}
}
// ************************************************************************
}
}
It was misunderstanding where to place width correction. This must be in ArrangeOverride:
private void ArrangeLine(double y, Size lineSize, double boundsWidth, int start, int end)
{
double x = 0;
UIElementCollection children = InternalChildren;
for (int i = start; i < end; i++)
{
UIElement child = children[i];
var w = child.DesiredSize.Width;
if (LastChildFill && i == end - 1) // last сhild fills remaining space
{
w = boundsWidth - x;
}
child.Arrange(new Rect(x, y, w, lineSize.Height));
x += w;
}
}

Textblock added to Panel resizes itself automatically

I have a custom Panel for laying out text. There is a DependancyProperty called "Text" and when that value changes, this piece of code runs:
if( !string.IsNullOrEmpty(Text))
{
Children.Clear();
foreach (char ch in Text)
{
TextBlock textBlock = new TextBlock();
textBlock.Text = ch.ToString();
textBlcok.Foreground = Foreground;
//The rest of these are DPs in the panel
textBlock.FontFamily = FontFamily;
textBlock.FontStyle = FontStyle;
textBlock.FontWeight = FontWeight;
textBlock.FontStretch = FontStretch;
textBlock.FontSize = FontSize;
Children.Add(textBlock);
}
}
}
Now, with font size of 15 and font Arial, these should be giving me a desired size of around 8 width and 10 height. However, when I do a Measure() and check the desired size, I get 40,18 every time!
So in trying to figure out what could've possibly changed the size, I put this code before and after the Children.Add in the code above:
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Console.WriteLine(ch.ToString() + ": " + textBlock.DesiredSize);
Children.Add(textBlock);
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Console.WriteLine(ch.ToString() + ": " + textBlock.DesiredSize);
What this gave me, was the proper desired size before it's added to the children collection, and a size of 40,18 (regardless of letter) after it's added to the collection.
What is causing this to happen?
Edit: You can find the full source for the control here:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using IQ.Touch.Resources.Classes.Helpers;
/* TextOnAPath.cs
*
* A slightly modified version of the control found at
* http://www.codeproject.com/KB/WPF/TextOnAPath.aspx
*/
namespace IQ.Touch.Resources.Controls
{
public class TextOnAPath : Panel
{
// Fields
PathFigureHelper pathFigureHelper = new PathFigureHelper();
Size totalSize;
// Dependency properties
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text",
typeof(string),
typeof(TextOnAPath),
new PropertyMetadata(OnFontPropertyChanged));
public static readonly DependencyProperty FontFamilyProperty =
DependencyProperty.Register("FontFamily",
typeof(FontFamily),
typeof(TextOnAPath),
new PropertyMetadata(new FontFamily("Portable User Interface"), OnFontPropertyChanged));
public static readonly DependencyProperty FontStyleProperty =
DependencyProperty.Register("FontStyle",
typeof(FontStyle),
typeof(TextOnAPath),
new PropertyMetadata(FontStyles.Normal, OnFontPropertyChanged));
public static readonly DependencyProperty FontSizeProperty =
DependencyProperty.Register("FontSize",
typeof(double),
typeof(TextOnAPath),
new PropertyMetadata(12.0, OnFontPropertyChanged));
public static readonly DependencyProperty FontWeightProperty =
DependencyProperty.Register("FontWeight",
typeof(FontWeight),
typeof(TextOnAPath),
new PropertyMetadata(FontWeights.Normal, OnFontPropertyChanged));
public static readonly DependencyProperty FontStretchProperty =
DependencyProperty.Register("FontStretch",
typeof(FontStretch),
typeof(TextOnAPath),
new PropertyMetadata(FontStretches.Normal, OnFontPropertyChanged));
public static readonly DependencyProperty ForegroundProperty =
DependencyProperty.Register("Foreground",
typeof(Brush),
typeof(TextOnAPath),
new PropertyMetadata(new SolidColorBrush(Colors.Black), OnFontPropertyChanged));
public static readonly DependencyProperty PathFigureProperty =
DependencyProperty.Register("PathFigure",
typeof(PathFigure),
typeof(TextOnAPath),
new PropertyMetadata(OnPathFigureChanged));
// Properties
public string Text
{
set { SetValue(TextProperty, value); }
get { return (string)GetValue(TextProperty); }
}
public FontFamily FontFamily
{
set { SetValue(FontFamilyProperty, value); }
get { return (FontFamily)GetValue(FontFamilyProperty); }
}
public FontStyle FontStyle
{
set { SetValue(FontStyleProperty, value); }
get { return (FontStyle)GetValue(FontStyleProperty); }
}
public double FontSize
{
set { SetValue(FontSizeProperty, value); }
get { return (double)GetValue(FontSizeProperty); }
}
public FontWeight FontWeight
{
set { SetValue(FontWeightProperty, value); }
get { return (FontWeight)GetValue(FontWeightProperty); }
}
public FontStretch FontStretch
{
set { SetValue(FontStretchProperty, value); }
get { return (FontStretch)GetValue(FontStretchProperty); }
}
public Brush Foreground
{
set { SetValue(ForegroundProperty, value); }
get { return (Brush)GetValue(ForegroundProperty); }
}
public PathFigure PathFigure
{
set { SetValue(PathFigureProperty, value); }
get { return (PathFigure)GetValue(PathFigureProperty); }
}
// Property-changed handlers
static void OnFontPropertyChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
(obj as TextOnAPath).OnFontPropertyChanged(args);
}
void OnFontPropertyChanged(DependencyPropertyChangedEventArgs args)
{
Children.Clear();
if (String.IsNullOrEmpty(Text))
return;
foreach (char ch in Text)
{
TextBlock textBlock = new TextBlock();
textBlock.Text = ch.ToString();
textBlock.FontFamily = FontFamily;
textBlock.FontStyle = FontStyle;
textBlock.FontWeight = FontWeight;
textBlock.FontStretch = FontStretch;
textBlock.FontSize = FontSize;
textBlock.Foreground = Foreground;
textBlock.HorizontalAlignment = HorizontalAlignment.Center;
textBlock.VerticalAlignment = VerticalAlignment.Bottom;
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Console.WriteLine(ch.ToString() + ": " + textBlock.DesiredSize);
Children.Add(textBlock);
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Console.WriteLine(ch.ToString() + ": " + textBlock.DesiredSize);
}
CalculateTransforms();
InvalidateMeasure();
}
static void OnPathFigureChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
(obj as TextOnAPath).OnPathFigureChanged(args);
}
void OnPathFigureChanged(DependencyPropertyChangedEventArgs args)
{
pathFigureHelper.SetPathFigure(args.NewValue as PathFigure);
CalculateTransforms();
InvalidateMeasure();
}
void CalculateTransforms()
{
double pathLength = pathFigureHelper.Length;
double textLength = 0;
double textDesiredWidth = 9;
totalSize = new Size();
foreach (UIElement child in Children)
{
child.Measure(new Size(Double.PositiveInfinity,
Double.PositiveInfinity));
textLength += child.DesiredSize.Width;
}
//textLength = Children.Count * textDesiredWidth;
if (pathLength == 0 || textLength == 0)
return;
//double scalingFactor = pathLength / textLength;
double baseline = FontSize; // * FontFamily.Baseline;
double progress = 0;
if (textLength <= pathLength)
{
progress = ((pathLength - textLength) / 2) / pathLength;
}
foreach (UIElement child in Children)
{
double width = child.DesiredSize.Width;
//double width = textDesiredWidth;
progress += width / 2 / pathLength;
Point point, tangent;
pathFigureHelper.GetPointAtFractionLength(progress,
out point, out tangent);
TransformGroup transformGroup = new TransformGroup();
//ScaleTransform scaleTransform = new ScaleTransform();
//scaleTransform.ScaleX = scalingFactor;
//scaleTransform.ScaleY = scalingFactor;
//transformGroup.Children.Add(scaleTransform);
RotateTransform rotateTransform = new RotateTransform();
rotateTransform.Angle = Math.Atan2(tangent.Y, tangent.X) * 180 / Math.PI;
rotateTransform.CenterX = width / 2;
rotateTransform.CenterY = baseline;
transformGroup.Children.Add(rotateTransform);
TranslateTransform translateTransform = new TranslateTransform();
translateTransform.X = point.X - width / 2;
translateTransform.Y = point.Y - baseline;
transformGroup.Children.Add(translateTransform);
child.RenderTransform = transformGroup;
BumpUpTotalSize(transformGroup.Value, new Point(0, 0));
BumpUpTotalSize(transformGroup.Value, new Point(0, child.DesiredSize.Height));
BumpUpTotalSize(transformGroup.Value, new Point(child.DesiredSize.Width, 0));
BumpUpTotalSize(transformGroup.Value, new Point(child.DesiredSize.Width, child.DesiredSize.Height));
progress += width / 2 / pathLength;
}
Point endPoint, endTangent;
pathFigureHelper.GetPointAtFractionLength(1, out endPoint, out endTangent);
totalSize.Width = Math.Max(totalSize.Width, endPoint.X);
}
void BumpUpTotalSize(Matrix matrix, Point point)
{
point = matrix.Transform(point);
totalSize.Width = Math.Max(totalSize.Width, point.X);
totalSize.Height = Math.Max(totalSize.Height, point.Y);
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in Children)
child.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
// return the size calculated during CalculateTransforms
return totalSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement child in Children)
child.Arrange(new Rect(new Point(0, 0), child.DesiredSize));
return finalSize;
}
}
}
You should horizontally align you TextBox to the left, right, or center. It is aligned as strech per default, thus expandig it to the available area.
Edit
Just testet it with a little class:
public class TextOnAPath : Panel
{
public TextOnAPath() {
var textBlock = new TextBlock();
textBlock.Text = "Test";
textBlock.Background = Brushes.Blue;
textBlock.VerticalAlignment = System.Windows.VerticalAlignment.Top;
textBlock.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
this.Children.Add(textBlock);
this.Background = Brushes.Gray;
}
protected override Size MeasureOverride(Size availableSize) {
return availableSize;
}
protected override Size ArrangeOverride(Size finalSize) {
foreach (UIElement child in this.Children)
child.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));
return finalSize;
}
}
Removing the alignments takes up all available space ... could it be that your CalculateTransforms method causes the effect? Especially, as the outcome is then used in the MeasureOverride method.
I figured out the problem, it turns out that the problem was not related to the control at all, but somewhere in the code the default template for a textblock got changed, and MinWidth and MinHeight got set to 40,18 for some reason... Now to find the suspect and to yell at them.
Thanks guys

Categories