There isn't going to be a one-size-fits-all solution to this as far as I'm able to imagine, but here is a solution to your specific problem that also demonstrates two different ways of handling this.
public static class AutoScale
{
public static readonly DependencyProperty AutoscaleFontProperty = DependencyProperty.RegisterAttached(
"AutoscaleFont",
typeof(bool),
typeof(AutoScale),
new PropertyMetadata((sender, e) =>
{
if (!(sender is Control c))
throw new NotSupportedException($"AutoscaleFont is for Control-derived classes only");
if (e.NewValue == e.OldValue || !(e.NewValue is bool value))
return;
if (value)
c.SizeChanged += OnSizeChangedRescaleFont;
else
c.SizeChanged -= OnSizeChangedRescaleFont;
}));
private static void OnSizeChangedRescaleFont(object sender, SizeChangedEventArgs e)
{
if (!(sender is Control c))
throw new NotSupportedException($"AutoscaleFont is for Control-derived classes only");
if (c is TextBox)
{
c.FontSize = c.ActualHeight * 0.8;
return;
}
Border border = null;
EnumVisual(c, fe =>
{
if (c is Button && fe is Border b)
{
border = b;
return true;
}
return false;
});
if (border == null)
return;
if (!(border.Child is FrameworkElement child))
return;
double scale = 1;
if (child.ActualWidth / child.ActualHeight > border.ActualWidth / border.ActualHeight)
{
// fit to width
scale = border.ActualWidth / child.ActualWidth;
}
else
{
// fit to height
scale = border.ActualHeight / child.ActualHeight;
}
child.RenderTransformOrigin = new Point(0.5, 0.5);
child.RenderTransform = new ScaleTransform
{
ScaleX = scale,
ScaleY = scale
};
}
public static bool GetAutoscaleFont (DependencyObject obj)
{
return (bool)obj.GetValue(AutoscaleFontProperty);
}
public static void SetAutoscaleFont(DependencyObject obj, bool value)
{
obj.SetValue(AutoscaleFontProperty, value);
}
private static void EnumVisual(FrameworkElement myVisual, Func<FrameworkElement, bool> action)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(myVisual); i++)
{
// Retrieve child visual at specified index value.
FrameworkElement child = VisualTreeHelper.GetChild(myVisual, i) as FrameworkElement;
if (child == null)
continue;
// Do processing of the child visual object.
if (action != null)
{
if (action(child))
break;
}
// Enumerate children of the child visual object.
EnumVisual(child, action);
}
}
}
To consume, just say:
<TextBox x:Name="username"
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="3"
Text="Username"
local:AutoScale.AutoscaleFont="True"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center" />
etc.
The meat of this is in OnSizeChangedRescaleFont
. The way to do this for any particular control is going to be control dependent. This is what I think is the best way to scale the font for both a default Button
and a default TextBox
.
You'll note these are completely different methods - for TextBox
I'd simply set the FontSize
property to be a multiple of the actual height because the TextBox
could horizontally scroll, and you probably don't want the font size to shrink as people type anyway.
For Button
where the content is static, once you locate the Border
and its child you can use a RenderTransform
to make it scale as the window resizes. This does a best-fit depending on the width of the content vs. the width of the button.
Of course this is far from perfect but hopefully it demonstrates the concepts and contains code you can use to build on. A completely robust solution would involve subclassing your controls, overriding ArrangeOverride
, and re-templating them. That is, indeed, much more complex. This should satisfy your literal example though.