« 2009年4月 | トップページ | 2009年7月 »

2009年6月30日 (火)

PDF作成の自動化・省力化 [6] (CADのPDF化 : LISPスクリプト部分)

[5]で作成したSdsTest.dllの関数をLISPから利用する例

---------------------------------------------------------------------------

(defun c:pdfprint(
  / ctrlsaveas pdfpath hprocess pdfwriter ret)
 
  (setq ctrlsaveas "D:\\..........\\CtrlSaveas.exe")  ; 「PDF作成の自動化・省力化 [3]」で作成
  (setq pdfpath "D:\\........\\PDFファイル名.pdf")  ; 出力ファイルパス
 
  (setq pdfwriter "いきなりPDF Professional")
  ;(setq pdfwriter "クセロPDF2")
 
  (xload "SdsTest")    ; SdsTest.dllをロードする
 
  ; CtrlSaveas.exe起動
  (setq hprocess (sdstest_createprocess ctrlsaveas pdfpath 30 pdfwriter))
  ; 印刷実行
  (command "-print" "yes" "" pdfwriter "" "M" "L" "No" "" "" "" "" "" "No" "Yes" "No" "No" "Yes")
  ; 印刷終了を待つ
  (setq ret (sdstest_waitforprocess hprocess 5))
  (if (= ret 0)
    (progn
      (if (/= (findfile pdfpath) nil)
        (print (strcat "PDFが作成されました : " pdfpath))
      )
    )
  )
  (if (= ret 1)
    (print "PDF作成がタイムアウトしました")
  )
  (print "終わり")
 
  (xunload "SdsTest")
)

---------------------------------------------------------------------------
このスクリプトによって現在開いているCAD図面をPDF化することができる。

[実行結果の例]
: pdfprint
sdstest_createprocess: D:\*****\CtrlSaveas.exe "D:\*****\XXXXX.pdf" 30 "いきなりPDF Professional"
プロセスハンドル=648
648
: -print
詳細な印刷環境設定 <Yes>/No: yes
レイアウト名を入力 or [?] <Model>:
出力デバイス名を入力 or [?] <いきなりPDF Professional>: いきなりPDF Professional
用紙サイズを入力 or [?] <A3>:

(途中省略)

印刷結果をファイルに出力 Yes/<No>: No
印刷を開始します <Yes>/No: Yes
プロセスハンドル=648
プロセスが正常に終了しました。
"PDFが作成されました : D:\\*****\\XXXXX.pdf"
"終わり"
"SdsTest"

===========================================================================
今まで制作してきたコードをシェイプアップした上で実運用に乗せてみますが、果たして現場でうまくいくだろうか??

PDF作成の自動化・省力化 [5] (CADのPDF化 : C言語部分)

今回の仕事の顧客はCAD図面のPDF化もしています。(利用しているCADは"IJCAD"というソフトです。バージョンは6)

やはり現状では手作業でPDFプリンタに印刷してPDF化しているわけです。この作業もCtrlSaveas.exeとVB6との組み合わせで実現したのと同様の考え方で自動化・省力化を試みてみました。(やりたかったのはこれで、"PDF作成の自動化・省力化 [1]~[4]" はそのための準備でありました。CコンパイラにVC++ Ver6を使ったのもIJCADの機能拡張にはこのバージョンが適切との情報によります。)

IJCADのC言語プログラミングインターフェース規格"SDS"に従ってCの関数を作成し(DLLにまとめられます)、IJCADのスクリプト言語であるLISPからそれを呼んでみます。

まずはC言語部分。
---------------------------------------------------------------------------
[SdsTest.dllのソースSdsTest.cppの抜粋]

static struct func_def func_table[] =        {
  {"sdstest_createprocess", sdstest_createprocess},
  {"sdstest_waitforprocess", sdstest_waitforprocess}
                                             };

/*
*  外部プログラムの起動。
*  LISPからの使い方:
*    (sdstest_createprocess コマンド [任意の個数のコマンドパラメータ])
*  戻り値:プロセスハンドル エラー時は0
*/
static int sdstest_createprocess(struct sds_resbuf *rb){
  struct sds_resbuf *p;
  char  commandline[1024];
  char  param[256];
  STARTUPINFO  stinf;
  PROCESS_INFORMATION  procinf;
  HANDLE  hProcess = 0;    // Process Handle

  // 最初のパラメータはコマンドライン文字列
  p = rb;
  if (p != NULL && p->restype == SDS_RTSTR){
    strcpy(commandline, p->resval.rstring);
  } else {
    sds_printf("\nコマンドライン文字列が不正です。");
    return sds_retint(0);
  }
  // 残りのパラメータはコマンドラインの引数
  for (p=p->rbnext; p!= NULL; p=p->rbnext){
    if (p->restype == SDS_RTSHORT){
      sprintf(param, " %d", p->resval.rint);
    } else if (p->restype == SDS_RTLONG){
      sprintf(param, " %ld", p->resval.rlong);
    } else if (p->restype == SDS_RTSTR){
      sprintf(param, " \"%s\"", p->resval.rstring);
    } else if (p->restype == SDS_RTREAL){
      sprintf(param, " %f", p->resval.rreal);
    } else {
      continue;
    }
    strcat(commandline, param);
  }

  // コマンドを起動
  sds_printf("\nsdstest_createprocess: %s\n", commandline);
  memset(&stinf, 0, sizeof(STARTUPINFO));
  stinf.cb = sizeof(STARTUPINFO);
  if (CreateProcess(NULL, commandline, NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS,
    NULL, NULL, &stinf, &procinf) == 0){
    // エラー
    sds_printf("\nsdstest_createprocessはコマンド%sの起動に失敗しました", commandline);
    return sds_retint(0);
  } else {
    hProcess = procinf.hProcess;
  }
  sds_printf("\nプロセスハンドル=%ld", hProcess);
  return sds_retint((int)hProcess);
}

/*
*  プロセスが終了するのを待つ
*  LISPからの使い方:
*    (sdstest_waitforprocess プロセスハンドル タイムアウト秒)
*  戻り値:0=プロセスの正常終了 1=タイムアウト 2=その他エラー
*/
static int sdstest_waitforprocess(struct sds_resbuf *rb){
  struct sds_resbuf *p;
  DWORD  timeout;
  int    ret = 2;
  HANDLE  hProcess;
  DWORD  res;

  p = rb;
  if (p != NULL && p->restype == SDS_RTSHORT){
    hProcess = (HANDLE)(p->resval.rint);
    p = p->rbnext;
  } else if (p != NULL && p->restype == SDS_RTLONG){
    hProcess = (HANDLE)(p->resval.rlong);
    p = p->rbnext;
  } else {
    sds_printf("\nプロセスハンドルが不正です。");
    return sds_retint(ret);
  }
  sds_printf("\nプロセスハンドル=%ld", hProcess);
  if (p != NULL && p->restype == SDS_RTSHORT){
    timeout = p->resval.rint;
  } else if (p != NULL && p->restype == SDS_RTLONG){
    timeout = p->resval.rlong;
  } else {
    sds_printf("\nタイムアウト時間が不正です。");
    return sds_retint(ret);
  }
  timeout *= 1000;

  // プロセスの終了を待つ
  if ((res = WaitForSingleObject(hProcess, (timeout==0 ? INFINITE : timeout))) == WAIT_OBJECT_0){
    sds_printf("\nプロセスが正常に終了しました。");
    ret = 0;
  } else if (res == WAIT_TIMEOUT){
    sds_printf("\nプロセス終了がタイムアウトしました。");
    ret = 1;
  } else {
    sds_printf("\nその他エラー");
    ret = 2;
  }
  CloseHandle(hProcess);

  return sds_retint(ret);
}

PDF作成の自動化・省力化 [4] (改良版CtrlSaveas.exeをVBから使う)

[3]のプログラム(CtrlSaveas.exe)をVB6から使います。

Shell関数でCtrlSaveas.exeを起動、ShellExecute関数で印刷を開始したあと、WaitForSingleObject関数でCtrlSaveas.exeの終了(すなわち印刷ジョブの終了)を待ちます
エクセルドキュメントの様にShellExecuteの中で印刷ジョブが終わってしまう場合も問題ない。
しかし、"自分の"印刷ジョブの終了を待つ仕様でないので、印刷ジョブを連続して投入する様な場合には問題があるかもしれない。

---------------------------------------------------------------------------

    doc = "PDF化対象ドキュメントファイルパス"
    pdf = "出力PDFファイルパス"
    dir = "......."
    pdfprinter = "いきなりPDF Professional"  ' PDF作成プリンタ名
   
    ctrlsaveas = ".....\CtrlSaveas.exe "
    ctrlsaveas = ctrlsaveas & """" & pdf & """"
    ctrlsaveas = ctrlsaveas & " 180 "     ' タイムアウト
    ctrlsaveas = ctrlsaveas & """" & pdfprinter & """"
    ProcessID = Shell(ctrlsaveas, vbNormalFocus)    ' VB関数
    ProcessHandle = OpenProcess(SYNCHRONIZE, True, ProcessID)   ' Win32 API
 
    ' 事前に通常使うプリンタをPDFプリンタに設定しておく。
    If ShellExecute(0, "print", doc, vbNullString, dir, SW_HIDE) > 0 Then       ' Win32 API
        WaitForSingleObject ProcessHandle, 180000   ' Win32 API
        CloseHandle ProcessHandle   ' Win32 API
    Else
        MsgBox "印刷実行でエラー: " & Str(GetLastError)
    End If

    Win32 API 関係の宣言をVB6の標準モジュールに記述しておく必要がある(VB6付属のWIN32API.TXTから関係する部分をコピーする)。また、VB6からプロセスを起動しプロセスハンドルを取得する方法についてはMSDNのドキュメント「[VB] Win32 アプリケーションを起動させ、終了させる方法」(文書番号: J043339)を参照しました。

2009年6月29日 (月)

PDF作成の自動化・省力化 [3] (「名前を付けて保存」を外から操作するプログラムの改良版)

[1]のCtrlSaveas.exeは「名前を付けて保存」ダイアログが表示されるのを待つわけですが、このダイアログの表示は印刷処理の一部です。つまりこのダイアログが表示されたときには印刷処理が開始しているわけです。
そこでさらに印刷処理の終了まで待つ機能も付けました。

---------------------------------------------------------------------------
[ CtrlSaveas.cpp ]

BOOL WaitAndCtrlSaveas(LPCSTR filepath, DWORD timeout, BOOL ExecEnter, LPCSTR printer);

int main(int argc, char* argv[])
{
  DWORD  timeout;
  TCHAR  printer[64];

  if (argc == 1){
    printf("CtrlSaveas.exe ファイルパス タイムアウト秒 [プリンタ名]\n");
    return 0;
  }
  timeout = atol(argv[2]);
  if (argc >= 4){
    _tcscpy(printer, argv[3]);
  } else {
    printer[0] = '\0';
  }

  WaitAndCtrlSaveas(argv[1], timeout, TRUE, printer);
  return 0;
}

// EnumChildWindowsのコールバック関数。
BOOL CALLBACK GetEdit(
  HWND hwnd,      // 子ウィンドウのハンドル
  LPARAM lParam   // アプリケーション定義の値
){

  TCHAR  clname[32];

  if (GetClassName(hwnd, clname, 32) > 0){
    if (strcmp(clname, "Edit") == 0){
      *((HWND *)(lParam)) = hwnd;
    }
  }

  return TRUE;
}

BOOL WaitAndCtrlSaveas(LPCSTR filepath, DWORD timeout, BOOL ExecEnter, LPCSTR printer)
{
  DWORD  starttime;
  HWND  hWnd;
  BOOL  ret = FALSE;

  timeout *= 1000;
  starttime = GetTickCount();
  for (;;){
    if ((GetTickCount() - starttime) > timeout){
      break;
    }
    hWnd = FindWindow(NULL, "名前を付けて保存");
    //if (hWnd != NULL /*&& hWnd == GetForegroundWindow()*/){
    if (hWnd && IsWindowVisible(hWnd)){
      HWND  hWndEdt = NULL;
      EnumChildWindows(hWnd, (WNDENUMPROC)GetEdit, (LPARAM)&hWndEdt);
      //
      if (hWndEdt != NULL){
        SendMessage(hWndEdt, WM_SETTEXT, 0, (LPARAM)filepath);
        if (ExecEnter != FALSE){
          PostMessage(hWnd, WM_KEYDOWN, VK_RETURN, 0);
        }
        hWndEdt = NULL;
        ret = TRUE;
        break;
      }
    } else {
      Sleep(100);
    }
  }
  // ↑ここまで[1]と同じ。
  // プリンタが指定されているときは印刷ジョブの終了も待つ
  if (ret == TRUE){
    if (printer[0]){
      HANDLE  hPrinter;
      PRINTER_INFO_2  *pPrinterInfo = NULL;  // プリンタ情報取得
      DWORD  dwPrinterInfo = 0;
      DWORD  bytes, jobs;
      
      ret = FALSE;
      for (;;){
        if ((GetTickCount() - starttime) > timeout){
          break;
        }
        jobs = 0xFFFF;
        if (OpenPrinter((char *)printer, &hPrinter, NULL) != 0){
          for (;;){  // このループは2回しか回らない。最初は必要なメモリ量の取得、次に印刷ジョブ数の取得
            // プリンタ情報取得
            if (GetPrinter(hPrinter, 2, (LPBYTE)pPrinterInfo, dwPrinterInfo, &bytes) == 0){
              if (GetLastError() == ERROR_INSUFFICIENT_BUFFER){
                if (pPrinterInfo != NULL) free(pPrinterInfo);
                dwPrinterInfo = bytes;
                pPrinterInfo = (PRINTER_INFO_2 *)malloc(dwPrinterInfo);
                continue;
              }    // その他エラーを無視
            } else {
              jobs = pPrinterInfo->cJobs;    // 現在の印刷ジョブ数
            }
            break;
          }
          ClosePrinter(hPrinter);
          if (jobs == 0){
            ret = TRUE;
            break;
          }
        }
        Sleep(1000);  // 印刷ジョブ数が0でない場合、時間を置いて再度ジョブ数を取得
      }
      if (pPrinterInfo != NULL) free(pPrinterInfo);
    }
  }
  return (ret);
}

PDF作成の自動化・省力化 [2] (CtrlSaveas.exeをVBから使う)

[1]のプログラム(CtrlSaveas.exe)をVB6から使います。

    doc = "PDF化対象ドキュメントファイルパス"
    pdf = "出力PDFファイルパス"
    dir = "......."
   
    ' 印刷前にCtrlSaveas.exeを起動しておく
    ctrlsaveas = ".....\CtrlSaveas.exe "
    ctrlsaveas = ctrlsaveas & """" & pdf & """"
    ctrlsaveas = ctrlsaveas & " 60"     ' タイムアウト
    Shell ctrlsaveas        ' VB関数
   
    ' 印刷実行:事前に"通常使うプリンタ"をPDFプリンタに設定しておく
    ShellExecute 0, "print", doc, vbNullString, dir, SW_HIDE      ' Win32 API

---------------------------------------------------------------------------

これで動きました。(PDFプリンタとして"いきなりPDF Professional"と"クセロPDF2"でテスト)

しかしドキュメントの種類と環境によっては問題がありそうです。
エクセルドキュメントならば印刷が終わるまでShellExecuteで止まっていて、ShellExecuteを抜けた時点でPDFが生成されています。
ところが(自分の環境においてですが)テキストドキュメントを上記のコードで印刷する(PDF化する)とテキストエディタMifesの印刷設定ダイアログが表示されるのですが、その時点でShellExecuteを抜けてしまっています。しかしPDFはまだ生成されていません。

ShellExecuteを抜けても印刷の開始と終了とを待たなければPDFファイルを利用(ファイルコピーなど)できないことになります。
そのため「名前を付けて保存」ダイアログを操作するプログラムCtrlSaveas.exeを改良してみます。

PDF作成の自動化・省力化 [1] (「名前を付けて保存」を外から操作する)

ある仕事で、VB6で作成したアプリからエクセルのドキュメントを生成するプログラムがありました。
そのプログラムは完成しすでに運用に乗っているのですが、その後、そのエクセルドキュメントをPDFにもしたいという話が出てきました。
各クライアントPCには廉価版のPDF作成ソフト(プリンタドライバとしてインストールされる)がインストールされているので、現状はオペレータに手作業でPDF化してもらっています。しかし、「名前を付けて保存」ダイアログでフォルダを選択してから名前を入力する作業がわずらわしいので何とか自動化または省力化ができないか、というわけです。

そこで「名前を付けて保存」ダイアログに"外部から"出力ファイル名をセットしENTERを入力するプログラムを作ってみました。開発言語はVC++6。
プログラムの構造はループの中でタイムアウトが来るまで「名前を付けて保存」ウインドウを待ち、それが出てきたらダイアログ上のEditにファイルパスをセットしENTERを送る、というものです。このプログラムを印刷に先立って起動しておきます。

このプログラムは
 ・ダイアログ上にはEditは一つしかなく、それが出力ファイル名欄であること。
 ・OKボタンがDefault Buttonであること。
が前提であり
 ・起動時に他のプログラム(メモ帳など)の「名前を付けて保存」ダイアログが出ていたらそれに反応してしまう。
などの問題があります。(GetTickCount関数の"巻き戻し"も無視している。出力ファイルが既存の時、無条件に削除するか、独自に上書き確認するか、PDFプリンタの上書き確認に任せるか、などが必要。)

---------------------------------------------------------------------------
[ CtrlSaveas.cpp ]

BOOL WaitAndCtrlSaveas(LPCSTR filepath, DWORD timeout, BOOL ExecEnter);

int main(int argc, char* argv[])
{
  DWORD  timeout;

  if (argc == 1){
    printf("CtrlSaveas.exe ファイルパス タイムアウト秒\n");
    return 0;
  }
  timeout = atol(argv[2]);

  WaitAndCtrlSaveas(argv[1], timeout, TRUE);
  return 0;
}

// EnumChildWindowsのコールバック関数。
BOOL CALLBACK GetEdit(
  HWND hwnd,      // 子ウィンドウのハンドル
  LPARAM lParam   // アプリケーション定義の値
){

  TCHAR  clname[32];

  if (GetClassName(hwnd, clname, 32) > 0){
    if (strcmp(clname, "Edit") == 0){
      *((HWND *)(lParam)) = hwnd;
    }
  }

  return TRUE;
}

BOOL WaitAndCtrlSaveas(LPCSTR filepath, DWORD timeout, BOOL ExecEnter)
{
  DWORD  starttime;
  HWND  hWnd;
  BOOL  ret = FALSE;

  timeout *= 1000;
  starttime = GetTickCount();
  for (;;){
    if ((GetTickCount() - starttime) > timeout){ // タイムアウトチェック
      break;
    }
    hWnd = FindWindow(NULL, "名前を付けて保存");
    if (hWnd && IsWindowVisible(hWnd)){ // ダイアログが表示されている
      HWND  hWndEdt = NULL;
      // ダイアログ上のEditを取得
      EnumChildWindows(hWnd, (WNDENUMPROC)GetEdit, (LPARAM)&hWndEdt);
      if (hWndEdt != NULL){
        SendMessage(hWndEdt, WM_SETTEXT, 0, (LPARAM)filepath);
        if (ExecEnter != FALSE){
          PostMessage(hWnd, WM_KEYDOWN, VK_RETURN, 0);
        }
        hWndEdt = NULL;
        ret = TRUE;
        break;
      }
    } else {
      Sleep(100);
    }
  }
  return (ret);
}

« 2009年4月 | トップページ | 2009年7月 »