まず、解像度という言葉の説明から始めよう。
解像度とは、読んで字の如く「画『像』をどれだけ細かく分『解』するかという『度』合い」である。
一般に、「画面解像度は1024×768」と言ったりするが、これは正しくない。1024も768も、単位はピクセル、言い換えればドットだが、解像度の本来の単位はDPI(Dots Per Inch)、すなわち「1インチあたり何ドットか」、言い換えれば「1インチを何ドットで描画するか」というものだからだ。
1インチを1ドットで描画すると、1.5インチは表現できない。1インチを10ドットで描画すると、1.5インチは15ドットで表せるが、1.25インチは正確には描画できない。1インチが100ドットなら、1.25インチは125ドットで描けばいい。要するに、解像度が高いほど、画像を精密に描画することができる。
解像度とは、分解の度合い、精緻さの度合いであって、画像の大きさではない。これは重要だ。
ところが、デジカメで撮影したり、スキャナで取り込んだりした画像は、解像度が高いほど大きくなる。何故か? それは、ドットの大きさが固定だからだ。
当然だが、カメラやスキャナの解像度が変わっても、被写体や原稿の大きさは変わらない。繰り返すが、カメラやスキャナの解像度とは、被写体や原稿を、どれだけ細部まで緻密に取り込むかという度合いだ。
しかし、それをパソコン上で表示すると、解像度の高い画像は大きく表示されてしまう。
紙に絵を描いて、それをバラバラに切って、ジグソーパズルを作ることを想像してほしい。
紙のサイズは固定なのだから、ピース数を増やすには、1つのピースを小さくする必要がある。
これがパソコンでは、ピース(ドット)の大きさが固定なため、ピース数を増やすには、元々の紙を大きいものにするしかない。
そういう理屈である。
さて、本題。画面に10cm×10cmぴったりの正方形を描く方法だ。これを実現するには、「マッピングモード」というのを使う。
画面に正方形を描くのには、FillRectという関数を使うことにしよう。第2引数で、描画したい正方形の4隅の座標を指定する。
第2引数の説明には、「論理座標」という言葉が出てくる。これがキモだ。
GDI系の関数だと、度々「論理座標」と「物理座標」という言葉に遭遇する。これは一体何なんだ。
先程の解像度の説明と混同しないように注意してほしい。解像度の本来の意味では、解像度がいくつだろうが1インチは1インチで、解像度が高くなるとドットが小さくなるというほうが正しい理解だった。だから、1インチが物理座標のような気がする。
だが、今いるのはコンピュータの世界だ。コンピュータの世界では立場が逆になる。
ドットの大きさは変わらないから、これが一番の基準になる。1ドットを単位にするのが「物理座標」だ(またの名を「デバイス座標」とも言う。モニタとプリンタの1ドットのサイズは違うので、デバイスに依存する座標系という意味だ)。
そして、1センチとか1インチとか、一見して「物理座標」に思えるものが、コンピュータ内では「論理座標」になる。画面に対する描画指令はドット単位で行われ、論理単位での指定は、それを何ドットで描くべきかに変換してから描画しているからだ。言わば仮想的な単位なのだ。
FillRectの座標は論理座標で指定するから、10cm×10cmという指定もできる。それを画面上に何ドットで描画するのかを決めるのがマッピングモードだ。
マッピングモードの設定にはSetMapMode関数を使う。ここらでサンプルコードを出そう。
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
SaveDC( hdc );
SetMapMode( hdc, MM_LOMETRIC );
RECT rect = { 100, -100, 1100, -1100 };
FillRect( hdc, &rect, ( HBRUSH )GetStockObject( BLACK_BRUSH ) );
RestoreDC( hdc, -1 );
EndPaint(hWnd, &ps);
break;
}
ネイティブWin32アプリケーションのWndProcの抜粋だ。
SetMapModeで、マッピングモードをMM_LOMETRICにしている。これは、論理単位を0.1mmとするものだ。この座標系は、x座標は普通なのだが、y座標が変だ。数値が増えるほど、y座標が表す点は上に行く。だから、y座標は負の数にしている。(実のところ、変なのはこっちではなく、通常使うMM_TEXT座標系だ。数学でグラフを書く時は、右上へ行くほど大きくなったでしょ?)
論理単位が0.1mmなので、10cmは1000単位だ。だからこのプログラムは、画面左上隅から1cm, 1cmの位置から、10cm×10cmの正方形を描くはずだ。
ところが、これじゃうまく行かない。環境にもよるが、実際に画面上に表示された正方形を定規で測ってみたら、9cm×9cmしかなかった。どうしてだろう?
ここに、Windowsの変な癖がある。
今、俺の目の前にあるパソコンの画面の横幅は、定規で測ってみたところ、28.5cmだった。28.5cmは約11.22インチで、解像度(と呼ばれているモノ)は1024×768ドットなので、(本当の意味での)解像度は1024/11.22≒91DPIのはずだ。が、Windowsはこれを96DPIだと勝手に決め付ける。
1インチ=96ドットだから、1024ドットは約10.67インチ。約27.1cmだ。
実際には28.5cmなのに、Windowsが27.1cmと勘違いすることで誤差が生じるため、プログラムは思い通りに動かない。
さて、どうしよう?
Windowsが勝手に解像度を96DPIだと決めつけるのがいけないので、ここは、実際の画面サイズから正しい解像度を求めたいところだ。が、プログラムから実際の画面サイズを取得する方法がわからない(ご存知の方は教えてください)。GetDeviceCapsにHORZSIZE、VERTSIZEを渡せば取得できそうに見えるが、これはデタラメな値しか返さないので使えない。
そこで今回は、実際に画面のサイズを測り、それに基づいてプログラミングした。
先にも言ったように、俺のPCの画面の横幅は28.5cmだった。高さは21.5cmだ。これが今回の論理座標の手掛かりとなる。
SetMapModeで使えるマッピングモードはいくつかあるが、今回使うのはMM_ANISOTROPICだ。
こいつを使うと、解像度を好きな値にすることができる。解像度の設定に使うのはSetWindowExtExとSetViewportExtExだ。
ここらでコードを紹介しよう。
case WM_PAINT:
{
static const unsigned int logicalWidth = 285;
static const unsigned int logicalHeight = 215;
static const unsigned int physicalWidth = GetSystemMetrics( SM_CXSCREEN );
static const unsigned int physicalHeight = GetSystemMetrics( SM_CYSCREEN );
hdc = BeginPaint(hWnd, &ps);
SaveDC( hdc );
SetMapMode( hdc, MM_ANISOTROPIC );
SetWindowExtEx( hdc, logicalWidth, logicalHeight, NULL );
SetViewportExtEx( hdc, physicalWidth, physicalHeight, NULL );
RECT rect = { 10, 10, 110, 110 };
FillRect( hdc, &rect, ( HBRUSH )GetStockObject( BLACK_BRUSH ) );
RestoreDC( hdc, -1 );
EndPaint(hWnd, &ps);
break;
}
やはりWndProcの抜粋である。ポイントは先の2つの関数。
logicalWidthとlogicalHeightは、実際の画面の大きさを測ってmm単位にしたもの。physicalWidthとphysicalHeightは、画面の解像度(と呼ばれているもの)で、今回は1024と768だ。
ウィンドウとは何か、ビューポートとは何かということを説明するのは面倒くさいので、上のプログラムを極めて直感的に説明すると、「(1論理単位=1mmとして)横は285単位を1024ドット、縦は215単位を768ドットとみなして、ウィンドウ左上隅から10単位, 10単位の位置から100単位×100単位の正方形を描く」というものだ。このように、「1論理単位を何ドットで描画するのか」、すなわち解像度(ここでは Dots Per Inch ではなく Dots Per Millimeter だが)をカスタマイズできるのが、このマッピングモードの特徴である(ちなみに、これを応用すると、画像の拡大・縮小ができる)。