GDI+ は難しい

というか、いまいち動きがおかしい気がする。とりあえず困っているのは、画面上でドット単位で敏感な絵をうまいことかけないことなんだが……誰か助けて!(笑

ペンの太さと座標の関係が変
どうも PageScale * Pen.Width が 1 になる場合だけ PenAlignment が無視されたり、指定した座標に線が書かれなかったり。
塗り潰しがない
スタオペレーションはどうでもいいから、ExtFloodFill 相当の塗り潰し機能はつけて欲しい。
未実装…
ドキュメントはきっちり記載されていて、例まであるのに、呼び出すと E_NOTIMPL *1 を返す API がある。
画像を整数倍に拡大描画できない
自分が何か間違えてるんだと思いたい。

塗り潰しに関しては、前に書いた*2ので、今回は画像を等倍描画できない件について。
画像を等倍に描画しようとして最初に思いつくレベルは、

  public void DrawImage1(Graphics g, Image img, int rate, Point pt)
  {
    GraphicsState gs = g.Save();
    try
    {
      g.InterpolationMode = InterpolationMode.NearestNeighbor;
      g.DrawImage(img, 
          new Rectangle(pt, new Size(img.Width * rate, img.Height * rate);
    }
    finally
    {
      g.Restore(gs);
    }
  }

こんなところだろうか? 適当な画像を指定して実行してみるとわかるが、たとえば10倍を指定して描画すると最も左と上のドットは5倍にしか拡大されず、右と下に5ドットの余白ができてしまう。これはかなり期待と違う結果になるが、指定座標の右下の周辺色を満遍なく見ているのであろうという予測が立つので、次のように変更することで回避できることが予測できた。

  public void DrawImage2(Graphics g, Image img, int rate, Point pt)
  {
    GraphicsState gs = g.Save();
    try
    {
      g.InterpolationMode = InterpolationMode.NearestNeighbor;
      g.DrawImage(img, 
          new RectangleF(pt.X, pt.Y, img.Width * rate, img.Height * rate),
          new RectangleF(-0.5F, -0.5F, img.Width. img.Height),
          GraphicsUnit.Pixel);
    }
    finally
    {
      g.Restore(gs);
    }
  }

元画像の範囲指定の始点を (-0.5, -0.5) とすることで周辺色をどの方向へ調べても画像の1ドットを示すように仕向け、画像の外側を描画しないようにしようというわけだ。これは一見うまくいったように見えるが、倍率をかえて見ていくと、左および上の最初のドットが指定倍率より1ドット大きく、右および下の最後のドットの大きさが1ドット小さい場合があることに気が付く。この文章を書きながら再確認した場合は、5倍や12倍ではズレが発生し、6倍や10倍ではズレが発生しなかった。このズレの発生は source rectangle ではなく、destination rectangle の指定座標で発生する。次のような処理で

  public void DrawImage3(Graphics g, Image imgs, int rate, Point pt)
  {
    for (int i = 0; i < 10; i++)
    {
      DrawImage2(g, img, rate, pt);

      pt.Y += img.Height * rate;
    }
  }

期待する結果は、イメージが指定倍率で縦にずらりと隙間なく並ぶというものだ。*3しかし、ズレの発生する倍率であっても、ズレの発生しない倍率であっても、描画には期待しない隙間ができてしまう。
たとえば、さきほどズレないと書いた20×20の画像の6倍を並べると、5つ目の描画が期待より1ドット下から開始し、4つ目と5つ目の間に隙間ができてしまう。このときの5つ目の最下段は5ドットしかなく、縦の長さが20×6の120ドットではなく119ドットしか描画が発生していない。同様に7、8、9番目も119ドットしかなく、10番目は120ドットある。基点を (24, 24) としていたのだが、(24, 25) に変更すると4、5,9番目が119ドットの描画になった。
この問題は destination rectangle だけの問題ではないらしく、source rectangle を -0.5 にいぢる前の DrawImage1 を使用すると、どの座標でもきっちり隙間なく並ぶことが確認できる。このことから、source rectangle に (-0.5, -0.5) を指定しているのが影響しているのではないかと考えられる。

それで、まあ

うまい解決策は未だに見つかってないわけだが、前回の塗り込みの問題で Graphics.FromImage(Bitmap) の生成物を渡してもらうかわりに、Bitmap を直接渡してもらうようにしてもらった。その結果 DC にビットマップを選択して、API で塗り潰しを実行してからビットマップに書き戻すということをしていたので、今回も Bitmap 前提の解決方法をとることにしてしまった。

  public static void DrawImage(Bitmap destBmp, Rectangle destRect, Bitmap srcBmp, Rectangle srcRect)
  {
    BitmapData srcBits = srcBmp.LockBits(srcRect, ImageLockMode.ReadOnly, PixelFormat.Argb32bpp);
    try
    {
      // SRCCOPY 相当のみサポートのため WriteOnly
      BitmapData destBits = srcBmp.LockBits(srcRect, ImageLockMode.WriteOnly, PixelFormat.Argb32bpp);
      try
      {
        // destRect と srcRect の比率を元に srcBits から destBits へコピー
      }
      finally
      {
        destBmp.UnlockBits();
      }
    }
    finally
    {
      srcBmp.UnlockBits();
    }
  }

とかしたわけだが、サックリ動きませんでした。どうも LockBits() の第0引数の Rectangle による部分ロックはうまく動かないようで、

  srcBits = srcBmp.LockBits(new Rectangle(0, 0, srcBmp.Width, srcBmp.Height), ...

と、画像全体をロックするように指定したらきちんと動きましたとさ...。
フォントの問題*4とかもあって、もう全部 GDI32 で置き換えちゃってもいいなじゃないかと思うぐらい(苦笑*5

*1:.NET では NotImplementException

*2:id:ladybug:20050418

*3:実際にこの問題に当たったときに多数のイメージを順番に拡大して並べる処理だった。

*4:id:ladybug:20050514

*5:GDI32 な System.Drawing ライクのクラスライブラリって需要ありますかね?