.NET MAUIで4K/高DPI対応:Windows・MacCatalystのLabel/Buttonフォントサイズを画面解像度に応じて自動スケールする実装ガイド

4Kモニターや高DPI環境で.NET MAUIアプリを動かすと、「WindowsやMacでページ幅は広いのに文字が小さくて読みにくい」という課題が頻発します。本稿では、Label/ButtonのFontSizeを“%指定”のように画面サイズや解像度へ比例させて自動調整する実装を、設計指針からコード、運用Tipsまで一気通貫で解説します。XAMLだけで完結しない現実的なアプローチに的を絞り、すぐ導入できるサンプルを多数掲載します。

目次

.NET MAUIに「%指定フォント」は無い―どう設計するか

標準のFontSizeは数値(絶対値)またはNamedSizeです。CSSのfont-size: 150%のような割合指定は組み込みで提供されていません。したがって、「どの値を基準に」「いつ」「どこに」倍率を掛けるかを自前で設計する必要があります。

論点推奨方針理由
基準ページ(ウィンドウ)の論理幅を基準にするユーザーがウィンドウをリサイズした時も自然に追従。マルチモニター/DPI変更にも強い。
イベントSizeChangedMainDisplayInfoChangedウィンドウ幅変化とOSのスケーリング変更(例:100%→150%)の双方を捕捉。
範囲MinScaleMaxScaleでクランプ超広幅で巨大化/狭幅で極小化を抑止し、可読性を維持。
適用対象Label/Button(必要ならEntry等も)まずは主要コントロールから。拡張は段階的に。

実装A:ウィンドウ幅に比例させてFontSizeを自動調整(推奨)

最も扱いやすいのは「ウィンドウの論理幅(device independent unit)に比例」させる方法です。次のBehaviorを各Label/Buttonに付けるだけで自動スケールします。

AutoFontScaleBehavior(共通ロジック)

using Microsoft.Maui.Controls;
using Microsoft.Maui.Devices;

namespace YourApp.Behaviors;

public class AutoFontScaleBehavior : Behavior
{
public static readonly BindableProperty BaseFontSizeProperty =
BindableProperty.Create(nameof(BaseFontSize), typeof(double), typeof(AutoFontScaleBehavior), 16d);


// 基準フォント(例:ボディ16、ボタン14、見出し24など)
public double BaseFontSize
{
    get => (double)GetValue(BaseFontSizeProperty);
    set => SetValue(BaseFontSizeProperty, value);
}

// 設計の基準幅(FHD=1920を推奨)
public double BaselineWidth { get; set; } = 1920d;

// 極端な拡大・縮小の抑止
public double MinScale { get; set; } = 0.85d;
public double MaxScale { get; set; } = 1.60d;

protected override void OnAttachedTo(VisualElement bindable)
{
    base.OnAttachedTo(bindable);
    bindable.SizeChanged += OnSizeChanged;
    DeviceDisplay.Current.MainDisplayInfoChanged += OnDisplayInfoChanged;
    Apply(bindable);
}

protected override void OnDetachingFrom(VisualElement bindable)
{
    base.OnDetachingFrom(bindable);
    bindable.SizeChanged -= OnSizeChanged;
    DeviceDisplay.Current.MainDisplayInfoChanged -= OnDisplayInfoChanged;
}

void OnSizeChanged(object? sender, EventArgs e)
{
    if (sender is VisualElement ve) Apply(ve);
}

void OnDisplayInfoChanged(object? sender, DisplayInfoChangedEventArgs e)
{
    if (AssociatedObject is VisualElement ve) Apply(ve);
}

void Apply(VisualElement ve)
{
    // ウィンドウ幅(論理px)を最優先で取得
    var window = ve.GetParentWindow();
    double width = window?.Width ?? 0;

    // レイアウト前やウィンドウ未確定の初期化タイミングではViewのWidthを利用
    if (width <= 0) width = ve.Width;

    // さらに取れない場合のフォールバック:ディスプレイの論理幅
    if (width <= 0)
    {
        var info = DeviceDisplay.Current.MainDisplayInfo;
        width = info.Width / info.Density;
    }

    var scale = Math.Clamp(width / BaselineWidth, MinScale, MaxScale);

    // 対象を限定(必要ならEntry/Editor/Pickerなども拡張)
    switch (ve)
    {
        case Label lbl:
            lbl.FontSize = BaseFontSize * scale;
            break;
        case Button btn:
            btn.FontSize = BaseFontSize * scale;
            break;
    }
}


} 

XAMLでの適用例

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:behaviors="clr-namespace:YourApp.Behaviors"
    x:Class="YourApp.Pages.SamplePage">




<Label Text="画面の論理幅に比例して拡大・縮小されます"
       FontAttributes="Bold">
  <Label.Behaviors>
    <behaviors:AutoFontScaleBehavior BaseFontSize="20"
                                     BaselineWidth="1920"
                                     MinScale="0.90"
                                     MaxScale="1.80" />
  </Label.Behaviors>
</Label>

<Label Text="これは本文用のサイズ(基準16pt)です">
  <Label.Behaviors>
    <behaviors:AutoFontScaleBehavior BaseFontSize="16" />
  </Label.Behaviors>
</Label>

<Button Text="決定">
  <Button.Behaviors>
    <behaviors:AutoFontScaleBehavior BaseFontSize="14" />
  </Button.Behaviors>
</Button>



 

この方式は「今表示しているウィンドウ幅」を基準にするため、以下の利点があります。

  • ウィンドウリサイズで即応(SizeChanged)。
  • マルチモニターでウィンドウを別モニターへ移動しても、レイアウトが破綻しにくい。
  • OSのスケール変更(100%⇔150%など)にも追従(MainDisplayInfoChanged)。

おすすめの基準フォント値(BaseFontSize)

用途基準サイズ(pt)想定
タイトル(H1)24–28セクション見出し。大画面で視認性を高める。
サブタイトル(H2)20–22カードやダイアログのヘッダーなど。
本文(ボディ)16最も読む量が多いテキスト用の基準。
キャプション/補足12–13内容量が少なく、意味が補助的な場合。
ボタンラベル14–16指先で押しやすい視認性とバランスを確保。

実装B:ディスプレイ解像度とDPI係数から比率を算出する

ページ幅ではなく、「ディスプレイ解像度とDPI(Density)」から論理幅を求めて倍率を計算するパターンです。フルHD(1920)を基準とした例を示します。

using Microsoft.Maui.Devices;

var info = DeviceDisplay.Current.MainDisplayInfo;

// Width/Height は物理解像度(px)、Density はDPIスケーリング係数
double logicalWidth = info.Width / info.Density;   // 論理ピクセル
double ratio        = logicalWidth / 1920.0;       // FHD=1920 を基準に比率算出

myLabel.FontSize  = 16 * ratio;  // 16pt を基準に調整
myButton.FontSize = 14 * ratio;  // 14pt を基準に調整

この方式は「常にディスプレイ」基準なので、フルスクリーンに近いユースケース(POSや専用端末など)で有効です。ただし一般的なデスクトップアプリでは、ウィンドウが狭いのにフォントが大きくなり過ぎる可能性があります。そのため、実装A(ウィンドウ幅基準)と併用し、より小さい方のスケールを採用するガードを入れておくと実用性が上がります。

double scaleByWindow = Math.Clamp(windowWidth / 1920.0, 0.85, 1.6);
double scaleByDisplay = Math.Clamp((info.Width / info.Density) / 1920.0, 0.85, 1.6);

// ウィンドウ幅を優先しつつ、極端な小画面での視認性も確保
double scale = Math.Min(scaleByWindow, scaleByDisplay);

XAMLの責務は最小限に:初期値だけを持たせ、実倍率はコードで上書き

「プラットフォームごとの固定値(OnPlatform)を辞書に並べる」という従来のやり方は、4K/150%環境や超ワイド画面で文字が小さすぎる問題を引き起こします。XAMLには“最低限読める初期値”だけを定義し、実際の倍率はBehaviorやコードビハインドで計算して上書きする構成がシンプルで拡張しやすく、おすすめです。

&lt;ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"&gt;

  &lt;Style x:Key="BodyLabel" TargetType="Label"&gt;
    &lt;Setter Property="FontSize"&gt;
      &lt;Setter.Value&gt;
        &lt;OnPlatform x:TypeArguments="x:Double"&gt;
          &lt;On Platform="WinUI"&gt;16&lt;/On&gt;
          &lt;On Platform="MacCatalyst"&gt;16&lt;/On&gt;
        &lt;/OnPlatform&gt;
      &lt;/Setter.Value&gt;
    &lt;/Setter&gt;
  &lt;/Style&gt;

&lt;/ResourceDictionary&gt;

この「最低限の初期値」に対し、前述のBehaviorで最終的なFontSizeを適用します。これにより、XAMLのメンテナンスコストが大幅に下がります。

代替策:文字をSVG化(静的テキスト/ロゴに限る)

ロゴや固定見出しなど動的変更の必要がないテキストは、SVGアイコンに置き換えると、MAUIが自動的に高DPIで鮮明に拡大縮小します。読み上げ対応が必要な場合はAutomationProperties.Nameを設定しましょう(ボタンのラベルなど動的文字列には不向き)。

&lt;Image Source="resource://YourApp.Resources.Images.Title.svg"
       HeightRequest="48"
       AutomationProperties.Name="アプリタイトル" /&gt;

ユーザー設定と掛け合わせる(アクセシビリティ)

個々のユーザーが「自分にとって読みやすい倍率」を選べるよう、係数を設定画面で保持し、計算結果に掛け合わせると満足度が上がります。

using Microsoft.Maui.Storage;

const string FontUserScaleKey = "FontUserScale"; // 例:0.9〜1.4
double userScale = Preferences.Get(FontUserScaleKey, 1.0);

// Behavior内の適用直前で掛け合わせる
var scale = Math.Clamp(width / BaselineWidth, MinScale, MaxScale) * userScale;

CommunityToolkit.MauiのBreakpointServiceで段階的スイッチ

連続スケールではなく、「幅によってスタイルを段階的に切り替えたい」場合は.NET MAUI CommunityToolkitBreakpointServiceが便利です。ブレークポイント(例:<=1280、<=1920、>1920)ごとにスタイルを差し替えることで、可読性とデザインの一貫性を両立できます(記事の主題は“連続的な%スケール”なので詳細コードは割愛)。

品質を高める実務Tips

  • BaselineWidthの決め方:フルHD(1920)を基準にしておくと、テスト機材が揃えやすく、比率の直感も掴みやすい。
  • 極端な値の抑止:MinScale/MaxScaleのクランプは必須。スプリットビューやウィンドウスナップ時の文字崩壊を防ぐ。
  • 複数モニター:ディスプレイ情報だけでなくウィンドウの幅から算出するのが堅実。移動・拡大縮小に強い。
  • 初期化タイミング:レイアウト前はWidthが0のため、BehaviorのOnAttachedToで一度フォールバック適用し、SizeChangedで追従させる。
  • パフォーマンス:全コントロールに都度バインドすると負荷が跳ね上がる。Behavior1本で計算し、必要なコントロールだけに適用する。
  • 国際化:多言語で文字長が伸びる場合は、行高(LineHeight)やパディングも見直す。

「設計とテスト」のチェックリスト

観点チェック方法合格基準
4K 100%スケール4Kモニターでフルスクリーンとウィンドウ幅50%を確認可読性(本文≧16pt相当)を維持し、見出しの視認性が十分
4K 150%スケールOSの表示スケールを150%へ変更して再確認崩れ(はみ出し/折返し過多)がない。ボタンのラベルは2行化しない
FHD(1920×1080)フルスクリーン、スナップ(1/2、1/3幅)で確認クランプにより極端に小さく/大きくならない
超ワイド(2560+)横幅が広い環境でのカードレイアウトMaxScale上限で読みやすさを保つ
ユーザー倍率設定で倍率0.9/1.2等を切替設定が即時反映。再起動後も維持

ページ全体を一括スケールする(応用・大規模画面向け)

画面全体で同じ基準倍率を採用したい場合、ページのルートで計算し、子孫のLabel/Buttonへ一括反映するアプローチが有効です。以下は簡易的な再帰適用サンプルです。

using Microsoft.Maui.Controls;
using Microsoft.Maui.Devices;

public partial class ScalablePage : ContentPage
{
    const double BaselineWidth = 1920.0;
    const double MinScale = 0.85;
    const double MaxScale = 1.60;

    public ScalablePage()
    {
        InitializeComponent();
        SizeChanged += (_, __) =&gt; ApplyScale();
        DeviceDisplay.Current.MainDisplayInfoChanged += (_, __) =&gt; ApplyScale();
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        ApplyScale();
    }

    void ApplyScale()
    {
        var windowWidth = this.GetParentWindow()?.Width ?? this.Width;
        if (windowWidth &lt;= 0)
        {
            var info = DeviceDisplay.Current.MainDisplayInfo;
            windowWidth = info.Width / info.Density;
        }

        var scale = Math.Clamp(windowWidth / BaselineWidth, MinScale, MaxScale);

        if (this.Content is not View root) return;
        ApplyToDescendants(root, scale);
    }

    void ApplyToDescendants(View view, double scale)
    {
        switch (view)
        {
            case Label lbl:
                double baseLabel = GetBaseSize(lbl, 16);
                lbl.FontSize = baseLabel * scale;
                break;
            case Button btn:
                double baseButton = GetBaseSize(btn, 14);
                btn.FontSize = baseButton * scale;
                break;
        }

        if (view is Layout layout)
        {
            foreach (var child in layout.Children)
                ApplyToDescendants(child, scale);
        }
        else if (view is ContentView cv &amp;&amp; cv.Content is not null)
        {
            ApplyToDescendants(cv.Content, scale);
        }
        else if (view is ScrollView sv &amp;&amp; sv.Content is not null)
        {
            ApplyToDescendants(sv.Content, scale);
        }
        // 必要に応じてCollectionView/CarouselViewの可視セルにも適用
    }

    // 既定値を属性で持たせる(XAMLで "maui:BaseFontSize.Value='16'" のように設定)
    public static readonly BindableProperty BaseFontSizeProperty =
        BindableProperty.CreateAttached("BaseFontSize", typeof(double), typeof(ScalablePage), 0d);

    public static void SetBaseFontSize(BindableObject view, double value) =&gt; view.SetValue(BaseFontSizeProperty, value);
    public static double GetBaseFontSize(BindableObject view) =&gt; (double)view.GetValue(BaseFontSizeProperty);

    static double GetBaseSize(BindableObject view, double fallback)
    {
        var v = GetBaseFontSize(view);
        return v &gt; 0 ? v : fallback;
    }
}

XAML側:基準サイズだけ記述(Attached Property)

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:maui="clr-namespace:YourApp"
    x:Class="YourApp.Pages.DemoScalablePage">




<Label Text="記事タイトル"
       FontAttributes="Bold"
       maui:ScalablePage.BaseFontSize="24" />

<Label Text="本文テキスト(基準16)"
       maui:ScalablePage.BaseFontSize="16" />

<Button Text="OK"
        maui:ScalablePage.BaseFontSize="14" />



 

この方式は「XAMLは基準値だけ」「最終値はコードで一括上書き」という明快な責務分担になります。長期運用で強みを発揮します。

よくある落とし穴と回避策

  • 初回にWidthが0のまま:表示直後はWidthが確定していない場合があるため、MainDisplayInfoの論理幅フォールバックを用意。
  • コレクション系の仮想化:CollectionViewはセルの生成・破棄が頻繁。セル生成時にスケールを適用するか、スタイルで初期値を上げておく。
  • 縦横回転:MacCatalystでは少ないが、iPadや一部環境では回転イベントも検知して再適用を。
  • 過剰な再レイアウト:毎フレームのように再計算すると重い。イベントはSizeChangedMainDisplayInfoChangedに限定し、値が変わった時のみ再適用。
  • 視覚的一貫性:フォントだけでなく、列幅/行間/アイコンサイズにも同じ係数を掛けると“揃ったUI”に。必要に応じてImage.WidthRequestHeightRequestも比例させる。

ミニマムなサンプル:まずはここから

「とりあえずLabelとButtonだけスケールしてみたい」という場合の最小コードです。

void ScaleOnce(Label lbl, Button btn)
{
    var info = DeviceDisplay.Current.MainDisplayInfo;
    double logicalWidth = info.Width / info.Density;
    double scale = Math.Clamp(logicalWidth / 1920.0, 0.9, 1.6);


lbl.FontSize  = 20 * scale;
btn.FontSize  = 14 * scale;


} 

サンプル比率の目安

環境論理幅の目安倍率(Baseline=1920)本文16ptの最終値
FHD 100%(全画面)≈19201.0016
4K 100%(全画面)≈38402.00(MaxScaleで抑止推奨)32(上限で例:25.6)
4K 150%(全画面)≈25601.3321.3
FHDの半分幅(スナップ)≈9600.50(MinScaleで抑止推奨)8(下限で例:13.6)

※ 実値はOSやMAUIのスケール実装、ウィンドウ装飾幅等で前後します。必ず実機で確認してください。

まとめ:実用的な解決策は二択(+段階切替)

  • 連続スケール:Behaviorやページ側ロジックで「ウィンドウ論理幅」や「ディスプレイ論理幅」に比例させてFontSizeを上書きする。
  • SVG化:ロゴや固定見出し等はSVGに置き換え、DPIを意識せず高精細表示を得る。
  • 段階切替:CommunityToolkitのブレークポイントでスタイルをスイッチし、デザインの一貫性を保つ。

標準のFontSizeに“%指定”はありません。だからこそ「基準」「イベント」「上限下限」という3点を押さえた自前実装が肝要です。まずは本稿のBehaviorを導入し、基準=1920MinScaleMaxScaleをチューニング。そこからボタン/本文/見出しへ基準値を割り当てれば、4K/高DPIのWindows・MacCatalystでも読みやすい文字サイズを継続的に提供できます。


付録:設計判断の比較表

方式メリットデメリット適用に向くケース
ウィンドウ幅比例(推奨)リサイズやマルチモニターに強い。直感的。縦長極端時に過小/過大化しやすい(クランプで緩和)。一般的なデスクトップアプリ全般。
ディスプレイ論理幅比例フルスクリーン前提で安定。小さなウィンドウでもフォントが大き過ぎる可能性。専用端末、キオスク、POS。
Breakpointによる段階切替デザインの一貫性を保ちやすい。連続的な最適化には弱い。閾値設計が必要。ブランド重視の業務アプリ。
SVG化ロゴ・固定見出しは超高精細で常に綺麗。動的文字列には不向き。i18nと相性が悪い。ロゴ、固定タイトル、装飾テキスト。

付録:ButtonやEntry等へ対象拡張する場合

Behaviorのswitch文にケースを追加するだけで、ほかのコントロールにも波及できます。例としてEntry/Editor/Pickerへの対応を示します。

switch (ve)
{
    case Label lbl:
        lbl.FontSize = BaseFontSize * scale;
        break;
    case Button btn:
        btn.FontSize = BaseFontSize * scale;
        break;
    case Entry ent:
        ent.FontSize = BaseFontSize * scale;
        break;
    case Editor ed:
        ed.FontSize = BaseFontSize * scale;
        break;
    case Picker pk:
        pk.FontSize = BaseFontSize * scale;
        break;
}

付録:テキスト以外のサイズも連動させる

フォントだけではUI全体のバランスが崩れやすいので、アイコンやパディング、行間も同スケールで調整すると見栄えが整います。

// 例:アイコン(ImageButton)もスケール
if (ve is ImageButton ib)
{
    double baseSize = 20; // 基準アイコンサイズ
    double size = baseSize * scale;
    ib.WidthRequest = size;
    ib.HeightRequest = size;
}

ここまでの設計とコードをそのまま導入すれば、.NET MAUIで「画面解像度やページ幅に応じてLabel/Buttonの文字サイズを自動でスケール」できるようになります。まずは小さな画面から大きな画面まで、読みやすさ優先でチューニングしてみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次