2010/10/27

設定ファイルにウィンドウ位置などの情報を保存する

ウィンドウを表示して入力の受け付けや情報の表示を行うタイプのプログラムでは、ウィンドウを表示する際、前回のウィンドウの位置・サイズを記憶して再現してくれるようにすると便利です。

このような設定情報の保存を行う際には、pref()関数を使います。
この関数は、適切なユーザー設定ディレクトリを自動認識して、設定ファイルを表すFile/Directoryオブジェクトを返してくれます。

設定ファイルの形式は、JavaScriptのオブジェクトリテラルの表記と同じ書式のテキストで保存するJSONという形式を使うと良いでしょう。
FileオブジェクトのloadJSON()やsaveJSON()というメソッドを使えば、簡単にオブジェクトとして読み込んだり保存できます。

下記の例は、ウィンドウを閉じようとしたときに位置とサイズを記憶して次回実行時に再現するだけのスクリプトです。
var config=(pref("windowPosTest.json")||{loadJSON:function()({})}).loadJSON();
var w=require('Window').Window.create({
 title:"test",
 width:100,height:100,
 x:config.windowX||0, y:config.windowY||0,
 events:{
  close:function(){
   config.windowX=this.x;
   config.windowY=this.y;
   pref("windowPosTest.json",true).saveJSON(config);
  },
 },
}).show();;

最初のpref()では、既に存在している設定ファイルを表すFileオブジェクトを取得しています。
この時、ファイルが存在していなくてnullが返された場合でもエラーにならずにデフォルトのオブジェクトがセットされるようにしておく必要があります。

Windowオブジェクトのcloseイベントでは、設定オブジェクトにウィンドウの現在位置をセットして保存を行っています。
保存時のpref()の呼び出しでは、第2引数をtrueにする必要があります。

スクリプトが不要になったらプログラムのディレクトリを削除するだけで完全に削除できるように、通常はスクリプトのあるディレクトリ内の「users」ディレクトリにユーザー名のディレクトリを作って保存されます。
しかし、スクリプトのあるディレクトリが書き込み禁止になっていたりする場合は、Application Dataディレクトリ内のスクリプトファイル名を名前にしたディレクトリが保存先として選択されます。
また、スクリプトと同じディレクトリに「config」というディレクトリがあれば、そちらが保存先になります。これはプログラムをリムーバブルメディアに入れて持ち出して複数の環境で利用したい場合などのための機能です。

なお、プログラム用のドライブに頻繁な書き込みをしたくないとか、まとめてバックアップ出来た方がいいなどの理由で、設定がApplication Dataディレクトリに保存されることを好むユーザーは、あらかじめApplication Dataディレクトリ内に該当するディレクトリを作成しておくことで、そちらに保存させることも出来ます。

これらの仕様により、pref()関数を使用すれば、簡単にユーザーの好みや実行環境に合った最適な場所にデータを保存できるようになっています。

2010/10/25

自前ウィンドウの任意の部分をドラッグして移動できるようにする

NILScriptの自前ウィンドウ生成機能では、タイトルバーや枠のないウィンドウも生成できます。
しかし、タイトルバーや枠のドラッグによる移動・リサイズが行なえないので、別途移動やリサイズのための動作を用意する必要があります。

ウィンドウの任意の部分をドラッグして移動できるようにするには、hitTestイベントを利用します。
このイベントは、カーソル下にあるのがウィンドウのどの部位かという問い合わせを受取ったときに実行されます。
ここでカーソル下がタイトルバーであると応答するようにすれば、マウスの左ボタンでドラッグしたときに、タイトルバーをドラッグしたときと同じようにウィンドウが移動されるようになります。

これを利用して前回のCPU使用率グラフ表示のウィンドウをドラッグ可能にしたのが、以下のスクリプトになります。
var img=require('Image').Image.create(100,100,"black");
var w=require('Window').PlainWindow.create({
    width:100,height:100,
    graphics:img,
    transparentColor:0x000000,toolWindow:true,
    topmost:true,
    events:{
        hitTest:function(o){
            o.result="caption";
            return(true);
        },
        mbuttonUp:function(o){
            this.close();
        },
    },
});
Thread.create(function(){
    var mon=new (require('SystemMonitor').CPU)(), rate=0;
    while(true){
        w.request(function(){
            img.fillEllipse(1,1,98,98,0x9999FF);
            img.fillPie(1,1,98,98,-Math.PI/2,(Math.PI*2)*rate,0x99FF99,true);
            img.strokeEllipse(1,1,98,98,0x010101,true);
            img.drawTo(w);
        });
        sleep(1000);
        rate=mon.get().usage;
    }
});
w.show();

自前ウィンドウ上に表示した画像を更新・再描画する

Imageオブジェクトの画像をウィンドウに表示するには、Window.create()のオプションオブジェクトのgraphicsメンバにセットするというのは以前説明しましたが、今回は表示する画像の内容を更新する方法について説明します。

ウィンドウの画像を更新するには、Imageオブジェクトの各種メソッドで画像を更新した後、描画対象のWindowオブジェクトを引数にしてdrawTo()メソッドを呼び出します。

この時、複数スレッドからImageオブジェクトに同時アクセスしないように注意する必要があります。
ウィンドウを所有するスレッド上では、Imageオブジェクトを使用した再描画が不特定のタイミングで発生するので、他のスレッド上でImageオブジェクトを操作すると、更新途中の画像が表示されてしまったり、更新処理に失敗してしまったりします。
これを避けるためには、画像の更新処理もウィンドウを所有するスレッド上で実行しなければなりません。

Windowオブジェクトのイベントハンドラ内で更新・再描画を行う場合は特に気にする必要はありませんが、他のスレッドから更新を行う場合は、Windowオブジェクトのrequest()メソッドを使用してください。
request()メソッドは、指定された関数をウィンドウを所有するスレッド上で実行します。この関数内でImageオブジェクトを操作すれば、安全に画像を更新できます。

下記のスクリプトは、SystemMonitorユニットのCPUクラスを利用して取得したCPU使用率を円グラフでウィンドウ上に表示するという例です。
var img=require('Image').Image.create(100,100,"black");
var w=require('Window').PlainWindow.create({
    width:100, height:100,
    transparentColor:0x000000,topmost:true,
    graphics:img,
});

Thread.create(function(){
    var mon=new (require('SystemMonitor').CPU)(), rate=0;
    while(true){
        w.request(function(){
            img.fillEllipse(1,1,98,98,0x9999FF);
            img.fillPie(1,1,98,98,-Math.PI/2,(Math.PI*2)*rate,0x99FF99,true);
            img.strokeEllipse(1,1,98,98,0x010101,true);
            img.drawTo(w);
        });
        sleep(1000);
        rate=mon.get().usage;
    }
});
w.show();

2010/10/23

テキストなどに影や縁取りを付けた画像を生成

Imageオブジェクトに用意されている様々な機能を組み合わせれば、以下の例のようにテキストや図形に影をつけたり縁取りをするなどのエフェクトを加えて描画することも可能です。

この例の画像を生成するスクリプトは、以下のようになります。
var {Image}=require('Image');
var base=Image.create(200,64);
base.drawText("NILScript",0,0,{name:"Impact",size:48,brush:{
    color:[0xFF0000,0x990000],
    gradient:{angle:Math.PI/2, width:60},
    gammaCorrect:true,
    focus:0.9,
    curve:true,
}});

var frame=base.clone();
frame.and(0xFF000000);
frame.matrixFilter([[1,1,1],[1,1,1],[1,1,1]]);
frame.matrixFilter([[1,1,1],[1,1,1],[1,1,1]]);
frame.replaceColor("black","yellow");

var shadow=frame.clone();
shadow.and(0xFF000000);
shadow.blur(3);

var img=Image.create(200,64,"white");
img.drawImage(shadow,{x:4,y:4});
img.drawImage(frame);
img.drawImage(base);

img.save("logo.jpg",{quality:90});
まず、最初に背景無しでImageオブジェクトを生成し、グラデーションブラシを用いてテキストを描画しています。
ブラシの定義オプションには様々なものがあるので、NILScriptのアーカイブ内のdoc/Image.txtの説明を参照してください。

次に、clone()メソッドでテキスト画像のコピーを生成し、and()メソッドでアルファチャンネル以外の値を0にします。
続いて、matrixFilter()メソッドを利用して不透明の領域を太らせます。
matrixFilter()は、各ピクセルと隣接ピクセルのRGBA各値に引数で指定した2次元配列の対応する値を掛けた値の和をそのピクセルの値にするという処理を行います。この時、0未満の値は0に、256以上の値は255になります。
「[[1,1,1],[1,1,1],[1,1,1]]」という配列では、隣接する9ピクセルに一つでも不透明度255のピクセルがあれば不透明度が255になります。

更に、replaceColor()メソッドで色の置換を行います。
この時点では、不透明度255の黒と不透明度0の黒の2つの部分がありますが、第1引数を"black"にした場合は、不透明度255の黒のみが置換されます。
単色ではなくグラデーションなどを使いたい場合は、そのスタイルで塗りつぶした画像にupdateAlpha()メソッドでアルファチャンネルをコピーすると良いでしょう。

次に、出来上がった縁取り部分のコピーを生成し、同様にして全体を黒にした後、blur()メソッドでぼかして、影っぽい画像を生成します。

最後に、白背景の画像を生成し、影を少しずらして貼り付けた後、縁取りと元のテキストを貼り付けて完成です。

2010/10/21

画像にテキストを描画する

NILScriptのImageユニットでは、下記のようにdrawText()メソッドを使用することで、画像上に文字列を書き込むことも出来ます。
var {Image}=require('Image');
var img=Image.create(256,256,"white");

m=img.measureText("NILScript");
img.strokeRect(15,15,m.width+2,m.height+2,"black");
img.drawText("NILScript",16,16);
img.drawText("NILScript",0,64,{name:"Impact",size:48,color:"red",bold:true});
img.drawText('NILScript',0,160,{file :'D:\\myfonts\\AoyagiKouzanFont2.ttf' , size:48});

img.save("drawText.jpg",{quality:90});
単に文字列を表示したいだけなら、文字列と描画開始位置のx,y座標を指定するだけです。この場合、デフォルトのフォント、サイズ、色で描画されます。

第4引数には、様々なオプションを格納したオブジェクトを指定できます。
「name」でフォント名、「size」でピクセル数、colorで塗りつぶし色が指定できます。
また、「file」でフォントファイル名を指定すれば、システムにインストールされていないフォントも使用できます。

実際に描画される文字列の大きさを知りたい場合には、measureText()メソッドを使用します。このメソッドは、第1引数に文字列、第2引数にオプションを指定します。drawText()と違い、x,y座標は指定しません。
このメソッドの返り値は、widthやheightなどのプロパティを持つオブジェクトになります。これは単なるオブジェクトなので、解放処理は不要です。
この情報を利用すれば、テキストの周りにstrokeRect()などで枠を描画したりすることも可能です。

上記の例を実行すると、以下のような画像が出来上がります。

一定時間キー・マウス操作がなかったときに処理を実行

NILScriptのSystemMonitorユニットには、最後にキーボードやマウスの操作が確認されてから経過した時間を取得する「InputIdle」クラスが用意されています。
これを使用すれば、ユーザーが退席している間に何らかの処理を行ったり、作業状況のログを記録したりするツールを作成できます。

InputIdleクラスの基本的な使い方は、以下のようになります。
Main.createNotifyIcon();
var mon=new (require('SystemMonitor').InputIdle)();
Thread.create(function(){
 var idle;
 while(true){
  if(mon.get(true)<5000){ //規定時間
   if(idle){
    idle=false;
    //↓一定時間以上振りに操作を行ったときに実行される処理
    println("wake");
   }
  }else if(!idle){
   idle=true;
   //↓一定時間操作がなかったときに実行される処理
   println("idle");
  }
  sleep(100);//監視間隔
 }
});

無操作時間を取得するには、InputIdleオブジェクトのget()メソッドを呼び出します。
引数がtrueの場合は、プログラムによって生成された操作を除き、実際にユーザーが行った操作のみを考慮した無操作時間が取得されます。
idle変数は、既に無操作時間が規定値を超えているかどうかのフラグです。
最初に無操作時間が規定値を超えた時にtrueにし、無操作時間が規定値以下の時にtrueになっていたらfalseにすることで、一定時間以上の放置と放置からの復帰を検出しています。
上記の例でprintln()関数を呼び出している部分に、放置や復帰を検出したときの処理を記述します。

円グラフを生成する

NILScriptのImageユニットには、扇形の描画を行うstrokePie()やfillPie()などのメソッドも用意されています。
これらを使用すれば、円グラフの生成や表示が行なえます。

円グラフを生成するには、下記のようなスクリプトを記述します。
今回は、グラフ化する項目群を格納した配列をitems変数としてスクリプト中に記述していますが、実際にはファイルから読み込んだり何らかの方法で収集して用意してください。
var {Image}=require('Image');
var img=Image.create(256,256,"white");
var items=[
    {count:100,color:0xFF9999},
    {count:80,color:0x99FF99},
    {count:70,color:0x9999FF},
    {count:40,color:0x66FFFF},
    {count:20,color:0xFF66FF},
    {count:10,color:0xFFFF66},
];
var sum=items.sum('count');
var angle=-Math.PI/2;
for(var i=0,l=items.length;i<l;i++){
    var sweep=(Math.PI*2)*(items[i].count/sum);
    img.fillPie(8,8,240,240, angle, sweep,items[i].color);
    img.strokePie(8,8,240,240, angle, sweep,"black");
    angle+=sweep;
}
img.save("pie_graph.jpg",{quality:90});
まず、配列の各要素のcountプロパティの合計値をsum変数に格納しておきます。
次に、グラフの描画開始角度をangle変数に代入します。0が→の向きを表し、一周がMath.PI*2なので、↑の方向から始めるには「-Math.PI/2」を初期値とします。

配列の各項目ごとのループでは、まず各項目の値を全項目の合計値で割った値に一周を表すMath.PI*2を乗算して、各項目の扇形の角度を求めています。
そして、fillPie()でその範囲を塗りつぶし、strokePie()で輪郭を描画します。
両メソッドの最初の4つの引数は、扇形を含む円が内接する長方形の位置と大きさを表します。
第5引数では開始角度、第6引数が扇形の角度を指定します。
第7引数では、今回は単に色を指定していますが、他にも線の太さや塗りつぶし方法などが指定できます。
描画が終ったら、次の項目の開始角度が今回の項目の範囲の直後になるように、今回の項目が占めた角度を加算します。
これで、以下のようなグラフが作成できます。
描画したグラフは、save()メソッドでファイルに保存したり、Window.create()のgraphicsオプションにセットしてウィンドウに表示したりして利用してください。

2010/10/20

C言語で作成した関数を埋め込み

スクリプト言語はC言語などと比べると10倍近くも処理速度が遅いのが難点です。
10倍しても1ミリ秒足らずで終るような処理でしたらあまり問題はありませんが、C言語なら500ミリ秒で済む処理が5秒もかかるようになってしまうと、実用に耐えません。
画像の全ピクセルに対する処理など、反復処理が何万回にも及ぶ可能性のある処理は、本体部分をC言語で作成しておいて、それを何らかの方法でスクリプトから呼び出すようにするとよいでしょう。
ここでは、NILScriptのImageユニットで各種フィルタ機能の実装に利用している、Cコンパイラで生成したネイティブコードをスクリプト中に埋め込む方法を紹介します。

まず、Microsoft Visual C++(無償のExpress Editionでも可)でプロジェクトを作成し、埋め込みたい関数を記述してコンパイルします。
すると、プロジェクトのディレクトリ内の「Release」ディレクトリにソースコードの拡張子を「.cod」にしたファイルが作られます。
このファイルには、ソースコードから生成されたネイティブコードの16進ダンプや、アセンブラのコードが記述されています。

NILScriptのディレクトリをカレントディレクトリとして「ng tool/codConv ".codファイルのパス"」というコマンドを実行すると、指定された.codファイル中の関数のネイティブコード部分が抽出され、以下のような形式でクリップボードに格納されます。
var negate=(new CFunction(Hex.decode(
 "55 8b ec 53 56 57 8b 75 0c c1 e6 02 8b 5d 14 8b d3 2b de 8b 45 08 8b 7d 10 0f af fa 03 f8 8b d0 03 d6 81 30 ff ff ff 00 83 c0 04 3b c2 7c f3 03 c3 3b c7 7c e9 5f 5e 5b 5d c3"
))).toFunction([],UInt);
これをスクリプト中に貼り付け、「[]」の中に引数の型の定義を記述し、必要なら返り値の型を修正すれば、ネイティブコードの埋め込み定義が出来上がります。
あとは普通の関数と同じように呼び出して利用できます。

なお、Imageユニットのネイティブコード関数の多くは、動作速度を高速化するため、処理のほとんどをインラインアセンブラで記述しています。
何度も使用する値でメモリの読み書きが発生しないようにレジスタに格納したままにするなどの工夫をすることで、C言語からコンパイルしたものより2倍くらい高速化できることもあります。
いきなりアセンブラでプログラムを作るのは大変なので、最初はコンパイラが生成した.codファイルを参考にするとよいでしょう。

2010/10/19

画像を透明度エフェクト付きで合成

Imageユニットによる画像の合成処理では、合成する画像に加工を施すことで、下図のようなエフェクト付きの合成も実現できます。
この例の画像は、下記のようなスクリプトで作成できます。(画像のパスなどの部分は適宜変更してください)
var Image=require('Image').Image;
var img=Image.load('input.jpg');
var logo=Image.load("logo.png");

var mask=Image.create(32,32);
mask.fillEllipse(4,4,24,24,"white");
mask.blur(5);
logo.updateAlpha(mask);

img.drawImage(logo,img.width-logo.width-16,16);

img.save('output.jpg',{quality:90});
free(mask,logo,img);
元画像と合成するロゴ画像を読み込んだら、ロゴ画像と同じ大きさのマスク用Imageオブジェクトを生成し、fillEllipse()で円を描画します。
次に、blur()メソッドで円をぼかします。マスク画像の白い円の部分以外は透明ですので、円周の付近では不透明と透明が混じって段階的に半透明になります。
これを引数として、ロゴ画像のupdateAlpha()メソッドを呼び出すと、不透明度情報がコピーされ、周辺が徐々に透明になるロゴ画像が出来上がります。
これを合成先の画像のdrawImage()メソッドで合成すれば出来上がりです。

半透明で画像を合成

NILScriptのImageユニットが提供する画像処理機能では、不透明度情報(アルファチャンネル)の取り扱いが可能です。
この機能を利用すれば、写真などの上に半透明のロゴを合成するウォーターマークのような処理も簡単に実現できます。

新たに図形やテキストを描画する場合は、半透明の描画色で描画するという手もありますが、既存の画像を半透明にして貼り付けたい場合は、下記のようにして「updateAlpha()」メソッドでピクセルの透明度を一括変更してからdrawImage()で貼り付けるといいでしょう。(ファイル名などは適宜変更してください)
var Image=require('Image').Image;
var img=Image.load('input.jpg');

var img2=Image.load("logo.png");
img2.updateAlpha(127);
img.drawImage(img2,img.width-img2.width-16,16);

img.save('output.jpg',{quality:90});
updateAlpha()は、0~255の値で画像の不透明度を指定します。
これにより、下記の例のように元の画像(波模様)の上に半透明で別の画像を合成できます。

2010/10/18

Imageオブジェクトの画像をウィンドウに表示

NILScriptのImageユニットでは、画像のリサイズや切り抜き、図形やテキストなどの追加、ぼかしや明るさ調節などのフィルタ処理を組み合わせて、様々な画像処理を実現できます。
しかし、処理が複雑になるほど、思い通りの処理を一発で実現することは難しくなります。
処理結果を確認しながら処理内容を修正したい場合などは、処理結果を自前ウィンドウ状に表示するといいでしょう。
単に処理結果の画像を表示するだけなら、下記のようにWindow.create()のオプションで「graphics」にImageオブジェクトを指定するだけです。
ウィンドウのクライアント領域全体にImageオブジェクトが保持する画像を表示するウィンドウが作成されます。
この場合、ウィンドウが閉じられるまでImageオブジェクトを解放しないように注意してください。
var Image=require('Image').Image;
var img=Image.create(256,256,"white");
img.fillPolygon([
    [240,16],
    [192,224],
    [80,96],
    [16,240],
    [64,32],
    [176,160],
],"black",true,true);

var w=require('Window').Window.create({
    width:400,height:400,
    graphics:img,
}).show();

2010/10/04

指定サイズ未満で最大の画質のJPEG画像を生成

Imageオブジェクトには、画像をファイルに保存するsave()の他にも、画像ファイルデータをメモリ上に出力し、そのバッファを指すポインタオブジェクトを返すtoBytes()メソッドも用意されています。

この機能を利用すれば、ファイルに保存することなく圧縮後のファイルサイズを知ることが出来ます。
下記のスクリプトは、圧縮後のサイズが上限値以下になる最大の画質でJPEG画像を保存するという応用例です。
var file=cwd().file('capture.jpeg');
sleep(2000);
try{
    var img=require('Window').Window.active.capture();
    for(var q=100;q>0;q--){
        try{
            var buf=img.toBytes('.jpeg',{quality:q});
            if((buf.size<100000)||(q==10)){
                file.update(buf);
                println(q);
                break;
            }
        }finally{
            free(buf);
        }
    }
}finally{
    free(img);
}
最初に、img変数にアクティブウィンドウのスクリーンショット画像のImageオブジェクトを代入してます。
次に、forループで最大画質の100から1ずつ減らしながら圧縮を試行しています。
toBytes()では、第1引数で拡張子やMIMEタイプで形式を指定します。第2引数の保存オプションで、q変数に格納されている画質を指定しています。
toBytes()が返すPointerオブジェクトでは、sizeプロパティでバイト数が取得できるので、その値が最大値以下ならばファイルへの保存を実行します。
Fileオブジェクトのupdate()メソッドはファイルを上書き保存するメソッドです。引数には多くの場合文字列を指定しますが、Pointerオブジェクトを指定すればバイト列を保存できます。
toBytes()が返したPointerオブジェクトは、不要になったらfree()関数で解放処理を行う必要があります。
最後に、Imageオブジェクトも解放します。

Webサイトにアップロードする画像を生成するときなどは、この例を応用して最適な画質のJPEG画像を生成させてみるといいでしょう。

画面のスクリーンショットを保存

画像処理を行うImageユニットには、ウィンドウのスクリーンショット画像を取得する機能も用意されています。
例えば、下記のようにすることで、アクティブウィンドウのスクリーンショット画像を保存できます。
try{
    var img=require('Window').Window.active.capture();
    img.save('capture.png');
}finally{
    free(img);
}
スクリーンショット画像のImageオブジェクトを取得するには、Windowオブジェクトのcapture()メソッドを呼び出します。Imageオブジェクトが定義されているのはImageユニットですが、WindowオブジェクトはWindowユニットで定義されているので、「require('Window').Window~」のようにすることに注意してください。
なお、capture()メソッドの第1引数にtrueを指定すると、タイトルバーなどのノンクライアント領域を除いた部分の画像を取得することも可能です。

取得したImageオブジェクトはimg変数に代入し、そのsave()メソッドでファイルに保存しています。
最後にfree(img)で取得したImageオブジェクトの解放処理を呼び出す必要がある事に注意してください。

今回は単に保存しているだけですが、縮小したサムネイル画像や、Webページに埋め込むためのHTMLを生成したりしてもよいでしょう。
また、Hotstrokesでホットキーからキャプチャを実行したり、ウィンドウの状態変化を監視するWindow.observe()などを利用して特定のタイミングで自動キャプチャを行わせるなど、様々な応用ツールに利用できるはずです。

画像処理を行うImageオブジェクト

画像処理に関する機能を提供するImageユニットが追加されました。
画像ファイルの読み書きや、リサイズなどの基本的な加工処理、多角形などの描画機能などが用意されています。

例えば、以下のようにすれば、NILScriptのアイコンなどに使われているロゴ画像を生成できます。
var file=cwd().file('nilscript.png');
var img=require('Image').Image.create(256,256,"white");
img.fillPolygon([
    [240,16],
    [192,224],
    [80,96],
    [16,240],
    [64,32],
    [176,160],
],"black",true,true);
img.save(file);
free(img);
Imageクラスのcreate()メソッドで、幅、高さ、塗りつぶしなどを指定して新規のImageオブジェクトを生成できます。
ImageオブジェクトのfillPolygon()メソッドで、一連の点を結ぶ直線で囲まれた領域を塗りつぶします。
そして、saveメソッドで作成した画像を指定したファイルに保存しています。
Imageオブジェクトはfree()関数で明示的に解放する必要があります。特に常駐スクリプトや大量の画像を処理するスクリプトでは、忘れないように注意してください。

描画系のメソッドはまだ一部しか実装されていませんが、近いうちに線や文字の描画なども実装する予定です。