たこやき部屋

SteamVRを使って、Unityでゲームを開発したい

WindowsAPIとGDI+で、好きな画像を時計にするアプリを作るん

普段はUnityでVRゲームを作って遊んでるんですが、たまにはWindowsアプリを作りたくなったので、自分の好きな画像を背景にできる時計でも作ってみようと思います。
画像はアルファ値が使えるpngを使いたいので、普段あまり使っていないGDI+を使って試してみます。

GDI+の初期化と終了が以下のような形で、この辺の処理を変える事は滅多にないです。

IShellDispatch *pShellDispatch;

// GDI+ 初期化
GdiplusStartup(&gdiToken, &gdiSI, NULL);

// COM初期化
CoInitialize(NULL);
CoCreateInstance(CLSID_Shell, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellDispatch));

//
// ここにいろんな処理を書く
//

// COM終わり
CoUninitialize();

// GDI+終わり
GdiplusShutdown(gdiToken);


画像ファイルを読み込ます時は、Bitmapクラスのコンストラクタで画像ファイルを指定したら良いのですが、なんと!全角文字のファイル名の時ちゃんと動きませんでした。(自分の環境の場合)

仕方なくUnicodeでソースを書き直し。
Unicodeを使う時はchar型をT_CHAR型にして、使う関数はT_CHAR型が使えるものにします。
また、文字列を指定したい時は「TEXT("文字列")」のようにしてあげます。

動かしたアプリのファイル名を使って、iniファイル名にする時はこんな風になります。

void GetIniName(TCHAR *pcIni)
{
    // exeファイル名をフルパスで取得
    GetModuleFileName(NULL, pcIni, MAX_PATH);
    // 拡張子を消す
    PathRemoveExtension(pcIni);
    // ".ini"をくっつける
    wcscat_s(pcIni, MAX_PATH, TEXT(".ini"));
}


さてさて、GDI+を使って画像を描画する処理は、ちょっと長いですが以下のコードを書きました。

時計の画像を管理している、Clockクラス(抜粋)

Clock::Clock(HWND hWnd, TCHAR *pFile)
{
    // 画像読み込み
    m_pImgBmp = new Bitmap(pFile);
    if (m_pImgBmp == NULL) {
        return;
    }

    // 時計のサイズ
    m_ClockSize = max(m_pImgBmp->GetWidth(), m_pImgBmp->GetHeight());

    // 針を中心に表示
    m_CenterX = m_ClockSize/2;
    m_CenterY = m_ClockSize/2;

    // 針の設定(Handクラスについては、後述のソースをダウンロードして下さい)
    m_Sec = new Hand(m_ClockSize/2, 2, TEXT("ffff0000"), TEXT("矢印"));
    m_Min = new Hand(m_ClockSize/2, 3, TEXT("ff404040"), TEXT("矢印"));
    m_Hour = new Hand(m_ClockSize/3, 8, TEXT("ff404040"), TEXT("矢印"));

    // 時計全体のBMP
    m_pClockBmp = new Bitmap(m_ClockSize, m_ClockSize);
    if (m_pClockBmp == NULL) {
        return;
    }

    // Windowサイズを変える
    SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, m_ClockSize, m_ClockSize, SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
}

//----------------------------------------------------
HBITMAP Clock::GetHBITMAP(void)
{
    // Bitmapのハンドルを返してあげる
    HBITMAP hBmp;
    m_pClockBmp->GetHBITMAP(0, &hBmp);
    return hBmp;
}

//----------------------------------------------------
void Clock::Draw(void)
{
    // 一応チェック
    if (m_pImgBmp == NULL) {
        return;
    }
    if (m_pClockBmp == NULL) {
        return;
    }

    // 描画
    Graphics graphics(m_pClockBmp);
    // アンチエイリアス有効
    graphics.SetSmoothingMode(SmoothingModeAntiAlias);
    // まず綺麗にする
    graphics.Clear(Color(0, 0, 0, 0));

    // 描画
    graphics.DrawImage(m_pImgBmp,
        (int)(m_pClockBmp->GetWidth()-m_pImgBmp->GetWidth())/2,
        (int)(m_pClockBmp->GetHeight()-m_pImgBmp->GetHeight())/2,
        m_pImgBmp->GetWidth(), m_pImgBmp->GetHeight());

    // 時刻を取る
    SYSTEMTIME tm;
    GetLocalTime(&tm);

    // 秒針も含めスムーズに動かすため、細かく計算
    // 時
    double angle = ((tm.wHour) % 12 * 60 + (tm.wMinute)) / 2;
    m_Hour->Draw(&graphics, m_CenterX, m_CenterY, angle);

    // 分
    angle = tm.wMinute * 6 + (tm.wSecond)*0.1;
    m_Min->Draw(&graphics, m_CenterX, m_CenterY, angle);

    // 秒
    angle = tm.wSecond * 6 + (tm.wMilliseconds)*0.006;
    m_Sec->Draw(&graphics, m_CenterX, m_CenterY, angle);
}

Windowプロシージャ側、WM_TIMERの中に入れています。

void Draw(HWND hWnd, Clock *pClock)
{
    // 時計描画
    pClock->Draw();
    // 時計のBitmapハンドルを取得
    HBITMAP hBmp = pClock->GetHBITMAP();

    // LayeredWindowの設定(255はWindow全体のアルファ値、小さくすると半透明になる)
    BLENDFUNCTION blend = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
    POINT ptSrc = { 0, 0 };
    SIZE szWin = { pClock->GetSize(), pClock->GetSize() };

    // デバイスコンテキストを作って
    HDC hMemDC = CreateCompatibleDC(NULL);
    // さっき作ったBitmapハンドルを使う & 昔のは残しとく
    HBITMAP hOldBmp = SelectBitmap(hMemDC, hBmp);

    // Windowに描画
    UpdateLayeredWindow(hWnd, 0, NULL, &szWin,
        hMemDC, &ptSrc, 0, &blend, ULW_ALPHA);

    // 昔のに戻して
    SelectBitmap(hMemDC, hOldBmp);

    // 作ったHDC消す
    DeleteDC(hMemDC);

    // Bitmapハンドル消す
    DeleteBitmap(hBmp);
}

Windowへの描画は「UpdateLayeredWindow」が引き受けてくれるわけですが、こいつを使うにはWindowのスタイルに「WS_EX_LAYERED」を指定する必要があるので「CreateWindowEx」でWindowを作る時に指定しましょう。

// Window作る
HWND hWnd = CreateWindowEx(
    WS_EX_TOOLWINDOW | WS_EX_LAYERED,
    APP_NAME, APP_NAME, WS_POPUP,
    100, 100, 300, 300, NULL, NULL, hInst, NULL);

これで基本的な所は終わり。
他の部分は通常のWindowsAPIを使ったアプリと同じです。

出来上がった時計はこうなりました。

初回起動直後、デフォルトのpng画像を読み込んでます。
f:id:takoyakiroom:20161126210828p:plain

こんな画像を用意して
f:id:takoyakiroom:20161126210849p:plainf:id:takoyakiroom:20161126210850p:plainf:id:takoyakiroom:20161126210851p:plain

Windowにドロップ!
f:id:takoyakiroom:20161126210826p:plain
微妙に見にくいですが、ちゃんと時計を表示しています。

他の画像をドロップで画像が切り替わります。
f:id:takoyakiroom:20161126210950p:plain

でも、時計としては最低限の機能しか持っていないため、後々いろんな機能を追加していきたいです。


画像はpng、jpg、gifとか使えるみたいですが、Windowsまかせなので何が読み込めるかよく分からんです。
で、よくわからないことになったらタスクマネージャでImgClock.exeを終了させ、iniファイルを消してください。

↓実行ファイルと、全ソースのダウンロードはここから
ImgClock.zip - Google ドライブ