Reputation: 20354
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>
Upvotes: 1
Views: 3015
Reputation: 20354
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);
// ...
}
Upvotes: 1
Reputation: 672
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?
Upvotes: 1