ズーム時のフォントが正確に描画されません


みなこ  2011-07-29 10:02:26  No: 72835  IP: 192.*.*.*

C++2008のMFCで、線と円と文字列だけ使えるペイントソフトを開発していますが、
ビューのズーム(拡大縮小)のところで悩んでいます。

ズーム時に、描画したものをそのままズームして画像が粗くなってしまうことを避けるため、
すべての描画オブジェクトの座標や線幅、フォントの高さに倍率を乗算して再描画する手法をとっています。

しかし、座標や線幅は上手くいくのですが、フォントだけ正確にズームされません。
たとえば「1234567890」と打った文字列をいろんな倍率でズームすると、
最後の「0」の文字と他のオブジェクトの位置の比率が倍率によって変わるのです。
その比率を統一する方法が知りたいです。
設定できるフォントはWindowsのメモ帳で設定可能なフォントとお考えください。

日本語になっていない、説明不足な点などあればご指摘ください。
宜しくお願い致します。

編集 削除
仲澤@失業者  2011-07-29 14:28:01  No: 72836  IP: 192.*.*.*

文字の高さはPIXL単位で指定した通りのフォントを作成できます。
しかし、このフォントを使った場合の、描画時の文字列の幅を正確に
指定倍率に設定する簡単な手段はありません。

ドロー系ソフトの中にはこの問題を解決するため、
  1.文字の横幅は正確に倍率に従わない、と最初からうたっているもの
  2.文字の外形をパスに変換し、図形として描画しているもの
があるようです。もちろん2.の方が手間がかかり大変です。

編集 削除
gak  2011-07-29 18:18:39  No: 72837  IP: 192.*.*.*

> 最後の「0」の文字と他のオブジェクトの位置の比率が倍率によって変わるのです。
  TextOut(x, y, "1234567890", ...);
のように”文字列”で描画していると倍率によっては誤差が生じる。

例えば、1.0倍時に↓サイズで(等倍フォントで)描画されていた文字列があるとする。
  ・文字幅16pixel x 10文字 = 160pixel
それを仮に2.4倍率で描画するとした場合↓(机上の計算)となる。
  ・int(文字幅16pixel * 2.4 + 0.5) x 10文字 = 380pixel
だが、正確には↓なので、小数点以下の値により誤差が生じてしまう。
  ・(1.0倍時の文字列幅)160pixel x 2.4倍 = 384pixel


> その比率を統一する方法が知りたいです。
案1:スマートじゃないが、1文字づつ位置を計算して1文字づつ文字描画

void textout(CDC &dc, int fontsize, POINT p, LPCWSTR string, double viewratio) {
    CFont zoomed, base;
    zoomed.CreateFont(fontsize * viewratio, ...);
    base.CreateFont(fontsize, ...);
    CDC attr; // 等倍表示(==1.0倍)時の位置計算用
    attr.CreateCompatibleDC(&dc);
    CFont *prevfont1 = dc.SelectObject(&zoomed);
    CFont *prevfont2 = attr.SelectObject(&base);
    double x = p.x;
    for (int i=0; string[i] != L'\0'; ++i) {
        ::TextOutW(dc, int(x + 0.5), p.y, &string[i], 1);
        SIZE size;
        ::GetTextExtentPoint32W(attr, &string[i], 1, &size);
        x += size.cx * viewratio;
    }
}

案2:GDI+(案1と比較すると少しズレるケースもあるがGDIよりは遥かにマシ)

void textout(CDC &dc, int fontsize, POINT p, LPCWSTR string, double viewratio) {
    Gdiplus::Graphics graphics(dc);
    graphics.DrawString(
        string, -1,
        &Gdiplus::Font(L"フォント名", fontsize * viewratio, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel),
        Gdiplus::PointF(p.x, p.y),
        Gdiplus::StringFormat::GenericTypographic(),
        &Gdiplus::SolidBrush(Gdiplus::Color(赤, 緑, 青))
    );
}

編集 削除
みなこ  2011-08-01 15:52:06  No: 72838  IP: 192.*.*.*

お二方様、お返事ありがとうございます。

gak様の案を両方試させていただきました。
案2ではおっしゃるとおり微妙にズレるところがあり、
案1ではそれが見られなかったので、ズレないことを優先してとりあえず案1を実装してみようと思います。
改行による折り返しを考慮したいのですが、文字の高さを取得してy軸に足していけば単純に可能ですよね。がんばって複数行対応にしてみます。

ただ案2については疑問点が1つあるのですが、私が単に関数の使い方を誤っている意味でのズレ方をしているような気がします。
ユーザが設定するフォントのサイズはメモ帳にもあるようなインターフェイスで、そこからLOGFONT構造体のlfHeightを使って関数の引数に渡したいのですが、
このlfHeightをそのまま使ってはいけないのでしょうか。
いけない場合はどうすればこのフォントの高さを、Gdiplus::Font()の第二引数に沿う形に変換することができるのでしょうか?

編集 削除
みなこ  2011-08-02 12:36:22  No: 72839  IP: 192.*.*.*

連投すみません。
たった数行書き足しただけですが…とりあえず複数行に対応させたものを貼っておきます。
デバイスコンテキストの後片付けも追加しておきました。問題があればご指摘ください。

void textout(CDC &dc, int fontsize, POINT p, LPCWSTR string, double viewratio) {
    CFont zoomed, base;
    zoomed.CreateFont(fontsize * viewratio, ...);
    base.CreateFont(fontsize, ...);
    CDC attr; // 等倍表示(==1.0倍)時の位置計算用
    attr.CreateCompatibleDC(&dc);
    CFont *prevfont1 = dc.SelectObject(&zoomed);
    CFont *prevfont2 = attr.SelectObject(&base);
    double x = p.x;
    double y = p.y;
    for (int i=0; string[i] != L'\0'; ++i) {
        ::TextOutW(dc, int(x + 0.5), y, &string[i], 1);
        SIZE size;
        ::GetTextExtentPoint32W(attr, &string[i], 1, &size);
        x += size.cx * viewratio;
        if(string[i] == L'\n') {
            x = p.x;
            y += size.cy * viewratio;
        }
    }
    attr.SelectObject(prevfont2);
    dc.SelectObject(prevfont1);
}

編集 削除
gak  2011-08-02 18:25:19  No: 72840  IP: 192.*.*.*

> このlfHeightをそのまま使ってはいけないのでしょうか。
LOGFONT構造体のlfHeightには
・lfHeight > 0 時、フォントの高さは”セルの高さ”を基準にする
・lfHeight < 0 時、フォントの高さは”文字の高さ”(セルの高さ - 内部レディング)を基準にする
という違いがある。
で、Gdiplus::Font に実数を指定(not LOGFONT)してフォントを作成する際は、フォントの「”文字の高さ”を”正数”」※で指定する。
※ GDI+のソースを見ると Gdiplus::Font 構築時に与えられたフォント高に対してわざわざ「-1」を掛けて正負反転してからフォントを作成している。

という事なのでそのまま使っちゃうと仕様上↓の違いが発生する事になる。
const lfHeight = 16;
::CreateFont(lfHeight, ...)    = セルの高さ基準
Gdiplus::Font(, lfHeight, ...) = 文字の高さ基準
//::CreateFont(-lfHeight, ...)   = 文字の高さ基準


> デバイスコンテキストの後片付けも追加しておきました。問題があればご指摘ください。
ゴメン、開放処理抜けてた。
厳密さを追求するなら TextOutW に渡してる y も小数点以下四捨五入してやれば良いかと。
も一つ云うならば、'\n'以外の制御文字が現れた場合は無視する等の判定があっても良いかも(制御文字は来ない!とルールとして決めちゃってもOKだろうけど)

編集 削除