C#でTWAINを操作してみる

今月やった仕事でTwain機器をコントロールしてほげほげってのがあったのでログ。
b:id:twisted0517:t:twain

ライブラリはcodeprojectのtwain.netを使う。
http://www.codeproject.com/KB/dotnet/twaindotnet.aspx

サンプルの通りやるだけで普通に使えるんだけど、

  • ADF付きのスキャナからの連続読み取り
  • ドライバ名指しでのTWAIN機器の選択

あたりをちょいと弄った。

ADF付きのスキャナからの連続読み取り

ADFを使って複数のイメージを連続スキャンすると、1読み取りアクションに対してn枚のイメージが取得できるのに、そのまま使ってたら何故か先頭1枚分のイメージしか取得できんかった。

twain.netのフォーラムを流し読みした感じ、TwainLibのAquire()の一部を直せばいいとのこと。

Re: ADF not working...? member Chris Totten 5:24 14 Jul '03

Hi there,

It turned out there was a small bug in the Acquire function - the original line that was incorrect read :

TwCapability cap = new TwCapability( TwCap.XferCount, 1);

...I changed it to be :

TwCapability cap = new TwCapability( TwCap.XferCount, -1);

I presume the "-1" means "scan until you run out of paper in the ADF" - I don't know why it was sitting at one in the first place - probably just for testing and left in there?

I also changed the version in the demo to return an ArrayList of Bitmap objects and specifically to my app I also converted them to 1bpp CCIT4 compressed B&W bitmaps although this line could be commented out. With the permission of the author I could post the amended version here but I think it would be better to mail it directly to the original author..... Also if you attempt to use the original image viewer window it won't of course work but you'd simply have to replace this original window with a simple one with a PictureBox on it since the ArrayList now contains standard Bitmaps...

TwCapabilityのコンストラクタの第二引数は最大取得枚数なのかしら。

ドライバ名指しでのTWAIN機器の選択

元々のTwainLibがSelect()でTwain機器の選択ダイアログを表示して、選択されたものをそのまま使う方式だった。
外部に設定を持っておいてロード時に自動選択にしたかったのでSelect()をオーバーロードしてドライバのproductNameから合致するものを選択するようにしてみた。

        /// <summary>
        /// TWAIN機器をドライバ指定で選択
        /// </summary>
        /// <param name="productName"></param>
        public bool Select(String productName) {
            TwRC rc;
            CloseSrc();
            if (appid.Id == IntPtr.Zero) {
                Init(hwnd);
                if (appid.Id == IntPtr.Zero)
                    return false;
            }
            rc = DSMident(appid, IntPtr.Zero, TwDG.Control, TwDAT.Identity, TwMSG.GetFirst, srcds);
            while (rc == TwRC.Success) {
                if (srcds.ProductName == productName) {
                    return true;
                }
                rc = DSMident(appid, IntPtr.Zero, TwDG.Control, TwDAT.Identity, TwMSG.GetNext, srcds);
            }
            return false;
        }

これだと同じPCに同じ機械が複数接続されてた時に、先頭のもんしか使われないけどキニシナイ。
まぁそういう状況もそうそうねーでしょってことで。

動作をさらっと見た感じの解説とか

まぁサンプル見ろっつーと終わっちゃうけど。

Twain.netを使うときの大まかな流れはこんなの。

  private void hoge(){
    Twain twain = new Twain(); //インスタンス作って
    twain.init(this.handle);  // 初期化して
    twain.Select(); // デバイス選んで
    twain.Aquire(); // 撮影
  }

init()でWindowHandleを渡してるのは、Aquire()のレスポンスがWindowのMessagePassingに乗っかって返ってくるから。

このメッセージを拾うのにDelphiなんかだとmessage修飾子を使って引っかけたりTForm.OnMessage()?みたいなので引いてた覚えがあるんだけど、C#ではIMessageFilterっつーインターフェースを実装することで拾わなきゃならんらしい。

このインターフェイスは、メッセージがコントロールまたはフォームにディスパッチされる前にアプリケーションがメッセージをキャプチャできるようにします。

IMessageFilter インターフェイスを実装するクラスは、アプリケーションのメッセージ ポンプに追加してメッセージにフィルタをかけたり、メッセージがフォームまたはコントロールにディスパッチされる前にほかの操作を実行したりできます。メッセージ フィルタをアプリケーションのメッセージ ポンプに追加するには、Application クラスの AddMessageFilter メソッドを使用します。

#まだ.netに慣れてないからこのへんの機構がまだしっくり来ないんだよなぁ。

さておき実装。

class Form1 : Form : IMessageFilter{
  
  private void hoge(){
    Twain twain = new Twain(); //インスタンス作って
    twain.init(this.handle);  // 初期化して
    twain.Select(); // デバイス選んで
    Application.AddMessageFilter(this); //フィルタを仕掛けて
    twain.Aquire(); // 撮影
  }
   
   /// <summary>
   /// メッセージを拾う
   /// </summary>
   /// <param name="m"></param>
   /// <returns></returns>
   bool IMessageFilter.PreFilterMessage(ref Message m) {
     //メッセージ受け処理とか
   }
}

動作の流れとしちゃこんな感じか

                               handle   
   |UserForm|◆-------|:Twain| - - - -> |twain32.dll| <- - -> |twain device|
      ↑                                     |
       +-------------------------------------+                                              
                  [message]

戻ってきたメッセージには取得したイメージへのポインタが入っててGDI+を使って取り出してるようだ。*1

あとはPreFilterMessage()で、

  TwainCommand cmd = twain.PassMessage(ref m); 

してTransferReadyだったら

   ArrayList pics = twain.TransferPictures(); // 写真リスト取得
   for (int i = 0; i < pics.Count; i++) {
       TwainImage tImg = new TwainImage((IntPtr)pics[i]);
       tImg.saveFile("hoge"+i+".jpg"); // ファイルに保存
   }

って感じ。

*1:そこまでは見てない