I have a problem with WPF grid. Need some help.
I have a grid splitted in two columns (e.g. "col1" and "col2" from left to right). This grid is bound to the window edges with no padding:
|--- col1 ---|--- col2 ---| ← right window border
How can I make these columns resize together with the app window in certain order?
I mean this:
when I shrink the window from the right border I need col2 to resize in first place. When col2 reaches its MinWidth — then col1 begins to shrink (I continue to move the right border of the window).
Is it possible to define the order in which columns change their size?
Or may be I need something else but the grid?
Thanks.
Understanding how to handle events and fire off your own is a cornerstone of understanding C# so I highly recommend you read up on it. That being said, this will get you started, although I'm not sure this is exactly what you want... See example code below. I leave writing code for when the window width is increased as an exercise to you.. Good luck =).
On my MainWindow I set Height="250" Width="600" and the Grid inside it gets two columns...
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="_col1" MinWidth="200" />
<ColumnDefinition x:Name="_col2" MinWidth="150"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="_txtCol1Width"
Grid.Column="0"
Background="Crimson" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<TextBlock x:Name="_txtCol2Width"
Grid.Column="1"
Background="Turquoise" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</Grid>
x
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
this.SizeChanged += MainWindow_SizeChanged;
}
void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (this.IsLoaded && e.WidthChanged)
{
double changeInWindowWidth = e.NewSize.Width - e.PreviousSize.Width;
if (changeInWindowWidth < 0)
{
if (_col2.ActualWidth + changeInWindowWidth >= _col2.MinWidth)
{
// col 2 has not yet reached its minimum (decrease col2, no change for col1)
_col2.Width = new GridLength(_col2.ActualWidth + changeInWindowWidth, GridUnitType.Pixel);
_col1.Width = new GridLength(_col1.ActualWidth + 0, GridUnitType.Pixel);
}
else if (_col1.ActualWidth + changeInWindowWidth >= _col1.MinWidth)
{
// col 2 has reached its minimum, but col1 has not (decrease col1, no change for col2)
_col1.Width = new GridLength(_col1.ActualWidth + changeInWindowWidth, GridUnitType.Pixel);
_col2.Width = new GridLength(_col2.ActualWidth + 0, GridUnitType.Pixel);
}
else
{
// both columns have reached their minimum, so decrease width of both equally
_col1.MinWidth = _col1.ActualWidth + 0.5 * changeInWindowWidth;
_col2.MinWidth = _col2.ActualWidth + 0.5 * changeInWindowWidth;
_col1.Width = new GridLength(_col1.MinWidth, GridUnitType.Pixel);
_col2.Width = new GridLength(_col2.MinWidth, GridUnitType.Pixel);
}
}
else
{
// todo: handle window width increased ...
}
UpdateTexts();
}
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
UpdateTexts();
}
private void UpdateTexts()
{
_txtCol1Width.Text = String.Format("column {0}\nActualWidth: {1}\n(MinWidth: {2})", 1, _col1.ActualWidth, _col1.MinWidth);
_txtCol2Width.Text = String.Format("column {0}\nActualWidth: {1}\n(MinWidth: {2})", 2, _col2.ActualWidth, _col2.MinWidth);
}
}
Related
I'd like to get the on-screen position and visibility of a TextBlock which is embedded in a PivotItem - e.g. the on-screen position and visibility of TB2 in:
<phone:Pivot>
<phone:PivotItem Header="first">
<TextBlock x:Name="TB1" Text="Hello 1"></TextBlock>
</phone:PivotItem>
<phone:PivotItem Header="second" >
<TextBlock x:Name="TB2" Text="Hello 2"></TextBlock>
</phone:PivotItem>
<phone:PivotItem Header="three" >
<TextBlock x:Name="TB3" Text="Hello 3"></TextBlock>
</phone:PivotItem>
</phone:Pivot>
For other Xaml controls I have achieved this using code like:
public static Rect Position(this FrameworkElement element)
{
if (element.Visibility == Visibility.Collapsed)
return Rect.Empty;
if (element.Opacity < 0.01)
return Rect.Empty;
// Obtain transform information based off root element
GeneralTransform gt = element.TransformToVisual(Application.Current.RootVisual);
// Find the four corners of the element
Point topLeft = gt.Transform(new Point(0, 0));
Point topRight = gt.Transform(new Point(element.RenderSize.Width, 0));
Point bottomLeft = gt.Transform(new Point(0, element.RenderSize.Height));
Point bottomRight = gt.Transform(new Point(element.RenderSize.Width, element.RenderSize.Height));
var left = Math.Min(Math.Min(Math.Min(topLeft.X, topRight.X), bottomLeft.X), bottomRight.X);
var top = Math.Min(Math.Min(Math.Min(topLeft.Y, topRight.Y), bottomLeft.Y), bottomRight.Y);
var position = new Rect(left, top, element.ActualWidth, element.ActualHeight);
return position;
}
However, for PivotItem children this calculation doesn't provide the expected answers when the PivotItem is off-screen. Instead it provides the answer of the position the item will take on-screen when the PivotItem is swiped back on-screen. During a transition, I can see that the position is correctly calculated using the above method - e.g. during the transition I will see the position correctly swipe in from the left/right of the screen.
I've looked through other properties like Visible and Opacity to see if there is some other mechanism being used here to hide the off-screen PivotItem or one of its Parent, GrandParent, etc - but all of those seem to yield Visible and Opacity == 1.0 results.
I've also tried iterating the VisualTree to see if any element with a solid background is masking the "off-screen" PivotItems - but I can't see any in the tree.
Is there some other property to consider here? How are the PivotItem contents hidden when they are "off-screen"? If I really have to, I know that I can use the SelectedIndex property of the Pivot to try to help with the PivotItem position calculations, but I'm hoping to avoid SelectedIndex if I can - I'd prefer to get the position using general Xaml and VisualTree methods if at all possible.
I looked into it, but I can't find how they are hiding the PivotItems. I guess the position is correct, but the item is just not drawn for unknown (to me) reasons. I'll look into it again, later, but for now I wanted to mention a few other potential issues I noticed.
Shouldn't you be using Math.Max for the bottom right corner? Reason being - ActualHeight and ActualWidth are not rotation-aware so to speak.
RenderSize.Width/Height and ActualWidth/Height are different. I'm not sure it's a good idea to use both of them for the calculation.
Don't you need to check everything in the visual tree above the element to see if it's actually visible?
Edit: I looked at Microsoft.Phone.dll's decompiled source (that's the dll with the Pivot control), and I couldn't find anything about how the PivotItems are hidden. There are some native method calls though.
I have made some Changes to Your code and its giving UIElement's true position
XAML:
<Grid x:Name="LayoutRoot" Background="Transparent">
<!--Pivot Control-->
<phone:Pivot x:Name="MyPivot">
<phone:PivotItem Header="first" x:Name="first">
<TextBlock x:Name="TB1" FontSize="30" Text="Hello 1" Tap="TB1_Tap"></TextBlock>
</phone:PivotItem>
<phone:PivotItem Header="second" x:Name="second">
<TextBlock x:Name="TB2" FontSize="50" Height="66" Width="155" Margin="20" Text="Hello 2" Tap="TB2_Tap"></TextBlock>
</phone:PivotItem>
<phone:PivotItem Header="three" x:Name="three">
<TextBlock x:Name="TB3" Text="Hello 3"></TextBlock>
</phone:PivotItem>
</phone:Pivot>
</Grid>
CS:
public partial class PivotPage1 : PhoneApplicationPage
{
public PivotPage1()
{
InitializeComponent();
}
private void TB1_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
Rect r = Pos.MyPosition(TB1,first);
string str = "Left" + r.Left.ToString() + "\nTop:" + r.Top.ToString() + "\nRight:" + r.Right.ToString() + "\nBottom:" + r.Bottom.ToString() + "\nHeight:" + r.Height.ToString() + "\nWidth" + r.Width.ToString();
MessageBox.Show(str);
}
private void TB2_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
Rect r = Pos.MyPosition(TB2,second);
string str = "Left" + r.Left.ToString() + "\nTop:" + r.Top.ToString() + "\nRight:" + r.Right.ToString() + "\nBottom:" + r.Bottom.ToString() + "\nHeight:" + r.Height.ToString() + "\nWidth" + r.Width.ToString();
MessageBox.Show(str);
}
}
public static class Pos
{
public static Rect MyPosition(FrameworkElement child,FrameworkElement parent)
{
if (child.Visibility == Visibility.Collapsed)
return Rect.Empty;
if (child.Opacity < 0.01)
return Rect.Empty;
// Obtain transform information based off root child
GeneralTransform gt = child.TransformToVisual(parent);
// Find the four corners of the child
Point topLeft = gt.Transform(new Point(0, 0));
Point topRight = gt.Transform(new Point(child.RenderSize.Width, 0));
Point bottomLeft = gt.Transform(new Point(0, child.RenderSize.Height));
Point bottomRight = gt.Transform(new Point(child.RenderSize.Width, child.RenderSize.Height));
var left = Math.Min(Math.Min(Math.Min(topLeft.X, topRight.X), bottomLeft.X), bottomRight.X);
var top = Math.Min(Math.Min(Math.Min(topLeft.Y, topRight.Y), bottomLeft.Y), bottomRight.Y);
var position = new Rect(left, top, child.ActualWidth, child.ActualHeight);
return position;
}
}
I set the autocompletebox just like this:
<StackPanel Orientation="Horizontal">
<StackPanel Width="120">
<Label Content="Address"/>
<Controls:AutoCompleteBox x:Name="AddressBox" MaxDropDownHeight="300" Populating="Address_Populating"/>
</StackPanel>
<StackPanel Width="120" Margin="40, 0, 0, 0">
<Label Content="Port"/>
<TextBox x:Name="PortBox" />
</StackPanel>
<Button x:Name="ConnectButton" Content="Connect" Margin="40, 0, 0, 0" VerticalAlignment="Bottom" Width="80" Height="35" Click="ConnectButton_Clicked"/>
</StackPanel>
But the max number of items displayed in the dropdown window is only 3. I am sure that the candidate number is larger than 3. I want to increase the number of items which will be displayed in the dropdown window.
For example, i want to show 15 candidateAddress's items. And the dropdown window will be displayed and 3 items will be showed firstly. But I hope it can show 5 items firstly, which means that the display area should be expanded.
The logical code of this control is:
private void Address_Populating(object sender, PopulatingEventArgs e)
{
string dirFile = "../../Config/Address.config";
if (File.Exists(dirFile))
{
var candidateAddress = new List<string>();
string input = null;
using (StreamReader sr = File.OpenText(dirFile))
{
while ((input = sr.ReadLine()) != null)
{
candidateAddress.Add(input);
}
}
AddressBox.ItemsSource = candidateAddress;
AddressBox.PopulateComplete();
}
else
{
System.Windows.MessageBox.Show("Address.config does not exist");
}
}
i can't add a pic to comment. so i add it here.
top pic : MaxDropDownHeight=50
bottom: MaxDropDownHeight=300
you mean like this?
you can creat a new project:
<Grid>
<control:AutoCompleteBox x:Name="AddressBox" FontSize="30" MaxDropDownHeight="300"
Populating="Address_Populating" Margin="0,0,0,287.283" />
</Grid>
background code:
private void Address_Populating(object sender, PopulatingEventArgs e)
{
List<int> lst = new List<int>();
for (int i = 10; i < 25; i++)
{
lst.Add(i);
}
AddressBox.ItemsSource = lst;
AddressBox.PopulateComplete();
}
the reason is found by kylejan:
i found the reason. the display area will be influenced by the former window. for example, the window i put this control on is opend by the main window. then this autocomplete box will be influenced by the size of the main window
There is no easy way (included in the Framework) to exactly set the number of items to be displayed. But this example gives you some code to create an attached property that manipulates the maxheight property in order to display any specified number of items.
So one of my latest side projects is developing a application detection and populating assistant. Programmatically I am absolutely fine populating the backend code for what I want accomplished. But I've run into a road block on the GUI. I need a GUI that is a Quarter circle that extends from the task bar to the bottom right of a standard windows operating system. When the user doubleclicks on the application, the circle rotates into view. I can do this with a typical windows form that has a transparent background and a fancy background image. But the square properties of the form will still apply when the user has the application open. And I do not want to block the user from higher priority apps when the circle is open.
I'm not really stuck on any one specific programming language. Although, I would prefer that it not contain much 3d rendering as it is supposed to be a computing assistant and should not maintain heavy RAM/CPU consumption whilst the user is browsing around.
Secondarily, I would like the notches of the outer rings to be mobile and extend beyond the gui a mere centimeter or so.
I would not be here if I hadn't had scoured the internet for direction on this capability. But what I've found is application GUI's of this nature tend to be most used in mobile environments.
So my questions are: How can I accomplish this? What programming language can I write this in? Is this a capability currently available? Will I have to sacrifice user control for design?
I wrote out some code doing something close to what you described.
I’m not sure to understand how you do want the circle to appear, so I just let a part of it always visible.
And I didn’t get the part about the mobile outer ring.
Creating and placing the window
The XAML is very simple, it just needs a grid to host the circle’s pieces, and some attributes to remove window decorations and taskbar icon:
<Window x:Class="circle.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Circle"
Width="250"
Height="250"
AllowsTransparency="True"
Background="Transparent"
MouseDown="WindowClicked"
ShowInTaskbar="False"
WindowStyle="None">
<Grid Name="Container"/>
</Window>
To place the window in the bottom right corner, you can use SystemParameters.WorkArea in the constructor:
public MainWindow()
{
InitializeComponent();
var desktopDim = SystemParameters.WorkArea;
Left = desktopDim.Right - Width;
Top = desktopDim.Bottom - Height;
}
Creating the shape
I build the circle as a bunch of circle pieces that I generate from code behind:
private Path CreateCirclePart()
{
var circle = new CombinedGeometry
{
GeometryCombineMode = GeometryCombineMode.Exclude,
Geometry1 = new EllipseGeometry { Center = _center, RadiusX = _r2, RadiusY = _r2 },
Geometry2 = new EllipseGeometry { Center = _center, RadiusX = _r1, RadiusY = _r1 }
};
var sideLength = _r2 / Math.Cos((Math.PI/180) * (ItemAngle / 2.0));
var x = _center.X - Math.Abs(sideLength * Math.Cos(ItemAngle * Math.PI / 180));
var y = _center.Y - Math.Abs(sideLength * Math.Sin(ItemAngle * Math.PI / 180));
var triangle = new PathGeometry(
new PathFigureCollection(new List<PathFigure>{
new PathFigure(
_center,
new List<PathSegment>
{
new LineSegment(new Point(_center.X - Math.Abs(sideLength),_center.Y), true),
new LineSegment(new Point(x,y), true)
},
true)
}));
var path = new Path
{
Fill = new SolidColorBrush(Colors.Cyan),
Stroke = new SolidColorBrush(Colors.Black),
StrokeThickness = 1,
RenderTransformOrigin = new Point(1, 1),
RenderTransform = new RotateTransform(0),
Data = new CombinedGeometry
{
GeometryCombineMode = GeometryCombineMode.Intersect,
Geometry1 = circle,
Geometry2 = triangle
}
};
return path;
}
First step is to build two concentric circles and to combine them in a CombinedGeometry with CombineMode set to exclude. Then I create a triangle just tall enough to contain the section of the ring that I want, and I keep the intersection of these shapes.
Seeing it with the second CombineMode set to xor may clarify:
Building the circle
The code above uses some instance fields that make it generic: you can change the number of pieces in the circle or their radius; it will always fill the corner.
I then populate a list with the required number of shape, and add them to the grid:
private const double MenuWidth = 80;
private const int ItemCount = 6;
private const double AnimationDelayInSeconds = 0.3;
private readonly Point _center;
private readonly double _r1, _r2;
private const double ItemSpacingAngle = 2;
private const double ItemAngle = (90.0 - (ItemCount - 1) * ItemSpacingAngle) / ItemCount;
private readonly List<Path> _parts = new List<Path>();
private bool _isOpen;
public MainWindow()
{
InitializeComponent();
// window in the lower right desktop corner
var desktopDim = SystemParameters.WorkArea;
Left = desktopDim.Right - Width;
Top = desktopDim.Bottom - Height;
_center = new Point(Width, Height);
_r2 = Width;
_r1 = _r2 - MenuWidth;
Loaded += (s, e) => CreateMenu();
}
private void CreateMenu()
{
for (var i = 0; i < ItemCount; ++i)
{
var part = CreateCirclePart();
_parts.Add(part);
Container.Children.Add(part);
}
}
ItemSpacingAngle define the blank between two consecutive pieces.
Animating the circle
The final step is to unfold the circle. Using a rotateAnimation over the path rendertransform make it easy.
Remember this part of the CreateCirclePart function:
RenderTransformOrigin = new Point(1, 1),
RenderTransform = new RotateTransform(0),
The RenderTransform tells that the animation we want to perform is a rotation, and RenderTransformOrigin set the rotation origin to the lower right corner of the shape (unit is percent).
We can now animate it on click event:
private void WindowClicked(object sender, MouseButtonEventArgs e)
{
for (var i = 0; i < ItemCount; ++i)
{
if (!_isOpen)
UnfoldPart(_parts[i], i);
else
FoldPart(_parts[i], i);
}
_isOpen = !_isOpen;
}
private void UnfoldPart(Path part, int pos)
{
var newAngle = pos * (ItemAngle + ItemSpacingAngle);
var rotateAnimation = new DoubleAnimation(newAngle, TimeSpan.FromSeconds(AnimationDelayInSeconds));
var tranform = (RotateTransform)part.RenderTransform;
tranform.BeginAnimation(RotateTransform.AngleProperty, rotateAnimation);
}
private void FoldPart(Path part, int pos)
{
var rotateAnimation = new DoubleAnimation(0, TimeSpan.FromSeconds(AnimationDelayInSeconds));
var tranform = (RotateTransform)part.RenderTransform;
tranform.BeginAnimation(RotateTransform.AngleProperty, rotateAnimation);
}
Not actually answering this, but I liked your question enough that I wanted to get a minimal proof of concept together for fun and I really enjoyed doing it so i thought I'd share my xaml with you:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing" x:Class="WpfApplication1.Window2"
Title="Window2" Height="150" Width="150" Topmost="True" MouseLeftButtonDown="Window2_OnMouseLeftButtonDown"
AllowsTransparency="True"
OpacityMask="White"
WindowStyle="None"
Background="Transparent" >
<Grid>
<ed:Arc ArcThickness="40"
ArcThicknessUnit="Pixel" EndAngle="0" Fill="Blue" HorizontalAlignment="Left"
Height="232" Margin="33,34,-115,-116" Stretch="None"
StartAngle="270" VerticalAlignment="Top" Width="232" RenderTransformOrigin="0.421,0.471"/>
<Button HorizontalAlignment="Left" VerticalAlignment="Top" Width="41" Margin="51.515,71.385,0,0" Click="Button_Click" RenderTransformOrigin="0.5,0.5">
<Button.Template>
<ControlTemplate>
<Path Data="M50.466307,88.795148 L61.75233,73.463763 89.647286,102.42368 81.981422,113.07109 z"
Fill="DarkBlue" HorizontalAlignment="Left" Height="39.606"
Stretch="Fill" VerticalAlignment="Top" Width="39.181"/>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</Window>
And it looks like this:
How do I add a color effect to a UI element?
For example, it should look more yellow, so pixels have a more yellow color.
All I need is to make my black form a bit white, while it is inactive.
I recently needed a gradient effect that would go from a specified color to a lighter version of that color. I came across this post which works very nicely.
Here is the code as an extension method
public static Color Interpolate(this Color color1, Color color2, float percentage)
{
double a1 = color1.A / 255.0, r1 = color1.R / 255.0, g1 = color1.G / 255.0, b1 = color1.B / 255.0;
double a2 = color2.A / 255.0, r2 = color2.R / 255.0, g2 = color2.G / 255.0, b2 = color2.B / 255.0;
byte a3 = Convert.ToByte((a1 + (a2 - a1) * percentage) * 255);
byte r3 = Convert.ToByte((r1 + (r2 - r1) * percentage) * 255);
byte g3 = Convert.ToByte((g1 + (g2 - g1) * percentage) * 255);
byte b3 = Convert.ToByte((b1 + (b2 - b1) * percentage) * 255);
return Color.FromArgb(a3, r3, g3, b3);
}
In my case I mix in 50 % white
BackgroundColor.Interpolate(Colors.White, .5f);
Based upon your clarifications, the effect you want to achieve is to place a translucent veneer over the client area, and adjust its appearance programmatically. The technique for this is to use the WPF Grid. This control allows for layering. Here's a Xaml fragment that sets up two layers...
<Window.Resources>
<SolidColorBrush Color="Yellow" x:Key="MyVeneerBrush"/>
</Window.Resources>
<Grid>
<Grid Background="{StaticResource MyVeneerBrush}" Opacity="{Binding VeneerOpacity}"/>
<Grid>
<DockPanel>
<!--Layout goes here-->
<TextBlock Text="Hello" FontSize="52"/>
</DockPanel>
</Grid>
</Grid>
The first layer contains the veneer and the second layer contains the content. Opacity on the first layer can be set from 0 (totally transparent) to 1 (totally visible), and in between values will give a translucent quality. You would need to write your ViewModel along these lines...
public class ViewModel :INotifyPropertyChanged
{
public ViewModel()
{
TurnVeneerOn();
}
private void TurnVeneerOff()
{
VeneerOpacity = 0;
}
private void TurnVeneerOn()
{
VeneerOpacity = 0.4;
}
private double _veneerOpacity;
public double VeneerOpacity
{
[DebuggerStepThrough]
get { return _veneerOpacity; }
[DebuggerStepThrough]
set
{
if (value != _veneerOpacity)
{
_veneerOpacity = value;
OnPropertyChanged("VeneerOpacity");
}
}
}
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string name)
{
var handler = System.Threading.Interlocked.CompareExchange(ref PropertyChanged, null, null);
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
This VM exposes a property that binds to the View and controls the opacity of they first layer in the Xaml. There's two indicative methods thrown in to help get you started.
You will need to experiment with the window background and the brush and various levels of opacity to get the exact effect you are after, but there's enough here to get how it works.
The key is to use the Grid's layering capability.
For enhancing your yellow pixels, you could blend in a colored layer Photoshop style. For Photoshop style blend modes, you could use Cory Plotts' library and experiment with different blend modes. However, this may be a bit overkill for what you want to do; in which case you should try the following:
If you want your element to have a simple faded look, simply add a semi-transparent layer on top of your element as keftmedei suggested. Here's an example:
<yourElement Width="100" Height="100" Canvas.Top="50" />
This would change to:
<Grid Width="100" Height="100" Canvas.Top="50">
<yourElement />
<Rectangle Fill="#60FFFFFF" />
</Grid>
Change the opacity level by changing the 60 in Fill="#60FFFFFF"
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);
// ...
}