2019年11月13日水曜日

【VBA】Power Point 埋め込み Excel グラフの目盛ラベルと軸の間の距離を調整する

用あってスライドを作成しているのだが、グラフの埋め込みで困った。
Excelで作ったグラフを埋め込む際、目盛ラベルと軸の間の距離を調整する方法が見つからない。
Excelで作成した散布図は目盛ラベルと軸の距離が調整できないから不便だということを聞いていおり、確かにそうかもしれないと思った。

しかし調べてみると、Excelのグラフについて、VBAを使えば目盛ラベルと軸の間の距離を調整できることが分かった[1]。
そこで、[2]のコードを参考に、目盛ラベルと軸の間の距離を調整するVBAコードを作った。

注意事項


シートのバックアップを取ったうえで、VBAコードを実行させてほしい。
誤った操作をした場合、UNDOは効かない。
VBAは便利なツールだが、数時間の成果が水泡に帰す危険と常に隣り合わせだ。
参考にする際は、自己責任で。

実行環境

Excel (Office 365)
Power Point (Office 365)

コード


Module1.bas

  1. Sub axisOffset()
  2. Dim oShape As Shape
  3. Dim oSlide As Slide
  4. ' Target slide is set
  5. Set oSlide = ActivePresentation.Slides(19)
  6. ' Search shapes which has graph objects
  7. For Each oShape In oSlide.Shapes
  8. If oShape.HasChart Then
  9. With oShape.Chart
  10. ' Modify offset of labels for axis
  11. ' X axis
  12. .Axes(xlCategory).TickLabels.Offset = 0
  13. ' Y axis
  14. .Axes(xlValue).TickLabels.Offset = 0
  15. End With
  16. End If
  17. Next oShape
  18. End Sub
コードの使い方について説明する。

  1. パワーポイントで、ターゲットのグラフがあるページを開く
  2. Alt+F11でVBAエディタを開く
  3. 標準モジュールModule1を追加する
    1. プロジェクトウィンドウで右クリックし、ポップアップメニューから「挿入」「標準モジュール」をクリックする
  4. 上記のコードをエディタに張り付ける
  5. スライド番号の指定
    1. Set oSlide = ActivePresentation.Slides(19)
    このサンプルではスライド19のグラフを調節している。
    ターゲットのグラフのあるページ番号に書き換える。
    たとえば、5枚目のスライドにあるグラフを調整する場合は以下のように書き換える。
    1. Set oSlide = ActivePresentation.Slides(5)
  6. 目盛ラベルと軸の間の距離の指定
    1. ' Modify offset of labels for axis
    2. ' X axis
    3. .Axes(xlCategory).TickLabels.Offset = 0
    4. ' Y axis
    5. .Axes(xlValue).TickLabels.Offset = 0
    このサンプルでは距離を最小にしている。
    距離は0から1000までの数で指定する。
  7. F5キーを押して実行する


参考文献


  1. Format axis => Labels => distance from axis & interval between labels options missing from excel
  2. Add axes to chart in powerpoint using vba?
  3. Slides オブジェクト

2019年11月5日火曜日

【Chrome拡張機能】ブックマークを使いやすくする その0 やりたいことを徒然と

ブックマークの数が多すぎて困る。
ITの力でどうにかしようと思う。

ポイント


ブックマーク肥大化の問題


ブックマークの数が増えすぎて困っている。
私のブックマークの総数は8千9百弱で、これでも何度か整理している。

Chromeには、ブックマークを操作するためのUIが整えられている。
デフォルトのブックマーク管理の仕組みはミニマリスト風で好きなのだが、ブックマーク数が肥大化すると使いやすくはなくなってしまった。

具体的には、以下のような問題が生じている。
  • ページに飛びたい時、目的のブックマークを探すのに手間がかかる

    フォルダを階層化している場合、フォルダにカーソルを合わせポップアップのリストを表示し、カーソルを合わせ...という操作を再帰的に行わなければならない。
    フォルダの数が多くなり、階層が深くなると、大変である。
    また、フォルダの状態は、メニューを閉じるたびリセットされてしまう。
     
  • ページを登録したい時、適切なフォルダを探す手間がかかる

    小さい枠の中に、フォルダがびっしり表示されている。
    目的のフォルダを見つけるまで、目を据えて長々とスクロールしなければならない。
    また、フォルダの折り込みを開くと、カーソルがルートフォルダまで戻ってしまい、スクロールをやり直さなければならない。
     
  • ブックマークを整理する際の手間がかかる

    アイテムを他のフォルダに移動したいとき、ウィンドウが一つだとやりにくい。
    少なくとも、二窓でアイテムのドラッグアンドドロップができる仕様になっているのは良心的である。

これらはブックマークの数が多くなればなるほど煩雑になる。
Chromeのブックマーク管理の仕様では非常にやりづらいのである。


Chrome拡張機能によるブックマークの扱い


デフォルトのブックマークマネージャーに不足する機能を補う、または利便性を向上させる拡張機能がアプリストアにて沢山公開されている。
私が好んで日常的に使用している拡張機能を紹介する

  • Bookmark Sidebar
    ブラウザの画面端にブックマークバーを表示する拡張機能だ。
    フォルダの展開状態を保存してくれる点が優れている。
     
  • Sprucemarks
    登録したブックマークを自動的にソートする拡張機能だ。
    登録日時順、ページ名 or URL のアルファベット順にソートさせることができる。
     
  • ランデ・イント
    重複するURL、空フォルダを検索、削除する拡張機能だ。
    ブラウザとアンドロイドから同じアカウントを使っていると、ブックマークがなぜか重複してしまう問題に対処する目的で導入した。
     
  • Recent Bookmarks
    直近の新たに登録したブックマークを表示する拡張機能だ。
    多くのブックマークは登録した直後の数日間が一番使われるのではないだろうか?
     
  • Über simple bookmark count
    ブックマークの総数をカウントする拡張機能だ。
    ブックマークの総数を知りたくなった際、調べて見つけたもの。
    「え、素のブラウザではカウントできないの?」と驚いたものだ。
     
  • LinkMemo
    URLをメモ感覚で登録することのできる拡張機能だ。
    あるトピックについて集中的に調べたい時に、わざわざフォルダを作りブックマークするのではなく、ここに一時的に登録しておくとよい。
    ブックマークとはちょっと違うが、気に入っているので紹介する。
     
このように、この世にはたくさんの便利な拡張機能が存在する。

問題は、拡張機能が完全に安全ではないかもしれないということだ。
ユーザー数の多い拡張機能が突如、マルウェアとしての本性を現した事件もある[1][2]。
プライベートなデータにアクセスさせる拡張機能は、自分で作るのが安心である。

問題の本質は何か


登録したページのうち、再び訪れているページはどの程度だろうか?
とりあえずブックマークしたものの、二度と訪れないページもそこそこありそうだ。
そうでなくとも、閲覧頻度の差は結構ありそうな気がする。

頭を抱えているだけでは分からないだろう。
調査ありきだ。

開発・実行環境


ブラウザ (Google Chrome)
バージョン: 78.0.3904.70(Official Build) (64 ビット)
OS: Windows 8.1 (64bit)

課題


定性的に問題を把握するために、ブックマークを閲覧履歴と対応させて調査したい。
そして、どのようにブックマーク(もしくはブックマークを超える一般的な情報整理の仕組み)と付き合っていくかを考えたい。

今後、ブックマークと閲覧履歴を対応させ、ブックマークがどの程度の頻度で使用されているかどうかを確かめたいと思う。

Chromeの閲覧履歴は以下のファイルにSQL形式で保存されている。
"C:\Users\<ユーザ名>\AppData\Local\Google\Chrome\User Data\Default\History"

Chromeのブックマークは以下のファイルにJSON形式で保存されている。
"C:\Users\<ユーザ名>\AppData\Local\Google\Chrome\User Data\Default\Bookmarks"

これらを照合して統計を取りたい。
両方ともExcelで読み込んで統計処理し、問題の本質を考察していきたい。

まとめ

  • ブックマークが肥大化したので、なんとかしたい
  • 拡張機能は便利だが、情報流出のリスクを伴う
  • 閲覧履歴とブックマークのデータのありかは分かったので、統計を取りたい

感想


新たな開発テーマを制定した。
SQL形式とJSON形式のデータをどのようにExcelに読み込むかが、最初の関門である。



とりあえずSQLサーバーとSSMSをインストールしたので、ぼちぼちデータベースについて勉強していこうと思う。

2019年11月3日日曜日

【C++、JavaScript】クロージャーについて

C++のラムダ式とJavaScriptの無名関数・匿名関数が似ているなと感じた。
調べてみると、クロージャーという機能の実装として共通点が多いことが分かった。

ポイント


C++のラムダ式とJavaScriptの無名関数(匿名関数)は、どちらもクロージャーを実現することのできる言語機能であるという点で似ている


まずは言葉について整理する。

  • クロージャー
    関数宣言の中に別の関数宣言を書ける機能、あるいはローカル変数を参照している関数内変数の機能である[1]。
  • クロージャー型
    生成される関数オブジェクトの型の名前である
  • クロージャーオブジェクト
    クロージャー型の関数オブジェクトである。

C++では「ラムダ式」という記述方法をとることで、クロージャーを実装することができる。
C++でクロージャーでは、通常の関数定義では不可能なクロージャー外部の変数を保存しておくことができる。この、クロージャー外部の変数を保存する機能のことを「キャプチャ」と呼ぶ。

一方で、JavaScriptでは「アロー」表記で関数を宣言する。
JavaScriptの関数は宣言された時点では名前を持たないため、無名関数(匿名関数)と呼ばれる。JavaScriptではクロージャーを含むスコープ内でvarで宣言された変数はクロージャー内部からもアクセスでき、これによりキャプチャが実現できる。

C++のクロージャーでは、変数へのアクセス方法をある程度細かく指定することができる


C++のクロージャーの基本的なシンタックスを以下に示す。
  1. [....ラムダ指定...]{...関数の本体...}
[]は「ラムダ導入子」といい、ここにクロージャーがスコープ内部の変数にどのようにアクセスするかを記述する。
アクセスの種類には、大きく分けて「コピー」、「参照」、「this」参照がある。
変数を「コピー」しておけば、クロージャーの定義元が消滅しても値を保持することができる。一方で、「参照」でアクセスすることを指定した変数は、クロージャーの定義元が消失した後にアクセスした場合の動作が未定義である。

JavaScriptのクロージャーでは、C++ほど直接的に変数へのアクセス設定をできない


JavaScriptのクロージャーの基本的なシンタックスを以下に示す[3]。
  1. (引数,...)=>{...関数の本体...}
C++では、ラムダ導入子により変数へのアクセス方法を詳しく指定できたが、JavaScriptでは、そのような詳細なアクセス指定の方法は調べた範囲では見つからなかった。
しかしながら、逆に考えると、クロージャーを用いて変数を掩蔽することができるといえる。

開発・実行環境


ブラウザ Google Chrome
バージョン: 78.0.3904.70(Official Build) (64 ビット)
起動時オプション:
--flag-switches-begin --flag-switches-end

PC
プロセッサ: 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
Visual Studio: 14.0

コード


main.cc

  1. #include <stdio.h>
  2. #include <functional>
  3.  
  4. #define show(EXPRESSION) \
  5. { \
  6. printf("%s\n", #EXPRESSION); \
  7. printf("%d\n", EXPRESSION); \
  8. printf("\n"); \
  9. }
  10.  
  11. std::function<int(int)> CreateAdd(int add) {
  12. return [add](int i) { return i + add; }; // Use copy of add.
  13. }
  14.  
  15. std::function<int(int)> CreateAddRef(int add) {
  16. return [&add](int i) { return i + add; }; // Use reference to add.
  17. }
  18.  
  19. int main(int argc, char* argv[]) {
  20. (void)argc;
  21. (void)argv;
  22.  
  23. auto add1 = CreateAdd(1);
  24. auto add2 = CreateAdd(2);
  25. show(add1(1))
  26. show(add2(1))
  27.  
  28. auto addref1 = CreateAddRef(1);
  29. auto addref2 = CreateAddRef(2);
  30. show(addref1(1)) // Behavior is undefined.
  31. show(addref2(1)) // Behavior is undefined.
  32.  
  33. return 0;
  34. }
  35.  
C++におけるクロージャーの実装例である。
  1. std::function<int(int)> CreateAdd(int add) {
  2. return [add](int i) { return i + add; }; // Use copy of add.
  3. }
この関数はクロージャーオブジェクトを生成する関数である。
生成されたクロージャーオブジェクトは、引数にある数を受け取り、クロージャー生成関数に渡した数を加える機能を持つ。
  1. auto add1 = CreateAdd(1);
  2. auto add2 = CreateAdd(2);
  3. show(add1(1))
  4. show(add2(1))
クロージャー生成関数に渡された引数は、この関数が消滅した後もクロージャーから利用することができる。なぜなら、消滅してしまったクロージャー生成関数の引数の値をコピーしてあるからだ。

  1. std::function<int(int)> CreateAddRef(int add) {
  2. return [&add](int i) { return i + add; }; // Use reference to add.
  3. }
この関数もクロージャーオブジェクトを生成する関数である。
先ほどのクロージャー生成関数と異なる点は、ラムダ指定子で変数へのアクセス方法を参照としている点だ。
  1. auto addref1 = CreateAddRef(1);
  2. auto addref2 = CreateAddRef(2);
  3. show(addref1(1)) // Behavior is undefined.
  4. show(addref2(1)) // Behavior is undefined.
このように、クロージャー生成関数が消滅した後に、クロージャーで参照するクロージャー生成関数の引数にアクセスすることは、動作が未定義であるため、してはいけない。
(一応動いたけど、わけの分からん値が表示された)

ここら辺のことについては、こちらの質問と回答に詳しい[4]。

test.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charser="utf-8" />
  5. <title>count</title>
  6. <link rel="manifest" href="/manifest.json">
  7. <script type="text/javascript">
  8. function createCounter() {
  9. var count = 0;
  10. return function() {
  11. let elem = document.getElementById("id_count");
  12. elem.innerText = count;
  13. ++count;
  14. }
  15. }
  16. var counter = createCounter();
  17. // console.log(count); // Error... 'count' cannot be seen from here.
  18. </script>
  19. </head>
  20. <body>
  21. <input type="button" value="Increment" onclick="counter();"><br />
  22. count: <span id="id_count"></span><br />
  23. </body>
  24. </html>
  25.  
JavaScriptにおけるクロージャーの実装例である。
  1. function createCounter() {
  2. var count = 0;
  3. return function() {
  4. let elem = document.getElementById("id_count");
  5. elem.innerText = count;
  6. ++count;
  7. }
  8. }
この部分の記述はクロージャーオブジェクトを生成する関数リテラルである。
  1. var counter = createCounter();
変数counterにクロージャーオブジェクトを代入する。
  1. <input type="button" value="Increment" onclick="counter();"><br />
ボタンをクリックした際に、クロージャー生成関数内部のスコープにある変数varへアクセスし、ドキュメントにカウンタの値を表示し、カウンタの値を増加させる。
  1. // console.log(count); // Error... 'count' cannot be seen from here.
クロージャー生成関数とクロージャーの外側からカウンタにアクセスすることはできない。

実行結果



count:

test.htmlの一部を埋め込んだ。

まとめ



  • クロージャーオブジェクトと通常の関数宣言の違いは、スコープ内部変数のコピーや参照をキャプチャしたクロージャーオブジェクトを生成することができる点である
  • C++、JavaScriptのいずれもクロージャーの実装をすることができる
  • C++では変数へのアクセス方法の詳細な指定ができる一方で、JavaScriptではC++ほど詳細なアクセス指定の仕組みが無いと思われる(調べた範囲では)。逆に考えると、JavaScriptではクロージャーを用いることにより変数の掩蔽をすることができるといえる

感想


ラムダ式というと関数型プログラミングのイメージが強い(関数型はhaskellでハローワールドしたことのある程度の初心者)。
一般的なラムダ式に関する理解は足りていないと思うが、少なくともC++では、ラムダ式よりもクロージャーとして考えた方が理解しやすいかもしれないと思った。

付録


今回のコードの最新版(C++の方)をここに上げる。

参考文献


  1. JavaScriptでクロージャー
  2. ラムダ式
  3. JavaScript アロー関数を説明するよ
  4. ラムダ式って何が便利なのです??

2019年10月31日木曜日

【JavaScript】関数内でconstで宣言された変数は、次の関数呼び出しで変更できる

JavaScriptのconstの使い方について気になって調べたのでメモ。

ポイント

関数内でconstで宣言された変数は、次の関数呼び出しで変更できる

【問題】
ある関数内で、変数をエイリアスとして使用することを考える。
例えば、以下のような感じ。
  1. function tmp() {
  2. const arrayLength= array.length;
  3. console.log(arrayLength);
  4. }
配列の長さが変わった際に、再度この関数が呼び出されるとどうなるのか。
このconstが「関数の呼び出しのたびに毎回値を設定します」ということであれば望む結果が得られるだろうが、もし仮に、「関数の最初の呼び出しで決まり、それ以降の呼び出しでは変更できません」だと困ったことになる。
C++を扱った感覚では、前者の動作が当然のように思われるが、気になってしまった以上調べないわけにはいかない。

【答え】
ちゃんと再設定される。

開発・実行環境

ブラウザ Google Chrome
バージョン: 78.0.3904.70(Official Build) (64 ビット)
起動時オプション:
--flag-switches-begin --flag-switches-end

PC
プロセッサ: 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

コード

test.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charser="utf-8" />
  5. <title>Odonata Bug Hunt Blog</title>
  6. <link rel="manifest" href="/manifest.json">
  7. <script type="text/javascript">
  8. var lastValue=0;
  9. const addOne = () => {
  10. const constValue=lastValue+1;
  11. // constValue = 1; // This code become an error.
  12. var elem = document.getElementById("id_value");
  13. elem.innerText = constValue;
  14. lastValue = constValue;
  15. };
  16. </script>
  17. </head>
  18. <body>
  19. <input type="button" value="Increment" onclick="addOne();"><br />
  20. value: <span id="id_value"></span><br />
  21. </body>
  22. </html>

lastValueは数を保存しておくためのグローバル変数だ。
constValueはlastValueに1を加えた値を入れておくための変数で、関数呼び出しのたびに更新さる。
ボタンクリックで関数が呼ばれる。

実行結果

Odonata Bug Hunt Blog
value:

(私の環境では更新されています)

2019年10月29日火曜日

Chrome のプラグインを作る! その5 ひとまず完成!

読書の秋です。
電子書籍が普及しているようですが、私は活字が好きです。
紙媒体だと鉛筆で線を引くことができるし、精密機器ではないので気楽にとり回せる。
そして何より、デジタルよりも物質的な温もりがある。

***

先週から思い付きで拡張機能を作り始め、本日、完成した。
「ブログを一週間更新していないと叱ってくれるプラグイン」だ。

ポイント


  • 今回開発したプラグインの概要
  • backgroundで本ブログのフィードを取得する
  • content_scriptsではクロスドメインリクエストができない点でハマった
 

今回開発したプラグインの概要

プラグインとして、以下の処理を実装した
①ブラウザの起動時に本ブログのフィードをJSON形式で取得する
②フィードを解読し、直近の投稿の投稿日時を得る
③現在時刻が直近の投稿の投稿日時よりも一週間以上経過していた場合、アラートで警告を出す

backgroundで本ブログのフィードを取得する

Chrome拡張ではバックグラウンドページで動作するスクリプトで様々な処理ができる[1]。
今回作ったプラグインでは、バックグラウンドページの起動時に本ブログのフィードを取得し、経過時刻の判定を行う。
ブラウザを立ち上げるたびに判定が行われ、嫌でもブログの更新を催促される仕組みだ。

content_scriptsではクロスドメインリクエストができない点でハマった

これは昔はできたらしいが、今ではChromeの仕様変更でできなくなったという[2][3]。
昨日の私はこのことを知らなかったため、content_scriptsでXMLHttpRequestを使ったフィードの取得を試みて失敗し、大いにハマった。


開発・実行環境

ブラウザ Google Chrome
バージョン: 78.0.3904.70(Official Build) (64 ビット)
起動時オプション:
--flag-switches-begin --flag-switches-end

PC
プロセッサ: 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

フォルダ構成

.
├── background.js
├── icon
│   └── icon.png
├── manifest.json
├── popup.html
└── popup.js
popup.html、popup.js、icon.pngは先日[4]と同じだ。
今回新たにbackground.jsが加わった。

コード

manifest.json

  1. {
  2. "name": "Blogger alarm",
  3. "description" : "Blogger alarm",
  4. "manifest_version": 2,
  5. "version": "1.0",
  6. "permissions": [
  7. "http://*/*", "https://*/*"
  8. ],
  9. "background": {
  10. "scripts": ["background.js"]
  11. },
  12. "browser_action": {
  13. "default_popup": "popup.html"
  14. },
  15. "icons": {
  16. "16": "icon/icon.png",
  17. "32": "icon/icon.png",
  18. "48": "icon/icon.png",
  19. "128": "icon/icon.png"
  20. }
  21. }
先日[4]のマニフェストに以下の三行を追加した。
  1. "background": {
  2. "scripts": ["background.js"]
  3. },
background.jsが今回新たに実装したスクリプトだ。

background.js

  1. window.onload = function() {
  2. var xhr = createXMLHttpRequest();
  3. var url = 'https://mamorunoblog.blogspot.com/feeds/posts/default?alt=json&orderby=published';
  4. xhr.onreadystatechange = function() {
  5. switch (xhr.readyState) {
  6. case 4: // DONE
  7. if (xhr.status != 200) {
  8. return;
  9. }
  10. // Parsing JSON.
  11. var data = JSON.parse(xhr.responseText);
  12. var limitDays = 7;
  13. if (data.feed.entry.length > 0) {
  14. var lastPost = new Date(data.feed.entry[0].published.$t);
  15. var now = new Date()
  16. let diff = now.getTime() - lastPost.getTime();
  17. diff = diff/(1000*60*60*24);
  18. if (diff > limitDays) {
  19. alert(`Your blog is not updated for ${limitDays} days!\nlast post: ${lastPost}`);
  20. }
  21. }
  22. break;
  23. default:
  24. // none.
  25. break;
  26. }
  27. }
  28. xhr.open('GET', url);
  29. xhr.send();
  30. }
  31.  
  32. function createXMLHttpRequest() {
  33. if (window.XMLHttpRequest) {
  34. return new XMLHttpRequest();
  35. }
  36. if (window.ActiveXObject) {
  37. try {
  38. return new ActiveXObject('Msxml2.XMLHTTP.6.0');
  39. } catch (e) {
  40. // none.
  41. }
  42. try {
  43. return new ActiveXObject('Msxml2.XMLHTTP.3.0');
  44. } catch (e) {
  45. // none.
  46. }
  47. try {
  48. return new ActiveXObject('Microsoft.XMLHTTP');
  49. } catch (e) {
  50. // none.
  51. }
  52. }
  53. return false;
  54. }
diffに直近の投稿の投稿日時から現在時刻までに経過した時間(日)を計算して代入する。
limitDaysは投稿間隔の閾値(日)である。
diffがlimitDaysを超えていた場合、投稿していないことを叱ってくれる。

実行結果



画像は、拡張機能をリロードしたところだ。
こんな感じでアラームを出して叱ってくれる。

ここ一週間は連日投稿しており叱られないですむ道理ではあるが、動作確認のために limitDays=1 と厳しく設定し、自ら進んで叱られてみた。
画像はリロードのタイミングでのアラームの表示だが、ブラウザのウィンドウを新しく開いた際にもアラートが出る。

今後の課題


  • Blogger API なるものがあるらしい
    あまり調べられてないが、専用のAPIがあるようだ。
    BloggerがAPIを用意しているのであれば、導入を検討する必要がありそうだ。
    使用すべき状況にあるか、ライセンス的にどうかが気になる。
     
  • 拡張機能の設定画面を実装して、閾値となる日数を設定できるようにしたい
    忙しさが変われば、目標とする投稿頻度も変わるだろう。
    そのため、limitDays を固定ではなく、任意に設定できるようにしたい。
    設定画面の実装方法の習得ができる点も魅力だ。
     
  • アイコンクリックで表示されるポップアップの表示を最適化したい
    デバッグのためアイコンクリックのポップアップに投稿一覧を表示したところ、割といい感じの一覧が出たので、正式な機能にしたい。
    表示しなくてもよい項目(例えばauthor)を削り、UIを洗練したい。
    また、タイトルをクリックすると投稿のページにジャンプするなど、実用的な機能がほしい。
     
  • 前例などを調査して、アプリストアに出したい(発展)
    あまり下調べしていないので、似たようなプラグインが既にあるかもしれない。
    そうであれば、そちらを導入する、コントリビューションを考えるなど、このプラグインを改良しアプリストアに出す以外に取るべき行動があるはずだ。
    しかし、もし似たようなプラグインが存在しないか、しても違った方向性で発展させられるのであれば、将来性がある。
    今の私はJavaScriptとChrome拡張の両方の初心者なので、アンチパターンの地雷原を駆け抜けている可能性がある。
    もうちょっとモノ作りながらJavaScriptの常識を身に着けてから深く考えたい。


感想

スピード感のある開発だった。
非常に荒削りなプラグインなので、使いながら研ぎ澄ましたい。

思ったのが、横着しないできちんと調べるのが基本だということ。
contents_scriptでクロスドメインリクエストができない問題なんかは特にそうだ。
XMLHttpRequestが動くことは先日調べてあるので、「なぜ動かないんだっ」てコードとにらめっこしてばかりいたから、ハマったのだ。
ハマった状態から抜け出すには、一度頭をリセットし、目の付け所を移す必要がある。

付録

特になし。

参考文献

  1. Chrome拡張の開発方法まとめ その1:概念編
  2. Chrome拡張のContent ScriptsからCross-Origin Read Blocking (CORB)を回避して外部のAPIと通信する
  3. Changes to Cross-Origin Requests in Chrome Extension Content Scripts
  4. Chrome のプラグインを作る! その4 本ブログのフィードを取得する

2019年10月27日日曜日

Chrome のプラグインを作る! その4 本ブログのフィードを取得する

今日は近所のクリーンデーに参加した。
普段会話しなくても、諍いなく存在を肯定できるご近所さん。
平穏のありがたみを感じる。

***

「ブログを一週間更新していないと叱ってくれるプラグイン」を作るには、ブログのフィードを取得する必要がある。
前回は、XMLHttpRequestクラスとJSONクラスを用いて、ローカルのJSONファイルを読み込んで解析したが、今回は応用として、実際に本ブログのフィードを取得する。

ポイント

  • アイコンのクリックでHTMLのポップアップを表示するには、マニフェストのbrowser_action.default_popupに呼び出すHTMLを指定する
  • 拡張機能のJavaScriptから外部URLのソースを取得する際には、マニフェストのpermissionsにURLを追加するだけでよい(クロスドメインリクエストの問題に頭を抱える必要はない)
  • 「ブロガー支援プラグイン」として、クリックで一覧表示+記事へのリンクを表示する機能などを加えたら面白いかもしれない(感想)

開発・実行環境

ブラウザ Google Chrome
バージョン: 78.0.3904.70(Official Build) (64 ビット)
起動時オプション:
--flag-switches-begin --flag-switches-end

PC
プロセッサ: 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
Visual Studio: 14.0

フォルダ構成

.
├── icon
│   └── icon.png
├── load.js
├── manifest.json
└── popup.html

popup.html は拡張機能アイコンを左クリックした際に呼ばれる。
load.js には popup.html の読み込み時に起動されるイベントハンドラを実装した。

コード

manifest.json

  1. {
  2. "name": "Blogger feed JSON parser",
  3. "description" : "Base Level Extension",
  4. "manifest_version": 2,
  5. "version": "1.0",
  6. "permissions": [
  7. "http://*/*", "https://*/*"
  8. ],
  9. "browser_action": {
  10. "default_popup": "./popup.html"
  11. },
  12. "icons": {
  13. "16": "./icon/icon.png",
  14. "32": "./icon/icon.png",
  15. "48": "./icon/icon.png",
  16. "128": "./icon/icon.png"
  17. }
  18. }
browser_action.default_popup にHTMLを指定する。
これにより、拡張機能アイコンを左クリックした際にpopup.html に記述されるポップアップを表示できる[1]。
permissionsに "http://*/*", "https://*/*" を指定することでCORSのエラーを回避できる[2][3]。

調べていて、拡張機能としてではなく、純粋にWEBページに組み込むJavaScriptからURLにアクセスする場合には、いろいろ大変そうだと思った[3]。
マニフェストを弄るだけでクロスドメインリクエストの問題に頭を抱えずに済むのだから、とてもありがたい。

popup.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charser="utf-8" />
  5. <title>Odonata Bug Hunt Blog</title>
  6. <!-- <link rel="manifest" href="/manifest.json"> -->
  7. <script type="text/javascript" src="load.js"></script>
  8. </head>
  9. <body>
  10. <div>About</div>
  11. title: <span id="id_title"></span><br />
  12. subtitle: <span id="id_subtitle"></span><br />
  13. authors: <span id="id_authors"></span><br />
  14. <br />
  15. <div>Posts</div>
  16. total posts: <span id="id_total_post"></span><br />
  17. <ul id="id_post_list"></ul>
  18. </body>
  19. </html>
ブログのタイトル、サブタイトル、著者、投稿本数は数が決まっている項目なので、spanタグを指定する。
一方で、投稿本数は変化するので、各投稿の情報の表示にはulタグを利用する。

load.js

  1. window.onload = function() {
  2. var xhr = createXMLHttpRequest();
  3. var url = 'https://mamorunoblog.blogspot.com/feeds/posts/default?alt=json&orderby=published';
  4. xhr.onreadystatechange = function() {
  5. switch (xhr.readyState) {
  6. case 4: // DONE
  7. if (xhr.status != 200) {
  8. return;
  9. }
  10. console.log('xhr.readyState: ' + xhr.readyState);
  11. console.log('xhr.status: ' + xhr.status);
  12. console.log('xhr.responseText:\n' + xhr.responseText);
  13.  
  14. // Parsing JSON.
  15. var data = JSON.parse(xhr.responseText);
  16.  
  17. // title.
  18. var elem = document.getElementById('id_title');
  19. elem.innerText = data.feed.title.$t;
  20.  
  21. // subtitle.
  22. var elem = document.getElementById('id_subtitle');
  23. elem.innerText = data.feed.subtitle.$t;
  24.  
  25. // authors.
  26. var authors = '';
  27. var i = 0;
  28. for (i = 0; i < data.feed.author.length; i++) {
  29. authors = authors + data.feed.author[i].name.$t + '(' +
  30. data.feed.author[i].email.$t + '), ';
  31. }
  32. var elem = document.getElementById('id_authors');
  33. elem.innerText = authors;
  34.  
  35. // total posts.
  36. var elem = document.getElementById('id_total_post');
  37. elem.innerText = data.feed.entry.length;
  38.  
  39. // entry.
  40. var list = document.getElementById('id_post_list');
  41. while (list.firstChild) {
  42. // Clear last result.
  43. list.removeChild(list.firstChild);
  44. }
  45. var i = 0;
  46. var show_max = 10;
  47. for (i = 0; i < data.feed.entry.length; i++) {
  48. if (i > show_max) {
  49. var li = document.createElement('li');
  50. list.appendChild(li);
  51. var node = document.createTextNode('omitted more ' +
  52. (data.feed.entry.length - show_max) + 'posts.');
  53. li.appendChild(node);
  54. break;
  55. }
  56.  
  57. // title.
  58. var li = document.createElement('li');
  59. list.appendChild(li);
  60. var node = document.createTextNode(data.feed.entry[i].title.$t);
  61. li.appendChild(node);
  62.  
  63. // sublist.
  64. var subList = document.createElement('ul');
  65. li.appendChild(subList);
  66.  
  67. // published
  68. var subLi = document.createElement('li');
  69. subList.appendChild(subLi);
  70. var node = document.createTextNode(
  71. 'published: ' + data.feed.entry[i].published.$t);
  72. subLi.appendChild(node);
  73.  
  74. // updated
  75. var subLi = document.createElement('li');
  76. subList.appendChild(subLi);
  77. var node = document.createTextNode(
  78. 'updated: ' + data.feed.entry[i].updated.$t);
  79. subLi.appendChild(node);
  80.  
  81. // authors.
  82. var authors = '';
  83. var j = 0;
  84. for (j = 0; j < data.feed.entry[i].author.length; j++) {
  85. authors = authors + data.feed.entry[i].author[j].name.$t + '(' +
  86. data.feed.entry[i].author[j].email.$t + '), ';
  87. }
  88. var node = document.createTextNode('authors: ' + authors);
  89. var subLi = document.createElement('li');
  90. subList.appendChild(subLi);
  91. subLi.appendChild(node);
  92.  
  93. // categories
  94. var categories = '';
  95. var j = 0;
  96. for (j = 0; j < data.feed.entry[i].category.length; j++) {
  97. categories =
  98. categories + data.feed.entry[i].category[j].term + ', ';
  99. }
  100. var node = document.createTextNode('categories: ' + categories);
  101. var subLi = document.createElement('li');
  102. subList.appendChild(subLi);
  103. subLi.appendChild(node);
  104. }
  105. break;
  106. default:
  107. // none.
  108. break;
  109. }
  110. }
  111. xhr.open('GET', url);
  112. xhr.send();
  113. }
  114.  
  115. function createXMLHttpRequest() {
  116. if (window.XMLHttpRequest) {
  117. return new XMLHttpRequest();
  118. }
  119. if (window.ActiveXObject) {
  120. try {
  121. return new ActiveXObject('Msxml2.XMLHTTP.6.0');
  122. } catch (e) {
  123. // none.
  124. }
  125. try {
  126. return new ActiveXObject('Msxml2.XMLHTTP.3.0');
  127. } catch (e) {
  128. // none.
  129. }
  130. try {
  131. return new ActiveXObject('Microsoft.XMLHTTP');
  132. } catch (e) {
  133. // none.
  134. }
  135. }
  136. return false;
  137. }
フィードを取得しHTMLを更新するスクリプトである。
createXMLHttpRequest関数でXMLHttpRequestクラスを作成するところは前回と一緒だが、今度は引数に指定するURLがローカルのJSONから本ブログのフィードになった。
著者とカテゴリ、投稿はJSONに配列として記述されているため、ループで処理する。

実行結果


アイコン(星印)をクリックすると、ポップアップで本ブログの投稿情報が表示される。
これでフィードが取得できることが確かめられた!

改善点

投稿のタイトルに記事へのリンクを張れば実用的になるだろう。
ほんのテストで実装したにしては、便利ツールに化けそうだ。
私はブロガーとして過去の自分の投稿を参照することが結構あるので、ブックマークとは別に素早いジャンプ機構を持つことはよいことだ。
リンクを張るだけなら、簡単な改造で済むだろう。
問題は表示する情報の選択と、表示方法の最適化だな。

投稿期間の設定とか、表示件数の上限とか、設定を変えられるようにできたらいいな。
まあ、そういう欲を出すのはアルファ版ができてからでも遅くはない。

まとめ

XMLHttpRequestクラスとJSONクラスにより本ブログのフィードをJSON形式で取得し解析した。そして、記事の公開日といった情報を取得することに成功した。

感想

左クリックの機能は考えていなかったが、投稿の一覧表示+リンクはブロガーの大きな助けになるだろう。
漠然と「ブログを一週間更新していないと叱ってくれるプラグイン」を作りたいと考えてきたが、「ブロガー支援プラグイン」として、より便利なツールにしたいと考え始めている。

付録

特になし。

参考文献

  1. Google Chrome 拡張機能を開発する 〜 ポップアップを表示するまで 〜
  2. Chrome拡張でCORS対応
  3. クロス ドメイン リクエスト (Cross-Domain Requests, XDR) の問題を理解する
  4. What does “http://*/*”, “https://*/*” and “<all_urls>” mean in the context of Chrome extension's permissions

2019年10月26日土曜日

Chrome のプラグインを作る! その3 ローカルのJSONファイルを解析する

「キリヌーク」というカッターナイフを買った。
刃にばねが仕込んであり、刃先の圧力を一定に保ち紙一枚をきれいに切り取れる。
「切る」よりも、「ひっかく」という感覚だ。
これは使いやすい。
アイデアは生活を豊かにする。

***

さて、プラグイン開発の続きを書こうと思う。
このブログのフィードを投稿日時順・JSON形式のテキストとして取得するには、以下のURLにアクセスするだけでよい[1]。

https://mamorunoblog.blogspot.com/feeds/posts/default?alt=json&orderby=published
拡張機能を実装するJavaScriptからこのフィードを取得し解析することで、最後にブログを更新した日時を取得できる。
いきなりURLにアクセスするのは難しそうなので、まずはローカルのJSONファイルを読み込む練習をしたいと思う。

ポイント

  • JavaScriptからローカルのファイルにアクセスするためには、ブラウザの起動オプションにより許可を出す必要がある
  • サーバと対話しデータを受け取るにはXMLHttpRequestクラスを用い、JSONのパースにはJSONクラスを利用する
  • アイコンの読み込みはマニフェストで指定できる

開発・実行環境

ブラウザ Google Chrome
バージョン: 77.0.3865.120(Official Build) (64 ビット)
起動時オプション:
--allow-file-access-from-files --renderer-process-limit=10 --flag-switches-begin --flag-switches-end

PC
プロセッサ: Intel(R) Core(TM) i5-5200U CPU @2.20GHz
メモリ: 8.00 GB
OS: Windows 8.1 (64bit)

注意すべきは、デフォルトではJavaScriptからローカルのファイルを読み込めないこと。
確かに、JavaScriptからローカルファイルを無制限に読み取れてしまっては、悪意のあるサイトがPCの大切なデータを物色できてしまい、大変に困る。
しかし、簡単にJavaScriptの動作確認をしたい場合に、この制限は都合が悪い。
--allow-file-access-from-files フラグを立てることでローカルのファイルを読み込むことができる[2]。
フラグが有効になっているかどうかは、chrome://version から確認できる。

フォルダ構成

├── data1.json
├── icon
│   └── icon.png
├── load.html
├── load.js
└── manifest.json
前回まではアイコンをHTMLファイルとJavaScriptファイルと同じレベルに置いていたが、階層を分けて整理した。

コード

load.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charser="utf-8" />
  5. <title>Odonata Bug Hunt Blog</title>
  6. <link rel="manifest" href="/manifest.json">
  7. <script type="text/javascript" src="load.js"></script>
  8. </head>
  9. <body>
  10. <input type="button" value="Click me!" onclick="load();">
  11. <div>Odonata Information</div>
  12. Scientific Name: <span id="id_scientific_name"></span><br />
  13. Japanese Name: <span id="id_ja_name"></span><br />
  14. Size: <span id="id_size"></span><br />
  15. Habitat: <span id="id_habitat"></span><br />
  16. </body>
  17. </html>
参考サイト様のソースを参考に実装した[3]。
ボタンを押すとJavaScriptのイベントハンドラが呼び出され、spanタグの部分がJSONから読み取られたデータに置き換えられる。
具体的には、<span id="id_scientific_name"></span>、<span id="id_ja_name"></span>、<span id="id_size"></span>、<span id="id_habitat">の部分がJSONに記されたデータに置き換えられる。

data1.json

  1. {
  2. "scientific_name":"Sympetrum infuscatum",
  3. "ja_name":"Noshime Tombo",
  4. "size":28,
  5. "habitat":"From Hokkaido to Kyuushuu"
  6. }
読み取られるJSONファイルである。

load.js

  1. var load = function loadFunc() {
  2. var xhr = createXMLHttpRequest();
  3. var url = "./data1.json"
  4. xhr.onreadystatechange = function () {
  5. switch (xhr.readyState) {
  6. case 4: // DONE
  7. // if (xhr.status != 200) {
  8. // return;
  9. // }
  10. console.log("xhr.readyState: " + xhr.readyState);
  11. console.log("xhr.status: " + xhr.status);
  12. console.log("xhr.responseText:\n" + xhr.responseText);
  13.  
  14. // Parsing JSON.
  15. var data = JSON.parse(xhr.responseText);
  16. var elem = document.getElementById("id_scientific_name");
  17. elem.innerText = data.scientific_name;
  18. var elem = document.getElementById("id_ja_name");
  19. elem.innerText = data.ja_name;
  20. var elem = document.getElementById("id_size");
  21. elem.innerText = data.size;
  22. var elem = document.getElementById("id_habitat");
  23. elem.innerText = data.habitat;
  24. break;
  25. default:
  26. // none.
  27. break;
  28. }
  29. }
  30. xhr.open("GET", url);
  31. xhr.send();
  32. }
  33.  
  34. function createXMLHttpRequest() {
  35. if (window.XMLHttpRequest) {
  36. return new XMLHttpRequest();
  37. }
  38. if (window.ActiveXObject) {
  39. try {
  40. return new ActiveXObject("Msxml2.XMLHTTP.6.0");
  41. } catch (e) {
  42. // none.
  43. }
  44. try {
  45. return new ActiveXObject("Msxml2.XMLHTTP.3.0");
  46. } catch (e) {
  47. // none.
  48. }
  49. try {
  50. return new ActiveXObject("Microsoft.XMLHTTP");
  51. } catch (e) {
  52. // none.
  53. }
  54. }
  55. return false;
  56. }
サーバと対話し、データを受け取るにはXMLHttpRequestオブジェクトを用いる[4]。
ウェブページの一部のみを更新することができ、ユーザの作業に支障をきたさない。
その名によらず、HTTP以外にもFILEやFTPといった通信方式、XML以外にもあらゆる種類のファイル形式に対応している(なぜこの名称にしたのだろうか?)。

InternetExplorerの古いバージョンではXMLHttpRequestのサポートがないため分岐処理が必要とのこと[3]。
今のところ、Chrome拡張機能の開発を見据えているため、IEをサポートする気はない。
したがって、以下のように書いてしまってもよい。
  1. var xhr = new XMLHttpRequest();
ただ、使いまわしのできるコードとするため、例に従っておく。

manifest.json

  1. {
  2. "icons": [
  3. {
  4. "src": "icon/icon.png",
  5. "sizes": "16x16","32x32","48x48","62x62"
  6. }
  7. }
アイコンはマニフェストに含めることができる。
headタグ内に直接指定するよりも、こちらのやり方のほうがリソースの記述を分離できて、私は好きだ。

実行結果

ブラウザに表示されたテキストを以下に示す。
Odonata Information
Scientific Name: Sympetrum infuscatum
Japanese Name: Noshime Tombo
Size: 28
Habitat: From Hokkaido to Kyuushuu

コンソール出力を以下に示す。
xhr.readyState: 4
load.js:18 xhr.status: 0
load.js:19 xhr.responseText:
{
  "scientific_name":"Sympetrum infuscatum",
  "ja_name":"Noshime Tombo",
  "size":28,
  "habitat":"From Hokkaido to Kyuushuu"
}

ちゃんとJSONから読み取ったデータがHTMLのspanタグ内に入った。

問題点

最初はJavaScriptで以下のエラーガードをコメントアウトせずに有効にしていた。
  1. if (xhr.status != 200) {
  2. return;
  3. }
読み込みに成功するとxhr.statusは200になるらしく[5]、それ以外は不適としてはじいてしまおうという考えのもとである。
しかし、読み込みができているにも関わらず、なぜか0となった。
おかげで10分近く頭を抱えてしまった。

対処療法的に、このエラーガードを外した。
どこか間違っているんじゃないかと思うと恐いが、まぁ動いたので、いったんは良しとする。
原因が分かったら加筆修正するかも。

感想

ローカルファイルを読み込むための方法を調べるのは、意外とめんどくさかった。
セキュリティの観点から見れば、簡単にJavaScriptからローカルファイルにアクセスできてしまうのは、確かにまずい。
今回はローカルファイルを用いてJSONファイル読み込みのテストを行ったが、本格的なプロの開発ではどのような環境で開発が行われているのか、興味が湧いた。

付録

特になし。

参考文献

  1. Blogger の フィード URL 構成 と パラメタ― まとめ
  2. Windows版chromeでローカルファイルに直接アクセスする方法[オレ得JavaScriptメモ]
  3. JavaScriptでのJSONのパーシング - JSONの読み込みと値の取得 (JavaScript プログラミング)
  4. XMLHttpRequest
  5. HTTP レスポンスステータスコード

2019年10月25日金曜日

Chrome のプラグインを作る! その2 ローカルのファイルを読み込む

今朝は雨が強く、研究室で服が濡れたまま過ごしたため寒かった。
いやな天気が続くな。

今日はHTMLとJavaScriptを触ったので記録する。

プラグインをいちいち読み込みなおしてスクリプトをテストするのは煩に堪えない。
某はフロントエンド初心者なので、簡単にHTMLでJavaScriptのテストをして、そのままプラグインのテンプレに移植するような開発をしたい。
プラグイン要素は正直皆無だが、開発の下地作りということで番号を与える。

ポイント

  • HTMLとJavaScriptのファイル分割方法
  • イベントハンドラとしてJavaScriptの関数を利用する
  • アイコンを指定するとちょっとかっこいい

開発・実行環境

下記の環境でコードを実行し、動作を確かめた。

ブラウザ Google Chrome
バージョン: 77.0.3865.120(Official Build) (64 ビット)

PC
プロセッサ: Intel(R) Core(TM) i5-5200U CPU @2.20GHz
メモリ: 8.00 GB
OS: Windows 8.1 (64bit)

コード

test.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charser="utf-8" />
  5. <title>Test page</title>
  6. <link rel="icon" href="iocn.png" sizes="16x16" type="image/png" />
  7. <link rel="icon" href="icon.png" sizes="32x32" type="image/png" />
  8. <link rel="icon" href="icon.png" sizes="48x48" type="image/png" />
  9. <link rel="icon" href="icon.png" sizes="62x62" type="image/png" />
  10. <script type="text/javascript" src="test.js"></script>
  11. </head>
  12. <body>
  13. <input type="button" value="Click me!" onclick="test()">
  14. </body>
  15. </html>
このローカルファイルをブラウザから読み込む。
ホームページを実際に作るとなると、<head>タグ内にSEO対策としてきちんと記載するべき項目がたくさんあるようだが[1]、今回はローカルのちょっとしたテストなので、エンコーディング、タイトル、アイコンだけで済ませる。

  1. <input type="button" value="Click me!" onclick="test()">
ボタンクリックのイベントが発生した際、test.jsに定義されたtest関数が呼ばれる[2]。

test.js

  1. var test = function testFunc() {
  2. alert("Hello World.")
  3. }
たったこれだけ。

実行結果


ボタンを押すとアラートを出す。

感想

微速前進。

付録

特になし。

参考文献

  1. head内に書くべきタグを総まとめ:SEO対策に有効なものは?
  2. HTML文章内でのJavaScript記述方法

2019年10月24日木曜日

Chrome のプラグインを作る! その1 拡張機能のテンプレを作る

こんばんは!
天気が悪いとテンションもモチベーションも下がります。
ブログの更新頻度も落ちそうですが、本ブログは毎週投稿を目標としています。

ブログを一週間更新していないと叱ってくれるプラグインがあればいい。
そんな思いから、Chrome ブラウザのプラグイン制作、始めました。

ポイント

  • JavaScript をブラウザの起動ページにフックする
  • JavaScript におけるコンソール、アラート
  • JavaScript における時刻の取得

開発・実行環境

下記の環境でコードを実行し、動作を確かめた。

ブラウザ Google Chrome
バージョン: 77.0.3865.120(Official Build) (64 ビット)

PC
プロセッサ: Intel(R) Core(TM) i5-5200U CPU @2.20GHz
メモリ: 8.00 GB
OS: Windows 8.1 (64bit)

コード

とりあえず、こちらの記事[1]を参考にさせていただき、手っ取り早く環境を整えた。
アイコンはここには載せないが、適当なものを用意した。

JavaScript によるプラグインのひな型

  1. window.onload = function() {
  2. // Who am I.
  3. console.log('Chrome Plugin Test');
  4.  
  5. // Get current time and display it to console.
  6. var time = new Date();
  7. const options = {
  8. year: 'numeric',
  9. month: 'narrow',
  10. day: 'numeric',
  11. hour: 'numeric',
  12. minute: 'numeric',
  13. second: 'numeric',
  14. timeZoneName: 'short'
  15. };
  16. const timeFromat = new Intl.DateTimeFormat('ja-JP', options);
  17. console.log(timeFromat.format(time));
  18. alert(timeFromat.format(time));
  19.  
  20. // Get time object members.
  21. var year = time.getYear() + 1900;
  22. var month = time.getMonth() + 1;
  23. var date = time.getDate();
  24. var weekDay = time.getDay();
  25. var hour = time.getHours();
  26. var min = time.getMinutes();
  27. var sec = time.getSeconds();
  28. console.log(year);
  29. console.log(month);
  30. console.log(date);
  31. console.log(weekDay);
  32. console.log(hour);
  33. console.log(min);
  34. console.log(sec);
  35. }
とりあえず以下のことを同時にやった
①プラグインから呼び出される JavaScript の関数を実装
後述のマニフェストでこのファイルを指定しておく。

②コンソールにログを出力
コンソール出力には console.log を呼ぶ。
先に F12 キーを押してデバッグ画面を出しておく。

③ローケル時刻の取得
Intl.DateTimeFormat について。
Intl は国際化 API の名前空間という。
ローケルに依存した処理がここにまとまっているらしい[2]。
Intl.DateTimeFormat はローケル時刻のフォーマットを扱うクラスのコンストラクタである。
コンストラクタの引数は、ローケルの指定と、オプションが多数。
表示形式はオプションを追加することで変更できる[3]。
注意点として、year は 1900 年オリジン、month は 0 オリジンである。

(C 言語といった手続き型言語ではフォーマットを引数とした関数を呼び出すところだが、JavaScript はオブジェクト指向だけあって、フォーマットをオブジェクトにしてしまったということなのだろうか)

④アラートの表示
alert を呼べばOK

マニフェスト

  1. {
  2. "name": "Chrome plugin test",
  3. "description" : "Base Level Extension",
  4. "manifest_version": 2,
  5. "version": "1.0",
  6. "browser_action": {
  7. "default_icon": "icon_32.png"
  8. },
  9. "content_scripts": [
  10. {
  11. "matches": ["https://www.google.com/", "https://www.google.com/"],
  12. "js": ["content_scripts.js"]
  13. }
  14. ]
  15. }
とりあえず参考[1]に従いコピペ編集した。
www.google.com にアクセスすると、スクリプトが実行される。
ブラウザの起動ページをここに設定しておけば、ブラウザの立ち上げをフックすることに近いのではないだろうか。

実行結果


わーい、動いた!
よしよし、ちゃんとアラートが出力されたぞ。


コンソール出力もばっちりだ。
なんかエラーが出てるが、ようわからんし、とりあえず現段階では無視でいいか

まとめ

最低限、JavaScript、マニフェスト、アイコンの3ファイルを用意するだけでプラグインが作れることが分かった。

感想

ものすごく敷居が低く、1時間もかからずに実装できたと思う。
分かりやすい入門記事を書いてくださった Qiita 投稿者の方には本当に感謝です。

JavaScript を扱ったは今回が初めてだが、雰囲気分かるものだなと思った。
オブジェクト指向という点では、C++ のノリがどこまで通じるのか、だな。

付録

特になし。

参考文献

  1. 外部に公開しないミニマムなchrome拡張機能を作るのは1時間も使わずにできる
  2. Intl
  3. Intl.DateTimeFormat

2019年10月19日土曜日

WM_NOTIFY について

WM_NOTIFY についてテクニカルノート[1]を読んだのでまとめた。

プロセス管理ツールを作りたい
→ データ指向の観点から配列でデータを渡したい
→ ListViewに列単位の一括でセット・ゲットできるラッパーを作りたい
→ WM_NOTIFYとはなんぞや(いまここ)

ポイント

  • Windows 3.x までの Windows API で通知メッセージの送信には WM_COMMAND が用いられてきたが、その方法にはいくつかの問題点があった
     
  • Win 32 から新しくコントロールが追加された際に、 WM_NOTIFY を用いた通知メッセージの送信機構が採用された
     
  • WM_NOTIFY メッセージの捌き方について例示する
     

WM_COMMAND について

  • Windows 3.x までの Windows API では、コントロールからのメッセージをウィンドウに通知する際に WM_COMMAND メッセージを用いた
     
  • WM_COMMAND では、wParam にコントロールの ID と通知コード、lParam にコントロールのハンドルが格納される
     
  • WM_COMMAND では wParam と lParam が埋まってしまい、新たに追加の通知コードを増設することが困難であった
     
  • 空きが無い点とは別に、WM_COMMAND には送れる情報が少ないという問題点があった
    (たとえば、マウスがクリックされた際に送信される BN_CLICKED では、マウスカーソルの場所などの情報を送ることができない。追加のデータを送るためには、別のメッセージを設ける必要がある。)

WM_NOTIFY について

  • Windows 3.1 までは WM_COMMAND で事足りていたが、Win32から複雑で洗練されたコントロールが追加されるこにとなった。Windows API の設計者は、新たにメッセージを追加するのではなく、WM_NOTIFY というたった一つのメッセージを追加することでコントロールの追加に対応した
     
  • WM_NOTIFY では wParam にメッセージを送信したコントロールの ID が格納される。一方で、lParam にはNMHDR 構造体か、コントロール依存のより大きな構造体(第一メンバがNMHDR 構造体でありキャストが可能)である
     
  • NMHDR 構造体のメンバはコントロールの ID とウィンドウハンドル、通知コードである
     

WM_NOTIFY メッセージの捌き方

ListView コントロールのメッセージ処理を例にとり、WM_NOTIFY の捌き方を述べる。

ListViewの例

  1. // In window procedure
  2. case WM_NOTIFY:
  3. return OnNofity(hwndDlg, (NMHDR*)lParam);
  4.  
  5. // Function called in WM_NOTIFY
  6. LRESULT OnNofity(HWND hwndDlg, NMHDR* nmhdr) {
  7. assert(nmhdr);
  8. if (nmhdr->hwndFrom == GetDlgItem(hwndDlg, IDC_LIST1)) {
  9. switch (nmhdr->code) {
  10. case LVN_COLUMNCLICK: {
  11. // Process for column click.
  12. LPNMLISTVIEW nmlistview = (LPNMLISTVIEW)nmhdr;
  13. } break;
  14. case NM_CLICK: {
  15. // Process for single click.
  16. } break;
  17. case NM_DBLCLK: {
  18. // Process for double click.
  19. } break;
  20. default:
  21. // none.
  22. break;
  23. }
  24. }
  25. return TRUE;
  26. }
  27.  

ウィンドウプロシージャ(もしくはダイアログプロシージャ)の switch 文の case ラベルに WM_NOTIFY を追加する。
WM_NOTIFY にはメッセージクラッカが用意されていないようなので、適当に関数を作る(好みの問題)。
GetDlgItem でダイアログ上 ListView コントロールのリソース ID からウィンドウハンドルを出力している。
NMHDR (lParam) 構造体には通知メッセージ送信元のウィンドウハンドルが格納されているので、これと ListView コントロールのウィンドウハンドルを比較し、一致するのであれば ポインタを LPNMLISTVIEW 構造体にキャストする。
code は通知の種類であり、ListView の場合は LVN_COLUMNCLICK (ListView に限定)や、NM_CLICK と NM_DBLCLK (コントロール間で共通) といったコードがある[2]。

まとめ

Win32 以降のコントロールの通知メッセージは WM_NOTIFY で捌く。
WM_NOTIFY にはメッセージクラッカが用意されていない。

感想

自前でラッパークラスを作ろうとすると、WM_NOTIFY の処理に悩む。
分かりやすくカプセル化できない?

へんちくりんなことをせず、素直にメッセージ処理はクラス外部のウィンドウ(ダイアログ)プロシージャで行い、一般化できるコントロールの管理とデータ処理はクラスで行うようにするなど、適度に分けるのが良いのかな?

作りたいアプリケーションのデータ設計の分析と、Windows API への十分な理解が大事なんやな。

付録

WM_NOTIFY の処理をしているコードの例はここに。

参考文献

  1. TN061: ON_NOTIFY and WM_NOTIFY Messages
  2. List View Notifications

2019年10月13日日曜日

64ビット整数型について

64ビット整数型についてのあれこれです。

ポイント

  • FILETIME構造体、ULONGLONG型、ULARGE_INTEGER型の変換は簡単だ。ULONGLONG型を基準に考えるのが良いかもしれない
     
  • __int64型について。ULONGLONG型の正体はunsigned __int64型である
     
  • __int64型のオーバーフローしない加算・乗算について。キャストのルールは16ビット整数や32ビット整数と同じように考えて良いようだ

開発・実行環境

下記の環境でコードを実行し、動作を確かめた。

プロセッサ: 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
Visual Studio: 14.0

ワンポイント

テスト用の関数とマクロ


  1. #define show(EXPRESSION) (_show(#EXPRESSION, (EXPRESSION)))
  2. void _show(const char* text, unsigned __int64 p) {
  3. printf("%s\n", text);
  4. printf(" 0x%I64x\n", p);
  5. printf("\n");
  6. }

本題に入る前に、工夫した点について説明する。
このマクロと関数を用いることで、プログラムの計算式と結果を両方同時にコンソールに表示することができる。
符号なし64ビット整数 unsigned __int64 の16進数のフォーマット指定子は%I64xを用いる[1]。

FILETIME、ULONGLONG、ULARGE_INTEGER

CPU時間を取得するGetSystemTimesなどのAPI関数は、測定したCPUのクロックサイクルをFILETIME構造体を通してプログラマに伝える。
FILETIME構造体をそのまま四則演算に用いることはできない。
加算や減算といった演算を行うためには、一度ULONGLONG型もしくはULARGE_INTEGER型に変換するのが便利である。

ULONGLONG

  1. ULONGLONG a;
  2. show(a = 0x0123456789abcdef);

ULONGLONG型はunsigned __int64のシノニム[2]である。
これを実行すると以下のようになる。

  1. a = 0x0123456789abcdef
  2. 0x123456789abcdef

ULARGE_INTEGER 

  1. ULARGE_INTEGER b;
  2. show(b.QuadPart = a);
  3. show(b.LowPart);
  4. show(b.HighPart);

ULARGE_INTEGER は64ビット整数を含む共用体である[3]。
QuadPartメンバはULONGLONG型であるため、そのまま代入できる。
LowPartメンバとHighPartメンバは、それぞれ上位32ビットと下位32ビットである。
これを実行すると以下のようになる。

  1. b.QuadPart = a
  2. 0x123456789abcdef
  3.  
  4. b.LowPart
  5. 0x89abcdef
  6.  
  7. b.HighPart
  8. 0x1234567

FILETIME

  1. FILETIME c;
  2. show(c.dwLowDateTime = (DWORD)(a & 0xffffffff));
  3. show(c.dwHighDateTime = (DWORD)(a >> 32));
  4. show(((unsigned __int64)c.dwHighDateTime << 32) | c.dwLowDateTime);

FILETIME型はULARGE_INTEGER型の同様、上位32ビットと下位32ビットごとにアクセスできるメンバを持つ[4]。
しかしながら、ULARGE_INTEGER型のQuadPataメンバのように一発で64bit整数を取得できるメンバが存在しない点が異なっている。
そのため、一度ULONGLONG型変数に代入する[5]。
これを実行すると以下のようになる。

  1. c.dwLowDateTime = (DWORD)(a & 0xffffffff)
  2. 0x89abcdef
  3.  
  4. c.dwHighDateTime = (DWORD)(a >> 32)
  5. 0x1234567
  6.  
  7. ((unsigned __int64)c.dwHighDateTime << 32) | c.dwLowDateTime
  8. 0x123456789abcdef

このキャスト方法が正解である。
以下のようにやろうとすると、下位32ビット分しか計算できずに失敗する(コンパイラに警告される)。

  1. show((unsigned)(c.dwHighDateTime << 32) | c.dwLowDateTime);
  2. show((c.dwHighDateTime << 32) | c.dwLowDateTime);

さて、ULONGLONG型、FILETIME型、ULARGE_INTEGER型についての説明が終わった。
これらの64ビット整数を扱う型はすべてunsigned __int64型がもとになっていることが分かった。
よって、64ビット整数の演算について考えるということは、__int64型の計算について考えることであるといってもよいだろう(良いのか?)。
ここではそういうことにする。

__int64型

__int{8|16|32|64}型はビット長指定のある整数型である[6]。
ポータブルなコードを実現するための型名である。

__int64型のオーバーフローしない加算・乗算

本題である。
キャストの仕方によっては、64ビットで計算されなくなってしまう[7]。
とりあえず、例にならい加算と乗算についてオーバーフローしないキャストの仕方を調べた。
キャストについては奥深いものがある[7]ので、落とし穴を把握しておくことは大切だ。

加算のテストコード

  1. unsigned int p = 0x10000000;
  2. unsigned int q = 0xffffffff;
  3.  
  4. printf("p = 0x%8x\n", p);
  5. printf("q = 0x%8x\n", q);
  6. printf("\n");
  7.  
  8. show(p + q);
  9. show((unsigned __int64)p + q);
  10. show((unsigned __int64)(p + q));
  11. show((unsigned __int64)p + (unsigned __int64)q);

pとqはともに32ビットの符号なし整数である。
キャストなしで加える、加えて括弧なしでキャスト、加えて括弧してキャスト、キャストしてから加える、の四通りのパターンを試す。

加算の実行結果

  1. p = 0x10000000
  2. q = 0xffffffff
  3.  
  4. p + q
  5. 0xfffffff
  6.  
  7. (unsigned __int64)p + q
  8. 0x10fffffff
  9.  
  10. (unsigned __int64)(p + q)
  11. 0xfffffff
  12.  
  13. (unsigned __int64)p + (unsigned __int64)q
  14. 0x10fffffff

実行結果より、加えて括弧なしでキャスト、キャストしてから加える、のパターンのみオーバーフローしなかった。

乗算のテストコード

  1. unsigned int r = 0x80000000;
  2. unsigned int s = 2;
  3. printf("r = 0x%8x\n", r);
  4. printf("s = %d\n", s);
  5. printf("\n");
  6.  
  7. show(p * r);
  8. show((unsigned __int64)p * r);
  9. show((unsigned __int64)(p * r));
  10. show((unsigned __int64)p * (unsigned __int64)r);

rとsはともに32ビットの符号なし整数である。
キャストなしで掛ける、掛けて括弧なしでキャスト、掛けて括弧してキャスト、キャストしてから掛ける、の四通りのパターンを試す。

乗算の実行結果

  1. r = 0x80000000
  2. s = 2
  3.  
  4. r * s
  5. 0x0
  6.  
  7. (unsigned __int64)r * s
  8. 0x100000000
  9.  
  10. (unsigned __int64)(r * s)
  11. 0x0
  12.  
  13. (unsigned __int64)r * (unsigned __int64)s
  14. 0x100000000
  15.  

実行結果より、掛けて括弧なしでキャスト、キャストしてから掛ける、のパターンのみオーバーフローしなかった。

まとめ

FILETIME型、ULONGLONG型、ULARGE_INTEGER型などいろいろあるが、元はunsigned __int64型である。
この性質を最も直線的に示す型がULONGLONG型であるため、パフォーマンス測定系のクラスを作成する際には、ULONGLONG型を基底として用いるのがわかりやすいだろう。

あと、64ビット整数のキャストについては、16ビット整数や32ビット整数に当てはまるルールがそのまま当てはまることが分かった。
何か特別なことをしなくても、自然にコーディングすれば事故は起こらないだろう。

感想

今回も脱線してしまった。
CPUパフォーマンス測定はなかなか完了しないなぁ。

付録

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

参考文献

  1. Format specification syntax: printf and wprintf functions
  2. ULONGLONG
  3. ULARGE_INTEGER 
  4. FILETIME 
  5. INFO: Working with the FILETIME Structure
  6. __int8, __int16, __int32, __int64
  7. VC++の__int64の罠にハマった\(^o^)/
  8. 少し詳しい型変換の説明

エディットコントロールについて

台風一過。

エディットコントロールについて調べたこと、考えたことをまとめる。

ポイント

  • 基本的なエディットコントロールの使用方法は、ユーザもしくはプログラムからテキストを範囲選択した状態で、選択範囲に対する操作をメッセージで指示すること
     
  • 全体と範囲選択に対する操作を混同してはならない
      
  • printf的な書式指定のできる出力関数を用意しておくと便利

開発・実行環境

下記の環境でコードを実行し、動作を確かめた。

プロセッサ: 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
Visual Studio: 14.0

コード

状態指定系、範囲選択系、テキスト取得系、テキスト編集系にカテゴリ分けして、エディットコントロールへのメッセージの送り方をまとめた。
主に参考にさせていただいたのは、こちらのサイト様です[1][2]。

状態指定系

  1. SendMessage(hEdit, EM_SETREADONLY, FALSE, 0); // 読み取り専用の解除(入力を許可する)
  2. SendMessage(hEdit, EM_SETREADONLY, TRUE, 0); // 読み取り専用の指定(入力を禁止する)
  3. ShowWindow(hEdit, SW_SHOW); // エディットコントロールの表示
  4. ShowWindow(hEdit, SW_HIDE); // エディットコントロールの非表示

範囲選択系

  1. SendMessage(hEdit, EM_SETSEL, 0, -1); // 全体を選択
  2. SetFocus(hEdit); // フォーカスを合わせる
  3.  
  4. // 末尾にカーソルを移動した状態でフォーカスを合わせる
  5. int index = GetWindowTextLength(hEdit);
  6. SetFocus(hEdit);
  7. SendMessage(hEdit, EM_SETSEL, (WPARAM)index, (LPARAM)index);

テキスト取得系

  1. SendMessage(hEdit, WM_COPY, 0, 0); // 選択範囲をコピーする
  2. SendMessage(hEdit, WM_CUT, 0, 0); // 選択範囲をカットする
  3. SendMessage(hEdit, WM_GETTEXT, (WPARAM)dst_size, (LPARAM)dst); // 選択範囲の文字列をクリップボードを介さずに取得する

テキスト編集系

  1. SendMessage(hEdit, WM_PASTE, 0, 0); // 選択範囲に貼り付けする
  2. SendMessage(hEdit, WM_CLEAR, 0, 0); // 選択範囲をクリアする

取得系、編集系のコマンドはユーザまたはプログラムにより選択された範囲に対してのみ実行される。
選択がなされていない場合には、何事も起こらない。
特にWM_CLEARなどはややこしいと思う。
端末でclearコマンドを入力すればバッファがクリアされるので、同じようにクリアされるのかと、暗黙の内に誤った認識を持っていた。

  1. void EditControl::Set(const wchar_t* format, ...) {
  2. wchar_t buffer[EDIT_BUFFER_MAX] = {0};
  3. va_list args;
  4. va_start(args, format);
  5. vswprintf_s(buffer, ARRAYSIZE(buffer), format, args);
  6. SetFocus(hEdit);
  7. SendMessage(hEdit, WM_SETTEXT, (WPARAM)0, (LPARAM)buffer);
  8. return;
  9. }
エディットコントロールの中身を丸ごと書き換える。
可変長引数を用いて実装。
可変長引数については、先日説明した[3]。
printfと同じように使うことができる。

  1. void EditControl::Add(const wchar_t* format, ...) {
  2. wchar_t buffer[EDIT_BUFFER_MAX] = {0};
  3. va_list args;
  4. va_start(args, format);
  5. vswprintf_s(buffer, ARRAYSIZE(buffer), format, args);
  6. int index = GetWindowTextLength(hEdit);
  7. SetFocus(hEdit);
  8. SendMessage(hEdit, EM_SETSEL, (WPARAM)index, (LPARAM)index);
  9. SendMessage(hEdit, EM_REPLACESEL, 0, (LPARAM)buffer);
  10. return;
  11. }
エディットコントロールにすでに含まれるテキストはそのままに、末尾にテキストを新たに追加する。
MSDNにテキストの追加方法に関する説明がある[4]。

まとめ

とりあえずクラス化した。
選択範囲に対する操作が基本のようだが、全体に対する操作として一まとめにした方がプログラマには優しいかもしれない。

付録


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


参考文献

  1. エディットコントロール
  2. エディットメッセージ
  3. 可変長引数について
  4. How To Programatically Append Text to an Edit Control

2019年10月10日木曜日

可変長引数について

日が短くなってきた。
ランニングにはちょうどよい季節だ。

さて、可変長引数について調べたのでまとめようと思う。

可変長引数とは?


  • C言語の標準ライブラリ関数である printf や scanf のように任意の個数をとることができる引数のことを、可変長引数、もしくは動的引数という[1]。これらは決して不思議な力でライブラリのみに使用の許された魔法の機能なんかではなく、ユーザーも可変長引数を持つ関数を定義して使うことができる。
     
  • 宣言は、たとえば以下のようにやる
    1. void function(int argc, ...);
    ... が可変長の引数が入るということを示す。
    引数の並び順として、最初に数の固定された引数が入り、後に数の可変な引数が入る。
    数の固定された引数は二個でも三個でも良いが、必ず一つは用意しなければならない。
     
  • 可変長引数の利用では、以下の専用のマクロを用いることになっている。
    1. va_start
    2. va_copy
    3. va_arg
    4. va_end
    これらのマクロは stdarg.h に定義されており、ISO C99に準拠した実装となっている。vargs.h にも定義がなされているが、そりたは ANSI C89 で書かれたコードの互換性のためにあり、現在では非推奨となっている[2]。

    さて、MSDNの解説によると、/clr オプションを付けてコンパイルした場合、native と CLR の違いのために予期しない結果を生む場合があるとのこと。
    CLRとはなんぞや?と思い調べてみたが、どうやら .Net Framework の土台となっているモノらしい。
    .NET Frameword についても調べないといけないようなので、これについてはまた別の機会に調べたいと思う。


開発・実行環境

下記の環境でプログラムを書いて動作を確かめた。

プロセッサ: 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

コード

デモ関数

  1. void print_va_collect(int arg0, ...) {
  2. va_list arg_ptr;
  3. va_start(arg_ptr, arg0);
  4. printf("va 0:%d\n", va_arg(arg_ptr, int));
  5. printf("va 1:%f\n", va_arg(arg_ptr, double));
  6. printf("va 2:%f\n", va_arg(arg_ptr, double));
  7. printf("va 3:%c\n", va_arg(arg_ptr, char));
  8. printf("arg5:%s\n", va_arg(arg_ptr, const char*));
  9. va_end(arg_ptr);
  10. }

試しに書いてみた、可変長引数を使う関数である。
va_list型は引数リストのポインタであるらしい。
va_start では arg_ptr に可変長引数の一個前の引数のポインタを渡す。
(ポインタをセットするマクロであることは分かったが、 &変数名 ではなく 変数名 としているのは生理的に気持ちが悪い。規格がそうなっているのだから文句を言っても仕方がないのだが…)

呼び出す側

  1. print_va_collect(0, 128, 0.1F, 0.2L, 'a', "abc");

固定の第一引数は、今回は特に意味を持たせていないので適当に 0 を入れておく。
あとは、128 (int)、0.1F (float)、0.2L (double)、'a' (char)、"abc" (const char*) を可変引数に入れる。
注意点は、可変長引数に float 型を入れる際は double に変換することである。
以下のようなことはできない(やってみたけど、壊れた動き方をした)。
  1. va_arg(arg_ptr, float)

余談だが、標準変換では、int 型よりも小さい short 型などは int 型に変換され、float 型は double 型に暗黙に変換される[3]。
scanf 関数の書式指定子が float 型には %f、double 型には %lf を用いていることと混同してはならない。
printf 関数に float 型と double 型を入れたとして、その書式指定子はともに %f なのである。
つい最近まで、この違いが分かっていなかった。
初心者にありがちな落とし穴かもしれない。
閑話休題。

感想

printf のような任意書式で情報を提示する機能を可変長引数を用いて実装する機会は多い。
例えば、メッセージボックスやエディットコントロールに表示する場合など。
この可変長変数という機能を用いることで、いちいちその場で sprintf でバッファに文字列を格納するめんどくさい処理を関数化することができる。

付録

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


参考文献


  1. たとえば 動的引数
  2. va_arg, va_copy, va_end, va_start
  3. va_arg()

2019年10月6日日曜日

【プロセス管理ツール】その2 タスクマネージャについて【調査】

Windows 8 のタスクマネージャについて調べた。
情報源は、Building Windows 8ブログの投稿[1]である。

要旨

  • 使用状況の調査と分析
    Windows 8 のタスクマネージャを開発するに当たり、カスタマーアンケートの結果とユーザから収集した情報を合わせ、Windows 7のタスクマネージャが使用されるシナリオを分析した。
     
  • Windows 8 タスクマネージャには以下の三つの目的が定められた
    • 使用される状況に合わせ情報表示を最適化する

      状況1:特定のアプリケーションを探して閉じる
      これは一般ユーザが使用することを想定している。
      この状況では、サードパーティ製ツール(Process Explorerなど)のように詳細な情報を大量に表示してしまうと、ユーザを驚かせてしまう。
      Minimalist experience なデザインが必要である。

      状況2:リソース使用量を調べ、炎上しているプロセスを探してKillする
      これはパワーユーザが使用することを想定している。

      それぞれの状況に対応して、素早く効率的にプロセスを終了させるシナリオ(シナリオ1)、パフォーマンスの問題を診断するシナリオ(シナリオ2)が主なシナリオとして設定された。
      シナリオに応じて最適な情報を提供することが必要である。
       
    • モダンなデザインと機能面のゴールを達成する
      情報デザインとデータ可視化に焦点を当てる。
      シナリオの要請があるにもかかわらず、従来のタスクマネージャではカバーできずにリソースモニタやプロセスエクスプローラをユーザが別に起動していたような機能まで、ちゃんとカバーできるようにする。
       
    • 従来の機能を損なわない
      シナリオに沿ってタスクマネージャを作り変える際に、Windows 7まであった主流のシナリオから漏れてしまうマイナーな機能を損なうことが無いようにする。
       
  • シナリオごとの設計への反映
    設計に当たっては、設定されたシナリオが満たされるような機能が盛り込まれた。
    また、情報の表示を確かめるため、アイトラッキングの技術を駆使して複数の表示方法をテストするなどの努力がなされた(参考元に動画あり[1])
    • シナリオ1:素早く効率的にプロセスを終了させる
      • 一般ユーザ向けの簡単なUI
      • 「Fewer details」ボタンで表示
      • 反応がないアプリケーションが分かる
      • 無駄を省く。タブ、メニューバー、停止できないプロセスは表示しない
      • アプリケーションの停止に当たり、意思確認のダイアログを表示しない
         
    • シナリオ2:パフォーマンスの問題を診断する
      • パワーユーザ向けの詳細なUI
      • 「More details」ボタンで表示
      • リソース使用量の順位がパッと見でわかるヒートマップ
      • ネットワークとディスクの列を追加
      • リソース使用量が閾値を超えると、リストのキーが着色される
      • プロセスをアプリケーション、バックグラウンドプロセス、Windowsプロセスの三つのカテゴリに分けて一覧表示できる
      • モジュールファイル名ではなく、分かりやすい名前で表示
      • プロセスを右クリックすることで表示されるポップアップメニューからオンライン検索が可能
      • アプリのトップレベルウインドウでサービスをグループ化して表示
      • PIDごとにサービスをまとめて表示

感想

Windows 8が発売された当初は、Windows 7に比べて良い印象を持たない人が周りに多かった。
起動時の画面が見慣れたデスクトップからスタートに置き換えられたり、シャットダウン方法が分かりづらかったり、スタートが消えてなくなっていたりと、Windows 7に馴染んでいたユーザにとって戸惑うところが多かったと思う。

しかし、バージョンアップするということは、基本的に良くなっているはずなのである。
今回調べてみて、タスクマネージャについては、ユーザの視点に配慮した変更がなされており、非常に良い印象を持った。

正直に言うと、プロセスエクスプローラの方がタスクマネージャよりも高機能なのだから、プロセスエクスプローラについて調べ終えた今、タスクマネージャについて調べる必要があるかどうか疑問に思うところがあった。
予定として調べると書いてしまった[2]ため、仕方なく調べたのである。

結果として、調べてみて良かったと思う。
調べる中で、従来のバージョンの使用状況を分析し、ユーザが使用するシナリオを練り直し、機能面や情報の表示でテストをしっかりとやるというソフトウェア開発の流れが良く分かったからだ。

参考文献









2019年10月2日水曜日

【プロセス管理ツール】その1 Process Explorerについて【調査】

プロセス管理ツールを自作するための調査として、Process Explorerについて調べた。

ポイント3つ

独断と偏見で3つのポイントをあげる。
 
  • Microsoftが無償で提供するツールである
    MicorsoftはWindows Sysinternalと呼ばれる開発者向けのツール群を無償で公開している[1]。
    これらのツールは、システムやアプリケーションの管理、トラブルシューティング、診断といった場面で役立つ。
    Process ExplorerはWindows Sysinternalの一つである。
     
  • 高機能なプロセス管理ツールである
    CPU使用率やメモリ、I/Oといったタスクマネージャで表示されるシステム情報に加え、プロセスがロードしたDLLやファイルの一覧を表示することができる。
    また、ファイルやフォルダをロックしている犯人プロセスを見つけることができる「検索」機能や、ウィンドウにアイコンをドラッグ&ドロップすることでプロセスを選択できる「照準」機能といった、PCのトラブルシューティングに役立つ機能がある[2]。
     
  • 高いカスタム性を持つ
    プロセス一覧に表示する項目の設定や、プロセスの状態などを示す色の設定、下部ペインの表示・非表示の切り替えなど、細かいカスタマイズが可能である。

ショートカット

よく使いそうなもの。

機能 ショートカット
DLLモードに切り替え Ctrl+D
ハンドルモードに切り替え Ctrl+H
下部ペインの表示/非表示 Ctrl+L
リフレッシュ F5
情報の保存 Ctrl+S、Ctrl+A
アプリケーションの起動 Ctrl+R
システム情報の表示 Ctrl+I
プロセスツリーの表示 Ctrl+T
検索 Ctrl+F
ヘルプ F1
プロセスの停止 Del
プロセスの一時停止 Shift+Del

プロセスツリーの配色

デフォルトの配色は以下のようになっている。
設定は Optioos > Configure Colors... から。



より詳しいこと

使いこなすうえでのコツなどは、こちらの記事[3]などが参考になりそう。
あとは、マニュアルに全部書いてあるハズ。

感想

良い機会なので、Process Explorerをタスクマネージャに置き換えた。
検索機能、照準機能、システム情報の表示と保存など、ぜひとも使いこなしたい。

一方で、新たな疑問が出てきた。

  • CPUの稼働を把握するのにCPU使用率が用いられているが、実はIPC(Instructions per cycle)の方が指標として適切なのではないか[4]
  • タスクマネージャとProcess Explorerでシステム情報の算出方法は同じか、異なるかどうか
  • Windows APIのパフォーマンスカウンタから、どのような情報を取得できるのだろうか

など。

参考文献

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. デスクトップのスナップショット