2019年9月29日日曜日

【プロセス管理ツール】その0 制作の目的や予定など

次に作りたいものが決まりました。
「プロセス管理ツール」です。
対象はWindowsです。

プロセス管理ツールについて

有名なプロセス管理ツールには、タスクマネージャやProcess Explorer[1]があります。
そんなものをイメージしています。

制作の目的

  1. Windowsの内部動作に関する洞察を得ること
  2. プロセス管理に関連するAPI関数を知ること
  3. 普段使いやデバッグに役に立つ設計を考えること
     
説明すると
  1. Windowsの内部動作に関する洞察を得ること
    以前の投稿で「モジュール、プロセス、スレッド」を知りたいと書きました[2]。
    ついでに「CPU」、「メモリ」、「ディスク」といった、名前は知っているけど、実際良く分かっていないものについて知っていきたいと思います。
     
  2. プロセス管理に関連するAPI関数を知ること
    軽くググったところ、「CPU」に関しては具体的な関数が用意されているようです[3]。
    おそらく、メモリやディスクに関してもAPI関数があるのではないでしょうか?
    そのような関数を調べ、実装のテンプレを自分なりにストックしたいです。
     
  3. 普段使いやデバッグに役に立つ設計を考えること
    せっかく作っても、使い勝手が悪くて使われないのは寂しいですよね。
    タスクマネージャやProcess Explorerの代わりに使える軽いソフトを目指します。
    自分で実装しておけば、後に手を加えて、ログ取得ツールなども作れるでしょう。
    そうなれば、デバッグに役立つでしょう。

今後の予定

だいたい、以下のような流れで進めていきたいと思います。
今年度中に完成すればいいな。
  1. 【プロセス管理ツール】その1 Process Explorerについて【調査】
  2. 【プロセス管理ツール】その2 タスクマネージャについて【調査】
  3. 【プロセス管理ツール】その3 「CPU」を測る【調査】
  4. 【プロセス管理ツール】その4 「メモリ」を測る【調査】
  5. 【プロセス管理ツール】その5 「ディスク」を測る【調査】
  6. 【プロセス管理ツール】その6 プロセスを列挙する【調査】
  7. 【プロセス管理ツール】その7 プロセス情報の一覧を取得する【調査】
  8. 【プロセス管理ツール】その8 全体の要求定義・仕様決定【SE】
  9. 【プロセス管理ツール】その9 部分の要求定義・仕様決定【SE】
  10. 【プロセス管理ツール】その10 設計【SE】
  11. 【プロセス管理ツール】その11 部分の実装・テスト【実装・テスト】
  12. 【プロセス管理ツール】その12 統合・テスト【実装・テスト】
  13. 【プロセス管理ツール】その13 リリース【総括】

参考文献


  1. Windows Sysinternals
  2. キャプチャソフト開発 最終回 統合「ScreenCaptureTool.exe」
  3. CPU使用率の計測

2019年9月26日木曜日

デバッグ情報の扱い~PDBファイルについて~

PDBファイルとデバッグ情報の関係などについて調べたことを整理した。
また、EXEをターゲットとした際に、(1) /Z7 でデバッグ用にビルドする場合、(2) /Zi でデバッグ用にビルドする場合、(3)/Zi と /O1 でリリース用にビルドする場合について、作成されるファイルとファイルサイズがどうなるかを比較した。

知識

コンパイラPDBとリンカPDB
PDBファイルには、コンパイラにより作成されるものとリンカにより作成されるものがある[1]。
前者を「コンパイラPDB」、後者を「リンカPDB」と呼び区別することにする。
 
<コンパイラPDB>
  • デフォルトの名前は、プロジェクト内でコンパイルされれば"プロジェクト名.pdb"、プロジェクト外でコンパイルされれば"vcx0.pdb"となる(xはVisual Studioのバージョン)
  • このPDBへの名前とパスがOBJファイルに含まれる

<リンカPDB>
  • デフォルトの名前は"$(TARGET).pdb"である
  • このPDBへの名前とパスがEXEファイルとDLLファイルに含まれる
  • 完全なデバッグ情報を含み、デバッグの際に必要となる

コンパイラのデバッグ情報出力の設定
コンパイラでデバッグ情報を出力する方法は複数存在する[2]。

<デバッグ情報をOBJファイルに含める場合>
  • コンパイラオプション /Z7 を指定することで、デバッグシンボルのすべてがOBJファイルに含まれるようになる
  • コンパイラPDBは作成されないが、OBJファイルがその分大きくなる
  • デバッグシンボルには、関数名と行数、変数の名前と型の情報がすべて含まれる

<デバッグ情報をコンパイラPDBに含める場合>
  • コンパイラオプションに /Zi もしくは /ZI を指定することで、デバッグシンボルをコンパイラPDBとしてOBJファイルと分けて作成することができる。その分、OBJファイルは /Z7 を指定した場合よりも小さくなる
  • /Zi を指定すると、最適化オプション(/OPT)のデフォルト値が変更される。
    (イメージのサイズが大きくなり、速度が遅くなってしまう)
  • /ZI は /Zi と同様にコンパイラPDBを出力するが、Eidt and Continueをサポートするという点で異なる

<デバッグ情報が必要ない場合>
  • /Z{7|i|I}を指定しなければよい。

リンカのデバッグ情報出力の設定
  • リンカオプション /DEBUG を指定してリンクすることで、デバッグシンボルを含むリンカPDBを出力することができる
  • リンカPDBの名前とパスがターゲット(EXEもしくはDLL)に含められる。別の見方をすれば、ターゲット自体にデバッグ情報が埋め込まれることはないということである
  • デバッグ時には、このリンカPDBが必要である


デバッグ時とリリース時

デバッグオプションと最適化オプションは、関連性はあるが同じものではない。
リリースのために最適化されたバイナリを作る場合も、デバッグに必要なPDBファイルを作成することができる。

開発中からデバッグ情報をPDBファイルに出力するようにしておき、リリース時には最適化オプションを付け加える[3]のがよいだろう。

テスト

(1) /Z7 でデバッグ用にビルドする場合、(2) /Zi でデバッグ用にビルドする場合、(3)/Zi と /O1 でリリース用にビルドする場合について、作成されるファイルとファイルサイズがどうなるかを確かめてみた。

実行環境
OS 名: Microsoft Windows 8.1
システム モデル: dynabook KIRA V73/PS
システムの種類: x64-based PC
プロセッサ: Intel64 Family 6 Model 61 Stepping 4 GenuineIntel ~2200 Mhz
物理メモリの合計: 8,103 MB

Visual Studioのバージョン: 14
コンパイラ: Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86

テストコード(共通)

main.cc

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <windows.h>
  4. #include "./sub.h"
  5.  
  6. namespace {
  7. constexpr wchar_t WINDOW_NAME[] = L"MyWindow";
  8. constexpr wchar_t CLASS_NAME[] = L"MyWindowClass";
  9. } // namespace
  10.  
  11. int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
  12. LPTSTR lpsCmdLine, int nCmdShow) {
  13. (void)hInstance;
  14. (void)hPrevInstance;
  15. (void)lpsCmdLine;
  16. (void)nCmdShow;
  17.  
  18. FILE* fp = nullptr;
  19. AllocConsole();
  20. _wfreopen_s(&fp, L"CONOUT$", L"w", stdout);
  21. _wfreopen_s(&fp, L"CONOUT$", L"w", stderr);
  22. _wfreopen_s(&fp, L"CONIN$", L"r", stdin);
  23.  
  24. // Use C++ features.
  25. PrintVector();
  26. system("pause");
  27.  
  28. FreeConsole();
  29. return 0;
  30. }


sub.cc
  1. #include "./sub.h"
  2. #include <stdio.h>
  3. #include <vector>
  4.  
  5. void PrintVector() {
  6. std::vector<int> array(3);
  7. array[0] = 0;
  8. array[1] = 1;
  9. array[2] = 2;
  10. for (unsigned int i = 0; i < array.size(); i++) {
  11. fwprintf(stdout, L"array[%d] = %d\n", i, array[i]);
  12. }
  13. }

sub.h

  1. #ifndef _SUB_H_
  2. #define _SUB_H_
  3.  
  4. void PrintVector();
  5.  
  6. #endif // _SUB_H_


CPPFLAGS およびLFLAGS の設定(makefileより抜粋)
nmakeのためのmakefileではCPPFLAGSにコンパイラオプションを、LFLAGSにリンカオプションを組み立てている。

(1) /Z7 でデバッグ用にビルドする場合
  1. CPPFLAGS = /nologo /W4 /EHsc /DUNICODE /D_UNICODE /Z7
  2. LFLAGS = /NOLOGO /SUBSYSTEM:WINDOWS /MAP:$(MAP) /PDB:$(PDB)

(2) /Zi でデバッグ用にビルドする場合
  1. CPPFLAGS = /nologo /W4 /EHsc /DUNICODE /D_UNICODE /Zi
  2. LFLAGS = /NOLOGO /SUBSYSTEM:WINDOWS /MAP:$(MAP) /PDB:$(PDB)

(3)/Zi と /O1 でリリース用にビルドする場合
  1. CPPFLAGS = /nologo /W4 /EHsc /DUNICODE /D_UNICODE /Zi /O1
  2. LFLAGS = /NOLOGO /SUBSYSTEM:WINDOWS /DEBUG /MAP:$(MAP) /PDB:$(PDB)

なお、
  1. MAP = main.map
  2. PDB = main.pdb
である。

結果と考察

それぞれの場合におけるファイルサイズを下表に示す(サイズの単位:Bytes)。

main.obj sub.obj vc140.pdb main.pdb main.exe
(1) /Z7 でデバッグ用にビルドする場合 92064 150624 N/A 6180864 441344
(2) /Zi でデバッグ用にビルドする場合 18224 50324 233472 6180864 441344
(3) /Zi と /O1 でリリース用にビルドする場合 19305 52899 233472 6115328 425984


  • /Z7 を付けた場合、コンパイラPDBである vc140.pdb が作成されず、/Zi を付けた場合よりもOBJファイルが3~5倍ほど大きくなった。
  • /Z7、/Zi でOBJファイルのサイズが違っても、最終的に作成されるリンカPDBのサイズは同じになった。
  • /O1 オプションを付けてファイルサイズを最適化すると、EXEのサイズがちょっとだけ小さくなった。また、リンカPDBのサイズも小さくなるった。


まとめ


  • PDBファイルには、コンパイラにより作成されるものとリンカにより作成されるものがある。
  • デバッグ情報をオブジェクトファイルに含めた場合と、コンパイラPDBに含めた場合とで、リンカPDBのサイズに違いはなかった
  • リリース時には、/Z{7|i|I} に加えて最適化オプションを指定するのがよい


感想

PDBファイルについては「なんとなく」でやっていた所があったので、オプションなどと合わせて整理できてよかった。

なんとなく設定している系のオプションは、/MT などほかにもあるので、適宜調べていきたい。

今回はEXEファイルがターゲットの場合でテストをしたが、ライブラリファイル(*.lib)がターゲットの場合では、PDBの扱いに違いがあると思われる。
ライブラリファイルを扱う必要が生じた際に調べたい。

GitHub

今回のコードの最新版はここに上げておく。

参考文献


  1. PDB Files – What are they and how to generate them.
  2. /Z7, /Zi, /ZI (Debug Information Format)
  3. よく使うコンパイル・リンカオプション


2019年9月22日日曜日

ひと段落した

キャプチャソフト開発 最終回 統合「ScreenCaptureTool.exe」

開発ブログを付けることを思い立って、やってみたら、できた。

誰も見ていないかもしれない。
けれど、「ブログを更新しなければならない」という意識は常に頭の片隅にあった
そのおかげで、空白期間が挟まってもソフトを形にすることができた。

開発ブログが林立する理由が分かった気がする。

今回はキャプチャソフトを作ったが、次は何を作ろうか。
ファイラーや、タスクマネージャーなんて面白いかも。
また次のシリーズにて。

キャプチャソフト開発 最終回 統合「ScreenCaptureTool.exe」

これまで6回にわたり、キャプチャソフトの開発日記を付けてきました。


これらの要素技術を統合し、スクリーンキャプチャソフトを完成させました。
その名も「ScreenCaptureTool.exe」です。
完成版したコードはここに上げてあります。

バイナリ配布はしません。
不親切ですが、気になる方は自分でビルドしてみてください。

ソフト概要(readme.txtより)

・デスクトップ全画面をキャプチャし、PNGファイルに保存
・保存の過程でクリップボードを介さない
・タスクトレイ常駐型
・PauseやScrLkキーを押すとキャプチャを実行
・出力先フォルダはデフォルトでは実行可能ファイルがあるフォルダ、変更が可能
・ファイル名は日時から自動的に決定


感想

開発ブログを付けながらのソフト制作は今回が初めてでした。
7月から初めて、9月末までかかってしまいました。

途中で開発を離れることがありました。
しかし、「ブログを更新しなきゃいけない」という意識があったので最後までやり遂げることができました。

ブログをやってみて良かったなと思います。

今後の課題

①実装面の課題
Windows プログラミングについて、分からない部分、疑問の残る部分があります。
後のためにメモをしておくと、

  • アイテムIDリスト、ファイルシステム
  • グローバルフック
  • モジュール、プロセス、スレッド
  • デスクトップ、シェル、カーネルの役割分担

です。
他にもあったかも。

今後は、これらを学習できるようなソフトウェアを開発したいと思っています。
例えば、エクスプローラやタスクマネージャーの代替ソフトウェアなどでしょうか。
私は「創作」よりも「開発」の方が得意だということが最近になって分かってきたので、ゲームより実用的ソフトウェアを開発していきたいです。

②開発手法の課題
今回は、ソフトの規模が小さいため、適当に済ませても何とかなってしまいました。
以下の点は変更の必要があると思います。

  • 要素ごとのデバッグ:臨機応変に → 一定の方法論
  • システム全体の設計:ステートマシン図もどきが一枚 →モデリング手法を齧る
  • パラダイム:手続き型 → オブジェクト指向、デザインパターンの活用
  • コーディング規則:おおよそGoogle → 細かい箇所にも注意する
  • 配布形態:不親切 → 親切
イメージとしては、「おままごと」を「実家の手伝い」にする感じです。
「家業を継ぐ」段階はまだまだ遠いです。

③情報発信の課題
勉強がメインの開発ブログというだけあって、どうしてもキュレーション色が強く出てしまいます。
「覚書」という言い訳がありますが、程度の問題こそあれ、ブログは情報発信の場です。
発信する情報に新たな価値を持たせるには、自分で課題を見つけ出して問題解決に向けて実装・試験をする必要があるのではないかと思うようになりました。

問題解決には正しい情報が必要で、正しい情報を得るには公式を当たるのが一番です。
参考文献として個人のHPやブログを当たることも多かったですが、今後は公式の情報の比重を上げていきたいと思います。

④体裁の課題
このブログの見やすさに関する課題です。
あまりに散文的だと趣旨が拡散してしまうし、研究論文みたいに堅苦しくもしたくはありません。
ポイントを三点ぐらいに絞って、文献調査で分かったこと、実装の中でわかったことなどを、わかりやすく伝えられるような構成にしたいですね。
試行錯誤していきたい思います。

2019年9月21日土曜日

キャプチャソフト開発 その6 画面キャプチャ

画面キャプチャについて研究した際の覚書。

TL;DR

画面を保存する以下の関数[1]を使ってみた。


PaintDesktopとPrintWindowはWindows 7とWindows 8.1で動作に違いがあった。
BitBltはともに同じ結果が得られたため、スクリーンキャプチャに採用する関数にはBitBltが適していると考えられる。

事前知識

かなり強引に要約した。

Q. 画面って何?
A. スクリーン、すなわちウィンドウを含むデスクトップ

「デスクトップ」と「スクリーン」を区別しよう[2]。
「デスクトップ」は壁紙やアイコンから成る画面であり、「スクリーン」はそれらに表示されているすべてのウィンドウを加えたもの。
今回作ろうとしているキャプチャソフトは、ウィンドウを含む画面全体なので、「スクリーン」を対象とすることになる。

Q. 「デスクトップ」と「シェル」の違いは?
A. 「デスクトップ」はトップレベルウインドウの親、「シェル」はUIを提供する

「デスクトップ」ウィンドウは、すべてのトップレベルウインドウの親である[3]。
一方で、「シェル」はログオン時にUIを提供する機能を持つものである[4]。
デフォルトのシェルには「explorer.exe」が指定されている。

Q. OSのバージョンによりシェルの機能が異なる?
A. Windows 7と8.1以降ではシェルの機能が変わっている

Windows 7では、壁紙の描画をカーネルが行い、デスクトップ上のアイコンやそのキャプション、タスクバーといったものをexplorer.exeが行っていた。
しかし、Windows 8以降では壁紙の描画はexplorer.exeが行うようになった[4]。
このことから、画面を保存する3つの関数の動作が、Windows 7とWindows 8以降で異なる可能性がある。

Q. ビットマップの種類が複数?
A. DIBとDDBという異なる形式がある

ビットマップにはDIB(Device Independent Bitmap)とDDB(Device Dependent Bitmap)の二種類が存在する[5]。

DDBの特徴
  • 特定のデバイスに依存する
  • 原点が左上(トップダウン)
  • 画素にアクセスするためには、ゲッタ・セッタ関数を用いる必要がある(安全だが遅い)
  • 画像データを長方形として一まとめで扱うため、描画の計算が軽い
DIBの特徴
  • 特定のデバイスに依存しない
  • 原点が左下(ボトムアップ)
  • 画素にアクセスするためには、メモリに直接アクセスするだけでよい(必ずしも安全ではないが速い)
  • 描画の計算がDDBに比べ遅い

実施内容

今回はWindows 7, Windows 8.1のOSをそれぞれ搭載した二台のPCを用いた。
画面を保存する以下の関数の動作を比較する。

開発・実行環境

Windows 7

プロセッサ: Intel(R) Core(TM) i5 CPU @2.67GHz
メモリ: 4.00 GB
OS: Microsoft Windows 7 Home Premium (64bit)
compiler: Microsoft(R) C/C++ Optimizing Compiler Version 19.11.25507.1 for x86

Windows 8.1

プロセッサ: Intel(R) Core(TM) i5-5200U CPU @2.20GHz
メモリ: 8.00 GB
OS: Windows 8.1 (64bit)
compiler: Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86

共通

ライブラリ: libpng 1.6.37

コード説明

テスト関数

  1. bool CaptureTest(TESTCASE test_case, PNGData *png_data) {
  2. assert(png_data);
  3.  
  4. // The size of desktop window is acquired.
  5. RECT rect;
  6. GetWindowRect(GetDesktopWindow(), &rect);
  7. const int width = rect.right;
  8. const int height = rect.bottom;
  9.  
  10. // Memory device context is created.
  11. HDC memDC = CreateCompatibleDC(NULL);
  12.  
  13. // DIB section and DDB is created.
  14. BITMAPINFOHEADER bmiHeader;
  15. ZeroMemory(&bmiHeader, sizeof(bmiHeader));
  16. bmiHeader.biSize = sizeof(bmiHeader);
  17. bmiHeader.biWidth = width;
  18. bmiHeader.biHeight = height;
  19. bmiHeader.biPlanes = 1;
  20. bmiHeader.biBitCount = 24; // 24 bit BITMAP.
  21.  
  22. BITMAPINFO bmi;
  23. bmi.bmiHeader = bmiHeader;
  24.  
  25. LPVOID memDIB = NULL;
  26. HBITMAP memBM = CreateDIBSection(NULL, (LPBITMAPINFO)&bmi, DIB_RGB_COLORS,
  27. &memDIB, NULL, 0);
  28.  
  29. // Set DDB to memory device context.
  30. HBITMAP prevBM = (HBITMAP)SelectObject(memDC, memBM);
  31.  
  32. // DIB Section is set to memory DC.
  33. switch (test_case) {
  34. case TESTCASE_PAINT_DESKTOP:
  35. if (PaintDesktop(memDC) == 0) {
  36. SelectObject(memDC, prevBM);
  37. DeleteObject(memBM);
  38. DeleteDC(memDC);
  39. return false;
  40. }
  41. break;
  42. case TESTCASE_PAINT_WINDOW:
  43. if (PrintWindow(GetShellWindow(), memDC, 0) == 0) {
  44. SelectObject(memDC, prevBM);
  45. DeleteObject(memBM);
  46. DeleteDC(memDC);
  47. return false;
  48. }
  49. break;
  50. case TESTCASE_BITBLT:
  51. if (BitBlt(memDC, 0, 0, width, height, GetWindowDC(GetDesktopWindow()), 0,
  52. 0, SRCCOPY) == 0) {
  53. SelectObject(memDC, prevBM);
  54. DeleteObject(memBM);
  55. DeleteDC(memDC);
  56. return false;
  57. }
  58. break;
  59. default:
  60. break;
  61. }
  62.  
  63.  
  64. // DIB is converted to PNG.
  65. png_data->width = width;
  66. png_data->height = height;
  67. png_data->bit_depth = 8;
  68. png_data->rowbytes = width * 4;
  69. png_data->channels = 4;
  70. png_data->color_type = PNG_COLOR_TYPE_RGBA;
  71. png_data->interlace_type = PNG_INTERLACE_NONE;
  72. png_data->compression_type = PNG_COMPRESSION_TYPE_DEFAULT;
  73. png_data->filter_type = PNG_FILTER_TYPE_DEFAULT;
  74.  
  75. png_data->red_buffer.resize(width * height);
  76. png_data->green_buffer.resize(width * height);
  77. png_data->blue_buffer.resize(width * height);
  78. png_data->alpha_buffer.resize(width * height);
  79.  
  80. int bm_width = width * 3;
  81. if ((width * 3) % 4) {
  82. bm_width += (4 - (width * 3) % 4); // Set padding.
  83. }
  84.  
  85. for (int y = 0; y < height; y++) {
  86. for (int x = 0; x < width; x++) {
  87. png_data->blue_buffer[y * width + x] =
  88. *((LPBYTE)memDIB + (height - 1 - y) * bm_width + x * 3);
  89. png_data->green_buffer[y * width + x] =
  90. *((LPBYTE)memDIB + (height - 1 - y) * bm_width + x * 3 + 1);
  91. png_data->red_buffer[y * width + x] =
  92. *((LPBYTE)memDIB + (height - 1 - y) * bm_width + x * 3 + 2);
  93. png_data->alpha_buffer[y * width + x] = 255;
  94. }
  95. }

三つの関数の比較を一つの関数にまとめた。
プログラムの流れを説明する。

  1. スクリーンの大きさの取得
    GetWindowRectを用いた。
     
  2. メモリデバイスコンテキストの取得
    CreateCompatibleDCを用いた。
     
  3. ビットマップセクションの取得
    メモリデバイスコンテキストにデフォルトで1x1のモノクロビットマップが選択されている[6]。
    これを自前で作成したビットマップで置き換える。
    そこで、CreateDIBSectionという関数を用いる。
    この関数を使うことでDDBと、それに対応したDIBを同時に作成できる。

    なお、CreateCompatibleBitmapという関数があるが、この関数はDDBを返す。
    DDBは直接ピクセルのメモリ領域にアクセスすることができず都合が悪い。
     
  4. 画面のキャプチャ
    1. PaintDesktopを使う場合
      1. PaintDesktop(memDC)
      指定されたデバイスコンテキストの"clipping region"(ビットマップのこと?)にデスクトップのパターンもしくは壁紙を設定する。
       
    2. PrintWindowを使う場合
      1. PrintWindow(GetShellWindow(), memDC, 0)
      指定されたデバイスコンテキストに指定されたウィンドウの画面をコピーする。
      この関数は、スクリーンキャプチャに限らず、任意のアプリケーションやコントロールのウインドウで使用できると思われる。
       
    3. BitBltを使う場合
      1. BitBlt(memDC, 0, 0, width, height, GetWindowDC(GetDesktopWindow()), 0, 0, SRCCOPY)
      あるデバイスコンテキストから別のデバイスコンテキストへ、矩形領域の画素をビットブロック転送する。
       
  5. ビットマップからPNGへの変換
    PNGDataは先日の投稿[7]で作成した構造体である。
    特に注意が必要なのは、画素をDIBからPNGに変換する部分。
    1. int bm_width = width * 3;
    2. if ((width * 3) % 4) {
    3. bm_width += (4 - (width * 3) % 4); // Set padding.
    4. }
    5.  
    6. for (int y = 0; y < height; y++) {
    7. for (int x = 0; x < width; x++) {
    8. png_data->blue_buffer[y * width + x] =
    9. *((LPBYTE)memDIB + (height - 1 - y) * bm_width + x * 3);
    10. png_data->green_buffer[y * width + x] =
    11. *((LPBYTE)memDIB + (height - 1 - y) * bm_width + x * 3 + 1);
    12. png_data->red_buffer[y * width + x] =
    13. *((LPBYTE)memDIB + (height - 1 - y) * bm_width + x * 3 + 2);
    14. png_data->alpha_buffer[y * width + x] = 255;
    15. }
    16. }
    17.  

ピクセルの変換の例:サイズ2x2の画像
DIBの一列の長さが4の倍数バイトとなるようパッディングを入れる。
DIBはボトムアップ配列、PNGはトップダウン配列で画素情報を格納する。
画像のプロパティにより、この図の限りではないので注意。

メイン関数

  1. #include <stdio.h>
  2. #include <wchar.h>
  3. #include <windows.h>
  4. #include <windowsx.h>
  5. #include "./resource.h"
  6. #include "./util.h"
  7.  
  8. namespace {
  9. constexpr wchar_t WINDOW_NAME[] = L"Template window";
  10. constexpr wchar_t CLASS_NAME[] = L"Template class";
  11. } // namespace
  12.  
  13. int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
  14. LPTSTR lpsCmdLine, int nCmdShow) {
  15. (void)hInstance;
  16. (void)hPrevInstance;
  17. (void)lpsCmdLine;
  18. (void)nCmdShow;
  19.  
  20. #ifdef DEBUG
  21. FILE* fp = nullptr;
  22. AllocConsole();
  23. _wfreopen_s(&fp, L"CONOUT$", L"w", stdout);
  24. _wfreopen_s(&fp, L"CONOUT$", L"w", stderr);
  25. _wfreopen_s(&fp, L"CONIN$", L"r", stdin);
  26. #endif
  27.  
  28. #ifdef DEBUG
  29. fwprintf(stdout, L"Hello world to stdout!\n");
  30. fwprintf(stderr, L"Hello world to stderr!\n");
  31. fwprintf(stderr, L"\n");
  32. #endif
  33.  
  34. PNGData png_data;
  35.  
  36. // Test case 1 : Use of PaintDesktop().
  37. if (!CaptureTest(TESTCASE_PAINT_DESKTOP, &png_data)) {
  38. MessageBox(NULL, L"Case 1 failed.", L"Error", MB_OK);
  39. } else {
  40. WritePNGFile(L"case_1.png", png_data);
  41. }
  42.  
  43. // Test case 2 : Use of PaintWindow().
  44. if (!CaptureTest(TESTCASE_PAINT_WINDOW, &png_data)) {
  45. MessageBox(NULL, L"Case 2 failed.", L"Error", MB_OK);
  46. } else {
  47. WritePNGFile(L"case_2.png", png_data);
  48. }
  49.  
  50. // Test case 3 : Use of BitBlt().
  51. if (!CaptureTest(TESTCASE_BITBLT, &png_data)) {
  52. MessageBox(NULL, L"Case 3 failed.", L"Error", MB_OK);
  53. } else {
  54. WritePNGFile(L"case_3.png", png_data);
  55. }
  56.  
  57. // Everything is done!
  58. MessageBox(NULL, L"End of test.", L"main.exe", MB_OK);
  59. DestroyWindow(NULL);
  60.  
  61. #ifdef DEBUG
  62. FreeConsole();
  63. #endif
  64. return 0;
  65. }
呼び出しはこんな感じで行う。

注意点

実行の際には、Windows 8.1では、main.exeを右クリックし、プロパティを選択、互換性のタブを開き、「高DPI設定では画面のスケーリングを無効にする」にチェックを入れた。

結果と考察

以下の結果が得られた。

  • PaintDesktopを用いた場合の画像
    • Windows 7: 関数が失敗した
    • Windows 8.1: 壁紙のみ得られた
Windows 7で失敗した原因は不明。
Windows 8で壁紙のみが取得できたのは、リファレンスにある通りの動作。
環境を変えると動かなくなる。嫌なパターンだ。

  • PrintWindowを用いた場合の画像
    • Windows 7: 壁紙とアイコンが得られた
    • Windows 8.1: 壁紙のみ得られた
文献[4]には、Windows 7では壁紙の描画がカーネルで行われているとある。
また、デスクトップのアイコンとキャプションのみが映ったという例もある[8]。
そのため、Windows 7はカーネルが担当する壁紙は映らずに、シェルが担当するであろうアイコンのみが映り、Windows 8ではシェルが担当する壁紙とアイコンの両方が映るのだろうと予測していた。

しかし、実際は違った。
Windows 7でアイコンが映りこみ、Windows 8.1壁紙が映りこむことは予想された。
しかし、Windows 7で壁紙が映りこみ、Windows 8.1ではアイコンが映らないのは予想されなかった。
この原因の追究は一旦、保留としておく。

  • BitBltを用いた場合の画像
    • Windows 7 & Windows 8.1:壁紙とアイコンに加え、すべてのウィンドウが映った 
綺麗に撮れた。
めでたしめでたし。


PaintDesktopとPrintWindowがうまくいかなかった原因は不明である。
まず考えられるのは、「お前の環境が悪い」ケースである。
PaintDesktopのようなわかりやすい関数がWindows 7で失敗しているのは怪しい。

一方で、OSやAPIが壊れている可能性は微粒子レベルかもしれないが、無いとは言い切れない。
特に、今回使用したWindows 7搭載のPCはかなり古いのだ。

まとめ


  • PaintDesktopはWindows 7で失敗した
  • PrintWindowはWindows 7とWindows 8.1で動作に違いがあった
  • BitBltはWindows 7とWindows 8.1でスクリーンのキャプチャに成功した

よって、BitBltが一番安心できる。

感想

ある環境では動いて、ある環境では動かないという、後味が悪い結果となってしまった。
とはいえ、BitBltがしっかり動くのだということが分かったのは大きな収穫だ。
これでキャプチャソフトの完成に一歩近づいた。

GitHub

今回使用したコードはここに上げておく(タグ:"v_scr_captured")。

参考文献

  1. ビットマップ / スクリーンキャプチャ
  2. スクリーンキャプチャ
  3. デスクトップウィンドウとシェルウィンドウ
  4. Windows 10 でのシェルの置き換えについて
  5. Windows bitmap
  6. DDBを作る
  7. キャプチャソフト開発 その5 libpngを使ってみる
  8. デスクトップのスナップショット


2019年9月17日火曜日

キャプチャソフト開発 その5 libpngを使ってみる

PNG画像のライブラリとして枯れているlibpng[1]を使ってみた。
気が付いた点などをメモしておく。

最初から「デスクトップ画面をPNG形式で保存」するという、ゆるふわ仕様を決めていたため、ライブラリの選択には迷わなかった。
libpngの公式PDFリファレンスに加え、分かりやすい解説[2]には大いに助けられた。


開発・実行環境

プロセッサ: Intel(R) Core(TM) i5-5200U CPU @2.20GHz
メモリ: 8.00 GB
OS: Windows 8.1 (64bit)
compiler: Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86
library: libpng 1.6.37

プログラム説明

基本的なPNG画像の読み出し・書き込みを確かめるプログラムを作成した。
プログラムの流れを示す。

①PNG画像を読み込む
②情報を表示する
③色を弄る
④PNG画像として保存する

画像読み出しと画像書き込みは、専用の関数と構造体を作ることで実現する。
情報の表示と色の変更は、main関数に直接書きこむ。

変換前: 
変換後:

扱う画像は32x32のRBGAフォーマットとした。
見えづらいが、四隅に赤、緑、青、黒のドットを打った。

実行結果


libpng test program. width = 32 height = 32 bit_depth = 8 rowbytes = 128 channels = 4 color_type = 6 (PNG_COLOR_TYPE_RGBA) interlace_type = 0 (PNG_INTERLACE_NONE) compression_type = 0 (PNG_COMPRESSION_TYPE_DEFAULT) filter_type = 0 (PNG_FILTER_TYPE_DEFAULT) (0,0), (255,0,0) (31,0), (0,255,0) (0,31), (0,0,255) (32,32), (0,0,0) src_img.png => dst_img.png

本プログラムはコンソールアプリケーションとして実装した。
出力は、読み取ったPNG画像の情報である。

PNG画像に含まれるデータには、縦横のピクセル数、ビット深度、画像バッファの一列当たりのサイズ、チャンネルの数、カラータイプ、インターレース、圧縮タイプ、フィルタータイプなどがある。

四隅に打ったドットの色を(x, y), (red, green, blue)の形式で表示した。
正しく読み込めているようだ。

コード解説

PNG画像に関するデータをまとめた構造体

  1. struct PNGData {
  2. png_uint_32 width;
  3. png_uint_32 height;
  4. int color_type;
  5. int bit_depth;
  6. int filter_type;
  7. int compression_type;
  8. int interlace_type;
  9. int rowbytes;
  10. int channels;
  11. // Pixels are read from PNG data.
  12. // This sample only supports RGBA format.
  13. std::vector<uint8_t> red_buffer;
  14. std::vector<uint8_t> green_buffer;
  15. std::vector<uint8_t> blue_buffer;
  16. std::vector<uint8_t> alpha_buffer;
  17. };
PNG画像に関するデータを構造体としてまとめておくことにした。
読み出し・書き込み関数の実装でpng_ptrやinfo_ptrといった特徴的な構造体が登場するが、それらはライブラリ内部のメモリ管理と紐づいており扱いに特別の注意を要する。
そのため、この構造体には含めないことにした。

現状ではRGBA形式に特化したデータ構造になっているため、インデックスカラーやグレースケールを扱う際には修正の必要がある。
リファクタリングの余地はありそうだが、キャプチャソフトにはこれで足りると思う。

main関数

  1. #include <png.h>
  2. #include <stdint.h>
  3. #include <stdio.h>
  4. #include <wchar.h>
  5. #include <windows.h>
  6. #include <vector>
  7. #include "./util.h"
  8.  
  9. namespace {
  10. constexpr wchar_t WINDOW_NAME[] = L"Template window";
  11. constexpr wchar_t CLASS_NAME[] = L"Template class";
  12. } // namespace
  13.  
  14. int main(int argc, char* argv[]) {
  15. (void)argc;
  16. (void)argv;
  17. fwprintf(stderr, L"libpng test program.\n");
  18. fwprintf(stderr, L"\n");
  19.  
  20. // Read image data from PNG file.
  21. PNGData png_data;
  22. if (!ReadPNGFile(L"src_img.png", &png_data)) {
  23. fwprintf(stderr, L"ReadPNGFile() failed.\n");
  24. return 1;
  25. }
  26. PrintPNGData(stderr, png_data);
  27. fwprintf(stderr, L"\n");
  28.  
  29. // Show corner colors.
  30. fwprintf(stderr, L"(%d,%d), (%d,%d,%d)\n", 0, 0, png_data.red_buffer[0],
  31. png_data.green_buffer[0], png_data.blue_buffer[0]);
  32. fwprintf(stderr, L"(%d,%d), (%d,%d,%d)\n", png_data.width - 1, 0,
  33. png_data.red_buffer[png_data.width - 1],
  34. png_data.green_buffer[png_data.width - 1],
  35. png_data.blue_buffer[png_data.width - 1]);
  36. fwprintf(stderr, L"(%d,%d), (%d,%d,%d)\n", 0, png_data.height - 1,
  37. png_data.red_buffer[png_data.width * (png_data.height - 1)],
  38. png_data.green_buffer[png_data.width * (png_data.height - 1)],
  39. png_data.blue_buffer[png_data.width * (png_data.height - 1)]);
  40. fwprintf(stderr, L"(%d,%d), (%d,%d,%d)\n", png_data.width, png_data.height,
  41. png_data.red_buffer[png_data.width * png_data.height - 1],
  42. png_data.green_buffer[png_data.width * png_data.height - 1],
  43. png_data.blue_buffer[png_data.width * png_data.height - 1]);
  44. fwprintf(stderr, L"\n");
  45.  
  46. // Modify image.
  47. for (int i = 0; i < static_cast<int>(png_data.width * png_data.height); ++i) {
  48. png_data.red_buffer[i] /= 4;
  49. png_data.blue_buffer[i] /= 4;
  50. png_data.alpha_buffer[i] = 255;
  51. }
  52.  
  53. // Write image data to PNG file.
  54. if (!WritePNGFile(L"dst_img.png", png_data)) {
  55. fwprintf(stderr, L"WritePNGFile() failed.\n");
  56. return 1;
  57. }
  58.  
  59. fwprintf(stderr, L"src_img.png => dst_img.png\n");
  60. return 0;
  61. }

  • ReadPNGFile
    PNGファイルを読み込み、画像データを取得する自作関数。
  • WritePNGFile
    画像データをもとに、PNGファイルを書きだす自作関数。
  • PrintPNGData
    PNG画像情報をフォーマットしてファイル出力する自作関数。
    主としてデバッグに用いることを想定している。

PNGファイル読み込み関数

  1. bool ReadPNGFile(const wchar_t *file_name, PNGData *png_data) {
  2. assert(file_name);
  3. assert(png_data);
  4.  
  5. FILE *fp = NULL;
  6. _wfopen_s(&fp, file_name, L"rb");
  7. if (!fp) {
  8. #ifdef DEBUG
  9. fwprintf(stderr, L"ERROR... Failed to open file.\n");
  10. #endif
  11. return false;
  12. }
  13.  
  14. // Signature check.
  15. png_byte sig[8] = {0};
  16. fread(sig, sizeof(sig), 1, fp);
  17. if (png_sig_cmp(sig, 0, sizeof(sig)) != 0) {
  18. #ifdef DEBUG
  19. fwprintf(stderr, L"ERROR... Signature is not PNG.\n");
  20. #endif
  21. fclose(fp);
  22. return false;
  23. }
  24.  
  25. // Memory initialization.
  26. png_structp png_ptr =
  27. png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  28. if (!png_ptr) {
  29. #ifdef DEBUG
  30. fwprintf(stderr, L"ERROR... Failed to allocate png_struct.\n");
  31. #endif
  32. fclose(fp);
  33. return false;
  34. }
  35.  
  36. png_infop info_ptr = png_create_info_struct(png_ptr);
  37. if (!info_ptr) {
  38. #ifdef DEBUG
  39. fwprintf(stderr, L"ERROR... Failed to allocate png_info.\n");
  40. #endif
  41. png_destroy_read_struct(&png_ptr, NULL, NULL);
  42. fclose(fp);
  43. return false;
  44. }
  45.  
  46. // Error handling with setjmp/longjmp.
  47. if (setjmp(png_jmpbuf(png_ptr)) != 0) {
  48. #ifdef DEBUG
  49. fwprintf(stderr, L"WARNING... longjmp() is called by libpng.\n");
  50. #endif
  51. png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
  52. fclose(fp);
  53. return false;
  54. }
  55.  
  56. // File pointer is set.
  57. png_init_io(png_ptr, fp);
  58.  
  59. // Skipped bytes are acknowledged.
  60. png_set_sig_bytes(png_ptr, sizeof(sig));
  61.  
  62. // Data access by high-level read interface.
  63. png_read_png(png_ptr, info_ptr,
  64. PNG_TRANSFORM_PACKING | PNG_TRANSFORM_STRIP_16, NULL);
  65. png_data->width = png_get_image_width(png_ptr, info_ptr);
  66. png_data->height = png_get_image_height(png_ptr, info_ptr);
  67. png_data->color_type = png_get_color_type(png_ptr, info_ptr);
  68. png_data->bit_depth = png_get_bit_depth(png_ptr, info_ptr);
  69. png_data->filter_type = png_get_filter_type(png_ptr, info_ptr);
  70. png_data->compression_type = png_get_compression_type(png_ptr, info_ptr);
  71. png_data->interlace_type = png_get_interlace_type(png_ptr, info_ptr);
  72. png_data->rowbytes = png_get_rowbytes(png_ptr, info_ptr);
  73. png_data->channels = png_get_channels(png_ptr, info_ptr);
  74.  
  75. // Pixels are read from PNG data.
  76. // This sample only supports RGBA format.
  77. if (png_data->color_type != PNG_COLOR_TYPE_RGBA) {
  78. #ifdef DEBUG
  79. fwprintf(stderr, L"color_type is not PNG_COLOR_TYPE_RGBA, exit.\n");
  80. #endif
  81. png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
  82. fclose(fp);
  83. return false;
  84. }
  85.  
  86. // Buffed image data is directly accessed with a pointer array.
  87. png_bytepp rows = png_get_rows(png_ptr, info_ptr);
  88. if (!rows) {
  89. #ifdef DEBUG
  90. fwprintf(stderr, L"ERROR... Failed to get rows.\n");
  91. #endif
  92. png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
  93. fclose(fp);
  94. return false;
  95. }
  96. png_bytep row = NULL;
  97. const int image_size = png_data->height * png_data->width;
  98. png_data->red_buffer.resize(image_size);
  99. png_data->green_buffer.resize(image_size);
  100. png_data->blue_buffer.resize(image_size);
  101. png_data->alpha_buffer.resize(image_size);
  102. for (int y = 0; y < static_cast<int>(png_data->height); y++) {
  103. row = rows[y];
  104. for (int x = 0; x < static_cast<int>(png_data->width); x++) {
  105. png_data->red_buffer[y * png_data->width + x] = *(row++);
  106. png_data->green_buffer[y * png_data->width + x] = *(row++);
  107. png_data->blue_buffer[y * png_data->width + x] = *(row++);
  108. png_data->alpha_buffer[y * png_data->width + x] = *(row++);
  109. }
  110. }
  111.  
  112. // Memory finalization.
  113. png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
  114. fclose(fp);
  115. return true;
  116. }
  • png_read_png
    画像読み込みの際には、「高水準読込インタフェース(high level read interface)」と「低水準読込インタフェース」が用意されている。
    png_read_png関数を用いるのは高水準の方である。
    高水準読込インタフェースでは、ビットのパッキングや画像形式の変換といった細かい処理を、フラグを指定するだけで自動で実施してくれる。
  • setjmp/longjmpとvolatile
    setjmp/longjmpは関数の外にジャンプする機構を実現する機能[4]であり、C言語における例外処理に使用されることがある(私はlibpngを使うまで、この機能を知らなかった)。
    今回は高水準読み込みインターフェースで簡単に済ませてしまったため問題にはならなかったが、setjmpと合わせて用いる変数はvolatileにして最適化を無効にする必要があるかもしれない[5]。

    libpngのcontrib/examples/pngpixel.cには、以下のようなコメントがある。
    "This program uses the default, <setjmp.h> based, libpng error handling mechanism, therefore any local variable that exists before the call to setjmp and is changed after the call to setjmp returns successfully must be declared with 'volatile' to ensure that their values don't get destroyed by longjmp"
  • 機能追加の余地
    • コールバック関数の指定
    • 画像フォーマットの対応を増やす

PNGファイル書きこみ関数

  1. bool WritePNGFile(const wchar_t *file_name, const PNGData &png_data) {
  2. assert(file_name);
  3.  
  4. FILE *fp = NULL;
  5. _wfopen_s(&fp, file_name, L"wb");
  6. if (!fp) {
  7. #ifdef DEBUG
  8. fwprintf(stderr, L"ERROR... Failed to open file.\n");
  9. #endif
  10. return false;
  11. }
  12.  
  13. // Memory initialization.
  14. png_structp png_ptr =
  15. png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  16. if (!png_ptr) {
  17. #ifdef DEBUG
  18. fwprintf(stderr, L"ERROR... Failed to allocate png_struct.\n");
  19. #endif
  20. fclose(fp);
  21. return false;
  22. }
  23.  
  24. png_infop info_ptr = png_create_info_struct(png_ptr);
  25. if (!info_ptr) {
  26. #ifdef DEBUG
  27. fwprintf(stderr, L"ERROR... Failed to allocate info_ptr.\n");
  28. #endif
  29. png_destroy_write_struct(&png_ptr, NULL);
  30. fclose(fp);
  31. return false;
  32. }
  33.  
  34. // Error handling with setjmp/longjmp.
  35. if (setjmp(png_jmpbuf(png_ptr))) {
  36. #ifdef DEBUG
  37. fwprintf(stderr, L"WARNING... longjmp() is called by libpng.\n");
  38. #endif
  39. png_destroy_write_struct(&png_ptr, &info_ptr);
  40. fclose(fp);
  41. return false;
  42. }
  43.  
  44. // File pointer is set.
  45. png_init_io(png_ptr, fp);
  46.  
  47. // Get access to some of the IHDR settings.
  48. png_set_IHDR(png_ptr, info_ptr, png_data.width, png_data.height,
  49. png_data.bit_depth, png_data.color_type, png_data.interlace_type,
  50. png_data.compression_type, png_data.filter_type);
  51.  
  52. // Image buffer is created.
  53. png_bytepp rows =
  54. (png_bytepp)png_malloc(png_ptr, sizeof(png_bytep) * png_data.height);
  55. if (!rows) {
  56. #ifdef DEBUG
  57. fwprintf(stderr, L"ERROR... Failed to allocate rows.\n");
  58. #endif
  59. png_destroy_write_struct(&png_ptr, &info_ptr);
  60. fclose(fp);
  61. return false;
  62. }
  63.  
  64. memset(rows, 0, sizeof(png_bytep) * png_data.height);
  65. bool alloc_is_ok = true;
  66. for (int y = 0; y < static_cast<int>(png_data.height); y++) {
  67. rows[y] = (png_byte *)png_malloc(png_ptr, png_data.rowbytes);
  68. if (!rows[y]) {
  69. #ifdef DEBUG
  70. fwprintf(stderr, L"ERROR... Failed to allocate rows[%d].\n", y);
  71. #endif
  72. alloc_is_ok = false;
  73. break;
  74. }
  75. }
  76. if (!alloc_is_ok) {
  77. png_destroy_write_struct(&png_ptr, &info_ptr);
  78. fclose(fp);
  79. return false;
  80. }
  81.  
  82. // Pixels are written to PNG format.
  83. // This sample only supports RGBA format.
  84. if (png_data.color_type != PNG_COLOR_TYPE_RGBA) {
  85. #ifdef DEBUG
  86. fwprintf(stderr, L"color_type is not PNG_COLOR_TYPE_RGBA, exit.\n");
  87. #endif
  88. png_destroy_write_struct(&png_ptr, &info_ptr);
  89. fclose(fp);
  90. return false;
  91. }
  92.  
  93. png_bytep row = NULL;
  94. for (int y = 0; y < static_cast<int>(png_data.height); y++) {
  95. row = rows[y];
  96. for (int x = 0; x < static_cast<int>(png_data.width); x++) {
  97. *(row++) =
  98. static_cast<png_byte>(png_data.red_buffer[y * png_data.width + x]);
  99. *(row++) =
  100. static_cast<png_byte>(png_data.green_buffer[y * png_data.width + x]);
  101. *(row++) =
  102. static_cast<png_byte>(png_data.blue_buffer[y * png_data.width + x]);
  103. *(row++) =
  104. static_cast<png_byte>(png_data.alpha_buffer[y * png_data.width + x]);
  105. }
  106. }
  107.  
  108. // Reflect buffered image to info structure.
  109. png_set_rows(png_ptr, info_ptr, rows);
  110.  
  111. // Data access by high-level write interface.
  112. png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
  113.  
  114. // Memory finalization.
  115. if (!rows) {
  116. for (int y = 0; y < static_cast<int>(png_data.height); y++) {
  117. if (!rows[y]) {
  118. png_free(png_ptr, rows[y]);
  119. }
  120. }
  121. png_free(png_ptr, rows);
  122. }
  123.  
  124. png_destroy_write_struct(&png_ptr, &info_ptr);
  125. fclose(fp);
  126. return true;
  127. }
関数呼び出しの手続きは読み込みと似ているのでわかりやすい。

  • png_write_png
    画像書き込みには、「高水準読込インタフェース(high level read interface)」と「低水準読込インタフェース」が用意されている。
    png_write_png関数を用いるのは高水準の方である。


感想

setjmp/longjmpという刺激的な機能に出会えて面白かった。
C言語を始めて6年近くたっているが、こうも未知の項目が多いと、万年初心者かもなぁと感じる。

GitHub

今回のプロジェクトの全体はここに上げておく。

参考文献

  1. libpng http://www.libpng.org/pub/png/libpng.html
  2. 伝説のお茶の間 http://dencha.ojaru.jp/index.html
  3. インターレース https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%AC%E3%83%BC%E3%82%B9
  4. longjmpと例外 http://www.nurs.or.jp/~sug/soft/super/longjmp.htm
  5. cast to volatile https://bytes.com/topic/c/answers/221923-cast-volatile

2019年9月14日土曜日

キャプチャソフト開発 その4 キーボードフックを使ってみる

二か月ぶりのキャプチャソフト制作の関連投稿です。

***注意***
この記事の技術的内容には自信がありません。
参考にする際には気を付けてください。
それでもよろしければ。
***  ***

開発中のキャプチャソフトのふんわりとした仕様として、「シャッターキーはPauseやScrLkキーなど、使用頻度の低いキーに固定」という項目があります。
例えば、ゲームのキャプチャソフトとして有名なロイロ ゲーム レコーダーでは、F6キーを押すことで録画開始・停止を行います。

キャプチャソフトでは、ソフト自体のウィンドウは被写体のウィンドウに隠れてアクティブではなくなっています。
自分が映りこむことは都合が悪いからです。

「Hello World」など、シングルスレッドで簡単に実装できる通常のプログラムでは、メインスレッドがウィンドウを作成し、そのウィンドウに対するキー入力メッセージをメッセージプロシージャで受け取ります。
しかし先述の都合のため、キャプチャソフトではキー入力メッセ―ジがウィンドウプロシージャに入ってこないのです。

アクティブでない隠れたウィンドウから、キー入力メッセージを取得するためには、すべてのスレッドに対して送信されるメッセージを知る必要が生じます。
そこで出てくるのが、「グローバルフック」の一種である「キーボードフック」です。
キーボードフックは、キー入力メッセージをメインスレッドのウインドウプロシージャに渡す役割を担う存在であり、DLLとして実装します。

参考:
システムフック
ローカルフック

復習:キャプチャソフト開発 その3 DLLを作ってみる
前回は明示的にDLLを読み込ませました。
今回は簡単のため、暗黙的に読み込ませます。

以下、余談です。
正直、グローバルフックの動作を完全に理解できていません。
フックの動作を把握するためには、ウィンドウ、プロセス、システム、スレッド、CPU、メッセージキュー、フックプロシージャといった、Windows OSの動作に関する広範な知識を関連づける必要があるからです。
Windowsの内部動作に関してはまだまだ勉強中ですので、フックの動作に関しては、もう少しWindowsの理解が進んでから、振り返りたいと思います。

頭で理解するインプット型の学習はもちろん大事ですが、実際にコードを組んで走らせるアウトプット型の学習も大事だ!
...と自分に言い聞かせつつ、とりあえず今は「動くものができればいい」という魂胆で実装します。
自分のPCなら弄り倒して壊してしまっても、誰も文句を言わないでしょう。
閑話休題。

メインソースファイル

  1. #include <stdio.h>
  2. #include <wchar.h>
  3. #include <windows.h>
  4. #include <windowsx.h>
  5. #include "./resource.h"
  6. #include "./util.h"
  7. namespace {
  8. constexpr wchar_t WINDOW_NAME[] = L"Template window";
  9. constexpr wchar_t CLASS_NAME[] = L"Template class";
  10. } // namespace
  11. BOOL Cls_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct) {
  12. (void)hwnd;
  13. (void)lpCreateStruct;
  14. // Start the keyboard hook.
  15. if (!SetKeyHook(hwnd)) {
  16. #ifdef DEBUG
  17. fwprintf(stderr, L"Failed to start hook\n");
  18. #endif
  19. FreeConsole();
  20. return 0;
  21. }
  22. return TRUE;
  23. }
  24. void Cls_OnDestroy(HWND hwnd) {
  25. (void)hwnd;
  26. // Stop the keyboard hook.
  27. if (!RemoveKeyHook()) {
  28. #ifdef DEBUG
  29. fwprintf(stderr, L"Failed to stop hook\n");
  30. #endif
  31. }
  32. PostQuitMessage(0);
  33. }
  34. void Cls_OnClose(HWND hwnd) { DestroyWindow(hwnd); }
  35. void Cls_OnCommand(HWND hwnd, int id, HWND hWndCtl, UINT codeNotify) {
  36. (void)hwnd;
  37. (void)id;
  38. (void)hWndCtl;
  39. (void)codeNotify;
  40. }
  41. void Cls_OnKeyHook(HWND hwnd, WPARAM wParam, LPARAM lParam) {
  42. (void)hwnd;
  43. const UINT vk = (UINT)wParam;
  44. const BOOL fDown = ((lParam & 0x80000000) == 0) ? TRUE : FALSE;
  45. if (fDown == TRUE) {
  46. fwprintf(stderr, L"key down, %2x:\n", vk);
  47. } else {
  48. fwprintf(stderr, L"key up, %2x:\n", vk);
  49. }
  50. }
  51. LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam,
  52. LPARAM lParam) {
  53. switch (message) {
  54. HANDLE_MSG(hwnd, WM_CREATE, Cls_OnCreate);
  55. HANDLE_MSG(hwnd, WM_DESTROY, Cls_OnDestroy);
  56. HANDLE_MSG(hwnd, WM_COMMAND, Cls_OnCommand);
  57. HANDLE_MSG(hwnd, WM_CLOSE, Cls_OnClose);
  58. HANDLE_MSG(hwnd, WM_KEYHOOK, Cls_OnKeyHook);
  59. default:
  60. return DefWindowProc(hwnd, message, wParam, lParam);
  61. }
  62. }
  63. int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
  64. LPTSTR lpsCmdLine, int nCmdShow) {
  65. (void)hPrevInstance;
  66. (void)lpsCmdLine;
  67. #ifdef DEBUG
  68. FILE* fp = nullptr;
  69. AllocConsole();
  70. _wfreopen_s(&fp, L"CONOUT$", L"w", stdout);
  71. _wfreopen_s(&fp, L"CONOUT$", L"w", stderr);
  72. _wfreopen_s(&fp, L"CONIN$", L"r", stdin);
  73. #endif
  74. #ifdef DEBUG
  75. fwprintf(stdout, L"Hello world to stdout!\n");
  76. fwprintf(stderr, L"Hello world to stderr!\n");
  77. #endif
  78. WNDCLASS wc;
  79. wc.style = CS_HREDRAW | CS_VREDRAW;
  80. wc.lpfnWndProc = WndProc;
  81. wc.cbClsExtra = 0;
  82. wc.cbWndExtra = 0;
  83. wc.hInstance = hInstance;
  84. wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
  85. wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  86. wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  87. wc.lpszMenuName = 0;
  88. wc.lpszClassName = CLASS_NAME;
  89. if (!RegisterClass(&wc)) {
  90. return FALSE;
  91. }
  92. HWND hwnd = CreateWindow(CLASS_NAME, WINDOW_NAME, WS_OVERLAPPEDWINDOW,
  93. CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
  94. CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
  95. if (hwnd == NULL) {
  96. return FALSE;
  97. }
  98. ShowWindow(hwnd, nCmdShow);
  99. UpdateWindow(hwnd);
  100. MSG msg;
  101. while (GetMessage(&msg, NULL, 0, 0) > 0) {
  102. TranslateMessage(&msg);
  103. DispatchMessage(&msg);
  104. }
  105. #ifdef DEBUG
  106. FreeConsole();
  107. #endif
  108. return 0;
  109. }

フックの登録と削除をON_CREATEとON_DESTROYのタイミングで実行しています。
関数やユーザーメッセージをまとめてあります。


フック関連のヘッダファイル

  1. #ifndef _UTIL_H_
  2. #define _UTIL_H_
  3.  
  4. #include <windows.h>
  5. #ifndef DLLAPI
  6. #define DLLAPI extern "C" __declspec(dllimport)
  7. #endif
  8. // Application-defined message identifiers.
  9. #define WM_KEYHOOK (WM_APP + 1)
  10. // void Cls_OnKeyHook(HWND hwnd, WPARAM wParam, LPARAM lParam)
  11. #define HANDLE_WM_KEYHOOK(hwnd, wParam, lParam, fn) \
  12. ((fn)((hwnd), (WPARAM)(wParam), (LPARAM)(lParam)), 0L)
  13. DLLAPI bool SetKeyHook(HWND hWnd);
  14. DLLAPI bool RemoveKeyHook();
  15. #endif // _UTIL_H_


フックの登録と削除をON_CREATEとON_DESTROYのタイミングで実行しています。
関数やユーザーメッセージをまとめてあります。

  • SetKeyHook
    キーボードフックを掛けるためのラッパー関数です。
  • RemoveKeyHook
    キーボードフックを外すためのラッパー関数です。
  • WM_KEYHOOK、Cls_OnKeyHook
    フックプロシージャからウインドウプロシージャに送られるメッセージのための、ユーザ定義メッセージ定数と、それに対応するメッセージクラッカです。


フック関連のソースファイル
  1. #define DLLAPI extern "C" __declspec(dllexport)
  2.  
  3. #include "./util.h"
  4. #include <stdio.h>
  5. #include <windows.h>
  6. #pragma data_seg(".shared")
  7. DLLAPI HWND hWndDest = NULL;
  8. DLLAPI HHOOK hHook = NULL;
  9. #pragma data_seg()
  10. #pragma comment(linker, "/SECTION:.shared,rws")
  11. namespace {
  12. HINSTANCE hInstance;
  13. } // namespace
  14. DLLAPI LRESULT CALLBACK KeyProc(int code, WPARAM wParam, LPARAM lParam) {
  15. if (code < 0) {
  16. // The hook procedure must pass the message to the CallNextHookEx
  17. // without further processing, then should return the return value.
  18. return CallNextHookEx(hHook, code, wParam, lParam);
  19. }
  20. switch (code) {
  21. case HC_ACTION:
  22. PostMessage(hWndDest, WM_KEYHOOK, wParam, lParam);
  23. break;
  24. case HC_NOREMOVE:
  25. break;
  26. default:
  27. break;
  28. }
  29. return CallNextHookEx(hHook, code, wParam, lParam);
  30. }
  31. DLLAPI bool SetKeyHook(HWND hwnd) {
  32. hHook = SetWindowsHookEx(WH_KEYBOARD, KeyProc, hInstance, 0);
  33. if (hHook == NULL) {
  34. hWndDest = NULL;
  35. return false;
  36. }
  37. hWndDest = hwnd;
  38. return true;
  39. }
  40. DLLAPI bool RemoveKeyHook() {
  41. if (UnhookWindowsHookEx(hHook) == 0) {
  42. return false;
  43. }
  44. hHook = NULL;
  45. hWndDest = NULL;
  46. return true;
  47. }
  48. BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
  49. (void)lpvReserved;
  50. switch (fdwReason) {
  51. case DLL_PROCESS_ATTACH:
  52. hInstance = hinstDLL;
  53. //MessageBox(NULL, L"DLL_THREAD_ATTACH", L"key_hook.dll", MB_OK);
  54. break;
  55. case DLL_THREAD_ATTACH:
  56. break;
  57. case DLL_THREAD_DETACH:
  58. break;
  59. case DLL_PROCESS_DETACH:
  60. break;
  61. }
  62. return TRUE;
  63. }


(2019年10月3日 追記)
DLL_PROCESS_ATTACH内でhHookとhWndDestを初期化していたのを削除した。

メイクファイル

  1. TARGET = main.exe
  2. PDB = main.pdb
  3. MAP = main.map
  4. RES =
  5. SRC = main.cc util.cc
  6. OBJ = $(OBJDIR)\main.obj
  7. DLL = util.dll
  8. LIB2 = util.lib
  9. OBJDIR = build
  10.  
  11. CC = "$(VCINSTALLDIR)\cl.exe"
  12. LINK = "$(VCINSTALLDIR)\link.exe"
  13. CPPFLAGS = /nologo /W4 /Zi /O2 /MT /EHsc /Fd"$(TMPDIR)/" /DUNICODE /D_UNICODE \
  14. /DDEBUG
  15. LIB1 = "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" \
  16. "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" \
  17. "odbc32.lib" "odbccp32.lib" "libcmt.lib"
  18. LFLAGS = $(LIB1) /NOLOGO /SUBSYSTEM:WINDOWS /DEBUG /PROFILE
  19.  
  20. ALL: $(TARGET)
  21.  
  22. $(TARGET): $(OBJ) $(LIB2) $(DLL) $(RES)
  23. $(LINK) $(LIB2) $(LFLAGS) /OUT:$(TARGET) /PDB:"$(PDB)" /MAP:"$(MAP)" \
  24. $(OBJ) $(RES)
  25.  
  26. .cc{$(OBJDIR)}.obj:
  27. @[ -d $(OBJDIR) ] || mkdir $(OBJDIR)
  28. $(CC) $(CPPFLAGS) /Fo"$(OBJDIR)\\" /c $<
  29.  
  30. .cc.lib:
  31. $(CC) $(CPPFLAGS) /LD $(LIB1) $<
  32.  
  33. clean:
  34. rm $(OBJ) $(TARGET) *.map *.pdb *.ilk *.obj *.lib *.dll *.exp
  35.  
  36.  
  37.  
DLLが暗黙的にリンクされるようになっています。
util.objが中間生成物用に作ったbuildディレクトリに入らないという欠陥がありますが、落ち着かないというほかに大きな問題はないので目をつぶっています。

実行の様子



メモ帳に「abcd123」と入力し、プリントスクリーンしました。
フックのプロシージャがキーイベントを横取りし、メッセージを呼び出し元のメインスレッドのウィンドウプロシージャに渡しています。
ウィンドウプロシージャ―では、フックプロシージャから受け取った内容を基に、コンソールにキーを押した・離したという情報を出力しています。

試しにデスクトップの何もないところ、Chromeの検索窓、Minttyで走らせているvim、そしてコンソール自体をそれぞれアクティブにして同じことを試し、うまくいくことを確認しました。
しかしながら、Notepad++(テキストエディタ)では、コンソールで確認できないばかりか、コンソールにフォーカスを戻しても、入力を受け付けない状態になってしまいました。
何か実装に抜け穴があるのかもしれませんが、先述の通りフックに関する理解が甘いため、原因が特定できていません

うまくいく場合とうまくいかない場合があるコードですが、とりあえずTODOとして残しておいて、先へと進もうと思います。
要素単体で問題があることを把握しておけば、問題の原因が分かった際に、仮組されたキャプチャソフトに修正を反映させることは容易でしょう。
最も、将来的に他の要素で別の問題が発生し、その問題と絡まって問題が複雑化する可能性は大いにありますが…

※(2019年10月3日 追記)
DLL_PROCESS_ATTACH内でhHookとhWndDestを初期化していたのを削除したところ、解決ました、多分。
DllMainが呼ばれるのはプログラムの起動時のみではないようです。

最新のコード

このページのソースコードはGitHubにも上げておきます。