【C++/Win32】通常アイコンを無効アイコンで描画する方法

C++
この記事は約7分で読めます。

C++ でテキストエディタを作り出した頃からの謎だったんですが、ツールバーに表示するアイコンは無効アイコンも用意する必要があるの?でした。C# の場合、普通のアイコンを用意しておけばフレームワークが勝手に無効アイコンを描画してくれます。

当然最初は何もわからなかったので、通常と無効アイコンの2種類を用意していたのですが、やっとプログラムで無効っぽく色変更することができました。

アイコンを作るのもめちゃくちゃ面倒で、png を bmp 形式に変換して、Visual Studioに取り込んで、そのままだと何故か背景が透明にならないので、また Visual Studioで bmp 形式に変換してようやくアプリに反映できていました。

喜びのツイートです。アイコンとか言ってますが、正確には png です。

通常アイコンを無効アイコンの見た目にする方法

色んな方法があると思うんですが、なにせ技術力が無いので今回は gdi+で実装しました。通常アイコンをカラー行列を利用してグレースケール変換(白、黒、灰)して明るさを調整して実現しました。

カラー行列による色変換

あらかじめ変換するカラー行列を用意します。設定値は元の色をどのように変換するかを0(暗くなる、黒に近づく)~1.0(明るくなる、白に近づく)の範囲で設定します。下記はとある色を半分暗くしている例です。RGBA(255,128,64,255)がRGBA(128,64,32,128)になっているのでちょうど半分になっていますね。

サンプルソース

カラー行列による色変換は gdi+ で簡単に実装できます。ソースは 16 px 固定でウィンドウに png を描画するサンプルです。ポイントは colorMatrix ですね。先ほどの表の値RGBAW(赤、緑、青、アルファ、Wは常に1)を設定すると勝手に変換してくれます。

分けわからん数字が羅列されていますが、上4行はグレースケール変換する規格として決まっているようです。そこに、5行目の係数(明るさ)を加算して完了です。※gdi+ を利用するための初期設定は省いています。

void OTZGraphics::DrawImageGdiplus(int x, int y, int x2, int y2) {
    Gdiplus::Image image = Gdiplus::Image(<ファイルパス>);
    Gdiplus::Graphics gp = Gdiplus::Graphics(<HDC>);
    //UINT width = image.GetWidth();
    //UINT height = image.GetHeight();

    Gdiplus::ImageAttributes attr;
    Gdiplus::ColorMatrix colorMatrix = {
        0.298912f, 0.298912f, 0.298912f, 0, 0,  // R([0][3]と[0][4]は0固定)
        0.586611f, 0.586611f, 0.586611f, 0, 0,  // G([1][3]と[1][4]は0固定)
        0.114478f, 0.114478f, 0.114478f, 0, 0,  // B([2][3]と[2][4]は0固定)
        0.0f,      0.0f,      0.0f,      1, 0,  // A(アルファ([3][3]以外は0固定))
        0.4f,      0.4f,      0.4f,      0, 1   // W(乗算後にRGBに加算される係数([4][4]は1固定))
    };

    attr.SetColorMatrix(&colorMatrix, Gdiplus::ColorMatrixFlagsDefault, Gdiplus::ColorAdjustTypeDefault);
    Gdiplus::Rect destRect = Gdiplus::Rect(x, y, 16, 16);
    gp.DrawImage(&image, destRect, x2, y2, 16, 16, Gdiplus::Unit::UnitPixel, &attr);
}

この分けわからん値(グレースケール変換値)は NTSC 加重平均法って言うんですね。勉強になりました。

NTSC 加重平均法とは、RGBの輝度にそれぞれ重みを付けて平均を求める方法で、輝度の計算には輝度=0.29891×R + 0.58661×G + 0.11448×Bを使います。重み係数は,国際電気通信連合の規格に規定されているものです。この計算式によって得られた輝度で画像作成すると、より自然な画像が得られます。自然界にあまり存在しない青の輝度の重み係数が小さくなるように設定されています。

https://talavax.com/grayscale2.html

実装後のイメージ

1番上が通常、2番目が純粋な NTSC 加重平均法、3番目が上で書いたソースの結果(NTSC加重平均法+独自ブレンド)です。それっぽくできたんじゃないかなと思います。

colorMatrixの5行目を変更すると明るさを調整できます。ちょっと検証してみないと何とも言えないですが、gdi+って遅いっていう変なイメージがあります。ただ、さすがに10個ぐらいの画像描くだけでもっさりするとは考えにくいですけどね。

その後、Visual Studio とか見てると無効になった時のツールバーの色に応じてグレーの色合いを変えているようです。そもそも、ライトモードとダークモードのアイコンの色合いも違いますね。すげーな。

そんでもってダークモードの無効アイコンは結構濃いグレーな感じですね。なので、濃いめの無効にできるか試してみました。colorMatrix は↓です。

void OTZGraphics::DrawImageGdiplus(int x, int y, int x2, int y2) {
:(省略)
    Gdiplus::ColorMatrix colorMatrix = {
        0.1f, 0.1f, 0.1f, 0, 0,
        0.1f, 0.1f, 0.1f, 0, 0,
        0.1f, 0.1f, 0.1f, 0, 0,
        0.0f, 0.0f, 0.0f, 1, 0,
        0.0f, 0.0f, 0.0f, 0, 1
    };
:(省略)

全体的に 90% 黒くしています。その結果が↓です。

ダークモードの場合はこっちの方がしっくりくるような気がしますね。試しにライトモードで適用するとこんな感じ。やっぱりツールバーの色に応じて無効時の色も調整した方が良さそうです。

.NET はどうしているのか?

では .NET はどうやってるのというと同じように colorMatrix を利用して描画しています。ControlPaint クラスの DrawImageDisabled メソッドの無効アイコンの描画には以下の値を利用しています。

public static void DrawImageDisabled(Graphics graphics, Image image, int x, int y, Color background)
:(省略)
float[][] array = new float[5][];
array[0] = new float[5] { 0.2125f, 0.2125f, 0.2125f, 0, 0 };
array[1] = new float[5] { 0.2577f, 0.2577f, 0.2577f, 0, 0 };
array[2] = new float[5] { 0.0361f, 0.0361f, 0.0361f, 0, 0 };
array[3] = new float[5] { 0, 0, 0, 1, 0 };
array[4] = new float[5] { 0.38f, 0.38f, 0.38f, 0, 1 };
:(省略)

おわりに

Windows API にも DrawState って関数で無効アイコンを描画できるのですが png は対象外?のようです。これで 1枚の png だけでアイコンを管理できるようになりました。png はリソースファイルから取るようにしたら一旦完成ですかね。

色変更できるってことはスライムとメタルスライムみたいにアイコン1個で色々できそうですね。それにしてもグラフィック関係も面白いですね。

コメント

タイトルとURLをコピーしました