Windows サービスアプリケーションから印刷する

みなさま、こんにちは。Platform SDK サポート チームの中野と申します。昨年からこのチームで Windows アプリケーション開発に関するお問い合わせを調査、回答しているのですが、「Windows サービス アプリケーションから印刷するには?」というお問い合わせを何度かいただきました。これまで、うまく説明されているドキュメントがなかったので皆様も困っていらっしゃるかと思います。最近になって、米国のサポートチームの Blog の記事にうまくまとめられているのを発見しましたので、これを日本語に翻訳してご紹介させていただきます。

 

 

Windows サービスからドキュメントをプリンターに出力させることができると望ましい場合がよくあります。Windows Win32 GDI API をつかってプリンターへ出力することはサポートされていますが、どのユーザー アカウントでサービスを実行するか、またそのアカウントがコンピューターにログインしたことがあるか、などいくつかの重要点を考慮する必要があります。

いくつかのシナリオは、バックグラウンドで動作してレポートをプリンターに印刷するような Windows サービスであったり、サーバー上でまれに発生するエラーを印刷する必要がある Windows サービスであったりします。

印刷するように設計された Windows サービスは、プリンターで文書を印刷するために Win32 GDI API を使用することができます。ただし、Windows サービスから印刷する際はいくつかの考慮すべき点があります。すべての出力ジョブを一人のユーザーから送ってよい場合、サービスをそのユーザーで実行することにより、そのユーザーがコンピューター上でアクセスできるすべてのプリンターが使用できます。その一方で、サービスの実行時に決定されるユーザーによって印刷する場合は、サービスは出力前に、LogonUser()、LoadUserProfile()、及び、ImpersonateLoggedOnUser() を呼ぶ必要があります。

プリンターのデバイス コンテキスト ハンドル取得して、そのハンドルを GDI+ のグラフィック コンストラクターに渡すことで GDI+ の出力をプリンターに送ることも可能ではあるものの、推奨されていません。GDI+ の関数やクラスを Windows サービス内で使用することはサポートされていません。Windows サービスからこれらの関数やクラスを使用すると、サービスのパフォーマンス低下や実行時の例外やエラーといった予期せぬ問題が発生する恐れがあります。

  https://msdn.microsoft.com/en-us/library/windows/desktop/ms533798(v=VS.85).aspx

さらに、Microsoft .NET でアプリケーションを開発しているのであれば、サービスや Web アプリケーションでは System.Drawing はサポートされていません。 System.Drawing をこれらのアプリケーションで使用すると、サービスのパフォーマンス低下や実行時例外・エラーといった予期せぬ問題が発生する恐れがあります。 (https://msdn.microsoft.com/ja-jp/library/system.drawing.aspx の「注意」の項目を参照してください)

System.Printing もサービスではサポートされていません。これは MSDN でも説明されています。System.Printing は、Windows Presentation Foundation の一部です。

  https://msdn.microsoft.com/ja-jp/library/bb613549(v=vs.90).aspx

Windows サービスから印刷する場合は Win32 GDI API を使用してください。

  https://msdn.microsoft.com/en-us/library/windows/desktop/dd145203(v=vs.85).aspx

以下の Win32 C++サンプル コードは Win32 GDI APIを使ってどうやって単一のページをプリンターに印刷するのかを示すものです。 以下はあくまでもサンプル コードです。

        TCHAR szPrinterName[MAX_PATH] = TEXT("");
        DWORD dwNameLen = MAX_PATH;
        HDC hDC;
        DOCINFO di;

        // デフォルトのプリンタを使用します
        GetDefaultPrinter( szPrinterName, &dwNameLen );
        // プリンタの HDC を作成します
        if( (hDC = CreateDC( TEXT("WINSPOOL"), szPrinterName, NULL, NULL )) == NULL )
            return FALSE;

        // StartDoc 関数によりプリントジョブを開始します
        ZeroMemory( &di, sizeof(DOCINFO) );
        di.cbSize = sizeof(DOCINFO);
        di.lpszDocName = TEXT("Print Job");

        if( StartDoc( hDC, &di ) <= 0 )
        {
            DeleteDC( hDC );
            return FALSE;
        }

        // StartPage 関数によりドライバのデータ受け入れを準備します
        if( StartPage( hDC ) <= 0 )
        {
            EndDoc( hDC );
            DeleteDC( hDC );
            return FALSE;
        }

        TCHAR szString[32] = TEXT("Printing from a service");
        TextOut( hDC, 0, 0, szString, lstrlen( szString ) );

        if( EndPage( hDC ) <= 0 )
        {
            EndDoc( hDC );
            DeleteDC( hDC );
            return FALSE;
        }

        if( EndDoc( hDC ) <= 0 )
        {
            DeleteDC( hDC );
            return FALSE;
        }

        //  HDC を削除します
        DeleteDC( hDC );

 

Microsoft .NET アプリケーションを開発する場合は、プラットフォーム呼び出しサービス (PInvoke) により Win32 GDI API を呼んで印刷します。次の C# サンプルコードは Win32 GDI APIs を用いてどうやって長方形をプリンターに出力するかを示すものです。 以下はあくまでもサンプル コードです。

        private void PrintRectangle()
        {
            const int ERROR_INSUFFICIENT_BUFFER = 122;

            IntPtr hdc;
            DOCINFO di = new DOCINFO();
            di.cbSize = 20;
            di.pDocName = "TestingGDI";

            int pcchBuffer = 256;
            StringBuilder pszBuffer = new StringBuilder(pcchBuffer);

            if (GetDefaultPrinter(null, ref pcchBuffer))
            {
                return;
            }

            int lastWin32Error = Marshal.GetLastWin32Error();
            if (lastWin32Error == ERROR_INSUFFICIENT_BUFFER)
            {
                pszBuffer.Capacity = pcchBuffer;
                GetDefaultPrinter(pszBuffer, ref pcchBuffer);
            }

            // デフォルトのプリンタです
            String szPrinter = pszBuffer.ToString();
            hdc = CreateDC(IntPtr.Zero, szPrinter,
                 IntPtr.Zero, IntPtr.Zero);

            Int32 nRes = StartDoc(hdc, di);

            if (nRes > 0)
            {
                // Win32 の StartPage を呼び HDC 上で新たにページを開始します
                nRes = StartPage(hdc); // できればエラーチェックを追加しましょう

                // 長方形を描画します
                Rectangle(hdc, 10, 10, 500, 500);

                nRes = EndPage(hdc);

                // まずドキュメントの終了を知らせる Win32 関数を呼び、
                EndDoc(hdc);

                //つぎにこのプリントジョブ用に作成した HDC を削除します。
                DeleteDC(hdc);
            }
        }

        [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool GetDefaultPrinter(StringBuilder pszBuffer, ref int pcchBuffer);

        // Win32 DOCINFO 構造体。印刷ドキュメント開始に使用します
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public class DOCINFO
        {
            public int cbSize;
            public string pDocName;
            public string pOutputFile;
            public string pDataType;
            public int fwType;
        }

        // CreateDC() はプリンタの HDC 作成に使用します
        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr CreateDC(IntPtr pNull1, string lpszDevice,
        IntPtr
        pNull2, IntPtr lpInitData);

        // StartDocW - StartDoc の Unicode 版 - プリントジョブを開始します
        [DllImport("gdi32.dll", EntryPoint = "StartDocW", SetLastError = true,
        CharSet = CharSet.Unicode, ExactSpelling = true,
        CallingConvention = CallingConvention.StdCall)]
        public static extern Int32 StartDoc(IntPtr hDC, [In,
        MarshalAs(UnmanagedType.LPStruct)] DOCINFO di);

        // StartPage - プリントジョブで新たにページを開始します
        [DllImport("gdi32.dll", EntryPoint = "StartPage", SetLastError = true,
        CharSet = CharSet.Auto, ExactSpelling = true,
        CallingConvention = CallingConvention.StdCall)]
        public static extern Int32 StartPage(IntPtr hDC);

        // EndPage - プリンタ DC で現在のページを終了します
        [DllImport("gdi32.dll", EntryPoint = "EndPage", SetLastError = true,
        CharSet = CharSet.Auto, ExactSpelling = true,
        CallingConvention = CallingConvention.StdCall)]
        public static extern Int32 EndPage(IntPtr hDC);

        // EndDoc - プリントジョブの終了をシグナルします
        [DllImport("gdi32.dll", EntryPoint = "EndDoc", SetLastError = true,
        CharSet = CharSet.Auto, ExactSpelling = true,
        CallingConvention = CallingConvention.StdCall)]
        public static extern Int32 EndDoc(IntPtr hDC);

        // DeleteDC - ジョブ終了後にプリンタ DC を片付けます
        [DllImport("gdi32.dll", EntryPoint = "DeleteDC", SetLastError = true,
        CharSet = CharSet.Auto, ExactSpelling = true,
        CallingConvention = CallingConvention.StdCall)]
        public static extern bool DeleteDC(IntPtr hDC);

        [DllImport("gdi32")]
        public static extern bool Rectangle(IntPtr hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);