Written in Japanese(Shift-JIS)
2008.11.13
INASOFT
/トップ/ユーティリティとゲーム/ウィキペディア・タイマ/半通過ウィンドウの作り方講座

■半通過ウィンドウの作り方講座


■概要

Windows 2000以降で、Visual C++ 2005を使い、ウィキペディア・タイマで使われているような半通過ウィンドウの作り方の説明をしています。

主に、次の技術が使われます。

さらに、UpdateLayeredWindow() 適用後のウィンドウに文字を書く試みも紹介しています。

ただし、リージョンを使ったウィンドウの形状を変える仕組み(SetWindowRgn)については、今回は適用外です。MFCとか.NETとかについても適用外ですが、応用すれば使えるかもしれません。

なお、最低でも、CreateWindowEx() でウィンドウを作ったり、デバイスコンテキストとかメモリデバイスコンテキストといったモノの使い方を知っていないと、理解は難しいかもしれません。

■はじめに

PicasaとかPhotoshopなんかで出てくるのですが、ウィンドウが四角い形をしていないことがあるんですね。

まぁ、ウィンドウが四角い形状をしていないだけならば、SetWindowRgnを使えば良いんだろうくらいの想像は付くのですが、背景との境目が半透明になって、背景とうまく融合していたりするんですね。

これってどうやっているんでしょう。

当初は不思議に思っていたんですが、今回開発したウィキペディア・タイマの新バージョン「Firefox Special Edition」を作るときにその技術を取り込もうとしたところ、けっこう苦労したモノのできあがりましたので、今日はそれの作り方についての紹介をしようかと思います。

半通過ウィンドウ(ふきだし)

ちなみに、半通過しているウィンドウのサンプルはこんな感じです。

実用的な「吹き出し」をサンプルに使っているのでわかりにくいかもしれません。



半通過ウィンドウ(ふきだし、拡大)

拡大してみるとわかりやすいかと思います。

このように、「吹き出し」には影が付けられているのですが、この影の部分と背景が自然な感じで融合していることがわかるかと思います。

これが半通過の部分です。

ちなみに、この半通過部分の背景の絵が変わっても(背後のウィンドウをぐりぐり動かしたりしても)、きちんと描画されます。

ところで、Windows 2000以降のOSのマウスカーソルが、ちょうどこんな感じで、影付きのマウスになっていることはお気づきでしょうか。

Windows 2000のマウスについても、この技術を応用したものなのだそうです。


■半通過の仕組み




この半通過の仕組みを実現するために、Windows 2000以降では、レイヤードウィンドウという新しい形状のウィンドウが作られました。レイヤードウィンドウは、次のいずれか、または両方を用いて、特定の部分を抜いたり、半通過部分を作ったりということをしているようです。

レイヤードウィンドウを作るためには、ウィンドウを作るときにWS_EX_LAYEREDというスタイルを指定するか、ウィンドウを作った後にWS_EX_LAYEREDを設定します。具体的には、

HWND hWnd = CreateWindowEx(WS_EX_LAYERED, ...

とか、

LONG lStyle = GetWindowLong(hWnd, GWL_EXSTYLE);
lStyle |= WS_EX_LAYERED;
SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_LAYERED);

といったようなことをします。

ところで、WS_EX_LAYERED スタイルはWindows 2000以降でのみ有効なスタイルであるため、_WIN32_WINNT0x500 以上に設定されていないと、この値を使うことはできません。#include <windows.h> の前に、#define _WIN32_WINNT 0x0500 を書いておくようにしましょう。

※子ウィンドウにはこのスタイルを付けられません。

バージョンは0x0500で

レイヤードウィンドウに通過部分を知らせるには、SetLayeredWindowAttributesUpdateLayeredWindowといったAPIを使います。

両方同時には使えません。特に、一度SetLayeredWindowAttributesを使ってしまうと、次からはUpdateLayeredWindowを使えなくなります(正確には、GetWindowLong/SetWindowLongでWS_EX_LAYEREDビットを「外して、付ける」という作業をしなければなりません)。

SetLayeredWindowAttributesは、部分的な半通過をさせることはできませんが、ウィンドウ全体を半通過にしたり、完全通過部分を作るだけならば簡単に使えるAPIです。

BOOL SetLayeredWindowAttributes(
    HWND hwnd,
    COLORREF crKey,
    BYTE bAlpha,
    DWORD dwFlags
);

第1引数のhwndには、レイヤードウィンドウのウィンドウハンドルを指定します。

第2引数のcrKeyには、dwFlagsLWA_COLORKEYを指定した場合に、通過色(カラーキー)を設定します。RGBマクロを使うとよいでしょう。例えば、緑色を通過させたければ、RGB(0, 0xff, 0) のように指定します。

第3引数のbAlphaには、dwFlagsLWA_ALPHAを指定した場合に、不透明度(アルファ値)を設定します。0が完全通過、255が完全不通過、1〜254が半通過(値に応じた不透明度)となります。

第4引数のdwFlagsはアクションフラグです。LWA_COLORKEYを指定した場合は、crKey が有効になり、LWA_ALPHAを指定した場合はbAlphaが有効になります。LWA_COLORKEY | LWA_ALPHAと指定した場合は、両方が有効になります。

簡単です。

ですが、SetLayeredWindowAttributesでは、最初に示したような影を付けるような処理はできません。つまり、ウィンドウ内の特定の部分を完全に表示し、特定の部分を半通過にする、のようなことはできません。

次以降は、それが可能なUpdateLayeredWindowを取り上げてみたいと思います。

■アルファチャネル付きpngファイル



さて、UpdateLayeredWindowを使うわけなのですが、このAPIを使うためには、半通過情報を持ったサーフェスのデバイスコンテキストが必要になります。

BOOL UpdateLayeredWindow(
  HWND hwnd,             // レイヤードウィンドウのハンドル
  HDC hdcDst,            // 画面のデバイスコンテキストのハンドル
  POINT *pptDst,         // 画面の新しい位置
  SIZE *psize,           // レイヤードウィンドウの新しいサイズ
  HDC hdcSrc,          // サーフェスのデバイスコンテキストのハンドル←ココ
  POINT *pptSrc,         // レイヤの位置
  COLORREF crKey,        // カラーキー
  BLENDFUNCTION *pblend, // ブレンド機能
  DWORD dwFlags          // フラグ
);

半通過情報を持ったサーフェスのデバイスコンテキストを作るのはなかなか難しいようです。これは、半通過の概念が初期のWindowsには無かったため、SetPixelのような古いGDI関数は通用しない等制限事項が多いためのようです。

私が知っている方法としては、CreateDIBSectionでビットマップを作るときにBITMAPV5HEADERを無理矢理与えてやる方法です。これですと、各色要素の最上位部25〜32ビットがアルファ値を示すようになるため、このビットマップをメモリデバイスコンテキストに描いてやれば良いと言うことになります。

で、CreateDIBSectionで一点一点ピクセルを描いてやるのは苦痛以外の何者でもありません。というわけで、通過色情報を持ったPNG形式のファイルをPhotoshopなどのツールでちゃちゃっと描いたことにして、ここではそれを読み込んで表示することを考えることにしようかと思います。

PNGを読み込む方法のデファクトスタンダードはlibpngがあります。こちらのサイトに良い感じの資料が置いてありましたので、参考にしました。

消えるといけないので、使い方を簡単に転記しておきますと、

  1. まずはzlibをダウンロードし、ビルドする。
  2. 続いてlibpngをダウンロードする。
  3. VC++で「Win32 Static Library」なプロジェクトを新規作成する。
  4. ↑で作られたプロジェクトフォルダに、libpng内のソースコード達を展開し、cファイルとhファイルをプロジェクトに登録する。
  5. 一番最初に作ったzlibのzlib.hzconf.hも登録する。
  6. ビルドするとlibpng.libが出来上がって、めでたしめでたし。

といったような感じです。

その後、自作したいプログラムのプロジェクトにpng.hpngconf.hzconf.hzlib.hを登録し、ライブラリとしてはlibpng.libunzip.libを追加します。

※さすがに、ヘッダファイルの登録の仕方とか、ライブラリファイルの追加の仕方とかまでは説明しませんヨ...

libpngの登録


■アルファチャネル付きpngファイル(2)



では、pngファイルを読み込むプログラムを作ることにしましょう。

これについても、こちらに良い感じの紹介サイトがありますね。応用して、こんな感じでサンプルプログラムを作成します。

#include "stdafx.h"
#include <png.h>

// グローバル変数:
HINSTANCE hInst;                                // 現在のインターフェイス
TCHAR *szTitle = _T("test_ulw");                // タイトル バーのテキスト
TCHAR *szWindowClass = _T("test_ulw");          // メイン ウィンドウ クラス名

HBITMAP hBitmap;                                // レイヤードウィンドウに使われるビットマップハンドル
unsigned uWidth = 0, uHeight = 0;               // pngファイルの縦横サイズを得るための変数

// このコード モジュールに含まれる関数の宣言を転送します:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);

// PNGファイルをメモリから読み込む
HBITMAP LoadPngFromFile(HWND hWnd, FILE *fp, unsigned &uWidth, unsigned &uHeight)
{
    // 引数チェック
    if (!fp) {
        return NULL; // エラーリターン
    }

    // PNG読み込み開始(メモリ確保・初期化)
    png_struct *png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png_ptr) {
        return NULL; // エラーリターン
    }

    // png_info 構造体の初期化(画像データの前にあるチャンク用)
    png_info *info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr) {
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        return NULL; // エラーリターン
    }

    // png_info 構造体の初期化(画像データの後にあるチャンク用)[本プログラムでは不要な部分です]
    png_info *end_info = png_create_info_struct(png_ptr);
    if (!end_info) {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        return NULL; // エラーリターン
    }

    // ファイルポインタを伝える
    png_init_io(png_ptr, fp);

    // 画像データ読み込み
    png_uint_32 nWidth, nHeight;
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);

    // 画像情報を取得
    nWidth = info_ptr->width;
    nHeight = info_ptr->height;

    // ビットマップハンドルの作成
    BITMAPV5HEADER bi;
    void *lpBits;

    ZeroMemory(&bi,sizeof(BITMAPV5HEADER));
    bi.bV5Size        = sizeof(BITMAPV5HEADER);
    bi.bV5Width       = nWidth;
    bi.bV5Height      = nHeight;
    bi.bV5Planes      = 1;
    bi.bV5BitCount    = 32;
    bi.bV5Compression = BI_BITFIELDS;
    bi.bV5RedMask     = 0x00FF0000;
    bi.bV5GreenMask   = 0x0000FF00;
    bi.bV5BlueMask    = 0x000000FF;
    bi.bV5AlphaMask   = 0xFF000000; 

    HDC hdc = GetDC(hWnd);
    HBITMAP hbmp = CreateDIBSection(hdc, (BITMAPINFO *)&bi, DIB_RGB_COLORS, 
        (void **)&lpBits, NULL, (DWORD)0);
    ReleaseDC(hWnd, hdc);
    DWORD *lpdwPixel = (DWORD *)lpBits;
    DWORD x, y;
    png_bytepp row_pointers = png_get_rows(png_ptr, info_ptr);

    // 画像データを順次読み出し
    for (x=0; x<nWidth; ++x) {
        for (y=0; y<nHeight; ++y) { // RGBA→ARGB 変換
            DWORD r     = row_pointers[y][4*x  ];
            DWORD g     = row_pointers[y][4*x+1];
            DWORD b     = row_pointers[y][4*x+2];
            DWORD alpha = row_pointers[y][4*x+3];

            // [↓あえて冗長に作ってあります]
            if (alpha == 255) {
                lpdwPixel[(nHeight-y-1)*nWidth+x] = (r << 16) | (g << 8) | (b << 0) | (alpha << 24);
            }
            else if (alpha == 0) {
                lpdwPixel[(nHeight-y-1)*nWidth+x] = 0;
            }
            else {
                r = r * alpha / 255;
                g = g * alpha / 255;
                b = b * alpha / 255;
                lpdwPixel[(nHeight-y-1)*nWidth+x] = (r << 16) | (g << 8) | (b << 0) | (alpha << 24);
            }
        }
    }
    
    // 読み出し用構造体メモリ解放
    png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);

    // 読み出し元にサイズ情報を返却
    uWidth = nWidth;
    uHeight = nHeight;

    return hbmp;
}

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR, int)
{
    BOOL bRet;
    MSG msg;

    MyRegisterClass(hInstance);

    if (!InitInstance(hInstance))
        return FALSE;

    while ( (bRet = GetMessage(&msg, NULL, 0, 0)) != 0 ) {
        if (bRet == -1)
            break;
        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }

    return (int) msg.wParam;
}

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEX wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style          = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = WndProc;
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = 0;
    wcex.hInstance      = hInstance;
    wcex.hIcon          = NULL;
    wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW);
    wcex.lpszMenuName   = NULL;
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = NULL;

    return RegisterClassEx(&wcex);
}

BOOL InitInstance(HINSTANCE hInstance)
{
    HWND hWnd;

    hInst = hInstance; // グローバル変数にインスタンス処理を格納します。

    hWnd = CreateWindowEx(WS_EX_LAYERED | WS_EX_TOPMOST, szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

    // タスクバーにボタンを表示したくない場合は、次のようにする
    //hWnd = CreateWindowEx(WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW, szWindowClass, szTitle, WS_POPUP,
    //  CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

    if (!hWnd) {
        return FALSE;
    }

    ShowWindow(hWnd, SW_SHOWNORMAL);
    UpdateWindow(hWnd);

    return TRUE;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) {
        case WM_CREATE:
            {
                // PNGファイルを読み出しておく
                FILE *fp = _tfopen(_T("test_ulw.png"), _T("rb"));

                if (fp == NULL) {
                    MessageBox(hWnd, _T("test_ulw.pngが開けません。"), _T("エラー"), MB_OK | MB_ICONSTOP);
                    DestroyWindow(hWnd);
                    break;
                }

                // メモリに読み込んだpngファイルを、ビットマップハンドルにする
                hBitmap = LoadPngFromFile(hWnd, fp, uWidth, uHeight);
                if (hBitmap == NULL) {
                    fclose(fp);
                    MessageBox(hWnd, _T("test_ulw.pngをPNGファイルとして読み込めませんでした。"), _T("エラー"), MB_OK | MB_ICONSTOP);
                    DestroyWindow(hWnd);
                    break;
                }

                fclose(fp);

                // ウィンドウの形状をビットマップに合わせて変更
                SetWindowPos(hWnd, 0, 0, 0, uWidth, uHeight, SWP_NOMOVE | SWP_NOZORDER);

                // ==========================
                // レイヤードウィンドウの設定
                // ==========================
                // 各種デバイスコンテキストの取得
                HDC hmemdc, hdc, hsdc;
                hsdc   = GetDC(0);                      // デスクトップのデバイスコンテキスト(色情報指定用)
                hdc    = GetDC(hWnd);                   // このウィンドウのデバイスコンテキスト
                hmemdc = CreateCompatibleDC(hdc);       // hdcの互換デバイスコンテキスト

                POINT wndPos;
                SIZE  wndSize;
                RECT  rc;

                // レイヤードウィンドウの画面位置とサイズ
                GetWindowRect(hWnd, &rc);
                wndPos.x = rc.left;
                wndPos.y = rc.top;
                wndSize.cx = uWidth;
                wndSize.cy = uHeight;

                // デバイスコンテキストにおけるレイヤの位置
                POINT po;
                po.x = po.y = 0;

                // レイヤードウィンドウの指定
                BLENDFUNCTION blend;
                blend.BlendOp = AC_SRC_OVER;
                blend.BlendFlags = 0;
                blend.SourceConstantAlpha = 255; // 不透明度(レイヤードウィンドウ全体のアルファ値)
                blend.AlphaFormat = AC_SRC_ALPHA;

                // 画像を描画をする
                HGDIOBJ hOldObj = SelectObject(hmemdc, hBitmap);
                BitBlt(hdc, 0, 0, uWidth, uHeight, hmemdc, 0, 0, SRCCOPY|CAPTUREBLT); // レイヤードウィンドウではCAPTUREBLTが必要

                // レイヤードウィンドウの位置、サイズ、形、内容、透明度を更新
                if (0 == UpdateLayeredWindow(hWnd, hsdc, &wndPos, &wndSize, hmemdc, &po, 0, &blend, ULW_ALPHA)) {
                    TCHAR strErrMes[80];
                    DWORD err = GetLastError();

                    wsprintf(strErrMes, _T("UpdateLayeredWindow失敗:エラーコード=%d"), err);
                    MessageBox(hWnd, strErrMes, _T("エラー"), MB_OK | MB_ICONSTOP);
                    DestroyWindow(hWnd);
                }

                SelectObject(hmemdc, hOldObj);
                DeleteDC(hmemdc);
                ReleaseDC(hWnd, hdc);
                ReleaseDC(0, hsdc);
            }
            break;

        case WM_NCHITTEST: // ウィンドウ上のどこをマウスで掴んでも、位置を移動できるようにする
            return HTCAPTION;

        case WM_DESTROY:
            if (hBitmap)
                DeleteObject(hBitmap);
            PostQuitMessage(0);
            break;

        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

stdafx.hの中身はこんな感じです。初期状態からほとんど買えていませんが、バージョン番号の定数を0x0500に変えるのを忘れないようにしましょう。

#pragma once

#ifndef WINVER
#define WINVER 0x0500
#endif

#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0500
#endif

#ifndef _WIN32_WINDOWS
#define _WIN32_WINDOWS 0x0500
#endif

#ifndef _WIN32_IE
#define _WIN32_IE 0x0500
#endif

#define WIN32_LEAN_AND_MEAN            // Windows ヘッダーから使用されていない部分を除外します。
// Windows ヘッダー ファイル:
#include <windows.h>

// C ランタイム ヘッダー ファイル
#include <malloc.h>
#include <tchar.h>

■サンプルで利用している吹き出しはコチラ → test_ulw.png

詳しい解説は行いませんが、重要な部分は太字にしてあります。このサンプルプログラム自体が、他のサイトで公開されているサンプルプログラムをあちこち書き換えて作ったものなので、あまり私がデカイ口を叩くことも出来ませんしね。

ちょっと解説が必要なのが、pngファイル読み込み中で[↓あえて冗長に作ってあります]と注の書いてある部分ですね。

実は生のpngファイルをそのまま読み込んで使おうとしてもダメらしいのです。

アルファ値が255の部分はそのままでOKなのですが、アルファ値が0の部分をそのままにしてしまうと、なぜか画面上に「加算ブレンド」っぽい画像が表示されてしまいます。

そこで、アルファ値が0の場合に限り、すべての色要素(RGB)を0にしてやる必要があります。

これだけで終わりかというとそういうわけでもないようで、さらにアルファ値が1〜254の場合も、色要素 = 画像ファイルの色要素 * アルファ値 / 255という変換を行わないといけないらしいのです。

ソースコード上で3つの式に分かれているのは、開発中の試行錯誤の跡なのですが、これは1つの式にまとめてしまっても良かったりしますね。

それからUpdateLayeredWindow関数。これのエラー時の挙動は、かなりややこしいので注意です。

例えば、戻り値0(エラー)にも関わらず、GetLastErrorの戻り値が0になることがあります。引数を与え間違えている(hBitmapがNULLだった)ことが原因だったりします。

また、「メモリ不足!」というエラーを返したと思ったら、実はhWndのウィンドウの縦横サイズとhmemdcの縦横サイズが異なっていたとか。

その他、WS_EX_LAYEREDを指定し忘れていたり、SetLayeredWindowAttributesを使っていたり、hmemdcに通過色情報が無かったり、そういったことでもエラーになります。GetLastErrorの戻り値はアテにならないと思っておいた方がよいでしょうね。

さて、このプログラムは、カレントディレクトリより「test_ulw.png」を読み込んで表示するプログラムです。サンプル画像として「吹き出し」を使った場合の実行結果は、下記のようになります。

サンプルプログラムの実行結果
実行結果


■半通過ウィンドウの上に文字を描画する



さて、吹き出しを表示したからには、中に文字を表示しないといけませんよね。

文字を表示するにはどうしたらよいでしょう。TextOutでもDrawTextでも、方法はいくらでもありそうなもんなのですが、どういうわけだかうまく行きません。

ネット上を走り回って得た情報によれば、これらのAPIは古い時代に作られた物であるため、アルファ値を書き込む機能が無く、常にアルファ値=0として描画されてしまうのだとか。

そのため、結果的には画面上に何も表示されない状態になってしまうようなのです。

うーむ、困ってしまいました...

というわけで、私の思いついた方法は、吹き出しの上にもう一つ透明なウィンドウを作り、そこに文字を描画すればよいのではないか!というアイディアです。

吹き出し(UpdateLayeredWindowで作成)の上に透明なウィンドウ(SetLayeredWindowAttributesで作成)を重ね、そこに文字を描画
こんなイメージ

透明なウィンドウの作成には、SetLayeredWindowAttributesを使います。白を背景色にしておいて、カラーキーを白にします。そして文字を黒で描画すれば、文字の部分以外は透けているウィンドウのできあがりです。

#include "stdafx.h"
#include <png.h>

// グローバル変数:
HINSTANCE hInst;                                // 現在のインターフェイス
TCHAR *szTitle = _T("test_ulw");                // タイトル バーのテキスト
TCHAR *szTitle2 = _T("test_ulw2");              // 文字描画ウィンドウのタイトル バーのテキスト
TCHAR *szWindowClass = _T("test_ulw");          // メイン ウィンドウ クラス名
TCHAR *szWindowClass2 = _T("test_ulw2");        // 文字描画 ウィンドウ クラス名

HWND ghMojiWindow;                              // 文字描画ウィンドウのウィンドウハンドル

HBITMAP hBitmap;                                // レイヤードウィンドウに使われるビットマップハンドル
unsigned uWidth = 0, uHeight = 0;               // pngファイルの縦横サイズを得るための変数
HFONT   hFont;                                  // 文字列描画用フォント

// このコード モジュールに含まれる関数の宣言を転送します:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK    WndProc2(HWND, UINT, WPARAM, LPARAM);

// PNGファイルをメモリから読み込む
HBITMAP LoadPngFromFile(HWND hWnd, FILE *fp, unsigned &uWidth, unsigned &uHeight)
{
    // 引数チェック
    if (!fp) {
        return NULL; // エラーリターン
    }

    // PNG読み込み開始(メモリ確保・初期化)
    png_struct *png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png_ptr) {
        return NULL; // エラーリターン
    }

    // png_info 構造体の初期化(画像データの前にあるチャンク用)
    png_info *info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr) {
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        return NULL; // エラーリターン
    }

    // png_info 構造体の初期化(画像データの後にあるチャンク用)
    png_info *end_info = png_create_info_struct(png_ptr);
    if (!end_info) {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        return NULL; // エラーリターン
    }

    // ファイルポインタを伝える
    png_init_io(png_ptr, fp);

    // 画像データ読み込み
    png_uint_32 nWidth, nHeight;
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);

    // 画像情報を取得
    nWidth = info_ptr->width;
    nHeight = info_ptr->height;

    // ビットマップハンドルの作成
    BITMAPV5HEADER bi;
    void *lpBits;

    ZeroMemory(&bi,sizeof(BITMAPV5HEADER));
    bi.bV5Size        = sizeof(BITMAPV5HEADER);
    bi.bV5Width       = nWidth;
    bi.bV5Height      = nHeight;
    bi.bV5Planes      = 1;
    bi.bV5BitCount    = 32;
    bi.bV5Compression = BI_BITFIELDS;
    bi.bV5RedMask     = 0x00FF0000;
    bi.bV5GreenMask   = 0x0000FF00;
    bi.bV5BlueMask    = 0x000000FF;
    bi.bV5AlphaMask   = 0xFF000000; 

    HDC hdc = GetDC(hWnd);
    HBITMAP hbmp = CreateDIBSection(hdc, (BITMAPINFO *)&bi, DIB_RGB_COLORS, 
        (void **)&lpBits, NULL, (DWORD)0);
    ReleaseDC(hWnd, hdc);
    DWORD *lpdwPixel = (DWORD *)lpBits;
    DWORD x, y;
    png_bytepp row_pointers = png_get_rows(png_ptr, info_ptr);

    // 画像データを順次読み出し
    for (x=0; x<nWidth; ++x) {
        for (y=0; y<nHeight; ++y) { // RGBA→ARGB 変換
            DWORD r     = row_pointers[y][4*x  ];
            DWORD g     = row_pointers[y][4*x+1];
            DWORD b     = row_pointers[y][4*x+2];
            DWORD alpha = row_pointers[y][4*x+3];

            if (alpha == 255) {
                lpdwPixel[(nHeight-y-1)*nWidth+x] = (r << 16) | (g << 8) | (b << 0) | (alpha << 24);
            }
            else if (alpha == 0) {
                lpdwPixel[(nHeight-y-1)*nWidth+x] = 0;
            }
            else {
                r = r * alpha / 255;
                g = g * alpha / 255;
                b = b * alpha / 255;
                lpdwPixel[(nHeight-y-1)*nWidth+x] = (r << 16) | (g << 8) | (b << 0) | (alpha << 24);
            }
        }
    }
    
    // 読み出し用構造体メモリ解放
    png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);

    // 読み出し元にサイズ情報を返却
    uWidth = nWidth;
    uHeight = nHeight;

    return hbmp;
}

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR, int)
{
    BOOL bRet;
    MSG msg;

    MyRegisterClass(hInstance);

    if (!InitInstance(hInstance))
        return FALSE;

    while ( (bRet = GetMessage(&msg, NULL, 0, 0)) != 0 ) {
        if (bRet == -1)
            break;
        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }

    return (int) msg.wParam;
}

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEX wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style          = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = WndProc;
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = 0;
    wcex.hInstance      = hInstance;
    wcex.hIcon          = NULL;
    wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW);
    wcex.lpszMenuName   = NULL;
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = NULL;
    ATOM atom = RegisterClassEx(&wcex);

    // 文字描画用ウィンドウ
    wcex.lpfnWndProc    = WndProc2;
    wcex.hbrBackground  = (HBRUSH)GetStockObject(WHITE_BRUSH); // ←背景の白いウィンドウを生成(白をカラーキーにするため)
    wcex.lpszClassName  = szWindowClass2;
    RegisterClassEx(&wcex);

    return atom;
}

// 文字描画
void PrintMessage(HDC hdc)
{
    RECT rc = {50, 100, 200, 200};

    HGDIOBJ hOldOBj = SelectObject(hdc, hFont);
    SetBkColor(hdc, RGB(0xff,0xff,0xff));
    SetTextColor(hdc, RGB(0,0,0));
    SetBkMode(hdc, OPAQUE); 
    DrawText(hdc, _T("しゃべってみます"), 8, &rc, DT_LEFT);
    SelectObject(hdc, hOldOBj);
}

BOOL InitInstance(HINSTANCE hInstance)
{
    HWND hWnd;

    hInst = hInstance; // グローバル変数にインスタンス処理を格納します。

    hWnd = CreateWindowEx(WS_EX_LAYERED | WS_EX_TOPMOST, szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

    // タスクバーにボタンを表示したくない場合は、次のようにする
    //hWnd = CreateWindowEx(WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW, szWindowClass, szTitle, WS_POPUP,
    //  CW_USEDEFAULT, 0, uWidth, uHeight, NULL, NULL, hInstance, NULL);

    if (!hWnd) {
        return FALSE;
    }

    // 文字描画用のウィンドウを生成
    // 位置はメインウィンドウと同じ位置で
    RECT rc;
    GetWindowRect(hWnd, &rc);
    ghMojiWindow = CreateWindowEx(WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT,
        szWindowClass2, szTitle2, WS_POPUP,
        rc.left, rc.top, rc.right-rc.left, rc.bottom-rc.top, hWnd, NULL, hInstance, NULL);

    if (!ghMojiWindow) {
        return FALSE;
    }

    ShowWindow(hWnd, SW_SHOWNORMAL);
    UpdateWindow(hWnd);

    ShowWindow(ghMojiWindow, SW_SHOWNORMAL);
    UpdateWindow(ghMojiWindow);

    // テスト的に文字を描画してみる
    HDC hdc = GetDC(ghMojiWindow);
    PrintMessage(hdc);
    ReleaseDC(ghMojiWindow, hdc);

    return TRUE;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) {
        case WM_CREATE:
            {
                // PNGファイルを読み出しておく
                FILE *fp = _tfopen(_T("test_ulw.png"), _T("rb"));

                if (fp == NULL) {
                    MessageBox(hWnd, _T("test_ulw.pngが開けません。"), _T("エラー"), MB_OK | MB_ICONSTOP);
                    DestroyWindow(hWnd);
                    break;
                }

                // メモリに読み込んだpngファイルを、ビットマップハンドルにする
                hBitmap = LoadPngFromFile(hWnd, fp, uWidth, uHeight);
                if (hBitmap == NULL) {
                    fclose(fp);
                    MessageBox(hWnd, _T("test_ulw.pngをPNGファイルとして読み込めませんでした。"), _T("エラー"), MB_OK | MB_ICONSTOP);
                    DestroyWindow(hWnd);
                    break;
                }

                fclose(fp);

                // ウィンドウの形状をビットマップに合わせて変更
                SetWindowPos(hWnd, 0, 0, 0, uWidth, uHeight, SWP_NOMOVE | SWP_NOZORDER);

                // ==========================
                // レイヤードウィンドウの設定
                // ==========================
                // 各種デバイスコンテキストの取得
                HDC hmemdc, hdc, hsdc;
                hsdc   = GetDC(0);                      // デスクトップのデバイスコンテキスト(色情報指定用)
                hdc    = GetDC(hWnd);                   // このウィンドウのデバイスコンテキスト
                hmemdc = CreateCompatibleDC(hdc);       // hdcの互換デバイスコンテキスト

                POINT wndPos;
                SIZE  wndSize;
                RECT  rc;

                // レイヤードウィンドウの画面位置とサイズ
                GetWindowRect(hWnd, &rc);
                wndPos.x = rc.left;
                wndPos.y = rc.top;
                wndSize.cx = uWidth;
                wndSize.cy = uHeight;

                // デバイスコンテキストにおけるレイヤの位置
                POINT po;
                po.x = po.y = 0;

                // レイヤードウィンドウの指定
                BLENDFUNCTION blend;
                blend.BlendOp = AC_SRC_OVER;
                blend.BlendFlags = 0;
                blend.SourceConstantAlpha = 255; // 不透明度(レイヤードウィンドウ全体のアルファ値)
                blend.AlphaFormat = AC_SRC_ALPHA;

                // 画像を描画をする
                HGDIOBJ hOldObj = SelectObject(hmemdc, hBitmap);
                BitBlt(hdc, 0, 0, uWidth, uHeight, hmemdc, 0, 0, SRCCOPY|CAPTUREBLT); // レイヤードウィンドウではCAPTUREBLTが必要

                // レイヤードウィンドウの位置、サイズ、形、内容、透明度を更新
                if (0 == UpdateLayeredWindow(hWnd, hsdc, &wndPos, &wndSize, hmemdc, &po, 0, &blend, ULW_ALPHA)) {
                    TCHAR strErrMes[80];
                    DWORD err = GetLastError();

                    wsprintf(strErrMes, _T("UpdateLayeredWindow失敗:エラーコード=%d"), err);
                    MessageBox(hWnd, strErrMes, _T("エラー"), MB_OK | MB_ICONSTOP);
                    DestroyWindow(hWnd);
                }

                SelectObject(hmemdc, hOldObj);
                DeleteDC(hmemdc);
                ReleaseDC(hWnd, hdc);
                ReleaseDC(0, hsdc);
            }
            break;

        case WM_NCHITTEST: // ウィンドウ上のどこをマウスで掴んでも、位置を移動できるようにする
            return HTCAPTION;

        case WM_MOVING: // 文字描画用ウィンドウにも同様のメッセージを伝える
            {
                LPRECT prc = (LPRECT)lParam;
                MoveWindow(ghMojiWindow, prc->left, prc->top, prc->right - prc->left, prc->bottom - prc->top, FALSE);
            }
            break;

        case WM_DESTROY:
            if (hBitmap)
                DeleteObject(hBitmap);
            PostQuitMessage(0);
            break;

        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

// 文字描画ウィンドウ用のウィンドウプロシージャ。基本的に何もしない。
LRESULT CALLBACK WndProc2(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) {
        case WM_CREATE:
            // 文字描画用ウィンドウはビットマップのサイズに合わせる
            SetWindowPos(ghMojiWindow, 0, 0, 0, uWidth, uHeight, SWP_NOMOVE | SWP_NOZORDER);

            // 子ウィンドウの白部分は通過色に
            SetLayeredWindowAttributes(hWnd, RGB(0xff, 0xff, 0xff), 255, LWA_COLORKEY | LWA_ALPHA);

            // 標準フォントの取得
            hFont = (HFONT)GetStockObject(SYSTEM_FONT);
            break;

        case WM_CLOSE: // 突然の終了禁止
            return 0;

        default:
            return DefWindowProc( hWnd, message, wParam, lParam );
    }
    return 0;
}

■サンプルで利用している吹き出しはコチラ → test_ulw.png

主な変更点を、太字で表しています。

重要な点としては、文字描画用のウィンドウの背景色を (HBRUSH)GetStockObject(WHITE_BRUSH) といった具合に白くしていること、それから、ウィンドウ生成時の拡張スタイルに WS_EX_TRANSPARENT を付けていることでしょうか。

WS_EX_TRANSPARENTを付けておくと、マウスクリックに反応せず、背後のウィンドウが反応するようになります(キーボードメッセージは受け取ってしまうようですが、気にしないことにしましょう)。

おまけ的な部分としては、文字描画用のウィンドウでは WM_CLOSE を受け取って return 0; を返すようにしている点でしょうか。これをしておかないと、上に書いたとおり、キーボードからのAlt+F4を受け取って、文字描画用ウィンドウだけ単独で終了してしまうことになりますので。

サンプルプログラムの実行結果は、下記のようになります。なお、マウスであんまりぐりぐりやりすぎたり、マウスで移動中にAlt+TABを押したりすると、文字が付いてこられなくなったりしますが、…まぁご愛敬。

サンプルプログラム2の実行結果
実行結果

※文字にアンチエイリアスがかかってしまう場合、ウィンドウの背景色(白)と文字色(黒)の中間色(灰色)が描かれてしまいます。透明効果は白にしかかかりませんので、灰色部分は描画され、おかしな見た目になってしまうことがあります。この場合は、フォント作成時に CreateFont を使い、出力品質として NONANTIALIASED_QUALITY を指定して、アンチエイリアスがかからないようにすると良いでしょう。

■ご参考


本ページのサンプルは、自由にご利用いただいて構いませんが、不具合など入り込んでいる可能性があるかもしれませんので、ご利用は自己責任にてお願いします。
本ページのサンプルは、一部のエラーチェック等を省略している箇所がありますので、適宜追加してください。
本ページへは、自由にリンクしていただいて構いません。
また、本サイトに掲載されている内容については、自由に転載していただいて構いません。しかしながら、本ページへのリンクや内容の転載については、自己責任で行ってください。また、このページのURLやアンカーは、サーバ運営・サイト運営・ページ運営・その他の都合により無告知で一時的あるいは永遠に消滅したり、変更したりする可能性がありますので、あらかじめご了承下さい。

/トップ/ユーティリティとゲーム/ウィキペディア・タイマ/半通過ウィンドウの作り方講座