3流プログラマのメモ書き

元開発職→社内SE→派遣で営業支援→開発戻り浦島太郎状態の三流プログラマのIT技術メモ書き。 このメモが忘れっぽい自分とググってきた技術者の役に立ってくれれば幸いです。

(VB.Net)[補足].NetからのPDFファイル印刷方法

以前に(VB.Net).NetからのPDFファイル印刷方法という記事を書きましたが、それの追記です。

実はあの方法だと1つ問題が生じることが分かりました。
それはプロセスの終了方法です。

'プロセス終了
pro.WaitForExit(5000)
pro.Kill()

というやり方は時間指定で、5秒待ったらAdobeReaderのプロセスを殺すというアプローチをとっていますが、この時間が環境によってまちまちなのです。

たとえばAdobeReader自体重いアプリケーションに入るのでスペックが低いマシンだとプロセス立ち上がり印刷スプールが終わるまで何十秒もかかるかもしれません。ということは5秒でプロセス殺すと結局印刷できないことになります。逆に速いマシンでは数秒で片付くでしょう。

まあ待ち時間をかなり長くするというのも手なのでしょうが、それは嫌だったので、実際にAdobeReaderが印刷ジョブにスプールし終わったことを確認してプロセス終了するという方法を考えてみました。

なお、.Net2.0のFCL(クラスライブラリ)には印刷キューやプリンタ情報関連を操作するクラスがほとんどありません。
Win32API使うしかないのかと思っていたところ、.Net3.0から追加された System.Printing.dll を参照設定で読み込めばVisualStudio2005(.Net2.0)でも印刷関連の各種情報を操作できることが分かりました。

ということで、VisualStudio2005で以下のソースを実行するには.Net3.0以上のインストールと System.Printing.dll 参照設定が必要になると思います。
(インストールしてなくても、PresentationCore.dll , System.Printing.dll さえ用意すればいいみたいですが。。)

'Imports System.Printing が必要!
 
    Private Function PrintPdf() As Boolean
        '====レジストリからAcrobat,AcrobatReaderのパス取得====

        Dim strRegPath As String
        Dim rKey As RegistryKey
 
        'キーを取得(最初にAcrobat,だめならAdobeReader)
        strRegPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Acrobat.exe"
        rKey = Registry.LocalMachine.OpenSubKey(strRegPath)
        If rKey Is Nothing Then
            strRegPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\AcroRd32.exe"
            rKey = Registry.LocalMachine.OpenSubKey(strRegPath)
 
        End If
 
        '値(exeのパス)を取得(既定の値の場合は空文字指定)
        Dim location As String
        Try
            '値(exeのパス)を取得(既定の値の場合は空文字指定)
            location = DirectCast(rKey.GetValue(""), String)
        Catch ex As NullReferenceException
            Throw New ApplicationException("AcrobatもしくはAdobeReaderがインストールされていないため、PDFファイルの印刷ができません。")
        Finally
            '開いたレジストリキーを閉じる
            rKey.Close()
        End Try
 
        '===Acrobatを起動し印刷===
        Dim filepath As String = "D:\print_test.pdf"
        Dim pro As New Process()
 
        '.Net的書き方(C#でも可能な書き方)
        'Acrobatのフルパス設定
        pro.StartInfo.FileName = location
        pro.StartInfo.Verb = "open"
        'Acrobatコマンドライン引数設定
        pro.StartInfo.Arguments = " /n /t " + filepath
        pro.StartInfo.WindowStyle = ProcessWindowStyle.Hidden
        'プロセスを新しいWindowで起動
        pro.StartInfo.CreateNoWindow = True
 
        'プリントサーバの情報取得
        Dim prtSv As New LocalPrintServer()
        '印刷キュー取得
        Dim que As PrintQueue = prtSv.DefaultPrintQueue
 
        'AdobeReaderプロセス起動して印刷
        pro.Start()
 
 
        '処理フラグ T:正常 F:異常
        Dim blnRtn As Boolean = True
        'ジョブ番号
        Dim intJobNum As Integer
 
        'ループ内での時間カウント
        Dim intCnt As Integer = 0
        '下記のループで待つ時間をミリ秒単位で指定
        Dim intWatiMiliTime As Integer = 500
 
        'PDFの印刷がキューに入るまで待つ。
        While True
            '指定ミリ秒待つ
            Threading.Thread.Sleep(intWatiMiliTime)
            intCnt += intWatiMiliTime
 
            '1分以上たっていたら、印刷に問題があったことにして処理異常フラグを立てる
            If intCnt > 60000 Then
                blnRtn = False
            End If
 
            'キューが1つ以上
            If que.NumberOfJobs > 0 Then
                Dim jobList As New List(Of PrintSystemJobInfo)()
                '印刷ジョブコレクション取得
                For Each i As PrintSystemJobInfo In que.GetPrintJobInfoCollection
                    jobList.Add(i)
                Next
                '最新ジョブ名に".pdf"が含まれるとき、このプログラムから印刷キューに入れたと判断する。
                If jobList(jobList.Count - 1).Name.EndsWith(".pdf", True, Nothing) Then
                    intJobNum = jobList(jobList.Count - 1).JobIdentifier
                    Exit While
                End If
            End If
        End While
 
        intCnt = 0
 
        '指定されたジョブが印刷完了するまで待つ
        While blnRtn
            '指定ミリ秒待つ
            Threading.Thread.Sleep(intWatiMiliTime)
            intCnt += intWatiMiliTime
 
            Dim jobNow As PrintSystemJobInfo
            Try
                'このプログラムから印刷キューに入れたジョブを取得
                '(ループの中でキューからジョブを取得しないとJobStatusが更新されないので注意)
                jobNow = que.GetJob(intJobNum)
            Catch ex As Exception
                '(すでに印刷が完了してジョブが破棄されたとき対策)
                '異常フラグ立てループ終了
                blnRtn = False
            End Try
 
            'ジョブが印刷中,印刷完了されていればスプールが終わっているはずなので処理正常終了
            If jobNow.JobStatus = PrintJobStatus.Completed _
            OrElse jobNow.JobStatus = PrintJobStatus.Deleted _
            OrElse jobNow.JobStatus = PrintJobStatus.Deleting _
            OrElse jobNow.JobStatus = PrintJobStatus.Printed _
            OrElse jobNow.JobStatus = PrintJobStatus.Printing _
            OrElse jobNow.JobStatus = PrintJobStatus.Retained Then
                'ループ終了
                Exit While
            End If
 
            'ジョブがなんらかのエラか、タイムアウト(1分)したなら処理異常終了
            If jobNow.JobStatus = PrintJobStatus.Error _
            OrElse jobNow.JobStatus = PrintJobStatus.Offline _
            OrElse jobNow.JobStatus = PrintJobStatus.PaperOut _
            OrElse jobNow.JobStatus = PrintJobStatus.UserIntervention _
            OrElse intCnt > 60000 Then
                '異常フラグ立てループ終了
                blnRtn = False
            End If
        End While
 
        'プロセス終了
        pro.Kill()
 
        Return blnRtn
    End Function


特に後半部分が追加した、印刷ジョブの監視機能となります。
ただ、スプールが終わったかのジョブステータス判断等のif文は詳しく調べたわけでないので、正しくないかもしれません。
もっと綺麗な方法があるっていう人は情報ください。

まだ、System.Printing名前空間配下のクラスの情報がほとんど出回ってないんですよね。
まあ、MSDNにはこのライブラリのリファレンスがあるので、使い方はわかるんですが。。

ちなみにAPIを使ってプリンタや印刷関連の情報を取得する方法は結構出回ってます。下記リンク参照。
プリンタのポート、状態を取得する
MS: Visual Basic .NET アプリケーションから EnumJobs 関数を呼び出す方法
プリンタのプロパティだけを表示 (2)

追記:この方法ではジョブやキューの状態取得に問題があることが判明しました。
詳しくは(VB.Net)[補足2].NetからのPDFファイル印刷方法を参照。