いきさつ
これは、昔々の話・・・。
Windows XPが世の中に生まれたころ(2000年前半)、
画面が妙に綺麗になっていたのにお気づきではなかっただろうか。
これはClearTypeと呼ばれる、MicrosoftとMacが共同開発したフォントの技術があり、
それがWindows XPから採用されたことが一つの要因とされる。
この記事なんかが顕著だろうか。
2009年の記事ではあるが。
このように、Windowsは昔からフォントに関しては改良に改良を重ね、
今の滑らかなレンダリングに至るのだが、
今のクライアントにとんでもない要求があった。
「Windows 2000辺りであったフォントの外観にしたアプリケーションを用意してほしい」
そんな謎需要、どこにあるんだ?
そもそも、win32apiから.NETに移行して相当年数経っていて、レガシー化しているのに、そんな実装どうやってやるんだ?
と考えて考えた挙句、数か月してようやく答えが出てきた。
そこでのキーワードは、そもそもレンダリングをしない画面を作るということである。
今のアプリケーションではそもそもそんな需要あり得ないと思うのだが、昔から開発を行なっている現場で、フォントに関する再現性が必要な条件になってくると無視するわけにもいかない、ということもわかっている。
どのように対応したかをまとめていきたい。
他の記事との関連
なお、この記事の続編の一つです
なので、プロジェクトもこちらです
変更内容
以前のプロジェクトにチェックボックスを加えて
チェックが入るとスムージングが入らないようにするようにしました。
チェックなしの各フォントでの表示結果は次の通り
MS Pゴシック
スムージングあり
スムージングなし
Meiryo UI
スムージングあり
スムージングなし
BIZ UDP明朝 Medium
スムージングあり
スムージングなし
特に明朝体にしてみるとよくわかるのだが、
日本語の「とめ・はね・はらい」に相当する部分の画素が大幅にギザギザになっていて、
スムージングなしだと相当汚くなると思う。
Windowsがスムージング関連の技術を見直さなかったら、
もしかしたらMicrosoftはAppleやGoogleにOSの覇権を奪われていたかもしれない、
と感じる程度には「汚いな」と思う見た目だ。
実現方法
この実現にあたって、2つの案を考えた。
- フォント設定時に、LOGFONT構造体のlfQualityをNONANTIALIASED_QUALITYに設定する方法。
- Windowsの設定において、パフォーマンスオプションから「スクリーンフォントの縁を滑らかにする」に相当する設定を使う方法。
できれば、2.より1.が望ましいが、1.の解決策を考えるうえで、2.を考察する必要があった。
考えた経緯
まず、1.の場合において、LOGFONT構造体がlfQuality = DEFAULT_QUALITYになってたとすると。
これはアプリケーションの設定によりフォントの品質を変えることを意味する。
従って、Win2000以前のようなアンチエイリアス未対応のアプリケーションみたいに、古すぎるアプリケーションを使っている場合、この設定が常にNONANTIALIASED_QUALITYになっていることがある。
以下は、Windows95時代からBorland C++を使用していたが、Windows XPまででバージョン対応が追い付かず、最終的にアップデートが困難になっているアプリケーション(エディタ)の例だ。
とあるフォント選択のメニュー画面をピックアップすると
これを画像処理ツールで可視化すると(ツール:ImageJ)、
お気づきになるだろうか。
MS P ゴシック、18の部分がくっきり白黒がかかっていることになる。
この場合は、アンチエイリアス未対応なので、LOGFONT構造体がlfQuality=DEFAULT_QUALITYとなると、NONANTIALIASED_QUALITYを設定していることになる。
一方で、いま新しく開発しているアプリケーションで同じ画面を開く。これは.NET Framework 4.8で開発中のWinFormsのアプリケーションだ。
大量にアンチエイリアスがかかっていることが分かる。この場合、lfQuality=DEFAULT_QUALITYはANTIALIASED_QUALITYなどが指定されているだろう(確認はしていない)。
多くの場合、新しいアプリケーションへの移行においては、現代の環境に合わせる需要の方が大きいのは確かであるので、このことが問題になることはほとんどないが、
プリンタを使う場合などで、過去の印刷品質に対して互換性を維持したい場合など、問題になるケースもあるのだ。
1.の方法
もしTextOut等でGDIのみで文字列出力することを行なう場合。
WinFormsから出力されるFontにはwin32api用のフォントハンドラが使えるようになっているので、
それから、さらにCreateFontIndirect関数を使って読み取ることで実行が可能。
public class TextOutGdi
{
[DllImport("gdi32.dll")]
public static extern bool TextOut(IntPtr hdc, int nXStart, int nYStart, string lpString, int cbString);
[DllImport("gdi32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr CreateFontIndirect(FontListGdi.LOGFONT lf);
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
}
として、実行側で
var selected_log_font = (LogFontWrapped)LogFontBindingSource.Current;
var text = textBox1.Text;
Color color = Color.Black;
using (var gr = Graphics.FromImage(bitmap))
{
gr.InterpolationMode = InterpolationMode.Bilinear;
if (text != null)
{
IntPtr hDC = gr.GetHdc();
// LOGFONT構造体を取得する
var log_font = selected_log_font.RawLogFont;
// アンチエイリアスをかけない
log_font.lfQuality = FontListGdi.FontQuality.NONANTIALIASED_QUALITY;
// フォント設定を再度読み取る(☆)
var hFont = TextOutGdi.CreateFontIndirect(log_font);
// フォント選択
IntPtr hOldFont = TextOutGdi.SelectObject(hDC, hFont);
// 文字色の設定
var color_flag = (int)(color.R + (color.G << 8) + (color.B << 16));
TextOutGdi.SetTextColor(hDC, color_flag);
// TextOutを実行する
var shift_jis_encoding = Encoding.GetEncoding("shift-jis");
TextOutGdi.TextOut(hDC, 0, 0, text, shift_jis_encoding.GetByteCount(text));
// オブジェクトを破棄
TextOutGdi.DeleteObject(TextOutGdi.SelectObject(hDC, hOldFont));
gr.ReleaseHdc(hDC);
// TextOutしたbitmapをImageAttributeを使って貼り付ける。
// 通常のDrawImageは使わないようにして、
// 拡大縮小補間などをかけさせないようにする
gr.DrawImage(bitmap, new Rectangle(0, 0, bitmap.Width, bitmap.Height),
0, 0, bitmap.Width, bitmap.Height, GraphicsUnit.Pixel, ia);
}
}
とすることで可能。
ところで、私はこの課題を考えるときに、(☆)のところでずいぶん悩まされた。
それは、WinFormsのFontクラスには
FromLogFontメソッド
ToHFontメソッド
が与えられているので、(☆)の部分で
var font_conv = Font.FromLogFont(log_font);
IntPtr hFont = font_conv.ToHfont();
と書いてしまっていたのだ。
そして、この状態は、確かにフォントの太さなどの情報は変わるが、アンチエイリアスはかからなかった。恐らく、FromLogFontメソッドはWinFormsのラッパーとして機能しているがアンチエイリアスを正確に与える設定がかからないのであろう。
2.の方法
こちらは基本的には非推奨だが一応まとめておく.
SystemParametersInfo関数のdllimportを行なう。
※参考 StackOverFlowのサイト
public class ControlDrawing
{
public static UInt32 SPI_SETFONTSMOOTHING = 0x004B;
[DllImport("user32.dll")]
public static extern bool SystemParametersInfo(UInt32 uiAction, UInt32 uiParam, IntPtr pvParam, UInt32 fWinIni);
}
非推奨である理由は、PC側の設定を書き換えてしまうために、他のアプリケーションに影響を及ぼしてしまうため。
不具合要因となるので、この方法は基本的に避ける