Clipping non-opaque double progress bar in WPF - wpf

I'm trying to make a custom progress bar in WPF that has two values (the second is always equal to or higher than the first). The basic bar works ok like so:
<wpft:ClippingBorder BorderBrush="{StaticResource Border}"
Background="{StaticResource Background}"
BorderThickness="1" CornerRadius="4">
<Grid Margin="-1" x:Name="Bars">
<Border BorderBrush="{StaticResource Border}"
Background="{Binding Value2Brush}"
BorderThickness="1" CornerRadius="4"
HorizontalAlignment="Left"
Width="{Binding Value2Width}" />
<Border BorderBrush="{StaticResource Border}"
Background="{Binding Value1Brush}"
BorderThickness="1" CornerRadius="4"
HorizontalAlignment="Left"
Width="{Binding Value1Width}" />
</Grid>
</wpft:ClippingBorder>
(Where ClippingBorder is this. It's used to prevent glitching at the outer corners when the values are near 0.)
The net result is a nice rounded display:
Zoomed view, to more clearly show the rounded corners:
In particular note that both of the inner bars share the same outer border and their right edge curves to the left, just like the outer border.
This works because it draws the longer bar first, then the shorter one on top of it. However, it only works reliably when the brushes are fully opaque -- in particular if Value1Brush is partially transparent then some of Value2Brush will show through it, which I don't want.
Ideally, I want the longer bar to only draw that portion of itself that extends beyond the shorter bar -- or equivalently, to set the clipping/opacity mask of the longer bar to be transparent in the area where the shorter bar is drawn.
But I'm not sure how to do that without losing the rounded corners.

This is not a general solution, unfortunately, but it seems to work for this case. I had to give an x:Name to each of the internal borders and then put this in the code behind:
Constructor:
DependencyPropertyDescriptor.FromProperty(ActualWidthProperty, typeof(Border))
.AddValueChanged(OuterBar, Child_ActualWidthChanged);
DependencyPropertyDescriptor.FromProperty(ActualWidthProperty, typeof(Border))
.AddValueChanged(InnerBar, Child_ActualWidthChanged);
Handler:
private void Child_ActualWidthChanged(object sender, EventArgs e)
{
var outerRect = new Rect(OuterBar.RenderSize);
outerRect.Inflate(5, 5);
var outer = new RectangleGeometry(outerRect);
var corner = InnerBar.CornerRadius.TopLeft;
var inner = new RectangleGeometry(new Rect(InnerBar.RenderSize), corner, corner);
OuterBar.Clip = new GeometryGroup()
{
Children = { outer, inner }
};
}
The basic idea is to start with a rectangle slightly larger than what the outer bar wants to draw, and then add a rectangle that exactly matches what the inner bar wants to draw -- this clips it out of the geometry. The whole is then used as a clip region for the outer bar so that it can't draw inside the inner bar's region.
I originally tried to do this in XAML with the following, but it didn't work (the converter was not called when the width changed); I'm not sure why, but just for posterity:
<Border.Clip>
<GeometryGroup>
<RectangleGeometry Rect="{Binding ElementName=OuterBar, Path=RenderSize,
Converter={StaticResource BoundsConverter}, ConverterParameter=5.0}" />
<RectangleGeometry Rect="{Binding ElementName=InnerBar, Path=RenderSize,
Converter={StaticResource BoundsConverter}}" RadiusX="4" RadiusY="4" />
</GeometryGroup>
</Border.Clip>
(Here the converter would take the RenderSize and make a Rect, with optional inflation, similar to the code above.)

I would try to use grid's columns. Putting two columns in the grid, first on with width="auto" second as "*".Put short border in the first column the other to the second column. When you change the width of your border column will resize accordingly to your border's width.

Related

Why is the Border CornerRadius not maintained for different BorderThickness?

Why does the BorderThickness changes the rendered CornerRadius?
Also, what is the design reasoning/philosophy behind it?
I just cannot understand it, maybe I'm missing something.
<Border Width="300"
Height="300"
Background="Red"
BorderBrush="Blue"
CornerRadius="5"
BorderThickness="50" />
<Border Width="300"
Height="300"
Background="Red"
BorderBrush="Blue"
CornerRadius="5"
BorderThickness="10" />
I see Rectangle has the same behavior.
Is there any element in WPF or WinUI which I can use to draw an exact radius so I can respect the designer's requirement?
Besides Path with custom points, I do not see any other way.
The issue with Path is I need to recompute the points myself when width/height changes which will hurt performance.
EDIT: Trying to tweak the corner radius so that it can match the design spec turns out to be impossible.
For example, let's assume the designer wants a Border with CornerRadius=5 and BorderThickness = 30.
In the image below, the top Border shows what an actual CornerRadius=5 looks like.
In the bottom Border, I try to meet the design spec. I set the BorderThicikness=30 and I tweak the CornerRadius to something very small so that it looks close to the corner radius of the Border above.
But the CornerRadius remains quite big even for a very small value 0.0000002:
<Border Width="100"
Height="100"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="Red"
BorderThickness="0"
CornerRadius="5"/>
<Border Width="100"
Height="100"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="Red"
BorderBrush="Blue"
BorderThickness="30"
CornerRadius="0.0000002" />
EDIT #2:
So that it's even more obvious how big the corner radius of the bottom Border is compared to the top one:
Why does the BorderThickness changes the rendered CornerRadius?
Looking at Border's method OnRender, there is a Pen which aim at drawing a rounded rectangle if CornerRadius property is set... the pen has Thickness Property, Pen's thickness = Border's thickness.
This is the flow of execution inside OnRender method when both CornerRadius and BorderThickness are set (in case of uniform thickness and uniform radius)..
pen1 = new Pen();
pen1.Brush = borderBrush;
pen1.Thickness = borderThickness.Left;
double topLeft1 = cornerRadius.TopLeft;
double num = pen1.Thickness * 0.5;
Rect rectangle = new Rect(new Point(num, num), new Point(this.RenderSize.Width - num, this.RenderSize.Height - num));
dc.DrawRoundedRectangle((Brush) null, pen1, rectangle, topLeft1, topLeft1);
So there is a Thickness-Radius trade-off that can not be avoided, And probably the only way to deal with it is to set a base border (the border that meets the designer's guidelines) and for the other border you would increase its radius if its thickness is less than the base border's thickness!
Update
Note that the dimensions of the border (Width and Height) is the third factor in the equation (see DrawRoundedRectangle call).
Imagine you are about to draw a rectangle of size 100x100 using a pen with thickness = 30.. You start drawing from the middle of the top line and moving the pen towards the left (image below).. Now, you will start shifting the pen early before reaching the corner because you are not allowed to get out of the 100x100, and that will result in a big curve so the value of the radius will not take affect in this case because the Width and Height has higher priority that the CornerRadius.
So to let a very small radius like 0.0000002 take affect, you have to enlarge the dimensions of the rectangle to a size that a pen with thickness = 30 will look small enough to reache near the corners.
Here is the first xaml code after tweaking radius of the second border (set it to 20) to make it looks like the first border..
And to make the first border looks like the second border, you can change its code to this
<Grid>
<Border
Width="300"
Height="300"
BorderBrush="Blue"
BorderThickness="10"
CornerRadius="5" />
<Border
Width="300"
Height="300"
Background="Red"
BorderBrush="Blue"
BorderThickness="50"
CornerRadius="5" />
</Grid>

How to adjust a canvas that child with position (-1,-1) is displayed completely

NEW INFORMATION!!!
Meanwhile i have found a solution but there is a new problem.
The solution is to set the margin of the canvas in code-behind to a new object of type Thickness with top and left 1 or 2.
But the canvas is lying on a tabcontrol.
When i switch between tabs or make a mousedown on the canvas the margin is lost.
I'm working with VS2015 on a WPF-application and have a very curious problem.
I got in one of my WPF windows a canvas as parent for some child elements.
One of these elements is a rectangle which shall show the user the size of a DIN-A4 page.
It is added in code-behind to the children collection of the canvas.
Normally i would place that rectangle at position (0,0).
But because of some problems i have to trick and set the position to (-1,-1) like that:
public static System.Windows.Shapes.Rectangle GetRectangle(double top, double left, double width, double height)
{
var rectangle = new System.Windows.Shapes.Rectangle();
System.Windows.Controls.Canvas.SetLeft(rectangle, -1);
System.Windows.Controls.Canvas.SetTop(rectangle, -1);
rectangle.Width = Math.Round(GetSizeInPoint(width)) + 2;
rectangle.Height = Math.Round(GetSizeInPoint(height));
rectangle.StrokeThickness = 1;
rectangle.Stroke = System.Windows.Media.Brushes.Red;
return rectangle;
}
But the result of it is that just a small part of the top and left border of the rectangle can be seen.
Do i have a chance to "move" the canvas so that the rectangle is displayed completely?
Hereby another important problematic point is that the Grid named "grdProtocolDesigner" can be serialized to XML and saved in the database.
So a complete restructuring would be a big problem.
Here the relevant part of my XAML including the canvas:
<ContentPresenter x:Name="protocolContainer"
Grid.Row="1"
Grid.Column="0">
<ContentPresenter.Content>
<Grid x:Name="grdProtocolDesigner"
Grid.Row="1"
Grid.Column="0">
<ScrollViewer>
<!--Important! The background of this panel has to be "Transparent" so that drag'n'drop-events can work.
Otherwise the events are not fired.-->
<Canvas x:Name="protocolDesignerPanel"
AllowDrop="True"
Visibility="{Binding DesignerPanelsVisibility}"
Width="2200" Height="4000"
MouseEnter="designerpanel_MouseEnter"
MouseLeave="designerpanel_MouseLeave"
MouseDown="designerpanel_MouseDown"
MouseMove="designerpanel_MouseMove"
MouseUp="designerPanel_MouseUp">
<Canvas.RenderTransform>
<ScaleTransform x:Name="scaleProtocolLayout"/>
</Canvas.RenderTransform>
<Canvas.Background>
<ImageBrush ImageSource="../../pictures/CanvasBackground.png"
TileMode="Tile"
Stretch="None"
Viewport="0, 0, 10, 10"
ViewportUnits="Absolute" />
</Canvas.Background>
</Canvas>
</ScrollViewer>
</Grid>
</ContentPresenter.Content>
</ContentPresenter>

WPF Transform Rectangle in Canvas to Selection in Image

I have a rectangle on a canvas that the user can resize, move and so on to make a selection.
I also have an image the size of the screen behind the canvas (basically a screenshot).
I'd like to translate the selection (the rectangle) in the canvas to a 1:1 selection in the image (I want the image directly behind the rectangle) given I have the rectangle's Canvas.Top, Canvas.Left, Width, Height.
<Grid Name="MainGrid" SnapsToDevicePixels="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image x:Name="MainImage" Stretch="None" RenderOptions.BitmapScalingMode="HighQuality"/>
<Border Background="Black" Opacity="0.4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<Canvas Name="MainCanvas" Width="{Binding Source={x:Static SystemParameters.PrimaryScreenWidth}}" Height="{Binding Source={x:Static SystemParameters.PrimaryScreenHeight}}" Background="Transparent">
<ContentControl Name="SelectionRect" />
</ContentControl>
</Canvas>
</Grid>
I tried doing this: (MainImage is the image under the canvas)
Rect rect = new Rect(Canvas.GetLeft(SelectionRect), Canvas.GetTop(SelectionRect), SelectionRect.Width, SelectionRect.Height);
Rect from_rect = SelectionRect.TransformToVisual(this).TransformBounds(rect);
BitmapSource cropped_bitmap = new CroppedBitmap(MainImage.Source as BitmapSource,
new Int32Rect((int)from_rect.X, (int)from_rect.Y, (int)from_rect.Width, (int)from_rect.Height));
SelectionRectImageSource = cropped_bitmap;
But the image I get (SelectionRectImageSource) is a moved aside version of the actual pixels behind the selection rectangle.
So basically, I don't understand how these transformations work and how I should use them if at all.
Example:
Thanks a lot!
Dolev.
Looks like you need to correct for the DPI difference between the image (usually 72dpi) and the presentation source (usually 96dpi). Additionally, your first Rect should not be offset by Canvas.Left and Canvas.Top; TransformToVisual will take care of the relative offset for you.
var source = (BitmapSource)MainImage.Source;
var selectionRect = new Rect(SelectionRect.RenderSize);
var sourceRect = SelectionRect.TransformToVisual(MainImage)
.TransformBounds(selectionRect);
var xMultiplier = source.PixelWidth / MainImage.ActualWidth;
var yMultiplier = source.PixelHeight / MainImage.ActualHeight;
sourceRect.Scale(xMultiplier, yMultiplier);
var croppedBitmap = new CroppedBitmap(
source,
new Int32Rect(
(int)sourceRect.X,
(int)sourceRect.Y,
(int)sourceRect.Width,
(int)sourceRect.Height));
SelectionRectImageSource= croppedBitmap;
Depending on where this code resides, you may also need to transform the selection rectangle to MainImage instead of this (as I did).
Also, in case MainImage.Source is smaller than the actual MainImage control, you should probably set the horizontal and vertical alignments of MainImage to Left and Top, respectively, less your translated rectangle end up outside the bounds of the source image. You'll need to clamp the selection rectangle to the dimensions of MainImage too.

Animating from a point other than the left side

I have an animation problem that I'm not sure how to Google or find a solution for. I'm trying to basically create a wrapping Marquee. I do this by having the following within a canvas:
|---Section A---|---Section B---|Section C---|
The animation begins with the left side of section B on the right side of the screen, and ends when the right side of section B hits the left side of the screen. Sections A and C are a mirror image of Section B, which creates the "wrap around" effect. When the animation ends, I move everything to the right the exact width of Section B. To the user, nothing appears to have happened .. it's all just wrapping around.
The problem is this ... some of the items in the marquee can be hidden, or can change size. So if you hide an item, the item is hidden from all three sections. Since things are being hidden in Section A, and all the sections are sized dynamically, the entire marquee moves to the left.
Is there any way to "anchor" the animation at a spot, say, in the middle of Section B, so that when items are hidden in section A, it doesn't slide sections B and C over? Instead, I want Section A to move right to fill in the space.
Edit: Let me rephrase ... because this is confusing.
I have a canvas, which contains a stackpanel, which contains three more stack panels. These three stack panels are actually copies of the same information, which gives the illusion of a marquee that wraps around when I animate. When items are added/removed in the marquee, those items in the stack panel change, which adjusts the overall size of the stack panel, which adjusts the size of the canvas. What I'd like to know, is if I can "anchor" a specific location within the stackpanel. Can I "anchor" on the first item in the 2nd stackpanel copy?
To answer what APPEARS to be the primary question:
Is there a way to animation the position of a canvas using something
other than the Canvas.Left property
Set the RenderTransform property of the Canvas to a TranslateTransform and animate the TranslateTransform.X Property:
<Canvas x:Name="myCanvas" RenderTransformOrigin="0.5,0.5">
<Canvas.RenderTransform>
<TranslateTransform X="0"/>
</Canvas.RenderTransform>
</Canvas>
Then in code:
EDIT: The second parameter of this method call should be of type DoubleAnimation, not Duration. I must have been sleeping at my desk when I typed this. Sorry.
(TranslateTransform)MyCanvas.RenderTransform.BeginAnimation(TranslateTransform.XProperty, new Duration(TimeSpan.FromSeconds(3)));
I hope this helps.
My other answer is becoming obsolete as you elaborate on your desired behavior, so forgive me for posting a second. Based on my latter comments on the above answer, consider the following example:
<Canvas x:Name="LayoutRoot">
<Grid x:Name="MarqueePanels">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Rectangle Grid.Column="0" Height="{Binding ElementName=PrimaryMarquee, Path=ActualHeight}" Width="{Binding ElementName=PrimaryMarquee, Path=ActualWidth}">
<Rectangle.Fill>
<VisualBrush Visual="{Binding ElementName=PrimaryMarquee}"/>
</Rectangle.Fill>
</Rectangle>
<StackPanel Grid.Column="1" x:Name="PrimaryMarquee">
<TextBlock Text="Marquee Item 1"/>
<TextBlock Text="Marquee Item 2"/>
<TextBlock Text="Marquee Item 3"/>
</StackPanel>
<Rectangle Grid.Column="2" Height="{Binding ElementName=PrimaryMarquee, Path=ActualHeight}" Width="{Binding ElementName=PrimaryMarquee, Path=ActualWidth}">
<Rectangle.Fill>
<VisualBrush Visual="{Binding ElementName=PrimaryMarquee}"/>
</Rectangle.Fill>
</Rectangle>
</Grid>
</Canvas>
As I mentioned above, setting the Visibility of any of the TextBox elements to Hidden will not cause the StackPanel parent to resize, however using Visibility.Collapsed WILL force an Arrange pass of the parent panel. So if you truly NEED to use a StackPanel to contain the visual elements, then I recommend based on your comments above using Visibility.Hidden in lieu of Visibility.Collapsed.
Also notice the use of VisualBrush to replicate the Marquee content. This will simplify the code, and ensure that all 3 visuals always match (with the exception of whatever positioning or transformation you apply to the Rectangle objects that house the VisualBrush).
Additionally, I should say that I personally would use a Canvas in lieu of the StackPanel, as the Canvas is much better suited for absolute positioning of child elements, hence the reason for your question.
Craig, I truly hope this helps. I (like most people on here) will try to help however I can. Good luck!
Got it.
I set the margin to -(theControl.ActualWidth / 2). All my code shifts everything back by that same amount to compensate.
Then I have an event handler for SizeChanged on the control, where I set this value. If the size of the control changes, the margin is update, and everything focuses on the middle of the control. If you have a specific point within the control that you want to center animation on, then use that point instead of the halfway point described above.

How do I configure a TextBox control to automatically resize itself vertically when text no longer fits on one line?

How do I configure a TextBox control to automatically resize itself vertically when text no longer fits on one line?
For example, in the following XAML:
<DockPanel LastChildFill="True" Margin="0,0,0,0">
<Border Name="dataGridHeader"
DataContext="{Binding Descriptor.Filter}"
DockPanel.Dock="Top"
BorderThickness="1"
Style="{StaticResource ChamelionBorder}">
<Border
Padding="5"
BorderThickness="1,1,0,0"
BorderBrush="{DynamicResource {ComponentResourceKey TypeInTargetAssembly=dc:NavigationPane,
ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleBorder}}}">
<StackPanel Orientation="Horizontal">
<TextBlock
Name="DataGridTitle"
FontSize="14"
FontWeight="Bold"
Foreground="{DynamicResource {ComponentResourceKey
TypeInTargetAssembly=dc:NavigationPane,
ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleForeground}}}"/>
<StackPanel Margin="5,0" Orientation="Horizontal"
Visibility="{Binding IsFilterEnabled, FallbackValue=Collapsed, Mode=OneWay, Converter={StaticResource BooleanToVisibility}}"
IsEnabled="{Binding IsFilterEnabled, FallbackValue=false}" >
<TextBlock />
<TextBox
Name="VerticallyExpandMe"
Padding="0, 0, 0, 0"
Margin="10,2,10,-1"
AcceptsReturn="True"
VerticalAlignment="Center"
Text="{Binding QueryString}"
Foreground="{DynamicResource {ComponentResourceKey
TypeInTargetAssembly=dc:NavigationPane,
ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleForeground}}}">
</TextBox>
</StackPanel>
</StackPanel>
</Border>
</Border>
</DockPanel>
The TextBox control named "VerticallyExpandMe" needs to automatically expand vertically when the text bound to it does not fit on one line. With AcceptsReturn set to true, TextBox expands vertically if I press enter within it, but I want it do do this automatically.
Although Andre Luus's suggestion is basically correct, it won't actually work here, because your layout will defeat text wrapping. I'll explain why.
Fundamentally, the problem is this: text wrapping only does anything when an element's width is constrained, but your TextBox has unconstrained width because it's a descendant of a horizontal StackPanel. (Well, two horizontal stack panels. Possibly more, depending on the context from which you took your example.) Since the width is unconstrained, the TextBox has no idea when it is supposed to start wrapping, and so it will never wrap, even if you enable wrapping. You need to do two things: constrain its width and enable wrapping.
Here's a more detailed explanation.
Your example contains a lot of detail irrelevant to the problem. Here's a version I've trimmed down somewhat to make it easier to explain what's wrong:
<StackPanel Orientation="Horizontal">
<TextBlock Name="DataGridTitle" />
<StackPanel
Margin="5,0"
Orientation="Horizontal"
>
<TextBlock />
<TextBox
Name="VerticallyExpandMe"
Margin="10,2,10,-1"
AcceptsReturn="True"
VerticalAlignment="Center"
Text="{Binding QueryString}"
>
</TextBox>
</StackPanel>
</StackPanel>
So I've removed your containing DockPanel and the two nested Border elements inside of that, because they're neither part of the problem nor relevant to the solution. So I'm starting at the pair of nested StackPanel elements in your example. And I've also removed most of the attributes because most of them are also not relevant to the layout.
This looks a bit weird - having two nested horizontal stack panels like this looks redundant, but it does actually make sense in your original if you need to make the nested one visible or invisible at runtime. But it makes it easier to see the problem.
(The empty TextBlock tag is also weird, but that's exactly as it appears in your original. That doesn't appear to be doing anything useful.)
And here's the problem: your TextBox is inside some horizontal StackPanel elements, meaning its width is unconstrained - you have inadvertently told the text box that it is free to grow to any width, regardless of how much space is actually available.
A StackPanel will always perform layout that is unconstrained in the direction of stacking. So when it comes to lay out that TextBox, it'll pass in a horizontal size of double.PositiveInfinity to the TextBox. So the TextBox will always think it has more space than it needs. Moreover, when a child of a StackPanel asks for more space than is actually available, the StackPanel lies, and pretends to give it that much space, but then crops it.
(This is the price you pay for the extreme simplicity of StackPanel - it's simple to the point of being bone-headed, because it will happily construct layouts that don't actually fit. You should only use StackPanel if either you really do have unlimited space because you're inside a ScrollViewer, or you are certain that you have sufficiently few items that you're not going to run out of space, or if you don't care about items running off the end of the panel when they get too large and you don't want the layout system to try to do anything more clever than simply cropping the content.)
So turning on text wrapping won't help here, because the StackPanel will always pretend that there's more than enough space for the text.
You need a different layout structure. Stack panels are the wrong thing to use because they will not impose the layout constraint you need to get text wrapping to kick in.
Here's a simple example that does roughly what you want:
<Grid VerticalAlignment="Top">
<DockPanel>
<TextBlock
x:Name="DataGridTitle"
VerticalAlignment="Top"
DockPanel.Dock="Left"
/>
<TextBox
Name="VerticallyExpandMe"
AcceptsReturn="True"
TextWrapping="Wrap"
Text="{Binding QueryString}"
>
</TextBox>
</DockPanel>
</Grid>
If you create a brand new WPF application and paste that in as the content of the main window, you should find it does what you want - the TextBox starts out one line tall, fills the available width, and if you type text in, it'll grow one line at a time as you add more text.
Of course, layout behaviour is always sensitive to context, so it may not be enough to just throw that into the middle of your existing application. That will work if pasted into a fixed-size space (e.g. as the body of a window), but will not work correctly if you paste it into a context where width is unconstrained. (E.g., inside a ScrollViewer, or inside a horizontal StackPanel.)
So if this doesn't work for you, it'll be because of other things wrong elsewhere in your layout - possibly yet more StackPanel elements elsewhere. From the look of your example, it's probably worth spending some time thinking about what you really need in your layout and simplifying it - the presence of negative margins, and elements that don't appear to do anything like that empty TextBlock are usually indicative of an over-complicated layout. And unnecessary complexity in a layout makes it much hard to achieve the effects you're looking for.
Alternatively, you could constrain your TextBlock's Width by binding it to a parent's ActualWidth, for example:
<TextBlock Width="{Binding ElementName=*ParentElement*, Path=ActualWidth}"
Height="Auto" />
This will force it to resize its height automatically too.
Use MaxWidth and TextWrapping="WrapWithOverflow".
I'm using another simple approach that allows me not to change the document layout.
The main idea is not to set the control Width before it starts changing. For TextBoxes, I handle the SizeChanged event:
<TextBox TextWrapping="Wrap" SizeChanged="TextBox_SizeChanged" />
private void TextBox_SizeChanged(object sender, SizeChangedEventArgs e)
{
FrameworkElement box = (FrameworkElement)sender;
if (e.PreviousSize.Width == 0 || box.Width < e.PreviousSize.Width)
return;
box.Width = e.PreviousSize.Width;
}
You can use this class which extends TextBlock. It does auto-shrinking and takes MaxHeight / MaxWidth into consideration:
public class TextBlockAutoShrink : TextBlock
{
private double _defaultMargin = 6;
private Typeface _typeface;
static TextBlockAutoShrink()
{
TextBlock.TextProperty.OverrideMetadata(typeof(TextBlockAutoShrink), new FrameworkPropertyMetadata(new PropertyChangedCallback(TextPropertyChanged)));
}
public TextBlockAutoShrink() : base()
{
_typeface = new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, this.FontStretch, this.FontFamily);
base.DataContextChanged += new DependencyPropertyChangedEventHandler(TextBlockAutoShrink_DataContextChanged);
}
private static void TextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var t = sender as TextBlockAutoShrink;
if (t != null)
{
t.FitSize();
}
}
void TextBlockAutoShrink_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
FitSize();
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
FitSize();
base.OnRenderSizeChanged(sizeInfo);
}
private void FitSize()
{
FrameworkElement parent = this.Parent as FrameworkElement;
if (parent != null)
{
var targetWidthSize = this.FontSize;
var targetHeightSize = this.FontSize;
var maxWidth = double.IsInfinity(this.MaxWidth) ? parent.ActualWidth : this.MaxWidth;
var maxHeight = double.IsInfinity(this.MaxHeight) ? parent.ActualHeight : this.MaxHeight;
if (this.ActualWidth > maxWidth)
{
targetWidthSize = (double)(this.FontSize * (maxWidth / (this.ActualWidth + _defaultMargin)));
}
if (this.ActualHeight > maxHeight)
{
var ratio = maxHeight / (this.ActualHeight);
// Normalize due to Height miscalculation. We do it step by step repeatedly until the requested height is reached. Once the fontsize is changed, this event is re-raised
// And the ActualHeight is lowered a bit more until it doesnt enter the enclosing If block.
ratio = (1 - ratio > 0.04) ? Math.Sqrt(ratio) : ratio;
targetHeightSize = (double)(this.FontSize * ratio);
}
this.FontSize = Math.Min(targetWidthSize, targetHeightSize);
}
}
}

Resources