Rustのeguiライブラリにおける中国語フォントの導入と表示手法

Rustエコシステムにおける即時モードGUIライブラリであるeguiは、その軽量さと柔軟性で知られています。しかし、デフォルトの設定ではラテン文字やキリル文字のみがサポートされており、日本語や中国語などのマルチバイト文字をそのまま描画しようとすると文字化け(いわゆる「豆腐」)が発生します。本記事では、eguiおよびeframeを使用したアプリケーションにおいて、独自のTTF/OTFフォントを読み込み、ウィンドウやウィジェット上で中国語などの非ASCII文字を正しく表示するための実装手順について解説します。

プロジェクトの依存関係

まず、Cargo.tomlファイルにeguiとそのウェブフレームワークであるeframeのクレートを追加します。バージョンは執筆時点の最新安定版を使用します。

[dependencies]
egui = "0.28"
eframe = "0.28"

フォント設定が行われない場合の挙動

デフォルトのフォント設定で日本語や中国語を含むテキストをレンダリングしようとすると、グリフが見つからないため文字が表示されません。以下に、ウィンドウを表示し、ボタン操作でモーダルダイアログを開くシンプルなコード例(TextDisplayApp構造体)を示します。この段階ではまだフォント設定を行っていません。

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use eframe::egui;

fn main() -> eframe::Result {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
        ..Default::default()
    };
    
    eframe::run_native(
        "Font Test",
        options,
        Box::new(|cc| Ok(Box::new(TextDisplayApp::default()))),
    )
}

#[derive(Default)]
struct TextDisplayApp {
    show_modal: bool,
}

impl eframe::App for TextDisplayApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.heading("Egui Font Test");
            if ui.button("メッセージを表示").clicked() {
                self.show_modal = true;
            }
        });

        if self.show_modal {
            egui::Window::new("確認")
                .collapsible(false)
                .resizable(false)
                .show(ctx, |ui| {
                    ui.label("これは中国語のテストです:测试中文字符");
                    if ui.button("閉じる").clicked() {
                        self.show_modal = false;
                    }
                });
        }
    }
}

上記のコードを実行すると、「テスト中文字符」の部分が正しく表示されず、代わりに空白や四角い記号が表示されます。これを解決するには、アプリケーションの起動時にFontDefinitionsを通じてフォントデータを登録する必要があります。

カスタムフォントの適用手順

eguiでフォントをカスタマイズするには、eframe::CreationContext内のegui_ctxegui::Context)に対してset_fontsメソッドを呼び出します。このメソッドには、FontDefinitions構造体を渡します。この構造体は、フォントファイルのバイナリデータ(font_data)と、どのフォントファミリー(ProportionalやMonospaceなど)でそのフォントを使用するかというマッピング情報(families)を保持しています。

実装にはまず、フォントファイル(例:SimSun.ttfやNotoSansCJKなど)をプロジェクトディレクトリに配置し、Rustのビルドシステムに取り込むためにinclude_bytes!マクロを使用します。

// フォントファイルのバイナリデータをコンパイル時に埋め込む
const CJK_FONT_BYTES: &[u8] = include_bytes!("../assets/SimSun.ttf");

次に、このバイト配列を使用してFontDefinitionsを構築し、コンテキストに設定します。以下はその設定を行うユーティリティ関数の例です。

fn configure_fonts(ctx: &egui::Context) {
    // デフォルトのフォント設定をベースにする
    let mut font_definitions = egui::FontDefinitions::default();

    // カスタムフォントデータを登録する
    font_definitions.font_data.insert(
        "my_cjk_font".to_owned(),
        egui::FontData::from_static(CJK_FONT_BYTES),
    );

    // Proportionalフォントファミリーのプライマリフォントとして登録
    font_definitions
        .families
        .entry(egui::FontFamily::Proportional)
        .or_default()
        .insert(0, "my_cjk_font".to_owned());

    // Monospaceフォントファミリーのフォールバックとして登録
    font_definitions
        .families
        .entry(egui::FontFamily::Monospace)
        .or_default()
        .push("my_cjk_font".to_owned());

    // 設定をeguiのコンテキストに適用
    ctx.set_fonts(font_definitions);
}

実装コード全体

最後に、上記のフォント設定関数をアプリケーションの初期化時に呼び出す完全なコードを示します。構造体名や変数名を変更し、ロジックを整理しています。

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use eframe::egui;

// フォントファイルへのパス(プロジェクト構造に合わせて調整してください)
const CJK_FONT_BYTES: &[u8] = include_bytes!("../assets/SimSun.ttf");

fn main() -> eframe::Result {
    env_logger::init();

    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
        ..Default::default()
    };

    eframe::run_native(
        "多言語フォント対応デモ",
        options,
        Box::new(|cc| {
            // 初期化時にフォント設定を行う
            configure_fonts(&cc.egui_ctx);
            Ok(Box::new(TextDisplayApp::default()))
        }),
    )
}

/// eguiのコンテキストにカスタムフォントを適用する関数
fn configure_fonts(ctx: &egui::Context) {
    let mut fonts = egui::FontDefinitions::default();

    // フォントデータの挿入
    fonts.font_data.insert(
        "cjk_main".to_owned(),
        egui::FontData::from_static(CJK_FONT_BYTES),
    );

    // Proportionalフォントの最優先として設定
    fonts
        .families
        .entry(egui::FontFamily::Proportional)
        .or_default()
        .insert(0, "cjk_main".to_owned());

    // Monospaceフォントのフォールバックとして設定
    fonts
        .families
        .entry(egui::FontFamily::Monospace)
        .or_default()
        .push("cjk_main".to_owned());

    ctx.set_fonts(fonts);
}

#[derive(Default)]
struct TextDisplayApp {
    is_modal_open: bool,
}

impl eframe::App for TextDisplayApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.heading("マルチバイト文字の表示");
            ui.label("デフォルトフォントでもASCIIは表示されます。");
            
            if ui.button("中国語テキストを表示").clicked() {
                self.is_modal_open = true;
            }
        });

        if self.is_modal_open {
            egui::Window::new("確認ダイアログ")
                .collapsible(false)
                .fixed_size([200.0, 100.0])
                .show(ctx, |ui| {
                    ui.label("こんにちは、世界!");
                    ui.label("你好,世界!");
                    
                    ui.horizontal(|ui| {
                        if ui.button("閉じる").clicked() {
                            self.is_modal_open = false;
                        }
                    });
                });
        }
    }
}

このコードでは、eframe::run_nativeのクロージャ内でconfigure_fontsを呼び出し、最初のフレーム描画前にフォント定義を完了させています。これにより、アプリケーション内のすべてのウィジェット(ui.labelui.buttonegui::Windowなど)で指定されたフォントが使用され、中国語や日本語が正しくレンダリングされます。フォントファイルはプロジェクトのassetsフォルダなどに配置してください。

タグ: rust egui eframe GUI FontConfiguration

6月25日 16:55 投稿