LoginSignup
1
1

More than 1 year has passed since last update.

C# windows FormでWallpaper Engineのまねごとをしたい ~2~

Last updated at Posted at 2022-05-27

こっち読んだ方がいいよ

こっち読んだ方がいいよ

スクリプトで壁紙に動きをつける

やったこと

壁紙を動画のように動かした

システム構想

Wallpaper Engineをもっかい作るんじゃなくて独自性が欲しい。任意の動画再生じゃなくて、アニメをプログラムに書き込んだものを再生する形にすれば超軽パフォーマンスのアプリケーションに?(動画再生ってそもそもどうやるの?)

プログラムを使ったアート作品という方向性でいこう!!
ぬくぬくにぎりめしさんの、このかわいいアニメを再生して、クリックした場所にピコピコハンマーを出したい!

参考にしたもの

Natsunekoさんのgit
ぬくぬくにぎりめしさんの作品
Natunekoさんの記事
Draw Behind Desktop Icons in Windows 8+

開発ログ(とばしてOK)

今回はちょっと面白いかも? とりあえずLoop処理をしないといけない。Natunekoさんのプログラムにあったのを拝借させてもらおう。コピペプログラマー爆誕!!
private void DrawLoop()
{
           var nextFrame = (double) Environment.TickCount;
           var period = 1000f / 30f;
           while (_isDraw)
           {
               var tick = (double) Environment.TickCount;
               if (tick < nextFrame)
               {
                   if (nextFrame - tick > 1)
                       Thread.Sleep((int) (nextFrame - tick));
                   continue;
               }

               if (Environment.TickCount >= nextFrame + period)
               {
                  nextFrame += period;
                  continue;
               }
              DrawDesktop(_hwnd);
              nextFrame += period;
           }

無限ループなのでWindowsFormでやるなら別スレッドに飛ばした方がよいですかね。面倒なので元の画像に戻す処理は省いてしまいましょう。各自でやってください。

調べもの

ぬくぬくにぎりめしさんのアニメ描画したいので、フレームレートや絵の枚数を調べましょう。もっとスマートな方法があるかもしれませんが、力業で行きます。

1. レイニーブーツの該当部分をキャプチャする

GeForceの機能でキャプチャしました。実際の情報と差があるかもしれませんが、大体同じに見えたらいいので気にしないで行きましょう。

2. Aviutlでコマ送りしてみる

フレームレートと何枚動かさないといけないのか調べよう。コマ送り4ポチ(0.066秒, 2/30秒)で一枚動いてますね。
image.png

まずい、女の子の絵がループしてるのかわからない。となりの鳥のループと周期が同じか調べよう。鳥は6枚(12/30秒)でループ。女の子は膝関節当たりの線で調べられそう。…わかんない。まあ普通に鳥と同じと考えていいでしょ。つまり0.4秒に一枚画像を更新すればよいね。

書き込む画像データの用意

ぬくぬくにぎりめしさんのアニメに惚れた理由の一つに、白黒であるという点が挙げられます。白黒(rgb 178,178,178|9,9,9)ということはTFの二種類でデータ化できるということ。こういう工夫が僕は大好きなのです。ついでに鳥と女の子の領域だけ抜き出してさらにコンパクト化しましょう。

1. 動画のスクショをmedibangで切り抜く

1920x1080で貼り付け、キャンパスサイズ変更(中央, 800x800)で切り抜いてpingで保存
image.png

2. 出力したping画像を二値化してbyte配列にする

800x800個の01なので、80000の要素になりますね。これ流石に外部ファイルに記述しないとエディタが大変なことになりそう…

public static void BitConvert(string folderPath)
        {
            BinaryWriter bw = new BinaryWriter(new FileStream(folderPath+"/ImageBin.bin", FileMode.OpenOrCreate));
            int sikii = 100;
            for (int num = 0; num < 6; num++)
            {
                Bitmap bitmap = new Bitmap(folderPath + "/" + num + ".png");
                for (int i = 0; i < 640000; i++)
                {
                    bw.Write(bitmap.GetPixel(i % 800, i / 800).R > sikii);
                }
            }
            bw.Close();
        }
        public static Bitmap[] LoadBin(string folderPath)
        {
            Bitmap[] res = new Bitmap[6];
            BinaryReader br = new BinaryReader(new FileStream(folderPath + "/ImageBin.bin", FileMode.Open));
            Color wh = Color.FromArgb(255, 178, 178, 178);
            Color bl = Color.FromArgb(255, 9, 9, 9);
            for(int iimg = 0; iimg < 6; iimg++)
            {
                Bitmap bitmap = new Bitmap(800, 800);
                for (int i = 0; i < 640000; i++)
                {
                    if (br.Read() > 0) bitmap.SetPixel(i % 800, i / 800, wh);
                    else bitmap.SetPixel(i % 800, i / 800, bl);
                }
                res[iimg] = bitmap;
            }
            br.Close();
            return res;
        }

これで多分変換できる??エラーはないので多分できた。

bool isDraw = false;
        Bitmap[] animes = new Bitmap[6];
        private void button3_Click(object sender, EventArgs e)
        {
            isDraw = true;
            Task task = Task.Run(() => {
                DrawLoop();
            });
        }
        private void DrawLoop()
        {
            animes = ImageToBytes.LoadRainyBootsAnime("C:/Users/utyuy/Pictures/レイニーブーツWallプロジェクト");
            int nextFrame = Environment.TickCount;
            int period = 66;
            while (isDraw)
            {
                int tick = Environment.TickCount;
                if (tick < nextFrame)
                {
                    if (nextFrame - tick > 1)
                        Thread.Sleep(nextFrame - tick);
                    continue;
                }

                if (Environment.TickCount >= nextFrame + period)
                {
                    nextFrame += period;
                    continue;
                }
                DrawDesktop();
                nextFrame += period;
            }
        }
        void DrawDesktop()
        {
            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);

            IntPtr dc_wall = GetDCEx(handle_wall, IntPtr.Zero, 0x403);
            IntPtr dc_canvas = CreateCompatibleDC(dc_wall);
            SelectObject(dc_canvas, animes[Environment.TickCount%6].GetHbitmap());
            BitBlt(dc_wall, 0, 0, animes[Environment.TickCount % 6].Width, animes[Environment.TickCount % 6].Height, dc_canvas, 0, 0, 0x00CC0020);

            DeleteDC(dc_canvas);
            ReleaseDC(handle_wall, dc_wall);
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            isDraw = false;
        }

これを実行してみると、常に最前面にアニメが出る事態に… DCの対象を変える必要がありそうだけど、とりあえず変数達のライフスパンを見直してみよう。うーん変わらん。

Wallpapper Engineみたいなのを作りたい その1のspy++を試してみよう。どこに描写しようかなー() とりあえずshell?

試行錯誤して気づいたことが。これ壁紙ハンドルの取得が全部、デフォルト値になってる。ちゃんと取得できればいけそうかも?spy++で調べたハンドルを直接プログラムに記述していくと、SysHeader32に描写しても何も映らない。その上のSysListView32に描写するとアイコンの上に描写。つまりこの二つの間に差し込めれば目標達成?

問題点が分かった。壁紙への描写は一度だけなら大丈夫だが、動画のような連続描写をすると、アイコンの上に描写されてしまう。アイコンの描写は最も低いレベルの、「壁紙とアイコンのウィンドウ」部分にある。これはおそらく、「壁紙を起動時に一度だけ描写して、アイコンを継続的に描写する」という処理を行っている。それらは基本的に不可分なので、動画などの継続的に変化する処理をアイコンと壁紙の間に描写することはWindowsの仕様から無理になっている。ではWallpaperEngineやNatunekoさんはどうやっているのか。Natunekoさんの記事と、そこで紹介されている英語の記事に答えがあった。

実装

壁紙を変更する際に、アイコンの描写と壁紙の描写の間に入り込めるウィンドウが生成され、そこのハンドルを取得してDCに書き込むことで、動く壁紙が実装できるようだ。そしてそのウィンドウを生成するのに一度「壁紙かわったよ」メッセージを発行するとよいらしい。すごいごり押し。WallpaperEnginの挙動を見てみると、前述した「壁紙とアイコンのウィンドウ」周辺のウィンドウ間の親子関係がめちゃめちゃに書き換わっていたので、WallpaperEngineの実装方法も原理は同じであると予想される。

        IntPtr[] animes = new IntPtr[6];
        public Form1()
        {
            InitializeComponent();
            var progman = FindWindow("Progman", null);
            SendMessageTimeout(progman, 0x52C, new IntPtr(0), IntPtr.Zero, 0x0, 1000, out var result);
            Bitmap[] _anime = ImageToBytes.LoadRainyBootsAnime(Directory.GetCurrentDirectory() + "/image");
            for(int i = 0; i < 6; i++)animes[i] = _anime[i].GetHbitmap();
        }
        
        private void DrawLoop()
        {
            int nextFrame = Environment.TickCount;
            int period = 66;
            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);

            IntPtr dc_wall = GetDCEx(handle_wall, IntPtr.Zero, 0x403);
            IntPtr dc_canvas = CreateCompatibleDC(dc_wall);

            while (isDraw)
            {
                int tick = Environment.TickCount;
                if (tick < nextFrame)
                {
                    if (nextFrame - tick > 1)
                        Thread.Sleep(nextFrame - tick);
                    continue;
                }

                if (Environment.TickCount >= nextFrame + period)
                {
                    nextFrame += period;
                    continue;
                }
                
                SelectObject(dc_canvas, animes[Environment.TickCount%6]);
                BitBlt(dc_wall, 0, 0, 800, 800, dc_canvas, 0, 0, 0x00CC0020);

                nextFrame += period;
            }
            DeleteDC(dc_canvas);
            ReleaseDC(handle_wall, dc_wall);
        }

Videotogif.gif

はい。動きました。疲れたので今回はこれで終わりです。今までの奴をまとめた記事を出したり出さなかったりする予定です。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1