はじめに
一番最初は、初音ミクのMMDを表示することが出来るデスクトップマスコットを作成していたのですが、MMDは一般ユーザーにとって扱いにくい(自分で作ることが出来ない等)ものであると考え、「好きな画像をデスクトップマスコットにすることが出来る」 というものにしました。
使った技術や工夫した点、苦労した点などを中心に備忘録として書いておこうと思います。
また、プログラムの詳細は私のGitHubリポジトリにて公開しているので是非ご覧ください。
使った言語や技術等
C++、DXライブラリ Ver3.24b https://dxlib.xsrv.jp/ 、WindowsAPI
基盤となる考え方
基本的なデスクトップマスコットを作るためには、以下の三つの要素が必要です。
- (右端に固定配置する場合)画面サイズの取得
- 透明なウインドウ
- 最上面への配置
さらに、そこに+αで何らかの機能をつけようと考えた結果、(現状は)二つの機能を搭載することにしました。
- マウスホイールでなでる動作をすると、何らかの反応が返ってくる
- ユーザが自由に画像を選択できるようにする
「基本的な」デスクトップマスコットを作成するには
まず、「1.」から見ていきましょう。
画面サイズの取得をするには、
width = GetSystemMetrics(SM_CXSCREEN);
height = GetSystemMetrics(SM_CYSCREEN);
を使用すればよいのですが、このままでは高確率で正しい解像度の取得が出来ません。
理由としては、DPIに関係があります。詳しくはこちらのサイトで確認してください。
なので、表示先のディスプレイに対応したDPI設定へ変更してあげる必要があります。
SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
これで正しい画面サイズの取得をすることが出来ます。
ただし、この関数を使用するには以下の記載が必要です。
#pragma comment(lib, "Shcore.lib")
次に「2.」を見ていきましょう。
透明なウインドウを作成する為に、「現在は」このような方法で行っています。
SetUseBackBufferTransColorFlag(TRUE);
しかし、この方法では黒色がすべて透明になってしまうので、ほかの案を考える必要があります(代替案は現在考慮中)。
最後に、「3.」についてです。
これはそこまで難しくありません。SetWindowPos()関数によってウインドウの座標等をきめるのですが、以下のように引数で指定すると、最上面に持ってくることが出来ます。
SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
何らかの機能をつけるには
まず、「1.」について。
撫でる機能は、以下のようなプログラムで実装しています。
if (po.x >= width - main_width and po.y >= height - main_height)
{
//SELECT_GRAPH = Arisa_Seifuku;
ULONGLONG start_time = ::GetTickCount64();
ULONGLONG now_time = 0;
int move_distance = 0;
int defalt_point = po.x;
while (now_time < 500)//マウスの移動距離計算
{
now_time = ::GetTickCount64() - start_time;
Sleep(5);
move_distance += abs(po.x - defalt_point);
GetCursorPos(&po);
}
if (GetKeyState(VK_LBUTTON) & 0x80 and move_distance >= 500 * 2 * 5 and abs(po.x - defalt_point) < 500)//基準値*なでなで(往復)回数をチェック、同時にポインターが離れ過ぎてないか確認
{
SELECT_WORD = 3;
}
}
要は、「デスクトップマスコットとして表示されている画像の中で一定量のカーソル移動がなされているか」を調べるプログラムとなっています。
次に「2.」について。
正直、一番時間がかかった部分です。
上ではかなりざっくりとした書き方をしましたが、実際に細かい手順を示すと以下の通りになります。
- メインウインドウ(デスクトップマスコットである画像が表示される透明なウインドウ)の子ウインドウとして、ツールバーウインドウを準備する
- ボタンを作成し、それに対応した関数を呼び出す
- 上記の関数でエクスプローラーを開き、ユーザが選択した画像のフルパスを記憶する
- ユーザが選択した画像のサイズを取得し、大きすぎる場合は縦横比率を変えずに適正サイズまで縮小する
- 縮小した画像を新たな画像として保存し、それをあらたなデスクトップマスコットとして表示する
- 画像サイズに合わせて、配置座標を設定する
一つずつ見ていきましょう。
ウインドウを作るには...
そもそも、ツールバーウインドウを作るということは、単純にウインドウをもう一つ作る必要があるということです。なので、ここで大まかな仕組みをまとめておこうと思います。
まず、「ウインドウハンドル」 が必要です。
ウインドウそれぞれに割り振られたIDのようなものです。これによってどのウインドウなのかを判別しています。
次に、「ウインドウクラス」 の登録が必要です。
こちらは、そのウインドウの属性を決定していきます。例えば、後述するウインドウプロシージャの関数はどうするか、背景色はどうするか、クラスの名前はどうするのか等です。つまり、人で言えば性格みたいなものです。
そして、先ほど述べた 「ウインドウプロシージャ」 を作ると、いろんな処理が出来ます。
メッセージを受けて、それに応じた処理を行うことが出来ます。今回の場合は、ボタンを押したときにエクスプローラーを表示してから...といったような処理が必要になってきます。
ツールバーを作ろう
では、「1.」について。
まず、上記で述べた 「ウインドウハンドル」 、「ウインドウクラス」 を準備します。
////ツールバーウインドウハンドル
HWND hMenuWindow = NULL;
// ウィンドウクラスの登録
WNDCLASS wc = {};
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = L"MenuWindowClass";
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
RegisterClass(&wc);
HWND hMainWindow = GetMainWindowHandle(); // メインウインドウのハンドルを取得
そして、「2.」でボタンを追加します。
// ボタンの作成
HWND hButton1 = CreateWindow(L"BUTTON", L"設定",
WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
20, 20, 160, 30,
hMenuWindow,
(HMENU)ID_TOOLBAR_BUTTON1,
GetModuleHandle(NULL),
NULL);
HWND hButton2 = CreateWindow(L"BUTTON", L"ボタン2",
WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
20, 60, 160, 30,
hMenuWindow,
(HMENU)ID_TOOLBAR_BUTTON2,
GetModuleHandle(NULL),
NULL);
ボタンで各機能を呼び出すために、call_back関数を作ります。
このcall_back関数に、2.~5.までの内容が入っています。
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
switch (iMsg)
{
case WM_COMMAND: {
switch (LOWORD(wParam))
{
case ID_TOOLBAR_BUTTON1:
{
TCHAR szFileName[MAX_PATH] = { 0 };
// OPENFILENAME 構造体の初期化
OPENFILENAME ofn = { 0 };
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.hwndOwner = hWnd; // オーナーウィンドウのハンドル
ofn.lpstrFile = szFileName;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFilter = TEXT("画像ファイル\0*.jpg;*.png;*.bmp;*.gif\0すべてのファイル\0*.*\0\0");
ofn.nFilterIndex = 1;
ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
// ファイル選択ダイアログの表示
if (GetOpenFileName(&ofn))
{
GetImageSize(szFileName, main_width, main_height);
// 画像を縮小して保存
TCHAR resizedFileName[MAX_PATH] = TEXT("resized_image.png");
if (main_width > 500 && main_height > 500) {
if (main_width > main_height) {
int resized_width = 500;
int resized_height = (int)(500.0 * main_height / main_width);
main_width = resized_width;
main_height = resized_height;
}
else {
int resized_height = 500;
int resized_width = (int)(500.0 * main_width / main_height);
main_width = resized_width;
main_height = resized_height;
}
}
ResizeImage(szFileName, resizedFileName, main_width, main_height);
// 縮小した画像を読み込む
UserIcon = LoadGraph(resizedFileName);
DestroyWindow(hWnd);
hMenuWindow = NULL;
return 0;
}
else
{
// ユーザーがキャンセルした場合の処理
MessageBox(hWnd, TEXT("ファイルの選択がキャンセルされました。"), TEXT("情報"), MB_OK);
DestroyWindow(hWnd);
hMenuWindow = NULL; // ウィンドウハンドルをクリア
return 0;
}
return 0;
}
case ID_TOOLBAR_BUTTON2:
MessageBox(hWnd, L"ボタン2がクリックされました!", L"通知", MB_OK);
return 0;
}
break;
}
case WM_DESTROY:
DestroyWindow(hWnd);
hMenuWindow = NULL; // ウィンドウハンドルをクリア
return 0;
default:
return DefWindowProc(hWnd, iMsg, wParam, lParam);
}
return 0;
}
各要素ごとに一つずつ見ていきましょう。
上記のプログラムは、ボタンを押したことによって発生した「メッセージ」を受け取った際に呼び出されています。
例えばボタン1を押すことによって、ID_TOOLBAR_BUTTON1というメッセージが送られます。それをキャッチして、
case ID_TOOLBAR_BUTTON1:
{//以下省略
これ以降に書いてある処理を実行します。
そして、いよいよエクスプローラーを呼び出すのですが、これは以下のプログラムで呼び出しています。
TCHAR szFileName[MAX_PATH] = { 0 };
// OPENFILENAME 構造体の初期化
OPENFILENAME ofn = { 0 };
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.hwndOwner = hWnd; // オーナーウィンドウのハンドル
ofn.lpstrFile = szFileName;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFilter = TEXT("画像ファイル\0*.jpg;*.png;*.bmp;*.gif\0すべてのファイル\0*.*\0\0");
ofn.nFilterIndex = 1;
ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
if (GetOpenFileName(&ofn))// ファイル選択ダイアログの表示
{//以下省略
これで、.jpg、.png、.bmp、.gifを含む画像ファイルのみを表示することが出来ます。
エクスプローラーを×で閉じる等、ファイルの選択がなされなかった場合は
GetOpenFileName(&ofn) == False
となるので、else文以降にある
// ユーザーがキャンセルした場合の処理
MessageBox(hWnd, TEXT("ファイルの選択がキャンセルされました。"), TEXT("情報"), MB_OK);
DestroyWindow(hWnd);
hMenuWindow = NULL; // ウィンドウハンドルをクリア
return 0;
という処理が実行されます。
ファイルの選択をした場合は、フルパスが変数szFileNameに保存されます。
しかし、このまま表示してしまうと、画像サイズによって大きくデスクトップマスコットのサイズ自体も変わってしまい、画像サイズが特に大きい場合に画面の半分近くを埋めてしまうことになりかねません。
そのため、縦か横の解像度が500以上になる場合、どちらか大きいほうを500以下になるように縮小し、縦横比を保ったままにする ように画像サイズを変換することにしました。
そのためには、そもそもユーザの選択した画像ファイルの解像度を取得する必要があります。その機能は、
GetImageSize(szFileName, main_width, main_height);
という関数で実現しています。
その中身は
void GetImageSize(const TCHAR* filePath, int& width, int& height)
{
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
{
Gdiplus::Image image(filePath);
width = image.GetWidth();
height = image.GetHeight();
}
Gdiplus::GdiplusShutdown(gdiplusToken);
}
という風になっています。
そして、いよいよ画像の縮小に入るのですが、この機能は
TCHAR resizedFileName[MAX_PATH] = TEXT("resized_image.png");
という関数で実現しており、その中身は
void ResizeImage(const TCHAR* srcFilePath, const TCHAR* destFilePath, int newWidth, int newHeight)
{
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
{
Gdiplus::Image srcImage(srcFilePath);
Gdiplus::Bitmap newImage(newWidth, newHeight, srcImage.GetPixelFormat());
Gdiplus::Graphics graphics(&newImage);
graphics.DrawImage(&srcImage, 0, 0, newWidth, newHeight);
CLSID pngClsid;
GetEncoderClsid(L"image/png", &pngClsid);
newImage.Save(destFilePath, &pngClsid, NULL);
}
Gdiplus::GdiplusShutdown(gdiplusToken);
}
となっています。
ちなみに、この中で使用している"Gdiplus"を用いるには
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
と記載する必要があります。
これで、ようやく適切に縮小された"resized_image.png"をデスクトップマスコットとして表示することが出来ました(ただし、このままではすべての画像が.pngファイルに強制変換されてしまうため、元ファイルと同じファイル形式に合わせられるようプログラムを改善する必要があるかもしれません)。
上記にある、画像の解像度取得や画像の縮小の際に使った"gdiplus"は少し使い勝手がほかの関数と違うので、これに関しては別の記事で解説しようと思います(出来次第URLを貼ります)。
おわりに
デスクトップマスコットとしては、まだ機能も少なく、現実的に使えるようなものではありませんが、今後いろんなアイデアを盛り込んで、カイルのように邪魔扱いされないようなアプリケーションにしたいと思います。
拙い文章でしたが、最後まで読んでいただきありがとうございました。