見出し画像

ウィンドウのちらつきを抑える

画像生成AIで絵を作っていると、出力した画像をちょっとだけ修正したい、と思うことがよくある。

ペイントブラシの塗りつぶしよりも賢くて、使いやすいツールが欲しい。たとえば、スーパーマンの赤いマントを、濃紺のマントに変えてみたい。同じ色で塗りつぶすのではなく、影とか布のしわとかをちゃんと残した自然な感じで。

T.T&K スーパーマン 6

そんなことを考えて、Visual Studio を使って C# でペイントブラシみたいなアプリケーションを作り始めてみた。

もちろん、まだマントの色を変えることはできていない。

けっこう道のりは長い。

今できるのは、指定した画像ファイルを読んで、画面に表示して、ちょっと拡大、縮小する、くらい。

たったこれだけなのに、けっこう時間がかかってしまった。

そしてウィンドウに表示された画像のちらつきに悩まされた。

この記事のテーマは、このちらつきを抑制する方法。


プログラムを書く前は、画像を表示して拡大縮小するくらいはすぐ終わるだろうと思っていた。思っていて、こんなプログラムを書いた。

開発言語はC#、.NET Framework 3.5を利用した、Windows.Forms アプリケーション。

スタイル的にはかなり古いかもしれないけれど、見た目が古いだけで使いやすさは劣らず、それでいて手軽にすばやくアプリケーションを構築できる。WPFはUIが凝り過ぎていてあまり手軽でないと思う。

using System.Windows.Forms;

class ImagePanel : UserControl
{
  private System.Drawing.Bitmap _bitmap; // 読み込んだ画像ファイル

  public ImagePanel()
  {
    InitializeComponent();
    DoubleBuffered = true; // これで画面のちらつきは抑えられると思った
  }

  protected override void OnPaint( PaintEventArgs e )
  {
    e.DrawImage( _bitmap, 0, 0 ) ;
  }
}

アプリケーションウィンドウに上で定義した ImagePanel を配置して画像ファイルを読み込んだら、画面上に画像が表示された。ここまでは順調だった。

だが、画像の拡大縮小をしてみたら、信じられないくらいに画面がちらついた。画像のサイズが変わるたび、いったん画面が真っ白になった後で画像が表示される。ダブルバッファを有効にしているのに、これはおかしいと思った。

DoubleBuffered = true; // これで画面のちらつきは抑えられると思った

おかしいと思って解決を試みたのだが、解決に至るまでにずいぶん悩まされ、時間もかかってしまった。


ちらつきの原因は、再描画の都度 ImagePanel の背景まで再描画されていたことだった。Windowsから再描画命令が来ると、ImagePanel はまず BackColor で自身を塗りつぶす(背景再描画)、その後 OnPaint() を実行する。これが UserControl の既定の動作だが、今回に関しては背景再描画は必要なかった。OnPaint() に、必要な描画処理が全部書いてあるので。

もちろん、ダイアログでよくある、ボタンやテキストボックス、チェックボックスなどが配置された UserControl は、ボタンやテキストボックス間のデータのやりとりを書くことはあっても UserControl の見た目(OnPaintの実装)を書くことはあまりないから既定の動作で何も問題ない。

必要のない背景再描画をやる非効率なプログラムだったこと、これがちらつきの原因。
背景描画をさせないために、OnPaintBackground を override して「何もしない」としたら、ちらつきが一気に解消した。

  protected override void OnPaintBackground( PaintEventArgs e )
  {
    // 何もしない
  }


まとめると、画面のちらつきを抑えるには、

  • ダブルバッファを有効にする。

  • OnPaintbackground() を override して、実装を空にする。

の両方をする必要がある。

あと、画面がちらつくかどうかは分からないが、再描画が遅い要因としてよくあるのが、再描画処理に UserControl.Refresh() を使っている、というもの。これは UserControl.Invalidate() に変更すると改善する。Refresh() の方は必ず再描画がかかるのに対し、Invalidate() の方は次に再描画が必要になった時には再描画がかかる状態にしておくだけなので無駄がない。


OnPaintBackgroundに関する知識がなかったことが、今回の敗因。

それにしても背景描画がこんなに重いなんて知らなかった。


いいなと思ったら応援しよう!