今回のお題は「マルチモニタ」です。前回作成したサンプルのカラーマネジメント表示をマルチモニタ対応に改修していきましょう。

今回のサンプル

前回作成したアプリケーションをマルチモニタ対応にします。モニタ間でウインドウを移動させると、途中で移動先のモニタに最適化された表示に切り替わります。

動画の左右中央部分にモニタの境界があります。前回のバージョン(前半)はモニタ間でウインドウを移動させても描画されるコンテンツのRGB値に変化がありませんが、今回のバージョン(後半)は右側のモニタと左側のモニタとでそれぞれ異なるRGB値で描画されているのが確認できます。

マルチモニタ対応とは

アプリケーションがコンテンツ(画像やグラフィックス)を表示する際、そのアプリケーションがカラーマネジメント対応であれば表示をコンテンツのカラーに近づけるために、

コンテンツのカラースペース→モニタのカラースペース

の変換が行われてモニタの表示に最適化されたRGB値で描画されます。2回目で説明した通り、デバイスコンテキストの入力カラースペースがコンテンツのカラースペースに対応づけられているわけですが、対するモニタのカラースペースに対応づけられるのが(デバイスコンテキストの)出力カラープロファイルです。通常、画面表示を行う目的でデバイスコンテキストを作成・取得すると、そのデバイスコンテキストの出力カラープロファイルはプライマリモニタの既定のプロファイルとなっていますので、ICMをオンにして描画した結果のRGB値はプライマリモニタに最適化されていることになります。

当然、そのRGB値で別のモニタの領域に描画されたとしたら、そのモニタ上ではコンテンツのカラーは再現されません。もし、それぞれのモニタの領域ごとに出力カラープロファイルを切り替えて描画を行うことができるならば、アプリケーションのウインドウがモニタ1の領域にあるとき

コンテンツのカラースペース→モニタ1のカラースペース

の変換が行われ、モニタ2の領域に移動すれば、

コンテンツのカラースペース→モニタ2のカラースペース

の変換が行われ……結果としてそれぞれのモニタに最適化されたRGB値で描画が行われるようになるでしょう。これが、カラーマネジメント表示におけるマルチモニタ対応の基本的な考え方です。

マルチモニタ対応=モニタ同士の表示を揃える(合わせる)ではない

既に説明したように、カラーマネジメント表示の原則はコンテンツのカラーに近づけるということです。マルチモニタであってもそれぞれの目標はあくまでコンテンツのカラーですから、あるモニタの表示カラーに他のモニタの表示カラーを揃える処理が行われるわけではありません(同じ目標に向かった結果として表示が近しくなる、ということはありえます)。これは今回のサンプルがそうだ、という話ではなくカラーマネジメント対応のアプリケーション全般に当てはまる話です。

もし、マルチモニタにおける表示の差を抑える機能を盛り込みたいのであれば、通常のマルチモニタ対応にプラスαの工夫が必要になるでしょう。今回はそこまで踏み込んではいきませんが、興味がある方はアイデアを考えてみてください。

ウインドウの移動で出力カラープロファイルを切り替える

ウインドウが表示されているモニタを調べる

ウインドウがどのモニタの領域上に配置されているのかを調べるには、MonitorFromWindow()関数を使用します。第1引数にはウインドウのハンドルを、第2引数にはMONITOR_DEFAULTTONEARESTを指定します。この関数を呼び出すとモニタのハンドル(HMONITOR)が返されます。

モニタの既定のプロファイルを調べる

先ほど取得したハンドルからモニタの既定のプロファイル(のファイル名)を取得する手順は、①GetMonitorInfo()関数でモニタのデバイス名を取得 ②CreateIC()関数でモニタに対応する情報コンテキストを作成 ③GetICMProfile()関数で情報コンテキストから出力カラープロファイルを取得、となります。

GetMonitorInfo()関数は引数としてモニタのハンドルとMONITORINFOEX構造体のポインタを与えます。MONITORINFO構造体とMONITORINFOEX構造体の区別のため、必ずcbSizeに構造体のサイズを代入してから関数を呼び出すようにしてください。情報の取得に成功すると、szDeviceにモニタのデバイス名が格納されます。

デバイス名が判ったら、次にCreateIC()関数を呼び出して情報コンテキストを作成します。第1引数には文字列"DISPLAY"を、第2引数にはモニタのデバイス名を指定し、残りはNULLにしておきます。ちなみに情報コンテキストという用語が唐突に出てきましたが、これは「描画処理に使えない」ことを除けばデバイスコンテキストとほぼ同じものです。今回は出力カラープロファイルを調べるのが目的ですので、軽量な情報コンテキストを作成すれば十分というわけです。

情報コンテキストが無事に作成できたら、最後にGetICMProfile()関数で出力カラープロファイルのファイル名を取得します。お約束の通り、第3引数にNULLを与えて呼び出すと第2引数に必要サイズが返ってきますので、メモリを確保してからそのポインタを第3引数に指定して再度呼び出せばOKです。

  MONITORINFOEX info;
  HMONITOR hm;

  info.cbSize = sizeof(MONITORINFOEX);

  hm = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);

  if (GetMonitorInfo(hm, &info))
  {
    TCHAR *outputProfileName = NULL;
    DWORD count = 0;

    HDC hdc = CreateIC(_T("DISPLAY"), monitorName, NULL, NULL);

    if (hdc)
    {
      GetICMProfile(hdc, &count, NULL);
      outputProfileName = (TCHAR *)malloc(count);
      if (GetICMProfile(hdc, &count, outputProfileName);
      {
        _tprintf(_T("Output pfofile : %s¥n"), outputProfileName);
        free(outputProfileName);
      }

      DeleteDC(hdc);
    }
  }

上記のコードは説明の簡便のため取得したプロファイルを文字列として出力しているだけですが、実際のサンプルアプリケーションでは、ウインドウプロシージャ内のWM_MOVE / WM_SIZEメッセージに対する処理としてウインドウが存在するモニタのチェックとプロファイルの取得を行います。モニタのデバイス名とプロファイル(のファイル名)をそれぞれ静的配列として保持しておき、新しく取得したデバイス名と比較してモニタ間の移動が発生したかどうかを判定、取得したデバイス名が保持していたデバイス名と異なる場合にはプロファイルの取得を行ってデバイス名とともに静的配列の方に格納してウインドウの更新(再描画)を要求します。

今回のサンプルアプリケーションでは、ウインドウが最小化されている、あるいは非表示の場合にはモニタ間の移動を検知して表示を更新する必要がありませんので処理をスキップしても構わないでしょう。また、ICMがオフになっている場合も、ウインドウが存在するモニタによって表示が変わることがないため再描画の要求は行いません(モニタ間移動のチェックとプロファイルの取得は忘れずに!)。

出力カラープロファイルを設定して描画する

取得して静的配列に保持済みのプロファイルは、WM_PAINTメッセージの処理内で入力カラースペースの設定と合わせて出力カラープロファイルとして設定し、描画を実行しています。デバイスコンテキストの出力カラープロファイルを設定するにはSetICMProfile()関数関数を使用します。

ダウンロード

BitbucketのGDICMTest02リポジトリにソースと実行ファイルがあります。ダウンロードページに入り「タグ」のタブをクリックすると前回分と今回分のタグがリストされていますので、ZIPファイルをダウンロードしてください。