画像のプリロード

一昔前、Webページ上での画像のロールオーバー(画像の上にマウスポインタを重ねると画像が変わるアレです)にJavascriptが使われていましたね。でも、今は、Javascriptではなく、CSSで実現する方法が標準となりつつあります。
たとえば・・・

a      {background: url(xxx.jpg);}
a:hover{background: url(yyy.jpg);}

こんな感じですかね。IE6では、アンカータグ(<a>)しか対応してませんが、他のブラウザなら他のタグでも可能です。

で、ここで僕がはまったのが、ページに初めてアクセスしたとき、ロールオーバーさせる要素にマウスを持っていくと一瞬空白になってしまうんですよねぇ・・・。初めてページを表示させたときまだロールオーバーさせる画像が読み込まれていないのが原因で、解決方法は簡単、画像を先読みさせてブラウザのキャッシュに入れてしまえばいい。要するにプリロードさせればいい。んな、こたぁ、言われなくても分かるんです。

その方法は・・・頭の悪い僕では2つぐらいしか思いつきません・・・

  1. javascriptを使って (new Image()).src = “yyy.jpg”; とかしておく。
  2. HTML内に幅/高さを1にした<img>タグを書いておく。
  3. 画像を一枚用意して、width,height,background-positionを駆使して切り分ける

無駄なタグを含める後者は論外。画像を一枚に・・・というのは新規につくるページだと最適ですが、既存の組みあがっているページとなると無駄に工数がかかってしまう。一番簡単にできそうなのは・・・やはりスクリプトで。・・・せっかくロールオーバー処理からJavascriptを排除したのに・・・(--;;;
ブラウザの最適化処理で自動的に読み込んでくれれば問題ないんですけどね・・・。そもそも、css内にある画像は先読みしてほしい気もします。まぁ、無駄にメモリー消費が増えてしまう、パフォーマンスの問題も絡むので無理か・・・。

結局、

/**********************************************
  画像を読み込んでおく関数
  preload(画像1,画像2,....);
***********************************************/
function preload()
{
  var len = 0;

  if((len = arguments.length) &lt;= 0)
    return;

   for(var i=0;i&lt;len;i++)
    (new Image()).src = arguments[i];
}

preload('yyy.jpg','zzz.png');

とかやってしまうんだけど・・・。

う~ん・・・スクリプトを使わない上手いやり方はないものか・・・。

ImageFlow and HighSlideJS

今日、仕事で画像中心のギャラリーページのためのギャラリー・スクリプトをネットで探してましたら・・・もう完璧じゃね?的なクールなJavascriptライブラリを発見。結構有名なものみたいです。

その名は ImageFlow

iPhoneなどでおなじみの CoverFlow を Javascript で再現したライブラリです。ただ残念なことに、営利サイト(会社のサイトとか)で使用するにはライセンスが必要・・・とのことで採用するのは却下。

でも、サイトのドキュメントを読むと、HighSlideJSと組み合わせて利用する方法が書いてあって、これがなかなかいいので個人で使おうと・・・思ってたんだけど・・・PHP環境が必要なんで無理っぽそうです。今度DTIの格安のVPSサービスを申し込もうと思っているので、もうちょっとしたら使えそうです。

で、ローカル環境のサーバーにインストールしてみました。
 

マウスホイールも効きます

 
クリックすると・・・

HighSlideJSと併用

使うときも簡単で、簡単なサンプルのHTMLファイルがあるのですぐ分かります。
が、いちいち <img>タグを列挙するのも面倒くさいというわけで、特定のディレクトリをスキャンしてjpegファイルを列挙させるphpファイルを書けばラクになります。

<?php
function DirectoryScanAndWrite($img_dir)
{
  if ($handle = opendir($img_dir))
    {
      while (false !== ($file = readdir($handle)))
        {
          if(preg_match("/^\.{1,2}$/",$file) || preg_match("/^refl_(.+)$/i",$file))
            continue;
          
          $path = "$img_dir/$file";
          print "<img src=\"$path\" longdesc=\"$path\" alt=\"$file\">\n";
        }
    }
}
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ja">
	<head>
		<meta http-equiv="content-type" content="text/html; charset=shift_jis">
		<title>ImageFlow</title>
		<meta name="robots" content="index, follow, noarchive" />
		<link rel="stylesheet" href="style.css" type="text/css" />

		<!-- This includes the ImageFlow CSS and JavaScript -->
		<link rel="stylesheet" href="imageflow.packed.css" type="text/css">
		<link rel="stylesheet" href="highslide.css" type="text/css">
		<script type="text/javascript" src="imageflow.packed.js"></script>
		<script type="text/javascript" src="highslide.packed.js"></script>
		<script type="text/javascript">
hs.graphicsDir = './graphics/';

domReady(function()
         {
           var Highslide = new ImageFlow();
           Highslide.init({ ImageFlowID: 'Highslide',
           opacity: true,
           onClick: function() 
             { 
               return hs.expand(this, { src: this.getAttribute('longdesc'),
               outlineType: 'rounded-white',
               fadeInOut: true } ); 
             } 
           });
         });
		</script>
		
		<style type="text/css">
			.highslide-container {z-index:10003 !important;}
			.highslide-credits {display:none !important; }
		</style>
	</head>
	<body>
		<h1>ImageFlow with HighSlide JS</h1>

		<!-- This is all the XHTML ImageFlow needs -->
		<div id="Highslide" class="imageflow">
			<?php DirectoryScanAndWrite('./img2/'); ?>
		</div>

	</body>
</html>

で、PHPは、画像の鏡像反射効果を作成するために必要みたいで同じディレクトリに反射効果が適用されたものが画像ファイルと同じ数だけ生成されます。

ImageFlowのインスタンスを作成するときに、いろいろなオプションが指定できたり、CSSファイルを編集することでかなり自由にデザインできそうです。PHP環境が使えるサイトでは、写真のデキがそこそこでも、かなりクール?な見せ方ができるので、僕にピッタリかもしれません(^^;;;

SQLite3

昨日、深夜にブラタモリの再放送やっててついつい観てしまいました。去年だったか一回だけ放送があって、帯でやってくれないかな~と期待してたんですが・・・10月から始まるようですね。「タモリ倶楽部」のスピンオフ的な番組のような気もしないでもないですが・・・さすがにゲストと番組のノリはNHKですね(^^;;;

それは、さておき・・・。

アプリケーションに組み込むタイプの軽量データベース・ライブラリ、SQLite3を使える環境をセットアップしようと思っていろいろファイルをダウンロードしました。

  • SQLite3 本体(コマンドライン版/DLL版/ソース)
  • System.Data.SQLite(C#から利用できるように)
  • GUIなDB管理マネージャ

適当なフォルダに突っ込んでパスを通してやれば使えるようになります。

ソースから静的リンクライブラリも一応作成して環境構築終了です。

で、ついでに、WSH( とくにJScript)で使用できるようにしたいな、と思い検索してみたんですが・・・探し方がバカなのか、見つからない。perlやpython,php,といったWEBアプリ系のスクリプトの記事はアホほどヒットするんだけど、JscriptやVBScriptなどから(要はCOMオブジェクトとして)使えるラッパーCOMサーバーのようなものをを見つけることができませんでした。COM自体もう時代遅れのトピックになりつつあって、多分に「イマサラ」感があるので需要が無いのかもしれませんね。JScriptはOS(正確にはIE?)に付属しているのでお手軽度的に言えばperlやphpやPythonやRubyや・・・・に勝っているとは思うんですけどね・・・。

・・・まぁ、データベースファイルをオープンして、SQLクエリを実行して、結果を得る、ということがなんとなくできれば・・・データベースファイルの管理はGUIで他のソフトに任せればいいんだし・・・と割り切って、昼休みの時間に作ってみようかなと。

C固有の型とJScriptで使用するVARIANT型の変換とか、SQLite3で定義されている構造体なんかをJscriptでどう表現すればいいのか? といった諸々を考えないといけないんですが・・・まぁなんとかなるでしょう。休み時間にコツコツと作ってみます(^^;;;

COMサーバーにJScriptのイベントハンドラを登録する 修正版

COMサーバーにJScriptのイベントハンドラを登録する の「修正版」です。

前回の例には、致命的なエラーが起こる場合があります。onCurrent というプロパティーに関数オブジェクトを設定し、取得することができます。しかし、まだonCurrentプロパティに関数オブジェクトを設定しない、未設定の場合、onCurrent を参照すると、スクリプト側にエラー値を戻すとので、エラーがスローされ実行が止まってしまいます。

つまり、

var obj = new ActiveXObject("MyServer.Utility");
WScript.Echo(obj.onCurrent);

とすると、エラーで止まってしまいます。まだプロパティが未設定ならば、nullを返すようにすれば万事OKです(^^;

というわけで、修正です。

まず、onCurrentのプロパティの設定と取得でのデータ受け渡しを、IDispatch* ではなく、VARIANT にします。関数オブジェクトは、このVARIANTで、vtメンバをVT_DISPATCHにして受け渡すことになります。

ところどころ修正したものが次のとおり。

/*
 compiler command line :
   cl /LD MyServer.cpp /link /TLBOUT:MyServer.tlb
*/

#pragma comment(lib,"comsuppw.lib")

#define STRICT
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0400
#endif
#define _ATL_ATTRIBUTES
#define _ATL_APARTMENT_THREADED
#define _ATL_NO_AUTOMATIC_NAMESPACE
#include <atlbase.h>
#include <atlcom.h>
#include <atlwin.h>
#include <atltypes.h>
#include <atlctl.h>
#include <atlhost.h>
using namespace ATL;

#include <comutil.h>

//マクロ定義
#define PPVOID(X) (reinterpret_cast<LPVOID*>(X))
#define QI(X,Y,Z) ((X)->QueryInterface((Y),(reinterpret_cast<LPVOID*>(Z))))
#define RELEASE(X) {if(X){(X)->Release(); X=NULL;}}

// DllMain,DllCanUnloadNow,DllRegisterServer and DllUnregisterServer
[ module(dll, name = "MyServer", helpstring = "MyServer 1.0 Type Library") ];
[ emitidl ];

/////////////////////////////////////////////////////////////////////////////
// インターフェイス定義  :   IMyServer 
[
   object,
   dual,
   oleautomation,
   helpstring("IMyServer Interface"),
   pointer_default(unique)
]
__interface IMyServer : IDispatch
{
  [id(1)] HRESULT CurrentDirectory([out,retval]BSTR *sPath);

  [id(2),propput] HRESULT onCurrent([in] VARIANTARG var);
  [id(2),propget] HRESULT onCurrent([out,retval] VARIANTARG *pvar);

};

/////////////////////////////////////////////////////////////////////////////
// イベントインターフェイス定義 : IMyServerEvent
[
  dispinterface,
  hidden,
  helpstring("IMyServerEvents Interface")
]
__interface IMyServerEvent
{
  [id(1)] HRESULT onCurrent();
};

/////////////////////////////////////////////////////////////////////////////
// インターフェイス実装  : CMyServer
[
   coclass,
   threading(apartment),
   source(IMyServerEvent),
   event_source(com),
   vi_progid("MyServer.Utility"),
   progid("MyServer.Utility.1"),
   version(1.0),
   helpstring("MyServer Class")
]
class ATL_NO_VTABLE CMyServer :
   public IMyServer
{
public:
   CMyServer()
     {

     }

  __event __interface IMyServerEvent;

  DECLARE_PROTECT_FINAL_CONSTRUCT()
  HRESULT FinalConstruct()
    {
      m_pDispatch = NULL;
      return S_OK;
    }

  void FinalRelease()
    {
      RELEASE(m_pDispatch);
      return;
    }

  HRESULT CurrentDirectory(BSTR *sPath)
    {
      TCHAR pBuffer[MAX_PATH+1] = {0};
      ::GetCurrentDirectory(MAX_PATH+1,pBuffer);

      *sPath = _bstr_t(pBuffer).Detach();

      Fire();
      return S_OK;
    }

  // スクリプトから渡されるオブジェクトの設定と取得
  HRESULT put_onCurrent(VARIANTARG var)
    {
      HRESULT hRes = E_INVALIDARG;
      LPDISPATCH pDispatchNew = NULL;

      if(var.vt == VT_DISPATCH)
        {
          if(SUCCEEDED(hRes = QI(var.pdispVal,IID_IDispatch,&pDispatchNew)))
            {
              /*if(m_pDispatch)
                m_pDispatch->Release();*/
              RELEASE(m_pDispatch);

              m_pDispatch = pDispatchNew;
            }
        }
      else
        hRes = E_INVALIDARG;

      return hRes;
    }

  //関数オブジェクトの取得
  HRESULT get_onCurrent(VARIANTARG *pvar)
    {
      HRESULT hRes = S_OK;
      IDispatch *pDisp = NULL;
      if(m_pDispatch)
        {
          if(SUCCEEDED(hRes = QI(m_pDispatch,IID_IDispatch,&pDisp)))
            {
              VariantClear(pvar);
              pvar->vt = VT_DISPATCH;
              pvar->pdispVal = pDisp;
            }
        }
      else
        {
          //もし保持している関数オブジェクトがなければ空に。
          VariantClear(pvar);
        }

      return hRes;
    }

private:

  LPDISPATCH m_pDispatch;

  void Fire()
    {
      DISPPARAMS dispparams = {NULL,NULL,0,0};

      //COMイベント発火
      IMyServerEvent_onCurrent();

      //設定された関数オブジェクトを実行,デフォルト
      if(m_pDispatch)
        m_pDispatch->Invoke((DISPID)DISPID_VALUE,
                            IID_NULL,
                            LOCALE_USER_DEFAULT,
                            DISPATCH_METHOD,
                            &dispparams,
                            NULL,
                            NULL,
                            NULL);
    }
};

VARIANT型とIDispatchインターフェイスは、C++とJScriptとでデータを受け渡しする上で必ず付いてまわるのですが、あまりこのあたりを詳しく解説してある本を見つけることができないのがネックです。

この件の備忘録終了。

COMサーバーにJScriptのイベントハンドラを登録する

VC++ 2008EE で COMサーバーをカンタンに作成」のつづきです。

C++属性を使ってC++で簡単なCOMサーバーを作成はできました。次に、ブラウザ上でやってるような、イベントを発生させて、それをjscriptの関数、たとえば、

function onExec()
{
  WScript.Echo("メソッドが実行されました");
}

というようなイベントハンドラを登録して、COMサーバーのあるメソッドを実行したときに、このonExec という関数を実行させるようにしたい! と思います。

要は、HTML+Javascriptでやってるような、window.onload = function(){…} というようなことを自分が作成したCOMサーバーにやらせよう、というわけです。実行環境は、Windows Scripting Hostです。

はじめはどうやればさっぱり分かんなかったんですが、ネットで検索していろいろ調べた結果の備忘録となります。

COMサーバーから、各種イベント通知を受け取るには、C++だとIConnectionPointとシンクオブジェクトを経由したものになるんですが、JScriptにはそういった接続ポイントをゴニョゴニョして・・・というやり方はできないようです。つまりCOMでシンクを経由したイベントを受け取る機構は通用しないということです。

ここから補足 (2012/2/10)

正確には、動的にインスタンスを生成した(new ActiveXObject()とかで作成した)オブジェクトでシンクを経由したイベントを受け取る機構がないのです。たとえばHTAとかで

<object id=’xxxx’ classid=”clsid:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee”></object>

という風にOBJECTタグをHTMLに埋め込んで、function xxxx::関数名(引数){…} というイベントハンドラを定義すれば、可能になります。たぶん。

JScriptをホストしているのがWSHならば、以下のようにイベントハンドラを定義して、シンクを経由した通常のCOMイベントを受け取れます。

var obj = WScript.CreateObject("MyServer.Utility","myserver_");
function myserver_onXXXX()
{
   WScript.Echo("Fire!");
}
obj.XXXX();

しかし、Webブラウザ上で、たとえば・・・

var button = document.getElementById('button');
button.onclick = function()
{
  window.alert("クリック!") ;
}

というようにもしたいわけです。

var myserver = WScript.CreateObject('MyServer.Utility');
myserver.onExec = function()
{
  WScript.Echo("実行");
}

とこんな感じにもできたらなぁ・・・と思ったのが今回の動機です。

単純に考えれば、COMサーバー側にonExecというプロパティを設置して、COMサーバーのあるメソッドが実行されたら、このonExecを実行させる(イベントを発生)させればいいだけかな、と思いました。function(){}の関数オブジェクトの部分。関数オブジェクトはC++で、IDispatch*で受け取ればいいですね。

ということで、いろいろネットで調べた結果、次のような単純なCOMサーバーを作成しました。

  • CurrentDirectory() というメソッドを一つ定義。
  • 関数オブジェクトの保持用にプロパティを一つ定義
  • CurrentDirectory()がコールされたら、プロパティに格納している関数オブジェクトを実行する。
    (つまりイベントを発生させる)

具体的なコードは・・・

/*
 コンパイル・コマンドライン :
   cl /LD MyServer.cpp /link /TLBOUT:MyServer.tlb
*/

#define STRICT
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0400
#endif
#define _ATL_ATTRIBUTES
#define _ATL_APARTMENT_THREADED
#define _ATL_NO_AUTOMATIC_NAMESPACE
#include <atlbase.h>
#include <atlcom.h>
#include <atlwin.h>
#include <atltypes.h>
#include <atlctl.h>
#include <atlhost.h>
using namespace ATL;

//マクロ定義
#define PPVOID(X) (reinterpret_cast<LPVOID*>(X))
#define RELEASE(X) (X ? X->Release() : 0)

// DllMain,DllCanUnloadNow,DllRegisterServer and DllUnregisterServer
[ module(dll, name = "MyServer", helpstring = "MyServer 1.0 Type Library") ];
[ emitidl ];

/////////////////////////////////////////////////////////////////////////////
// インターフェイス定義  :   IMyServer 
[
   object,
   dual,
   oleautomation,
   helpstring("IMyServer Interface"),
   pointer_default(unique)
]
__interface IMyServer : IDispatch
{
  [id(1)] HRESULT CurrentDirectory([out,retval]BSTR *sPath);

  [id(2),propput] HRESULT onCurrent([in] LPDISPATCH pDisp);
  [id(2),propget] HRESULT onCurrent([out,retval] LPDISPATCH *ppDisp);
};

/////////////////////////////////////////////////////////////////////////////
// イベントインターフェイス定義 : IMyServerEvent
[
  dispinterface,
  hidden,
  helpstring("IMyServerEvents Interface")
]
__interface IMyServerEvent
{
  [id(1)] HRESULT onCurrent();
};

/////////////////////////////////////////////////////////////////////////////
// インターフェイス実装クラス  : CMyServer
[
   coclass,
   threading(apartment),
   source(IMyServerEvent),
   event_source(com),
   vi_progid("MyServer.Utility"),
   progid("MyServer.Utility.1"),
   version(1.0),
   helpstring("MyServer Class")
]
class ATL_NO_VTABLE CMyServer :
   public IMyServer
{
public:
   CMyServer()
     {

     }

  //COMイベントのためのおまじない。
  __event __interface IMyServerEvent;

  DECLARE_PROTECT_FINAL_CONSTRUCT()
  HRESULT FinalConstruct()
    {
      //プライベート・メンバ初期化
      m_pDispatch = NULL;
      return S_OK;
    }

  void FinalRelease()
    {
      //後処理
      RELEASE(m_pDispatch);
      return;
    }

  //単にカレントディレクトリを返す。
  HRESULT CurrentDirectory(BSTR *sPath)
    {
      WCHAR pBuffer[MAX_PATH+1] = {0};
      ::GetCurrentDirectoryW(MAX_PATH+1,pBuffer);

      *sPath = ::SysAllocString(pBuffer);

      Fire();
      return S_OK;
    }

  // IDispatch*(スクリプトから渡されるオブジェクト)の設定と取得
  // スクリプトでいう、obj.onCurrent = function(){...} の部分
  HRESULT put_onCurrent(LPDISPATCH pDispatch)
    {
      LPDISPATCH pDispatchNew = NULL;
      HRESULT hRes = S_OK;

      if(SUCCEEDED(hRes =
           pDispatch->QueryInterface(IID_IDispatch,PPVOID(&pDispatchNew))))
        {
          RELEASE(m_pDispatch);
          m_pDispatch = pDispatchNew;
        }

      return hRes;
    }

  //関数オブジェクトの取得
  HRESULT get_onCurrent(LPDISPATCH *ppDisp)
    {
      HRESULT hRes = E_NOTIMPL;

      if(m_pDispatch)
        hRes = m_pDispatch->QueryInterface(IID_IDispatch,PPVOID(ppDisp));

      return hRes;
    }

private:

  LPDISPATCH m_pDispatch;

  void Fire()
    {
      DISPPARAMS dispparams = {NULL,NULL,0,0};

      //COMイベント発火
      IMyServerEvent_onCurrent();

      //設定された関数オブジェクトを実行,デフォルトDISPID(0)
      m_pDispatch->Invoke((DISPID)DISPID_VALUE,
                          IID_NULL,
                          LOCALE_USER_DEFAULT,
                          DISPATCH_METHOD,
                          &dispparams,
                          NULL,
                          NULL,
                          NULL);
    }
};

一応、シンクオブジェクト経由でもイベントを受け取れるようにもしました。仕組みが分かれば意外と簡単です。以前にも書きましたが、ほとんどサンプルの流用で、ATLを使った部分や属性部分はサンプルそのまま。

ただ、このテストサンプルは複数のイベントハンドラを登録できません。複数のイベントハンドラを登録できるようにするには、複数のIDispatch*ポインタを管理しなければなりません。内部でstd::vectorなどのポインタ配列用のコンテナ(COMだったらsafe arrayあたり?)を用意して・・・というような処理が必要になってくるでしょう。

できたMyServer.dllを regsvr32 で登録して、以下のJScriptで動作確認。

//シンクを経由した、イベントハンドラ
//WSHホストで動作可能です。
function myserver_onCurrent()
{
  WScript.Echo("Sync Event Occured!");
}

try
{
  //IEがホストなら・・・
  // var obj = new ActiveXObject("MyServer.Utility");
  // WScript.Echo は window.alert になります。
  var obj = WScript.CreateObject("MyServer.Utility","myserver_");

  obj.onCurrent = function()
    {
      WScript.Echo("Fire Current!");
    }

  WScript.Echo(obj.CurrentDirectory());
}
catch(e)
{
	WScript.Echo(e.message);
}

というわけで、備忘録終了。