如何在WPF中正确实现动态字体缩放?

问题描述 投票:0回答:2

我正在 WPF 中制作一个应用程序,它需要能够让用户决定他们想要的字体和字体大小(或者更确切地说是字体比例值)。在应用程序设置视图中,我计划添加一个滑块,其

MinValue
MaxValue
分别为 0.5 和 2.0。此滑块将更改应用程序
AppFontScale
中双变量
ResourceDictionary
的值。更改 FontScale 将影响整个应用程序的字体大小,如下所示:
(Any_Control).FontSize *= AppFontScale
。我主要尝试在 XAML 中以最少的代码隐藏干预来实现这一目标。

需要注意的是,程序需要能够在运行时更改字体缩放和字体,而无需重新启动。这是绝对的要求之一。

到目前为止我已经尝试过什么以及为什么它不起作用。

解决方案1.预定义字体大小设置
在我的项目中,我创建了一个文件夹./Fonts/FontSizes。在这里,我创建了 3 个资源字典,名为

SmallFontSize
MediumFontSize
LargeFontSize
。每个文件有 9 种不同的大小。例如,MediumFontSize.xaml 包含:

<sys:Double x:Key="TitleFontSize1">24</sys:Double>
<sys:Double x:Key="TitleFontSize2">22.5</sys:Double>
<sys:Double x:Key="TitleFontSize3">21</sys:Double>
<sys:Double x:Key="HeadingFontSize1">18</sys:Double>
<sys:Double x:Key="HeadingFontSize2">16.5</sys:Double>
<sys:Double x:Key="HeadingFontSize3">15</sys:Double>
<sys:Double x:Key="DescriptionFontSize1">12</sys:Double>
<sys:Double x:Key="DescriptionFontSize2">10.5</sys:Double>
<sys:Double x:Key="DescriptionFontSize3">9</sys:Double>

这个解决方案提供了很大的便利,但没过多久我就意识到这些字体大小对于很多字体来说要么太大要么太小。因此,我寻找一种更灵活的解决方案,并提出了提供缩放因子而不是静态大小集的想法,这使我想到了下一个解决方案。

解决方案 2。带有转换器的 DynamicResourceBindingExtension
我本质上做的是:

FontSize="{DynamicResourceBinding AppFontScale, Converter={StaticResource FontScalingHelper}, ConverterParameter=12}"

FontScalingHelper
是一个
IValueConverter
,其中 Convert 方法如下。

public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
    double scaleFactor = System.Convert.ToDouble(value);
    double baseSize = System.Convert.ToDouble(parameter);
    if (scaleFactor < 0.5 || scaleFactor > 2.0) scaleFactor = 1.0;
    if (baseSize < 1) baseSize = 12;
    return baseSize * scaleFactor;
}

当直接在 xaml 中的控件主体中使用时,此解决方案效果很好。 然而,在样式设置器中使用它会在设计时引发以下异常,从而使设计变得不可能(有趣的是,尽管设计时出现异常,但运行时没有异常)。

"'DynamicResourceBindingExtension' is not valid for Setter.Value. The only supported MarkupExtension types are DynamicResourceExtension and BindingBase or derived types."

除了这个例外,我已经找到了解决方案。只需继承

DynamicResourceExtension
即可。我这样做了,却发现
ProvideValue
从未被调用过。阅读文档然后消除了我的疑虑。

此方法支持 WPF XAML 处理器实现,但不支持 旨在直接调用。 XAML 处理器实现使用 此方法用于正确处理 DynamicResource 标记扩展 对象创建期间的值。

解决方案3.直接使用转换器
我修改了

FontScalingHelper
类以继承
DependencyObject
IValueConverter
。它有一个 DP
FontScale
,使用
AppFontScale
绑定到
DynamicResource
。此解决方案的问题在于,更改 FontScale 不会调用
Convert
方法,因为使用的控件类不知道
AppFontScale
已更改。这让我找到了解决方案 4。

解决方案 4. 修改

FontScalingHelper
并定义大小范围的全局实例

public class FontScalingHelper : System.Windows.DependencyObject, System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

    public FontScalingHelper() : base()
    {

    }

    public static readonly System.Windows.DependencyProperty FontScaleProperty = System.Windows.DependencyProperty.Register("FontScale", typeof(double), typeof(FontScalingHelper), new System.Windows.PropertyMetadata(1d, OnScaleChanged));

    public static readonly System.Windows.DependencyProperty FontSizeProperty = System.Windows.DependencyProperty.Register("FontSize", typeof(double), typeof(FontScalingHelper), new System.Windows.PropertyMetadata(12d, OnScaleChanged));

    public static readonly System.Windows.DependencyProperty ScaledFontSizeProperty = System.Windows.DependencyProperty.Register("ScaledFontSize", typeof(double), typeof(FontScalingHelper), new System.Windows.PropertyMetadata(12d));

    public double ScaledFontSize
    {
        get { return (double)GetValue(ScaledFontSizeProperty); }
        private set { SetValue(ScaledFontSizeProperty, value); PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs("ScaledFontSize")); }
    }

    public double FontSize
    {
        get { return (double)GetValue(FontSizeProperty); }
        set { SetValue(FontSizeProperty, value); PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs("FontSize")); ScaledFontSize = FontSize * FontScale; }
    }

    public double FontScale
    {
        get { return (double)GetValue(FontScaleProperty); }
        set { SetValue(FontScaleProperty, value); PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs("FontScale")); ScaledFontSize = FontSize * FontScale; }
    }

    private static void OnScaleChanged(System.Windows.DependencyObject d, System.Windows.DependencyPropertyChangedEventArgs e)
    {
        if (d is FontScalingHelper helper)
        {
            double newFontSize = helper.FontScale * helper.FontSize;
            helper.SetValue(ScaledFontSizeProperty, newFontSize);
        }
    }
}

我必须在资源字典中创建此类的一系列全局实例,以包含预设的基本大小,如下所示。

<FontSupport:FontScalingHelper x:Key="ScaledFontSize30" FontScale="{DynamicResource ApplicationFontScale}" FontSize="30"/>
.
.
.
<FontSupport:FontScalingHelper x:Key="ScaledFontSize7" FontScale="{DynamicResource ApplicationFontScale}" FontSize="7"/>

到目前为止,该解决方案在控件主体和样式设置器中运行良好。然而,这感觉非常不直观,对于我必须使用的每个独特大小,我必须定义另一个资源。

我的主要问题:是否有更好的方法可以在设计和运行时工作,无论我是直接在控件主体中还是在样式设置器中使用它?我基本上希望能够做类似的事情:

<TextBox Text="Test" FontSize="{Binding BaseValue=13, FontScale={DynamicResource AppFontScale}}"/>

我知道这种确切的格式是不可能的,但类似的格式会很棒。如果不是,您更喜欢上述哪种解决方案,为什么?您喜欢的方法有哪些可以改进的地方?

奖励:为什么 WPF 中没有内置功能来支持字体缩放?如果有的话,为什么我在互联网上搜索了好几个星期都找不到它?

编辑 1:重新措辞问题,将焦点从“最佳方式”更改为“正确的实施方法”。

wpf data-binding
2个回答
0
投票

使用 DependencyObject 是有效且正确的解决方案。不过,我有几点意见:

请勿同时使用 System.Windows.DependencyObject 和 System.ComponentModel.INotifyPropertyChanged。对控件和其他 WPF 相关内容使用 DependencyObject,对视图模型使用 INotifyPropertyChanged。

在属性内部设置值时,使用

SetCurrentValue
而不是
SetValue
来防止破坏 XAML 中可能设置的绑定。

ScaledFontSize
设为只读。

添加属性验证。

public class FontScalingHelper : DependencyObject
{
    public static readonly DependencyProperty FontScaleProperty = DependencyProperty.Register(
        name: "FontScale",
        propertyType: typeof(double),
        ownerType: typeof(FontScalingHelper),
        typeMetadata: new FrameworkPropertyMetadata(
            defaultValue: 1.0,
            propertyChangedCallback: FontScaleChanged),
        validateValueCallback: ValidateDoubleValue);

    public double FontScale
    {
        get { return (double)GetValue(FontScaleProperty); }
        set { SetValue(FontScaleProperty, value); }
    }

    public static readonly DependencyProperty FontSizeProperty = DependencyProperty.Register(
        name: "FontSize",
        propertyType: typeof(double),
        ownerType: typeof(FontScalingHelper),
        typeMetadata: new FrameworkPropertyMetadata(
            defaultValue: 12.0,
            propertyChangedCallback: FontSizeChanged),
        validateValueCallback: ValidateDoubleValue);

    public double FontSize
    {
        get { return (double)GetValue(FontSizeProperty); }
        set { SetValue(FontSizeProperty, value); }
    }

    // Readonly property key:
    private static readonly DependencyPropertyKey _scaledFontSizePropertyKey = DependencyProperty.RegisterReadOnly(
        name: "ScaledFontSize",
        propertyType: typeof(double),
        ownerType: typeof(FontScalingHelper),
        typeMetadata: new FrameworkPropertyMetadata(
            defaultValue: 12.0),
        validateValueCallback: ValidateDoubleValue);

    // Public readonly property:
    public static readonly DependencyProperty ScaledFontSizeProperty = _scaledFontSizePropertyKey.DependencyProperty;

    public double ScaledFontSize
    {
        get { return (double)GetValue(ScaledFontSizeProperty); }
        private set { SetValue(_scaledFontSizePropertyKey, value); } // private setter
    }

    private static void FontScaleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is FontScalingHelper helper)
        {
            double newFontSize = ((double)e.NewValue) * helper.FontSize;
            helper.ScaledFontSize = newFontSize;
        }
    }

    private static void FontSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is FontScalingHelper helper)
        {
            double newFontSize = helper.FontScale * ((double)e.NewValue);
            helper.ScaledFontSize = newFontSize;
        }
    }

    private static bool ValidateDoubleValue(object value)
    {
        return value is double dValue && Double.IsFinite(dValue);
    }
}

但是如果

ApplicationFontScale
是静态资源,那么事情对你来说可能会简单得多。

例如:

public class FontScaleHolder : DependencyObject
{
    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
        name: "Value",
        propertyType: typeof(double),
        ownerType: typeof(FontScaleHolder),
        typeMetadata: new FrameworkPropertyMetadata(
            defaultValue: 1.0),
        validateValueCallback: ValidateDoubleValue);

    public double Value
    {
        get { return (double)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }

    private static bool ValidateDoubleValue(object value)
    {
        return value is double dValue && Double.IsFinite(dValue);
    }
}

然后将其添加到App.xaml中:

<FontSupport:FontScaleHolder x:Key="ApplicationFontScale" />

最后你可以使用转换器:

FontSize="{Binding Source={StaticResource ApplicationFontScale}, Path=Value, Converter={StaticResource FontScalingHelper}, ConverterParameter=12}

就像第二个解决方案一样。


0
投票

我不清楚您所面临的要求,因此我假设您正在寻找一致地更改控件字体大小的方法。

以下是使用附加属性的概念证明。需要根据实际使用情况添加管理Controls集合的代码。

using System.Windows;
using System.Windows.Controls;
namespace WpfFontSize;

public static class FontSizeHelper
{
    private static readonly List<Control> _controls = [];

    public static void ChangeFontSize(double factor)
    {
        foreach (var control in _controls)
        {
            control.FontSize = GetDefaultFontSize(control) * factor;
        }
    }

    public static double GetDefaultFontSize(DependencyObject obj)
    {
        return (double)obj.GetValue(DefaultFontSizeProperty);
    }
    public static void SetDefaultFontSize(DependencyObject obj, double value)
    {
        obj.SetValue(DefaultFontSizeProperty, value);
    }
    public static readonly DependencyProperty DefaultFontSizeProperty =
        DependencyProperty.RegisterAttached(
            "DefaultFontSize",
            typeof(double),
            typeof(FontSizeHelper),
            new PropertyMetadata(0D, OnDefaultFontSizeChanged));

    private static void OnDefaultFontSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is Control control)
        {
            control.FontSize = (double)e.NewValue;
            _controls.Add(control);
        }
    }
}

它可以在控件或样式中使用。

<Window x:Class="WpfFontSize.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfFontSize"
        Width="800" Height="200">
    <Window.Resources>
        <Style x:Key="ButtonFontSize12" TargetType="{x:Type Button}">
            <Setter Property="local:FontSizeHelper.DefaultFontSize" Value="12"/>
        </Style>
    </Window.Resources>

    <StackPanel>
        <Button local:FontSizeHelper.DefaultFontSize="24"
                Content="Header"/>
        <Button Style="{StaticResource ButtonFontSize12}"
                Content="body"
                Click="Increment_Click"/>
    </StackPanel>
</Window>
using System.Windows;
namespace WpfFontSize;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private double _factor = 1D;

    private void Increment_Click(object sender, RoutedEventArgs e)
    {
        _factor *= 1.1;
        FontSizeHelper.ChangeFontSize(_factor);
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.