午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、インストールされているフォント一覧取得、Fonts.SystemFontFamiliesそのままでは不十分だった

結果

f:id:gogowaten:20211209075624p:plain
今回の結果
左がFonts.SystemFontFamiliesからそのまま取得できたフォントリストで303種、右はそれプラスひと手間かけて取得したフォントリストで376種
Arialっていうフォントで見ると左はArialとArial Unicode MSの2種類しか取得されていないけど、右ではArial BlackとArial Narrowも取得されている
次のBahnschriftってフォントに至っては左では1種だけど、実際には12種類もあった




コード

/// <summary>
/// SystemFontFamiliesから日本語フォント名で並べ替えたフォント一覧を返す、1ファイルに別名のフォントがある場合も取得
/// </summary>
/// <returns></returns>
private SortedDictionary<string, FontFamily> GetFontFamilies()
{
    //今のPCで使っている言語(日本語)のCulture取得
    //var language =
    // System.Windows.Markup.XmlLanguage.GetLanguage(
    // CultureInfo.CurrentCulture.IetfLanguageTag);
    CultureInfo culture = CultureInfo.CurrentCulture;//日本
    CultureInfo cultureUS = new("en-US");//英語?米国?

    List<string> uName = new();//フォント名の重複判定に使う
    Dictionary<string, FontFamily> tempDictionary = new();
    foreach (var item in Fonts.SystemFontFamilies)
    {
        var typefaces = item.GetTypefaces();
        foreach (var typeface in typefaces)
        {
            _ = typeface.TryGetGlyphTypeface(out GlyphTypeface gType);
            if (gType != null)
            {
                //フォント名取得はFamilyNamesではなく、Win32FamilyNamesを使う
                //FamilyNamesだと違うフォントなのに同じフォント名で取得されるものがあるので
                //Win32FamilyNamesを使う
                //日本語名がなければ英語名
                string fontName = gType.Win32FamilyNames[culture] ?? gType.Win32FamilyNames[cultureUS];
                //string fontName = gType.FamilyNames[culture] ?? gType.FamilyNames[cultureUS];
                
                //フォント名で重複判定
                var uri = gType.FontUri;
                if (uName.Contains(fontName) == false)
                {
                    uName.Add(fontName);
                    tempDictionary.Add(fontName, new(uri, fontName));
                }
            }
        }
    }
    SortedDictionary<string, FontFamily> fontDictionary = new(tempDictionary);
    return fontDictionary;
}

戻り値はKeyがフォント名、ValueがFontFamilyのSortedDictionary形式
Sortedなのでフォント名順で並んだ状態

Fonts.SystemFontFamiliesで取得したFontFamilyのGetTypefaces()でTypefaceを取得、
TypefaceのTryGetGlyphTypeface()でGlyphTypefaceを取得、
このGlyphTypefaceから本当のフォント名とUri(フォントファイルのパスみたいなの)が取得できるので、これを使って改めてFontFamilyを作成して、フォント名をKey、FontFamilyをValueにしたDictionaryに詰め込んで、最後にSortedDictionaryで並べ替えをして完成
もっと楽な方法がありそうだけど、今回はこうなった




本当のフォント名はWin32FamilyNamesで

フォント名に関係あるGlyphTypefaceのプロパティには2種類あって

  • FamilyNames
  • Win32FamilyNames

Arial Narrowフォントの場合でみてみると
FamilyNamesでは

f:id:gogowaten:20211209090253p:plain
FamilyNamesではArial、これじゃない

Win32FamilyNamesだと

f:id:gogowaten:20211209090338p:plain
本当のフォント名
ってことでフォント名の取得にはWin32FamilyNamesプロパティからするほうがいい
で、このプロパティはKey、ValueのDictionary形式で、欲しいのはフォント名のValue、これを引き出すためのKeyの型はCultureInfo、この聞き慣れない型は国(言語)を識別するためのものらしくて、フォントにはそれぞれの国の言語に合わせて表示できるようにCultureInfoが使われているみたい。

using System.Globalization;
CultureInfo culture = CultureInfo.CurrentCulture;//日本
CultureInfo cultureUS = new("en-US");//英語?米国?

基本は英語名で、日本語フォントだと英語名と日本語名の2種類が入っていることが多い感じだった

f:id:gogowaten:20211209091924p:plain
フォントの英語名と日本語名


Uri、ファイルのパス
さっきのArialとArial Narrowだと、それぞれファイルのパスというかファイル名が違うからわかりやすい
Arialは

f:id:gogowaten:20211209093611p:plain
Arialのファイル名はarial.ttf

Arial Narrowは末尾にnが付いている

f:id:gogowaten:20211209093647p:plain
Arial Narrowのファイル名はarialn.ttf
これはいい、これはわかる

1つのフォントファイルに複数のフォント

f:id:gogowaten:20211209094836p:plain
別フォントなのにファイルパスは同じ
これがFontFamilyって呼ばれる所以ってことかしらねえ
他にはMeiryo.ttcを参照しているフォントだと
f:id:gogowaten:20211209100603p:plain
Meiryo.ttcのフォント?
フォント名で分けると2種類だけど、Uriだと4種類、拡張子の後に#と数値が付いている、わけがわからん

Fontフォルダを見てみる

f:id:gogowaten:20211209113653p:plain
Arial
このファイルを開くと
f:id:gogowaten:20211209113712p:plain
ArialのFamily?
9種類のフォントが入っていた、やや狭いってあるのがArial Narrowで極太ってのがArial Blackだった
f:id:gogowaten:20211209113958p:plain
Arial Narrow
f:id:gogowaten:20211209114010p:plain
Arial Black

Bahnschrift

f:id:gogowaten:20211209114206p:plain
Bahnschrift
f:id:gogowaten:20211209114220p:plain
Bahnschrift
15種類も入っている




フォント一覧表示のアプリ

f:id:gogowaten:20211209102421p:plain
今回のアプリ

f:id:gogowaten:20211209120747p:plain
M+、Mriyo、Microsoft

f:id:gogowaten:20211209120910p:plain
MS Pゴシック、Noto Sans Japanese系

f:id:gogowaten:20211209121003p:plain
源ノ角ゴシック、源真ゴシック
ダウンロード先

github.com

20211208_フォント一覧表示.zip


作成、動作環境

動作に必要なのは.NET 5がインストール済みのWindows.NET Frameworkだけでは動かないはず
Windows 11なら.NET 5にインストールは不要かも




アプリのコード

github.com


MainWindow.xaml

<Window x:Class="_20211208_フォント一覧表示.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20211208_フォント一覧表示"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
  <Grid UseLayoutRounding="True">
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <DockPanel Grid.Column="0" Margin="10">
      <TextBlock DockPanel.Dock="Top" x:Name="MyTextBlock1" FontSize="20" 
                 Background="MediumAquamarine" Foreground="White" TextAlignment="Center"/>
      <ListBox Name="MyListBox1" FontSize="20">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <!--Fonts.SystemFontFamilysを直接ItemsSourceにする場合-->
            <!--<TextBlock Text="{Binding Source}" FontFamily="{Binding }"/>-->
            <TextBlock Text="{Binding Key}" FontFamily="{Binding Value}"/>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </DockPanel>
    <DockPanel Grid.Column="1" Margin="10">
      <TextBlock DockPanel.Dock="Top" x:Name="MyTextBlock2" FontSize="20" 
                 Background="MediumOrchid" Foreground="White" TextAlignment="Center"/>
      <ListBox Name="MyListBox2" FontSize="20">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <TextBlock Text="{Binding Key}" FontFamily="{Binding Value}"/>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </DockPanel>
  </Grid>
</Window>

このXAMLではListBoxのDataTemplateをいじってTextBlockのTextにKey、FontFamilyにValueを指定しているだけ、あとはC#のほうでこのListBoxのItemsSourceにフォントリストのDictionaryを渡せば、フォント名がそのフォントで表示される
こういうのはWPFは楽ちんねえ

MainWindow.xaml.cs

using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Globalization;
using System.Windows.Markup;



namespace _20211208_フォント一覧表示
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            MyListBox1.ItemsSource = GetFontFamilies2();
            MyTextBlock1.Text = "Fonts.SystemFontFamilies\nフォント数:" + MyListBox1.Items.Count.ToString();
            //MyListBox1.ItemsSource = Fonts.SystemFontFamilies;

            MyListBox2.ItemsSource = GetFontFamilies();
            MyTextBlock2.Text = "Fonts.SystemFontFamilies+ひと手間\nフォント数:" + MyListBox2.Items.Count.ToString();
        }

        /// <summary>
        /// SystemFontFamiliesから日本語フォント名で並べ替えたフォント一覧を返す、1ファイルに別名のフォントがある場合も取得
        /// </summary>
        /// <returns></returns>
        private SortedDictionary<string, FontFamily> GetFontFamilies()
        {
            //今のPCで使っている言語(日本語)のCulture取得
            //var language =
            //    System.Windows.Markup.XmlLanguage.GetLanguage(
            //    CultureInfo.CurrentCulture.IetfLanguageTag);
            CultureInfo culture = CultureInfo.CurrentCulture;//日本
            CultureInfo cultureUS = new("en-US");//英語?米国?

            List<string> uName = new();//フォント名の重複判定に使う
            Dictionary<string, FontFamily> tempDictionary = new();
            foreach (var item in Fonts.SystemFontFamilies)
            {
                var typefaces = item.GetTypefaces();
                foreach (var typeface in typefaces)
                {
                    _ = typeface.TryGetGlyphTypeface(out GlyphTypeface gType);
                    if (gType != null)
                    {
                        //フォント名取得はFamilyNamesではなく、Win32FamilyNamesを使う
                        //FamilyNamesだと違うフォントなのに同じフォント名で取得されるものがあるので
                        //Win32FamilyNamesを使う
                        //日本語名がなければ英語名
                        string fontName = gType.Win32FamilyNames[culture] ?? gType.Win32FamilyNames[cultureUS];
                        //string fontName = gType.FamilyNames[culture] ?? gType.FamilyNames[cultureUS];
                        
                        //フォント名で重複判定
                        var uri = gType.FontUri;
                        if (uName.Contains(fontName) == false)
                        {
                            uName.Add(fontName);
                            tempDictionary.Add(fontName, new(uri, fontName));
                        }
                    }
                }
            }
            SortedDictionary<string, FontFamily> fontDictionary = new(tempDictionary);
            return fontDictionary;
        }

        /// <summary>
        /// SystemFontFamiliesから日本語フォント名で並べ替えたフォント一覧を返す
        /// </summary>
        /// <returns></returns>
        private SortedDictionary<string, FontFamily> GetFontFamilies2()
        {
            //今のPCで使っている言語(日本語)取得
            XmlLanguage language =
                XmlLanguage.GetLanguage(
                CultureInfo.CurrentCulture.IetfLanguageTag);
            //英語のXmlLanguage取得
            XmlLanguage[] lang0 = FontFamily.FamilyNames.Select(a => a.Key).ToArray();
            
            List<string> uName = new();//フォント名の重複判定に使う
            Dictionary<string, FontFamily> tempDictionary = new();

            foreach (var item in Fonts.SystemFontFamilies)
            {
                //フォント名取得、日本語名がなければ英語名
                string name;
                if (item.FamilyNames.TryGetValue(language, out name) == false)
                {
                    name = item.FamilyNames[lang0[0]];//[0]は英語
                }
                //フォント名で重複判定
                if (uName.Contains(name) == false)
                {
                    uName.Add(name);
                    tempDictionary.Add(name, item);
                }
            }
            SortedDictionary<string, FontFamily> fontDictionary = new(tempDictionary);
            return fontDictionary;
        }

    }
}




SystemFontFamiliesからそのままのGetFontFamilies2()は、日本語フォント名取得と、それでの並び替えをしているから長くなってしまった、これももっといい方法がありそう。

日本語フォント名も並び替えも必要ないならSystemFontFamiliesをそのまま

MyListBox1.ItemsSource = Fonts.SystemFontFamilies;

こう渡して、XAMLのほうでのBindingを

<TextBlock Text="{Binding Source}" FontFamily="{Binding }"/>

たったこれだけでも

f:id:gogowaten:20211209104705p:plain
左側がそれ
ある程度表示できる




参照したところ

blog.livedoor.jp

今回の要になっているFontFamilyからGlyphTypefaceを取得する方法はここから

resanaplaza.com ListBoxとのBindingはこちらから


感想、WinFormsと比較してみる

f:id:gogowaten:20211209105150p:plain
WinFormsアプリのPixtack紫陽花

f:id:gogowaten:20211209105423p:plain
フォント選択
Arial BlackやArial Narrow他、WPFのSystemFontFamiliesそのままでは取得できないフォントもある
f:id:gogowaten:20211209105820p:plain
Meiryo系もある
これがあったからWPFだと、なんか足りないなあって気づいた
このアプリは

dobon.net

ここを見ながら作っていた、フォント一覧取得もこのdobonさんのサイトにあるそのままの方法で苦もなくできたんだよねえ、WPFでも簡単だろうと思いつつ検索して、あちこちで紹介されていたのがSystemFontFamiliesを使った方法なんだけど、試したらなんか足りない、どうしたらいいのかといろいろ試してたどり着いたのが今回の回りくどい方法、しかもForEachの中にIfの中にIfとかで長くなった、LINQを使いこなせればかなり短くできるはずなんだけど難しくてわからん、でもできてよかったわ

インストールされていないフォントも表示させたかったんだけど、ちょっと調べたらWPFだとアプリに埋め込まないとできない感じで、そこまでするんだったらインストールしたほうが早いので諦めた

それにしてもインストールされているフォント一覧取得がここまでめんどくさいはずないと思うんだよねえ

Visual Studioは2022をインストールしたけどメモリ使用量が多すぎて、メモリ16GBのPCではムリで、2022のアイコンを横目に見ながら2019を使っているのは悲しいw



関連記事

gogowaten.hatenablog.com