I need to display a block of text in a resizable column. The text should wrap with overflow but, for a given column size, the user should be able to scroll horizontally to view the overflown text.
I do not believe this can be achieved w/ out of the box controls; if I'm wrong about that please tell me. I have attempted to achieve this with a custom control:
public class Sizer : ContentPresenter
{
static Sizer()
{
ContentPresenter.ContentProperty.OverrideMetadata(typeof(Sizer), new FrameworkPropertyMetadata(ContentChanged)); ;
}
public Sizer() : base() {}
protected override Size MeasureOverride(Size constraint)
{
var childWidth = Content==null ? 0.0 : ((FrameworkElement)Content).RenderSize.Width;
var newWidth = Math.Max(RenderSize.Width, childWidth);
return base.MeasureOverride(new Size(newWidth, constraint.Height));
}
private static void ContentChanged(DependencyObject dep, DependencyPropertyChangedEventArgs args)
{
var #this = dep as Sizer;
var newV = args.NewValue as FrameworkElement;
var oldV = args.OldValue as FrameworkElement;
if (oldV != null)
oldV.SizeChanged -= #this.childSizeChanged;
if(newV!=null)
newV.SizeChanged += #this.childSizeChanged;
}
private void childSizeChanged(object sender, SizeChangedEventArgs e)
{
this.InvalidateMeasure();
}
}
...and I can test it in a simple WPF application like so:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto" VerticalAlignment="Top">
<local:Sizer>
<local:Sizer.Content>
<TextBlock Background="Coral" Text="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaa"
TextWrapping="WrapWithOverflow" />
</local:Sizer.Content>
</local:Sizer>
</ScrollViewer>
<GridSplitter Grid.Column="1" Background="Black" VerticalAlignment="Stretch" HorizontalAlignment="Left" Width="5" />
</Grid>
This works, after a fashion. It wraps and displays the text correctly, and the horizontal scrollbar lets me view the overflown text. If I resize the column to the right (larger) the text re-wraps correctly. However, if I resize the column to the left (smaller) the text will not re-wrap. So, for instance, if I resize the column to the right so that all the text is on one line it will remain all on one line regardless of any subsequent re-sizing. This is an unacceptable bug.
I have tinkered w/ this code a great deal although I haven't had what you'd a call a good strategy for finding a solution. I do not know and have not been able to discover any mechanism for forcing a textblock to re-wrap its contents. Any advice?
I was able to get this working (w/ some limitations) by adding the following to the custom control:
//stores first ScrollViewer ancestor
private ScrollViewer parentSV = null;
//stores collection of observed valid widths
private SortedSet<double> wrapPoints = new SortedSet<double>();
protected override void OnVisualParentChanged(DependencyObject oldParent)
{
parentSV = this.FindParent<ScrollViewer>();
base.OnVisualParentChanged(oldParent);
}
... and editing MeasureOverride list this:
protected override Size MeasureOverride(Size constraint)
{
if (parentSV != null)
{
var childWidth = Content == null ? 0.0 : ((FrameworkElement)Content).RenderSize.Width;
var viewportWidth = parentSV.ViewportWidth;
if (childWidth > viewportWidth + 5)
wrapPoints.Add(childWidth);
var pt = wrapPoints.FirstOrDefault(d => d > viewportWidth);
if (pt < childWidth)
childWidth = pt;
return base.MeasureOverride(new Size(childWidth, constraint.Height));
}
else
return base.MeasureOverride(constraint);
}
I do not like this at all. It doesn't work for WrapPanels (although that isn't a required use case for me), it flickers occasionally and I'm concerned that the wrapPoints collection may grow very large. But it does what I need it to do.
Related
I'm using SizeToContent="WidthAndHeight" so that I can size a window to the size of the currently displayed controls. This works fine, however, I'm running into an issue when displaying and adding items to a listbox. When adding a great number of items to the listbox this will cause the window to extend below the edge of the monitor. I notice that without the SizeToContent="WidthAndHeight" I can easily get the Listbox to limit itself and use a scrollbar. However, this has the negative of displaying all the extra unused space that the listbox with take up before I have made it visible. I have added a small sample solution below to display this behaviour.
NOTE window does not need to be resizable. It just needs to be efficient on space and I would rather not use absolute values if possible.
Explaination of desired behaviour; user only sees the button, window has no unused space around the button. When the button is clicked the Listbox is now visible but has not extended beyond the screen (i.e. a scroll bar has been added) and the button beneath is visible.
...ResizeMode="CanMinimize" SizeToContent="WidthAndHeight">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*"/>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button Content="hello" Click="Button_Click"/>
<ListView Name="lbx" Grid.Row="1" Visibility="Collapsed">
</ListView>
<Button Name="btn" Content="hello" Grid.Row="2" Visibility="Collapsed"/>
</Grid>
private void Button_Click(object sender, RoutedEventArgs e)
{
btn.Visibility = Visibility.Visible;
lbx.Visibility = Visibility.Visible;
//Have tried various combinations of settings in code at runtime
this.SizeToContent = SizeToContent.Width;
this.Height = Double.NaN;
for (int i = 0; i < 1000; i++)
{
lbx.Items.Add("Num:" + i);
}
}
I've also tried this with a dockpanel instead of a Grid;
<Button Content="hello" Click="Button_Click" DockPanel.Dock="Top"/>
<Button Name="btn" Content="hello" DockPanel.Dock="Bottom" Visibility="Collapsed"/>
<ListView Name="lbx" Visibility="Collapsed">
</ListView>
Try swapping SizeToContent="WidthAndHeight" with SizeToContent="Width", the window starts up showing all the empty space that will later be used by the collapsed controls.
You could set the MaxHeight of the window based on the actual height of the ListBox and the screen, e.g.:
private void Button_Click(object sender, RoutedEventArgs e)
{
btn.Visibility = Visibility.Visible;
lbx.Visibility = Visibility.Visible;
for (int i = 0; i < 5; i++)
{
lbx.Items.Add("Num:" + i);
}
Dispatcher.Invoke(() =>
{
MaxHeight = Math.Min(SystemParameters.WorkArea.Height - Top, grid.ActualHeight);
}, System.Windows.Threading.DispatcherPriority.Background);
}
This seems like a good option. One drawback is that with a small number of items it still introduces a scrollbar even when the listbox items should be able to fit.
Introduce an offset then:
private void Button_Click(object sender, RoutedEventArgs e)
{
btn.Visibility = Visibility.Visible;
lbx.Visibility = Visibility.Visible;
for (int i = 0; i < 5; i++)
{
lbx.Items.Add("Num:" + i);
}
grid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
grid.Arrange(new Rect(new Size(grid.DesiredSize.Width, grid.DesiredSize.Height)));
Height = Math.Min(SystemParameters.WorkArea.Height - Top, grid.ActualHeight + 40);
}
I have a Border with a Content of TextBlock that I want to be perfectly centered both horizontally and vertically. No matter what I try it never looks centered. What am I missing?
Using the code below the top of the text is 19px below the border, the bottom of the text is 5px above the border. It's also off center left or right depending on the Text value which I assume is related to the font.
The solution should work for varying text (1-31) with any font.
Code
<Grid Width="50" Height="50">
<Border BorderThickness="1" BorderBrush="Black">
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
</Grid>
Result
Well then, challenge accepted ;-) This solution is based on the following idea:
Fit the TextBlock inside the border and make sure the entire text is rendered, even if not visible.
Render the text into a bitmap.
Detect the glyphs (i.e. characters) inside the bitmap to get the pixel-exact position.
Update the UI layout so the text is centered inside the border.
If possible, allow simple, generic usage.
1. TextBlock inside border / fully rendered
This is simple once you realize that the entire content of a ScrollViewer is rendered, so here is my UserControl XAML:
<UserControl x:Class="WpfApplication4.CenteredText"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<ScrollViewer x:Name="scroll"
IsHitTestVisible="False"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Hidden" />
</Grid>
</UserControl>
With the code behind as:
public partial class CenteredText : UserControl
{
public CenteredText()
{
InitializeComponent();
}
public static readonly DependencyProperty ElementProperty = DependencyProperty
.Register("Element", typeof(FrameworkElement), typeof(CenteredText),
new PropertyMetadata(OnElementChanged));
private static void OnElementChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var elem = e.NewValue as FrameworkElement;
var ct = d as CenteredText;
if(elem != null)
{
elem.Loaded += ct.Content_Loaded;
ct.scroll.Content = elem;
}
}
public FrameworkElement Element
{
get { return (FrameworkElement)GetValue(ElementProperty); }
set { SetValue(ElementProperty, value); }
}
void Content_Loaded(object sender, RoutedEventArgs e) /*...*/
}
This control is basically a ContentControlwhich allows to handle the Loaded event of the content generically. There may be a simpler way to do this, I'm not sure.
2. Render to Bitmap
This one is simple. In the Content_Loaded() method:
void Content_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElement elem = sender as FrameworkElement;
int w = (int)elem.ActualWidth;
int h = (int)elem.ActualHeight;
var rtb = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
rtb.Render(elem);
/* glyph detection ... */
}
3. Detect the glyphs
This is surprisingly easy since a TextBlock is rendered with fully transparent background by default and we are only interested in bounding rectangle. This is done in a separate method:
bool TryFindGlyphs(BitmapSource src, out Rect rc)
{
int left = int.MaxValue;
int toRight = -1;
int top = int.MaxValue;
int toBottom = -1;
int w = src.PixelWidth;
int h = src.PixelHeight;
uint[] buf = new uint[w * h];
src.CopyPixels(buf, w * sizeof(uint), 0);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
// background is assumed to be fully transparent, i.e. 0x00000000 in Pbgra
if (buf[x + y * w] != 0)
{
if (x < left) left = x;
if (x > toRight) toRight = x;
if (y < top) top = y;
if (y > toBottom) toBottom = y;
}
}
}
rc = new Rect(left, top, toRight - left, toBottom - top);
return (toRight > left) && (toBottom > top);
}
The above method tries to find the leftmost, rightmost, topmost and bottommost pixel which is not transparent and returns the results as a Rect in the output parameter.
4. Update Layout
This is done later in the Content_Loaded method:
void Content_Loaded(object sender, RoutedEventArgs e)
{
/* render to bitmap ... */
Rect rc;
if (TryFindGlyphs(rtb, out rc))
{
if (rc.Height > this.scroll.ActualHeight || rc.Width > this.scroll.ActualWidth)
{
return; // todo: error handling
}
double desiredV = rc.Top - 0.5 * (this.scroll.ActualHeight - rc.Height);
double desiredH = rc.Left - 0.5 * (this.scroll.ActualWidth - rc.Width);
if (desiredV > 0)
{
this.scroll.ScrollToVerticalOffset(desiredV);
}
else
{
elem.Margin = new Thickness(elem.Margin.Left, elem.Margin.Top - desiredV,
elem.Margin.Right, elem.Margin.Bottom);
}
if (desiredH > 0)
{
this.scroll.ScrollToHorizontalOffset(desiredH);
}
else
{
elem.Margin = new Thickness(elem.Margin.Left - desiredH, elem.Margin.Top,
elem.Margin.Right, elem.Margin.Bottom);
}
}
}
This UI is updated using the following strategy:
Compute the desired offset between the border and the glyph rectangle in both directions
If the desired offset is positive, it means that the text needs to move up (or left in the horizontal case) so we can scroll down (right) by the desired offset.
If the desired offset is negative, it means that the text needs to move down (or right in the horizontal case). This cannot be done by scrolling since the TextBlock is top-left-aligned (by default) and the ScrollViewer is still at the initial (top/left) position. There is a simple solution though: Add the desired offset to the Margin of the TextBlock.
5. Simple Usage
The CenteredText control is used as follows:
<Border BorderBrush="Black" BorderThickness="1" Width="150" Height="150">
<local:CenteredText>
<local:CenteredText.Element>
<TextBlock Text="31" FontSize="150" />
</local:CenteredText.Element>
</local:CenteredText>
</Border>
Results
For border size 150x150 and FontSize 150:
For border size 150x150 and FontSize 50:
For border size 50x50 and FontSize 50:
Note: There is a 1-pixel error where the space to the left of the text is 1 pixel thicker or thinner than the space to the right. Same with the top / bottom spacing. This happens if the border has an even width and the rendered text an odd width (no sub-pixel perfectness is provided, sorry)
Conclusion
The presented solution should work up to a 1-pixel error with any Font, FontSize and Text and is simple to use.
And if you haven't noticed yet, very limited assumptions were made about the FrameworkElement which is used with the Elem property of the CenteredText control. So this should also work with any element which has transparent background and needs (near-)perfect centering.
What you are talking about is related to the specific font (and characters within that font) that you are using. Different fonts will have different baselines, heights and other attributes. In order to combat that, just use Padding on the Border or Margin on the TextBlock to make it fit where you want it:
<Grid Width="50" Height="50">
<Border BorderThickness="1" BorderBrush="Black">
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center"
FontSize="50" Margin="0,0,3,14" />
</Border>
</Grid>
Note: You can also use the TextBlock.TextAlignment Property to make adjustments to the horizontal alignment of text content.
I'd add this as a comment but I haven't got enough reputation :P
It looks off center because the height and width you have specified for the grid (50x50) is too small to house a font size of 50. Either increase the size to 100x100 or lower the font size to something smaller.
To demonstrate that they will be perfectly aligned in the center by doing this - view this code in visual studio somewhere. You will see the numbers of these textblocks overlap perfectly.
<Grid Height="100" Width="100">
<Border BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
<Border BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="31" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
</Grid>
I hope this helps you out :)
This is hard to explain:
I have two lists that, each can contain a large number of items, and they are placed one on top of the other so like this:
<StackPanel>
<List Name="ListOne"/>
<List Name="ListTwo"/>
</StackPanel>
ListOne has a little bit of a margin at the bottom to make it look neat.
The stack panel is contained within another control that has a fixed height. Say there is enough room for 8 items between the two lists before it overflows.
What I want is that when there is 8 items between the two lists there is no scroll bars, even if there is an uneven spread like 7-1 , 4-4, 3-5 etc. and once there is more than 8 elements then a scroll bar appears but only where it's needed.
For example if ListOne has 5 elements and ListTwo has 4 elements then ListOne has a scroll bar and they are both the same height, where as if it's 6-3 then ListOne has a scroll bar and takes up as much room as it can while still giving ListTwo enough room to display without needing a scrollbar.
Any idea how that would be possible? Not sure if that makes much sense but I'm having difficulty explaining it. Will reply quickly if you leave a comment. Thanks in advance.
OK so I have something that appears to work to a decent level using an attached property.
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace Sus.Common.UI
{
public class PanelHeightManagerExtension
{
public static readonly DependencyProperty ManageHeightsProperty =
DependencyProperty.RegisterAttached("ManageHeights",
typeof(bool),
typeof(PanelHeightManagerExtension),
new UIPropertyMetadata(false, OnManageHeightsChanged));
public static bool GetManageHeights(DependencyObject obj)
{
return (bool)obj.GetValue(ManageHeightsProperty);
}
public static void SetManageHeights(DependencyObject obj, bool value)
{
obj.SetValue(ManageHeightsProperty, value);
}
private static void OnManageHeightsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var element = ((Panel)d);
element.Loaded += (sender, args) => Update((Panel)sender);
}
private static void Update(Panel panel)
{
var children = panel.Children.OfType<FrameworkElement>().OrderBy(x=>x.ActualHeight).ToList();
var remainingHeight = panel.DesiredSize.Height;
for (int i = 0; i < children.Count; i++)
{
if (children[i].ActualHeight > remainingHeight / (children.Count - i))
{
remainingHeight -= children[i].MaxHeight = remainingHeight / (children.Count - i);
}
else
{
remainingHeight -= children[i].ActualHeight;
children[i].MaxHeight = children[i].ActualHeight;
}
}
}
}
}
And you just add the attached property as follows:
<Grid ui:PanelHeightManagerExtension.ManageHeights="True">
Still open to better answers
StackPanel will give its children all the space they want, so as you can see ListOne can push ListTwo out of view if it contains enough items.
You can wrap the StackPanel in a ScrollViewer, but i would suggest you to use a Grid instead
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<ListBox x:Name="ListOne"/>
<ListBox x:Name="ListTwo" Grid.Row="1"/>
</Grid>
I have a simple TextBlock defined like this
<StackPanel>
<Border Width="106"
Height="25"
Margin="6"
BorderBrush="Black"
BorderThickness="1"
HorizontalAlignment="Left">
<TextBlock Name="myTextBlock"
TextTrimming="CharacterEllipsis"
Text="TextBlock: Displayed text"/>
</Border>
</StackPanel>
Which outputs like this
This will get me "TextBlock: Displayed text"
string text = myTextBlock.Text;
But is there a way to get the text that's actually displayed on the screen?
Meaning "TextBlock: Display..."
Thanks
You can do this by first retrieving the Drawing object that represents the appearance of the TextBlock in the visual tree, and then walk that looking for GlyphRunDrawing items - those will contain the actual rendered text on the screen. Here's a very rough and ready implementation:
private void button1_Click(object sender, RoutedEventArgs e)
{
Drawing textBlockDrawing = VisualTreeHelper.GetDrawing(myTextBlock);
var sb = new StringBuilder();
WalkDrawingForText(sb, textBlockDrawing);
Debug.WriteLine(sb.ToString());
}
private static void WalkDrawingForText(StringBuilder sb, Drawing d)
{
var glyphs = d as GlyphRunDrawing;
if (glyphs != null)
{
sb.Append(glyphs.GlyphRun.Characters.ToArray());
}
else
{
var g = d as DrawingGroup;
if (g != null)
{
foreach (Drawing child in g.Children)
{
WalkDrawingForText(sb, child);
}
}
}
}
This is a direct excerpt from a little test harness I just wrote - the first method's a button click handler just for ease of experimentation.
It uses the VisualTreeHelper to get the rendered Drawing for the TextBlock - that'll only work if the thing has already been rendered by the way. And then the WalkDrawingForText method does the actual work - it just traverses the Drawing tree looking for text.
This isn't terribly smart - it assumes that the GlyphRunDrawing objects appear in the order you'll want them. For your particular example it does - we get one GlyphRunDrawing containing the truncated text, followed by a second one containing the ellipsis character. (And by the way, it's just one unicode character - codepoint 2026, and if this editor lets me paste in unicode characters, it's "…". It's not three separate periods.)
If you wanted to make this more robust, you would need to work out the positions of all those GlyphRunDrawing objects, and sort them, in order to process them in the order in which they appear, rather than merely hoping that WPF happens to produce them in that order.
Updated to add:
Here's a sketch of how a position-aware example might look. Although this is somewhat parochial - it assumes left-to-right reading text. You'd need something more complex for an internationalized solution.
private string GetTextFromVisual(Visual v)
{
Drawing textBlockDrawing = VisualTreeHelper.GetDrawing(v);
var glyphs = new List<PositionedGlyphs>();
WalkDrawingForGlyphRuns(glyphs, Transform.Identity, textBlockDrawing);
// Round vertical position, to provide some tolerance for rounding errors
// in position calculation. Not totally robust - would be better to
// identify lines, but that would complicate the example...
var glyphsOrderedByPosition = from glyph in glyphs
let roundedBaselineY = Math.Round(glyph.Position.Y, 1)
orderby roundedBaselineY ascending, glyph.Position.X ascending
select new string(glyph.Glyphs.GlyphRun.Characters.ToArray());
return string.Concat(glyphsOrderedByPosition);
}
[DebuggerDisplay("{Position}")]
public struct PositionedGlyphs
{
public PositionedGlyphs(Point position, GlyphRunDrawing grd)
{
this.Position = position;
this.Glyphs = grd;
}
public readonly Point Position;
public readonly GlyphRunDrawing Glyphs;
}
private static void WalkDrawingForGlyphRuns(List<PositionedGlyphs> glyphList, Transform tx, Drawing d)
{
var glyphs = d as GlyphRunDrawing;
if (glyphs != null)
{
var textOrigin = glyphs.GlyphRun.BaselineOrigin;
Point glyphPosition = tx.Transform(textOrigin);
glyphList.Add(new PositionedGlyphs(glyphPosition, glyphs));
}
else
{
var g = d as DrawingGroup;
if (g != null)
{
// Drawing groups are allowed to transform their children, so we need to
// keep a running accumulated transform for where we are in the tree.
Matrix current = tx.Value;
if (g.Transform != null)
{
// Note, Matrix is a struct, so this modifies our local copy without
// affecting the one in the 'tx' Transforms.
current.Append(g.Transform.Value);
}
var accumulatedTransform = new MatrixTransform(current);
foreach (Drawing child in g.Children)
{
WalkDrawingForGlyphRuns(glyphList, accumulatedTransform, child);
}
}
}
}
After rooting around I Reflector for a while, I found the following:
System.Windows.Media.TextFormatting.TextCollapsedRange
which has a Length property that contains the number of characters that are NOT displayed (are in the collapsed/hidden portion of the text line). Knowing that value, it's just a matter of subtraction to get the characters that ARE displayed.
This property is not directly accessible from the TextBlock object. It looks like it is part of the code that is used by WPF to actually paint the text on the screen.
It could end up being quite a lot of fooling around to actually get the value of this property for the text line in your TextBlock.
Well, it's a bit of a specific request so I'm not sure there's a ready made function in the framework to do it. What I would do is to calculate the logical width of each character, divide the ActualWidth of the TextBlock by this value and there you have the number of characters from the start of the string that are visible. That is of course assuming that clipping will only occur from the right.
If you need the text for an effect - might it then be enough with the image of the rendered text?
If so you could use a VisualBrush or System.Windows.Media.Imaging.RenderTargetBitmap
Also I reproduced on .Net framework with the following xaml:
<Window x:Class="TestC1Grid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
TextOptions.TextFormattingMode="Display"
TextOptions.TextRenderingMode="Auto"
ResizeMode="CanResizeWithGrip"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock TextTrimming="CharacterEllipsis"
FontFamily="Tahoma"
FontSize="12"
HorizontalAlignment="Stretch"
TextAlignment="Left" xml:lang="nl-nl">My-Text</TextBlock>
<TextBlock Grid.Column="1" TextTrimming="CharacterEllipsis"
FontFamily="Tahoma"
FontSize="12"
IsHyphenationEnabled="True">My-Text</TextBlock>
<TextBlock Grid.Column="2" TextTrimming="CharacterEllipsis"
FontFamily="Tahoma"
FontSize="12"
IsHyphenationEnabled="True">My-Text</TextBlock>
</Grid>
</Grid>
</Window>
if you remove
TextOptions.TextFormattingMode="Display"
TextOptions.TextRenderingMode="Auto"
or remove
xml:lang="nl-nl"
is working ok
Essentially I want a wrapPanel, but I would like items to snap to a grid rather than be pressed up to the left, so I can get a nice uniform looking grid, that automatically consumes available space.
WrapPanel handles the resize part.
WPF.Contrib.AutoGrid handles a nice automatic grid.
Anyone got a control that combines them?
My use case is I have a series of somewhat irregularly shaped controls. I would like them to appear in nice columns so the wrap panel should snap to the next "tabstop" when placing a control
When I read your question I assumed you wanted something like this:
public class UniformWrapPanel : WrapPanel
{
protected override Size MeasureOverride(Size constraint)
{
if(Orientation == Orientation.Horizontal)
ItemWidth = Children.Select(element =>
{
element.Measure(constraint);
return element.DesiredWidth;
}).Max();
else
... same for vertical ...
return base.MeasureOverride(constraint);
}
}
but I see someone else has already implemented a "UniformWrapPanel" and from your comments you indicate this is not what you were looking for.
The comment I don't understand is:
I want it to not force items to be a given size, but use their already existing size and therefore determine column widths automatically
Can you please provide an example to illustrate how you want things laid out with varying sizes? A picture might be nice. You also mention "tabstop" but don't give any definition of what that would be.
Here is some code that I whipped up based on some of the other controls that are close. It does a decent job of doing the layout, although it has an issue where grand-child controls do not fill up all their available space.
protected override Size ArrangeOverride(Size finalSize)
{
double rowY = 0;
int col = 0;
double currentRowHeight = 0;
foreach (UIElement child in Children)
{
var initialSize = child.DesiredSize;
int colspan = (int) Math.Ceiling(initialSize.Width/ ColumnSize);
Console.WriteLine(colspan);
double width = colspan * ColumnSize;
if (col > 0 && (col * ColumnSize) + width > constrainedSize.Width)
{
rowY += currentRowHeight;
col = 0;
currentRowHeight = 0;
}
var childRect = new Rect(col * ColumnSize, rowY, width, initialSize.Height);
child.Arrange(childRect);
currentRowHeight = Math.Max(currentRowHeight, initialSize.Height);
col+=colspan;
}
return finalSize;
}
Size constrainedSize;
protected override Size MeasureOverride(Size constraint)
{
constrainedSize = constraint;
return base.MeasureOverride(constraint);
}
Try setting ItemWidth (or ItemHeight) property of the WrapPanel:
<WrapPanel ItemWidth="48">
<TextBlock Text="H" Background="Red"/>
<TextBlock Text="e" Background="Orange"/>
<TextBlock Text="l" Background="Yellow"/>
<TextBlock Text="l" Background="Green"/>
<TextBlock Text="o" Background="Blue"/>
<TextBlock Text="!" Background="Violet"/>
</WrapPanel>