WPF 客戶端經常需要執行在各種不同大小螢幕下,為了顯示友好,所以開發的時候都需要考慮響應式設計。
佈局往往通過指定比例,而不直接指定準確的大小來實現響應式佈局(如 Width="3*" ),但是具體控制元件的大小(如 Thickness、CornerRadius)就沒有開箱即用的響應式功能了,用 viewbox 來包裝,比例就跟設計稿不一樣了,看起來很怪。
嗐,所以又只能自己開發了!
實現目標
- 實現類似 css @media 媒體查詢類似的功能。
- 設計稿都是 1920 × 1080 實現的,在 3840 × 2160 下,應該將所有控制元件的大小,邊框放大兩倍。
- 要考慮使用者在系統下設定的縮放比例。
- 同事用起來要舒服,要支援熱過載。
實現邏輯
根據螢幕大小和螢幕縮放比例來計算縮放係數。
- 螢幕的 api 當然是白嫖別人寫的庫啦,我這裡用的是 WpfScreenHelper。
public static class AppExtension
{
private static double? _factor;
/// <summary>
/// 獲取當前應用的縮放係數
/// 如果 4K 螢幕,需要放大 2 倍
/// </summary>
/// <param name="app"></param>
/// <returns></returns>
public static double GetFactor(this Application app)
{
if (_factor is not null) return _factor.Value;
var screen = app.MainWindow?.GetScreen() ?? throw new ArgumentNullException(nameof(app.MainWindow));
_factor = screen.PixelBounds switch
{
{ Width: >= 3840, Height: >= 2160 } => screen.ScaleFactor / 2,
_ => screen.ScaleFactor
};
Debug.WriteLine($"螢幕大小: {screen.PixelBounds.Width} × {screen.PixelBounds.Height}");
Debug.WriteLine($"螢幕縮放: {screen.ScaleFactor * 100}%");
return _factor.Value;
}
/// <summary>
/// 根據螢幕大小和縮放系統,轉換不同的資料型別
/// </summary>
/// <param name="app"></param>
/// <param name="o"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
public static object ConvertForScreen(this Application app, object o) =>
o switch
{
double d => app.ConvertDoubleForScreen(d),
Thickness t => app.ConvertThicknessForScreen(t),
CornerRadius t => app.ConvertCornerRadiusForScreen(t),
_ => throw new NotSupportedException("不支援的轉換型別")
};
public static double ConvertDoubleForScreen(this Application app, double value)
{
var factor = app.GetFactor();
return value / factor;
}
public static Thickness ConvertThicknessForScreen(this Application app, Thickness value)
{
var factor = app.GetFactor();
return new Thickness(value.Left / factor, value.Top / factor, value.Right / factor, value.Bottom / factor);
}
public static CornerRadius ConvertCornerRadiusForScreen(this Application app, CornerRadius value)
{
var factor = app.GetFactor();
return new CornerRadius(value.TopLeft / factor, value.TopRight / factor, value.BottomRight / factor,
value.BottomLeft / factor);
}
/// <summary>
/// 獲取當前窗體所在的螢幕
/// </summary>
/// <param name="window">當前窗體</param>
/// <returns>窗體所在的螢幕</returns>
public static Screen GetScreen(this Window window)
{
var intPtr = new WindowInteropHelper(window).Handle; //獲取當前視窗的控制程式碼
return Screen.FromHandle(intPtr); //獲取當前螢幕
}
}
自定義 xaml 標記
- 適配 Setter 上賦值和直接在控制元件上賦值的場景。
- 通過各種轉換器來轉換值。
public class ResponsiveSizeExtension : MarkupExtension
{
private static readonly Lazy<DoubleConverter> _lazyDouble = new();
private static readonly Lazy<ThicknessConverter> _lazyThickness = new();
private static readonly Lazy<CornerRadiusConverter> _lazyCornerRadius = new();
private DoubleConverter _doubleConverter => _lazyDouble.Value;
private ThicknessConverter _thickConvert => _lazyThickness.Value;
private CornerRadiusConverter _cornerRadiusConvert => _lazyCornerRadius.Value;
[ConstructorArgument("value")]
public object Value { get; set; }
public ResponsiveSizeExtension(object value)
{
if (value is string s && string.IsNullOrWhiteSpace(s)) value = "0";
Value = value;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
var target = (IProvideValueTarget)serviceProvider;
var type = target switch
{
{ TargetObject: Setter setter } => setter.Property.PropertyType,
{ TargetProperty: DependencyProperty dp } => dp.PropertyType,
_ => throw new NotSupportedException($"不是 Setter 物件或者依賴屬性")
};
TypeConverter converter = type switch
{
not null when type == typeof(double) => _doubleConverter,
not null when type == typeof(Thickness) => _thickConvert,
not null when type == typeof(CornerRadius) => _cornerRadiusConvert,
_ => throw new NotSupportedException($"{type} 型別不支援")
};
var originValue = converter.ConvertFrom(Value) ?? throw new ArgumentException(nameof(Value));
var newValue = Application.Current.ConvertForScreen(originValue);
PrintLog(originValue, newValue);
return newValue;
}
private void PrintLog(object originValue, object newValue) => Debug.WriteLine($"originValue: {originValue}, newValue: {newValue}, factor: {Application.Current.GetFactor()}");
}
使用
<Window
...
xmlns:local="clr-namespace:Responsive" >
<Window.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="{local:ResponsiveSize 16}" />
</Style>
</Window.Resources>
<Border
Margin="50"
BorderBrush="LightSeaGreen"
BorderThickness="{local:ResponsiveSize 12}">
<Grid Width="{local:ResponsiveSize 200}" Background="LightBlue">
<TextBlock Text="Test" />
</Grid>
</Border>
</Window>
效果
螢幕大小: 1920 × 1080
螢幕縮放: 100%
originValue: 12,12,12,12, newValue: 12,12,12,12, factor: 1
originValue: 200, newValue: 200, factor: 1
originValue: 16, newValue: 16, factor: 1
螢幕大小: 3840 × 2160
螢幕縮放: 100%
originValue: 12,12,12,12, newValue: 24,24,24,24, factor: 0.5
originValue: 200, newValue: 400, factor: 0.5
originValue: 16, newValue: 32, factor: 0.5
螢幕大小: 3840 × 2160
螢幕縮放: 150%
originValue: 12,12,12,12, newValue: 16,16,16,16, factor: 0.75
originValue: 200, newValue: 266.6666666666667, factor: 0.75
originValue: 16, newValue: 21.333333333333332, factor: 0.75