Silverlight Grid - display value that doesnt fit its cell - silverlight

I know we can stretch item to the right or down using ColumnSpan and RowSpan
However I got an Item I want to display in grid cell and have it centered according to this cell center, so it will span both right and left in case of text, here I provide an image of how it currently looks, "12345" text can't fit the column and is displayed as "234", is there a way to make 1 & 5 visible leaving all columns untouched?
Text can be changed to anything and has to be centered correctly, I can hack a way to display 12345 this way , but need a solution that will work with any text, just in case,and textBlock grid.Column has to be left untouched 1 (or 2 to be precise for my case) , not 0 for this picture case.

Solution 4
So what you actually want is a way to center the text dynamically if I now understand correctly. You don't want to have to work out fiddling with the padding yourself, you want a number that works for everything? I sure hope you appreciate this! :)
/// <summary>
/// Takes a FrameworkElement that is spanned across
/// multiple columns and RETURNS a Thickness that
/// can be applied to the Padding property to
/// centre it across the spanned columns
/// </summary>
/// <param name="fe">The FrameworkElement to be centred</param>
/// <param name="centerColumn">The index of the desired centre column</param>
/// <returns>A Thickness that will center the FrameworkElement</returns>
private Thickness GetCenteringPadding(FrameworkElement fe, int centerColumn)
{
Grid parentGrid = fe.Parent as Grid;
if (parentGrid == null)
throw new ArgumentException();
// Variables
int firstColumn = (int)fe.GetValue(Grid.ColumnProperty);
int columnSpan = (int)fe.GetValue(Grid.ColumnSpanProperty);
double totalWidth = 0.0;
double leftWidth = 0.0;
double leftPaddingWidth = 0.0;
// Total the width for all the spanned columns
for (int i = firstColumn; i < columnSpan + firstColumn; i++)
{
// This part can be dangerous, especially if you're using a '*'
totalWidth += parentGrid.ColumnDefinitions[i].ActualWidth;
}
// Get the total width from the left side of the first
// spanned column, to the center of the 'centerColumn'
for (int j = firstColumn; j <= centerColumn; j++)
{
if (j != centerColumn)
leftWidth += parentGrid.ColumnDefinitions[j].ActualWidth;
else // Only take half the width for the center column
leftWidth += parentGrid.ColumnDefinitions[j].ActualWidth / 2;
}
// Calculate the padding width
// (Abbr. rightWidth = tW - lW, lPW = lW - rW)
leftPaddingWidth = 2*leftWidth - totalWidth;
// Check whether the padding needs to be on the left or the right
if (leftPaddingWidth > 0.0)
{
// The excess space is on the left
return new Thickness(leftPaddingWidth, 0.0, 0.0, 0.0);
}
else
{
// The excess space is on the right
return new Thickness(0.0, 0.0, -leftPaddingWidth, 0.0);
}
}
A couple of notes. It uses the .ActualWidth of the columns in the Grid. You can change this of course to .Width, however you may run into problems when you're using ColumnDefinition Width="30*" because I'm quite sure you won't get a double as the result then. Secondly, this function doesn't set the padding on the object you pass for two reasons:
- You can take the returned Thickness and add it to the .Padding already applied to a Control so that you don't destroy it's formatting.
- You can pass any FrameworkElement, whereas only Controls have padding. Perhaps you could add the returned thickness to the .Margin property instead, or even create a container and apply padding within that?
Finally, you could easily combine the two for loops within the function, but I left them seperate from clarity.
Cheers.

I'm not sure I fully understand the restrictions on your problem, so it's possible that neither of these solutions will work, but here's two quick suggestions. (Personally, I like Solution 2 better.)
<Edit: Additional Thought>
Solution 0
If you're able to change the Grid.ColumnSpan of the textblock, you can do solution 2 without the container. Just span the textblock over a couple of columns and adjust the padding of the textblock to center the contents!
</Edit>
Solution 1
First off, if your current XAML structure looks something like this:
<Grid>
<TextBlock Grid.Column="2" Text="12345" />
</Grid>
Consider changing it to something like this?
<Grid>
<!-- Draw up columns so that the textblock looks as you wish -->
<TextBlock Grid.Column="2" Text="12345" />
<!-- Span an inner grid across all the columns/rows in the outer grid -->
<Grid Grid.ColumnSpan=".."
Grid.RowSpan="..">
<!-- All the other stuff in the Grid -->
</Grid>
</Grid>
Solution 2
If that can't work, consider housing the textblock in another container such as a Label, and spanning that across the columns.
<Grid>
<!-- Use the left/right padding on the label to center the textbox -->
<sdk:Label Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
HorizontalAlignment="Center"
Padding="13,0,0,0">
<!-- In my example, setting the left pading to 13 centered the textbox -->
<TextBlock HorizontalAlignment="Center"
Text="......!......" />
<!-- Text like the test above, really helped find the correct padding -->
</sdk:Label>
</Grid>
I hope this works within your restrictions, and if not, helps you figure out a solution yourself!

Related

Binding of relative position of a control

I have a draggable user control within another user control indicating the distance of a vessel from another vessel. The placement of the object is bound to X, Y values in the back end. If the user loads a "mission", the location of this draggable object should snap to the position based on the X and Y values. Additionally, if the user drags the boat but then cancels, the position of the boat should snap back to its original position before it was dragged.
I find that if I bind to the Margin field, the margin is set correctly on the control but the draggable object stays in the same place, as if it is at 0,0,0,0 with the updated values (I can force it to move by editing the xaml and proving that any margin changes act as if the new values are 0,0,0,0:
public Thickness DaughtershipMargin
{
get { return _daughtershipMargin; }
set
{
_daughtershipMargin = value;
OnPropertyChanged();
}
}
I have also experimented with using a Canvas instead with similar results.
When initially dragging, the position is based on the relative position of the control based on a parent control - so it needs I need to do something similar
private void MouseDragElementBehavior_Dragging(object sender, MouseEventArgs e)
{
double x = System.Math.Round((e.GetPosition(colabdragObjectGrid).X - 280), 3);
double y = System.Math.Round((-1 * (e.GetPosition(colabdragObjectGrid).Y - 280)), 3);
if (x >= _minFollowDistance && x <= _maxFollowDistance && y >= _minFollowDistance && y <= _maxFollowDistance)
{
_parent.ProcessDragEvent(x.ToString(), y.ToString());
}
}
<Grid x:Name="colabdragObjectGrid" Margin="0,-10,0,0" Height="575" Width="575">
<local:colabdrag x:Name="colabdragObject" Height="100" Width="100" RenderTransformOrigin="0.5,0.5" Margin="{Binding DaughtershipMargin}">
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior ConstrainToParentBounds="True" Dragging="MouseDragElementBehavior_Dragging"/>
</i:Interaction.Behaviors>
</local:colabdrag>
</Grid>
Edit:
I got this to work by creating a point relative to the draggable area and another point for the desired position based on that point, then using TranslateTranform to get the object where it needs to be
var pointA = (colabdragObject.TransformToAncestor(colabDragObjectGrid).Transform(new Point(X,Y))
var pointB = colabdragObjectGrid.TranslatePoint(pointA, colabDragObject);
colabdragObject.RenderTransform = new TranslateTransform(pointB.X, -1 * pointB.Y);

Dynamically adjust ItemWidth in WrapPanel

I am using WPF MVVM. I have the following code:
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ListView ItemsSource="{Binding ItemCollection}" Height="160" Width="810">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Width="500" Height ="150" ItemWidth="100" ItemHeight="30"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Checked}">
<TextBlock Text="{Binding Label}"/>
</CheckBox>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ScrollViewer>
The code shows that each CheckBox has a width of 100. Each line in the WrapPanel can contain at most 5 CheckBoxes due to its size and the ItemWidth (500 / 100).
I have multiple CheckBox with different widths.
Most of the check boxes have a width <=100
One check box has a width equal to 280
In the future I might have a check box with a width >300 and <=400
I do not want to set the ItemWidth explicitly to 280, because most of the items are smaller. Instead, I want each CheckBox to take up the space of a multiple of 100 that can display all of its content. If the current line in the WrapPanel does not have enough space, then move it to next line.
For the sample widths above I expect this.
Width <= 100: Occupy 1 item space (Width = 100)
Width <= 280: Occupy 3 item spaces (Width = 300)
Width > 300 and <= 400: Occupy 4 item spaces (Width = 400)
How can I achieve that?
The ItemWidth explicitly defines the width for all items.
A Double that represents the uniform width of all items that are contained within the WrapPanel. The default value is NaN.
Do not set an ItemWidth. Then each item occupies its individual size.
A child element of a WrapPanel may have its width property set explicitly. ItemWidth specifies the size of the layout partition that is reserved by the WrapPanel for the child element. As a result, ItemWidth takes precedence over an element's own width.
Now, if you do not explicitly define the widths of your items, they are sized to fit their content, but do not align with a multiple of 100. Scaling items up to a multiple of a defined size automatically is not supported in WrapPanel.
If you want to enable this kind of dynamic sizing, you will have to create a custom wrap panel or you can write a custom behavior. I show you an example of the latter, as it is reusable and more flexible. I use the Microsoft.Xaml.Behaviors.Wpf NuGet package that contains base classes for that.
public class AlignWidthBehavior : Behavior<FrameworkElement>
{
public static readonly DependencyProperty AlignmentProperty = DependencyProperty.Register(
nameof(Alignment), typeof(double), typeof(AlignWidthBehavior), new PropertyMetadata(double.NaN));
public double Alignment
{
get => (double)GetValue(AlignmentProperty);
set => SetValue(AlignmentProperty, value);
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.LayoutUpdated += OnLayoutUpdated;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.LayoutUpdated -= OnLayoutUpdated;
}
private void OnLayoutUpdated(object sender, EventArgs e)
{
var size = AssociatedObject.ActualWidth;
var alignment = Alignment;
var isAligned = size % alignment < 10E-12;
if (!double.IsNaN(alignment) && !isAligned)
AssociatedObject.Width = Math.Ceiling(size / alignment) * alignment;
}
}
This behavior is triggered, when the layout of an item changes. It then checks, if the width of the associated item is aligned with the value given by the Alignment property and adapts it if not.
<DataTemplate>
<CheckBox IsChecked="{Binding Checked}">
<b:Interaction.Behaviors>
<local:AlignWidthBehavior Alignment="100"/>
</b:Interaction.Behaviors>
<TextBlock Text="{Binding Label}"/>
</CheckBox>
</DataTemplate>
You have to attach the behavior in XAML as shown above and set the Alignment to your desired value and do not forget to remove the ItemWidth property from WrapPanel.

Find out the real (!) displayed size of a control in WPF

I'm doing some layouting on a toolbar-like control and need to hide texts of buttons when there's not enough space. I've successfully done this in Windows Forms already and now I've ported this logic to WPF. But there is a huge problem here: For my algorithm to work properly, I need to know the desired width of a container control (to know what size would be required if everything was visible) and the actual width of the control (to know how wide it really is and whether there's enough space for the desired width). The first one is available, albeit a bit backwards at times. (If there's more space available than required, the DesiredSize increases to fill it all out, although less would be fine.) The latter one is entirely unavailable!
I've tried with ActualWidth, but if the Grid is wider than the window, the ActualWidth is more than is actually visible. So this must be wrong already. I've then tried the RenderSize, but it's the same. Using Arrange after my Measure call leads to more weirdness.
I need to know how wide the control really is, and not how wide it believes itself to be. How can I determine that size?
Update: Okay, here's some code. It's already quite long for this question and still incomplete. This is from the Window's code-behind.
private void ToolGrid_LayoutUpdated(object sender, EventArgs e)
{
AutoCollapseItems();
}
private void AutoCollapseItems()
{
if (collapsingItems) return;
if (ToolGrid.ActualWidth < 10) return; // Something is wrong
try
{
collapsingItems = true;
// Collapse toolbar items in their specified priority to save space until all items
// fit in the toolbar. When collapsing, the item's display style is reduced from
// image and text to image-only. This is only applied to items with a specified
// collapse priority.
Dictionary<ICollapsableToolbarItem, int> collapsePriorities = new Dictionary<ICollapsableToolbarItem, int>();
// Restore the display style of all items that have a collpase priority.
var items = new List<ICollapsableToolbarItem>();
EnumCollapsableItems(ToolGrid, items);
foreach (var item in items)
{
if (item.CollapsePriority > 0)
{
item.ContentVisibility = Visibility.Visible;
collapsePriorities[item] = item.CollapsePriority;
}
}
// Group all items by their descending collapse priority and set their display style
// to image-only as long as all items don't fit in the toolbar.
var itemGroups = from kvp in collapsePriorities
where kvp.Value > 0
group kvp by kvp.Value into g
orderby g.Key descending
select g;
foreach (var grp in itemGroups)
{
//ToolGrid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
//ToolGrid.Arrange(new Rect(ToolGrid.DesiredSize));
//ToolGrid.UpdateLayout();
System.Diagnostics.Debug.WriteLine("Desired=" + ToolGrid.DesiredSize.Width + ", Actual=" + ToolGrid.ActualWidth);
if (ToolGrid.DesiredSize.Width <= ToolGrid.ActualWidth) break;
foreach (var kvp in grp)
{
kvp.Key.ContentVisibility = Visibility.Collapsed;
}
}
//ToolGrid.UpdateLayout();
}
finally
{
collapsingItems = false;
}
}
More code: Here's part of the Window XAML:
<Window>
<DockPanel>
<Grid Name="ToolGrid" DockPanel.Dock="Top" LayoutUpdated="ToolGrid_LayoutUpdated">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
...
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
</Grid>
From what I understood you are using Grid but you set the columns width to Auto, how about you use * for the Width of your Grid.Column istead of Auto. If Auto then Grid stretches its Width and Height to fit its content hence why your Grid.Width is greater than windows width. When you use * the column wont care about content but it will always be inside the windows boundaries.
Now after implementing *, you use the column.width/height, which is inside window boundaries as your final width/height and inside the Grid you can measure the desized size of your nested innner controls. Thats how you get the final size and the desized size of controls.
Show some more code/xaml and we will be able to help you furthermore.
Edited:
<Window>
<DockPanel x:Name="dockyPanel>
<Grid Name="ToolGrid" DockPanel.Dock="Top" LayoutUpdated="ToolGrid_LayoutUpdated">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
...
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
</Grid>
var itemGroups = from kvp in collapsePriorities
where kvp.Value > 0
group kvp by kvp.Value into g
orderby g.Key descending
select g;
double x = 0.0;
foreach (var grp in itemGroups)
{
// x will be increased by the sum of all widths of items
x += grp.SumOfAllWidthOfGroup;
// if x greater than available space then this group needs to collaps its items
if(x > this.dockyPanel.ActualWidth)
{
foreach (var kvp in grp)
{
kvp.Key.ContentVisibility = Visibility.Collapsed;
}
}
}
How about this? Will my pseudocode help you any further?
It's turned out that WPF won't give me predictable sizes of the Grid that contains all the auto-sized columns with the button elements in them. But the parent element of this Grid, no matter what it is and how it is layouted, provides usable information on that. So my solution is basically to insert another level of Grid container between what was already there and my actual toolbar layout grid and compare the different sizes of both. The central test to find out whether the grid would fit in the given space is now this:
foreach (var grp in itemGroups)
{
InnerToolGrid.UpdateLayout();
if (InnerToolGrid.RenderSize.Width - extentWidth <= ToolGrid.ActualWidth) break;
foreach (var kvp in grp)
{
kvp.Key.ContentVisibility = Visibility.Collapsed;
}
}
It iterates through all priority classes of elements that may be collapsed together (whereever they are located on the grid) and collapses all elements in a class (group). Then the inner grid's layout is updated to reflect the changes of the buttons and the inner grid's RenderSize is compared with the outer grid's ActualWidth. If it's smaller, it fits in and no more items need to be collapsed.
All of this is invoked from the LayoutUpdated event of the inner grid, still preventing recursion through a lock variable. This allows me to react on size changes of any button on the toolbar, for example when a text was updated.
Since the LayoutCompleted event seems to be triggered asynchronously, the lock variable must remain set until the next layout run has completed, and cannot be reset again at the end of the LayoutUpdated handler:
private bool collapsingItems;
private void InnerToolGrid_LayoutUpdated(object sender, EventArgs e)
{
if (collapsingItems) return;
// Prevent further calls before the layouting is completely finished
collapsingItems = true;
Dispatcher.BeginInvoke(
(Action) (() => { collapsingItems = false; }),
System.Windows.Threading.DispatcherPriority.Loaded);
// ...
}

Can a WPF ListBox's height be set to a multiple of its item height?

Is there some way to set the Height attribute of a WPF multi-select ListBox to be a multiple of the item height, similar to setting the size attribute of an html select element?
I have a business requirement to not have half an item showing at the bottom of the list (if it's a long list with a scrollbar), and not have extra white space at the bottom (if it's a short list with all items showing), but the only method I can find to do this is to just keep tweaking the Height until it looks about right.
(What else have I tried? I've asked colleagues, searched MSDN and StackOverflow, done some general Googling, and looked at what VS Intellisense offered as I edited the code. There's plenty of advice out there about how to set the height to fit the ListBox's container, but that's the opposite of what I'm trying to do.)
Yeah, one could imagine there would be an easier way to do it (a single snapToWholeElement property). I couldn't find this property as well.
To achieve your requirement, I've wrote a little logic. Basically, In my Windows object I've a public property lbHeight which is calculate the listbox height by calculating the height of each individual item.
First, let's take a look at the XAML:
<Window
x:Class="SO.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="120" SizeToContent="Height"
Title="SO Sample"
>
<StackPanel>
<ListBox x:Name="x_list" Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}, Path=lbHeight}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Border x:Name="x" Background="Gray" Margin="4" Padding="3">
<TextBlock Text="{Binding}" />
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Window>
Note that the ItemTemplate is somewhat non trivial. One important thing to notice is that I gave this item a Name - so I can find it later.
In the code-behind constructor I put some data in the list box:
public MainWindow( )
{
InitializeComponent( );
this.x_list.ItemsSource = Enumerable.Range( 0, 100 );
}
next, I'm implementing a findVisualItem - to find the root element of the data template. I've made this function a little generic, so it get a predicate p which identify whether this is the element I want to find:
private DependencyObject findVisualItem( DependencyObject el, Predicate<DependencyObject> p )
{
DependencyObject found = null;
if( p(el) ) {
found = el;
}
else {
int count = VisualTreeHelper.GetChildrenCount( el );
for( int i=0; i<count; ++i ) {
DependencyObject c = VisualTreeHelper.GetChild( el, i );
found = findVisualItem( c, p );
if( found != null )
break;
}
}
return found;
}
I'll use the following predicate, which returns true if the element I'm looking for is a border, and its name is "x". You should modify this predicate to match your root element of your ItemTemplate.
findVisualItem(
x_list,
el => { return ( el is Border ) ? ( (FrameworkElement)el ).Name == "x" : false; }
);
Finally, the lbHeight property:
public double lbHeight
{
get {
FrameworkElement item = findVisualItem(
x_list,
el => { return ( el is Border ) ? ( (FrameworkElement)el ).Name == "x" : false; }
) as FrameworkElement;
if( item != null ) {
double h = item.ActualHeight + item.Margin.Top + item.Margin.Bottom;
return h * 12;
}
else {
return 120;
}
}
}
I've also made the Window implementing INotifyPropertyChanged, and when the items of the list box were loaded (Loaded event of ListBox) I fired a PropertyChanged event for the 'lbHeight' property. At some point it was necessary, but at the end WPF fetched the lbHeight property when I already have a rendered Item.
It is possible your Items aren't identical in Height, in which case you'll have to sum all the Items in the VirtualizedStackPanel. If you have a Horizontal scroll bar, you'll have to consider it for the total height of course. But this is the overall idea. It is only 3 hours since you published your question - I hope someone will come with a simpler answer.
This is done by setting parent control Height property to Auto, without setting any size to the Listbox itself (or also setting to Auto).
To limit the list size you should also specify MaxHeight Property

WPF: how to make the (0,0) in center inside a Canvas

The WPF Canvas has a coordinate system starting at (0,0) at the top-left of the control.
For example, setting the following will make my control appear on the top-left:
<Control Canvas.Left="0" Canvas.Top="0">
How can I change it to the standard cartesian coordinates?
Basically:
(0,0) at center
flip Y
I noticed this post is similar, but it does not talk about translating the coordinate system. I tried adding a TranslateTransform, but I can't make it work.
There is no need to create a custom Panel. Canvas will do just fine. Simply wrap it inside another control (such as a border), center it, give it zero size, and flip it with a RenderTransform:
<Border>
<Canvas HorizontalAlignment="Center" VerticalAlignment="Center"
Width="0" Height="0"
RenderTransform="1 0 0 -1 0 0">
...
</Canvas>
</Border>
You can do this and everything in the canvas will still appear, except (0,0) will be at the center of the containing control (in this case, the center of the Border) and +Y will be up instead of down.
Again, there is no need to create a custom panel for this.
It was very easy to do. I looked at the original Canvas's code using .NET Reflector, and noticed the implementation is actually very simple. The only thing required was to override the function ArrangeOverride(...)
public class CartesianCanvas : Canvas
{
public CartesianCanvas()
{
LayoutTransform = new ScaleTransform() { ScaleX = 1, ScaleY = -1 };
}
protected override Size ArrangeOverride( Size arrangeSize )
{
Point middle = new Point( arrangeSize.Width / 2, arrangeSize.Height / 2 );
foreach( UIElement element in base.InternalChildren )
{
if( element == null )
{
continue;
}
double x = 0.0;
double y = 0.0;
double left = GetLeft( element );
if( !double.IsNaN( left ) )
{
x = left;
}
double top = GetTop( element );
if( !double.IsNaN( top ) )
{
y = top;
}
element.Arrange( new Rect( new Point( middle.X + x, middle.Y + y ), element.DesiredSize ) );
}
return arrangeSize;
}
}
You can simply change the Origin with RenderTransformOrigin.
<Canvas Width="Auto" Height="Auto"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="0.5,0.5">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="-1" ScaleX="1" />
</TransformGroup>
</Canvas.RenderTransform>
</Canvas>
The best thing is to write a custom Canvas, in which you can write ArrangeOverride in such a way that it takes 0,0 as the center.
Update : I had given another comment in the below answer (#decasteljau) I wont recommend deriving from Canvas, You can derive from Panel and add two Attached Dependancy properties Top and Left and put the same code you pasted above. Also doesnt need a constructor with LayoutTransform in it, And dont use any transform on the panel code use proper measure and arrange based on the DesiredSize of the panel So that you can get nice content resize behavior too. Canvas doesn't dynamically position items when the Canvas size changes.

Resources