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.