Wallpaper Engineのまねごとをする。
ゲーム、Vtuber系の企業様、就職させて♡
こっち読んだ方がいいよ
やること
Windowsの壁紙をC#スクリプトで好きに書き換えたい。
「壁紙を一回だけbitmap変数のデータに書き換える」ことをやりました。
参考にしたもの
Natsunekoさんのgit
Wallpapper Engineみたいなのを作りたい その1
Windows API/画像の操作
dll呼び出しのエラー対処法
BitBlt WindowsAPIを用いて画面にビットマップ画像を描画する (C#プログラミング)
動機
稲葉曇さんのMVを見て、ぬくぬくにぎりめしさんの、かわいくうごめく絵に出合いました。とても好きになったので、この絵を自分のPCのなかで自由に動かしたいと思いました。
勉強ログ(身のない話。飛ばしてください)
みたい?
壁紙への描写をする関数が作れたら、あとは自由に実装できそうだなあ「C# Wallpaper Engine」とかで調べてるとNatsunekoさんのgithubを見つけたので、ここから壁紙への描写方法を調べよう。このDrawDesktopの中にそれが含まれてそうだけど… >![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286137/92bc9da5-e6c5-036d-4722-2277fb8c597c.png)dll産の関数が並んでて全然わからん…というかbitmap渡してねーじゃん!!(誤解) なんかウィンドウ作って描画して、そこの色を描画してそう?何らかの画像渡せた方が直感的だし、違う資料探そう…user32って確か本来c++で使える関数だったような。c++の資料探してみよう。お、ドンピシャの記事が。読んでみるか。
ふーん(流し読み)
完成まで飛ばしてみるか。
…
第二回まででエタってるー!!
第二回は描画したいものの説明だったので読む必要はなさそう。第一回の描画に関する部分読んでみるか。ふむふむ。レクトアングルだから四角形の描写だけか。レクトアングル(desk_hdc, なんとかかんとか) ってことは、desk_hdcに対して何かをすれば描写されるのか。desk_hdcとはいったい何…?
HDCという型らしいので「c++ HCD」で検索。
見つけたサイトに見たことあるコードを発見!!
どうやらHDCというのは画面描写につかうクラスであり、Natsunekoさんのコードでは「表示したい画像HDC」を「壁紙HDC」に転写して、壁紙への描写をしているようであることが分かった。
壁紙のHDCを取得しオリジナルのHDCを転写するシステムはNatsunekoさんのコードを参考にできるので、あとはオリジナルのHDCを作成できれば解決?
よくわからないけど、ハンドルというものを開放しないと前のメモリが残っていてまずいらしい。前のメモリが残っているというのは最適化に使えるかもしれない??
wikibooksにはpingやjpeg画像を重ねるやり方が書いてあったけど、ピクセル単位の編集もしたいなあ… 冒頭に書いてあったSetPixel調べてみるか。満を持して公式ドキュメントの登場。
そしておそらくハンドルというものがHDCを取得する上で重要。Natsunekoさんのコードで重要そうな部分を抜粋して、ハンドルの扱いに関して勉強します。
「hWndSrc」の内容をデスクトップに描画する処理の部分です。
var workerw = IntPtr.Zero; User32.EnumWindows((hwnd, lParam) => { var shell = User32.FindWindowEx(hwnd, IntPtr.Zero, "SHELLDLL_DefView", >null); if (shell != IntPtr.Zero)workerw = User32.FindWindowEx(IntPtr.Zero, hwnd, >"WorkerW", null); return true; }, IntPtr.Zero); var hWndDest = workerw; var hdcDest = User32.GetDCEx(workerw, IntPtr.Zero, 0x403); GDI32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, GDI32.SRCCOPY); User32.ReleaseDC(hWndSrc, hdcSrc); User32.ReleaseDC(hWndDest, hdcDest);
下から見ていきます(ReleaseDCはいったん放置)。下から三行目のBitBlt関数では、HDCの「hdcDest」に対し、(0,0)から(width,height)の範囲でHDCの「hdcSrc」を転写しています。ここからhdcDestを描写の目標にすればよいとわかりました。
hdcDestはどうやって取得するのか。下から4行目のGetDCExによって取得されています。GetDCExはHDCを取ってくるウィンドウのハンドルを引数にとるらしいので、workerwがそれです。
workerwはどうやって取得するのか。冒頭でworkerwを宣言した後、workerwに中身を入れるための呪文が書いてあります。見た感じで内容を想像すると、「EnumWindows」関数で列挙された何かに対し、ラムダ式の処理を行い、ラムダ式は列挙されたパラメーターに対し、"SHELLDLL_Defview"を探す。見つけられたら、"WorkerW"を取得している("SHELLDLL_Defview"と"WorkerW"は連番だから?)といった感じでしょうか。呪文の中に出てくる他の変数hwnd, lParamなどはラムダ式の引数名であり、末端のローカル変数名なので遡るのはここまでです。
あとはReleaseDCについてですね。Src関係は自分で用意するので最後の行だけが重要になります。ReleaseDC(ハンドル,ハンドルから取ったHDC)という使い方のようですが、どういう効果がある関数なのでしょうか。こちらのサイトによると「デバイスコンテキストを解放し、他のアプリケーションからつかえるようにする。」らしいです。絶対やらないと…!!
大体のここまでで、作るべきシステムが見えてきましたね。
//イメージ
var workerw = IntPtr.Zero;
User32.EnumWindows((hwnd, lParam) =>
{
var shell = User32.FindWindowEx(hwnd, IntPtr.Zero, "SHELLDLL_DefView", null);
if (shell != IntPtr.Zero)workerw = User32.FindWindowEx(IntPtr.Zero, hwnd, "WorkerW", null);
return true;
}, IntPtr.Zero);
var hdcDest = User32.GetDCEx(workerw, IntPtr.Zero, 0x403);
//Win32 / GDI / Wingdi.h /SetPixel function (wingdi.h)等で書き込み
User32.ReleaseDC(workerw, hdcDest);
SetPixcelはどのdllに入っているのか。Natsunekoさんが使っていたgdi32.dllに一緒に入っていると予想をつけた。試しにBltBltを公式ドキュメントで見てみるとおなじ階層にあった。つまり同じような感じでインポートすればよい。引数は… COLORREF 型の引数ってc#側でなんて宣言するんだ?
「c# COLORREF」で検索。こちらのページがヒット。構造体のメモリの使い方が一緒なら何でもいいみたーい(5)。コピペさせてもらいます。
そして書いてみたのがこちら。WindowsFormで書いてみました。
[StructLayout(LayoutKind.Sequential)]
public struct COLORREF
{
public uint ColorDWORD;
public COLORREF(System.Drawing.Color color)
{
ColorDWORD = (uint)color.R + (((uint)color.G) << 8) + (((uint)color.B) << 16);
}
public COLORREF(int r,int g, int b)
{
ColorDWORD = (uint)r + (((uint)g) << 8) + (((uint)b) << 16);
}
public System.Drawing.Color GetColor()
{
return System.Drawing.Color.FromArgb((int)(0x000000FFU & ColorDWORD),
(int)(0x0000FF00U & ColorDWORD) >> 8, (int)(0x00FF0000U & ColorDWORD) >> 16);
}
public void SetColor(System.Drawing.Color color)
{
ColorDWORD = (uint)color.R + (((uint)color.G) << 8) + (((uint)color.B) << 16);
}
}
public partial class Form1 : Form
{
public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("gdi32.dll")]
public static extern COLORREF SetPixel(IntPtr hdc, int x, int y, COLORREF color);
[DllImport("user32.dll")]
public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
[DllImport("user32.dll")]
public static extern IntPtr GetDCEx(IntPtr hWnd, IntPtr hrgnClip, ulong flags);
[DllImport("user32.dll")]
public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
var workerw = IntPtr.Zero;
EnumWindows((hwnd, lParam) =>
{
var shell = FindWindowEx(hwnd, IntPtr.Zero, "SHELLDLL_DefView", null);
if (shell != IntPtr.Zero) workerw = FindWindowEx(IntPtr.Zero, hwnd, "WorkerW", null);
return true;
}, IntPtr.Zero);
var hdcDest = GetDCEx(workerw, IntPtr.Zero, 0x403);
for(int ix = 0; ix < 1920; ix++)
{
for(int iy = 0; iy < 1080; iy++)
{
int col= (int)((ix + iy) / (1920 + 1080) * 255);
COLORREF color = new COLORREF(col,col,col);
SetPixel(hdcDest, ix, iy, color);
}
}
ReleaseDC(workerw, hdcDest);
}
}
エラーの原因は引数が違ったことだった。よくわかんないけど
[DllImport("user32.dll")]
//public static extern IntPtr GetDCEx(IntPtr hWnd, IntPtr hrgnClip, ulong flags);
//の最後の引数の型をuintに変更して
public static extern IntPtr GetDCEx(IntPtr hWnd, IntPtr hrgnClip, uint flags);
これでビルドエラーが消え、いざ実行。
…
左からゆーーっくり壁紙が真っ黒になっていく。その間ウィンドウは反応なし。つまり処理がくssssっそ重いってことだ。多分割り算。もしかしたらSetPixcelのたびに再描画が入ってる。まず前者の修正から。colの計算を無しにしてみた。
for(int ix = 0; ix < 1920; ix++)
{
for(int iy = 0; iy < 1080; iy++)
{
int col= 100;
COLORREF color = new COLORREF(col,col,col);
SetPixel(hdcDest, ix, iy, color);
}
}
実行!!変わらず!!
あーこれSetPixelのたびに描画してますわ…
動画みたいに一瞬で変わってほしいんだよなあ…。先人に倣い、編集用のHDCを転写してみるか。ん…?Natsunekoさんのコードのここはもしや…
var hdc = User32.GetDCEx(workerw, IntPtr.Zero, 0x403);
using (var graph = Graphics.FromHdc(hdc))
graph.DrawImage(_bitmap, new PointF(0, 0));
User32.ReleaseDC(workerw, hdc);
bitmapを描画してるー!
ずいぶん遠回りしたが、最初から答えはそこにあった…さっそくコードに。
Bitmap bitmap = new Bitmap(1920, 1080);
for(int ix = 0; ix < 1920; ix++)
{
for(int iy = 0; iy < 1080; iy++)
{
int c = (int)((ix + iy)*255 / (1920 + 1080));
Color col = Color.FromArgb(c,c,c);
bitmap.SetPixel(ix, iy, col);
}
}
//bitmap.Save("test.jpg", System.Drawing.Imaging.ImageFormat.Jpeg );
using (var graph = Graphics.FromHdc(hdcDest))
graph.DrawImage(bitmap, new PointF(0, 0));
なぜか画面の下半分以降描写されない(;_;) これの下側にSetPixelによる描写のバージョンをくっつけたら画面下まで描写されるので範囲指定、対象は問題ないのですがね… (画像は、ラグトレインの壁紙の上に、画像の左側の黒いのがSetPixcelによる描写で右側のグラデーションがbitmapによる描写。)
やはり転写用のHDCを作成するしかないか…?調べてみると「SetPixel(),GetPixel()は低速なことで有名」らしいです。
SelectObjectという関数をすることで画面下半分が描写されないバグは治りました。ついでにグラフィックを使うより早そうなので、BitBltを使って書き直してみました。
やり方
「壁紙を一回だけbitmap変数のデータに書き換える」最小コードです。実行するためにdllのインポートで同じくらい行数が必要になります。そちらはgitで見てください。
//壁紙は大きいし、あらかじめ用意
Bitmap bitmap = new Bitmap(1920, 1080);
//windows formのボタンを使ってみた
private void button1_Click(object sender, EventArgs e)
{
/*
お絵かき用のデバイスコンテキスト(DC)を作成し、SelectObjectでbitmapを適用。
これでbitmapの変更がお絵かきDCに反映される。適用しないとちょっとだけバグる
*/
IntPtr dc_canvas = CreateCompatibleDC(dc_wall);
SelectObject(dc_canvas, bitmap.GetHbitmap());
/*
壁紙のハンドルを取得する。(参照みたいなイメージ?)
PC上にあるwindow全てに対して匿名関数を実行。
匿名関数はSHELLウィンドウを探して、見つけたら壁紙ハンドルを取得。
多分SHELLの次とかに壁紙があるからだと思う。
*/
IntPtr handle_wall = IntPtr.Zero;
EnumWindows((hwnd, lParam) =>
{
IntPtr shell = FindWindowEx(hwnd, IntPtr.Zero, "SHELLDLL_DefView", null);
if (shell != IntPtr.Zero) handle_wall = FindWindowEx(IntPtr.Zero, hwnd, "WorkerW", null);
return true;
}, IntPtr.Zero);
/*
壁紙のハンドルから壁紙のDCを取得。
壁紙DCにお絵かきDCの内容を転写する
*/
IntPtr dc_wall = GetDCEx(handle_wall, IntPtr.Zero, 0x403);
BitBlt(dc_wall, 0, 0, bitmap.Width, bitmap.Height, dc_canvas, 0, 0, 0x00CC0020);
//解放
DeleteDC(dc_canvas);
ReleaseDC(handle_wall, dc_wall);
}
気になる点
Graphicsによる描写とBitBltによる描写のパフォーマンス比較
dc_canvas, dc_wall, handle_wallはいつまで保持してもよいのか。右に行くにつれて権限が大きそうなので早く解放しないといけないかも?
まとめ
これでC#コードから自由に壁紙へ描写できるようになりました。デバイスコンテキスト, グラフィック, dll, c++への理解も深まりました。wallpaper engineのように動画やインタラクションがやりたいので、次回からはそれらのシステム構築や、パフォーマンスの向上を検討します。