...ing logging 4.0

はてなブログに移行しました。D言語の話とかいろいろ。

DFL: 印刷のサンプルコード

2024/5/15更新:Graphicsオブジェクトの座標系を、MM_TEXT(96dpi)から、GraphicsUnit.DISPLAY相当に変更したので、サンプルコードの座標系が変わりました。

DFLの印刷モジュールがだいたい動くようになったので、試験的に公開しました。

基本的な使い方は、WinFormsのPrintDocument、PrintDialog、PrintSetupDialog関係と大体同じなのですが、 WinFormsでは色々な使い方ができるように汎用性を高めてあるせいか仕様が直感的でなかったので、 アレンジしたところがあります。

サンプルコードの説明

まずは、本記事の末尾にあるサンプルコードの説明です。 実行すると、ページ設定ボタンと印刷ボタンがあるウィンドウが開きます。

ページ設定ボタンをクリックすれば、コモンダイアログのページ設定ダイアログが開かれます。 WinFormsと同じく、このウィンドウにはPageSetupDialogクラスが対応します。

既定のプリンタに応じて、

  • 用紙サイズ
  • 給紙方法
  • 印刷の向き
  • 余白の大きさ

が変更できます。横書きに設定した例は、下図のとおりです。

また、印刷ボタンをクリックすれば、コモンダイアログの印刷ダイアログが開かれます。 こちらもWinFormsと同じく、このウィンドウにはPrintDialogクラスが対応します。

ここでは次の項目が変更できます。

  • 使用するプリンタ
  • ページ範囲(「すべて」、「選択した部分」、「現在のページ」、「ページ指定」のいずれか)
  • 部数

まだ、「ファイルへ出力」、「適用」は、ちゃんと動いていないかもしれません(未確認)。

WinFormsとの違い

  • PrinterSettingsだけが既定のページ設定情報defaultPageSettingsを持ちます。 WinFormsのPrintDocumentは、既定のページ設定情報DefaultPageSettingsを持ちますが、DFLでは直接持たず、 その役割はPrinterSettingsに集約されます。 つまり、PrintDocumentはPrinterSettingsを持ち、PrinterSettingsはPageSettings(defaultPageSettings)を持ちます。
  • DFLでは、印刷範囲の種類は、PrintRange列挙型ではなく、PrintRangeKind列挙型で表します。
  • DFLでは、PrintRangeは構造体であり、一組のページ範囲(fromPageからtoPageまで)を表します。
  • WinFormsでは、印刷範囲をPrinterSettingsがfromPageとtoPageというプロパティで持ちますが、 (1-10,20-30)のような飛び飛びのページ範囲を素直に表現できないので、 DFLでは、印刷範囲をPrinterSettingsが持つPrintRangeSettingsクラスで表します。
  • PrintRangeSettingsクラスは、印刷範囲の種類PrintRangeKindを持ち、また、PrintRange構造体の配列を持ちます。 例えば、印刷ダイアログで「ページ指定」を選択して印刷するときに「1,2-3,4」と入力されると、 (1,1),(2,3),(4,4)の構造に対応したPrintRange構造体の配列が作成されます。
  • WinFormsでは、beginPrint、endPrint、printPage、queryPageSettingsイベントハンドラの4種類がありますが、 DFLでは、PrintDocumentが新しいイベントハンドラprintRangeを持ちます。 DFLでは、印刷の開始前(beginPrintよりも前)に、printRangeイベントハンドラが呼ばれます。 システムはこのときに印刷ダイアログで選択された印刷範囲の種類をユーザーに与えるので、 ユーザーは与えられた印刷範囲の種類に応じて、印刷したいページ番号を設定します。 印刷ダイアログで「ページ指定」が選択された場合は、システムの方が印刷範囲を知っているので、 ユーザー側で印刷範囲を設定する必要はありません。 印刷ダイアログで「すべて」、「現在のページ」、「選択された部分」が選択されたときは、 具体的なページ番号を知っているのはユーザー側でありPrintDocument側なので、このようにしました。
  • DFLでは、PrintPageEventArgsとQueryPageSettingsEventArgsが印刷ページ番号currentPageプロパティを持ちます。 ユーザーはイベントハンドラから与えられた印刷ページ番号currentPageを見て、そのページに印刷したい内容を描画します。
  • WinFormsでは、印刷描画処理の中でhasMorePageプロパティをfalseに設定することで印刷の終了をシステムに伝える仕組みがあります。 DFLでは、ページ範囲を前述のpageRangeイベントハンドラで設定するので、hasMorePageを使わなくても、 ページ範囲の終端まで印刷された時点で自然に印刷が終了します。 今のところ、hasMorePageは残してあります。
  • DFLでは、PageSetupDialogとPrintDialogのコンストラクト時にPrintDocumentを与える必要があります。 システムは、PrintDocumentからPrinterSettingsを取得し、また、PrinterSettingsから(default)PageSettingsを取得します。 WinFormsのように、各Dialogクラスに直接PrinterSettingsクラスとPageSettingsクラスを与えることはできません。
  • 座標系の自動変換にはまだ対応していません。 1/10mm、1/1000mm、1/100インチ、1/100DPIなどの座標系単位が混ざっているので変換を自前でするのが面倒です。
  • 途中のページから用紙方向を変えることはまだできません。

サンプルコード

長くなりますがGitHubの方には上げていないので全文記載します。

import dfl;
import std.conv;

class MainForm : Form
{
    private Button _printButton;     /// 印刷ボタン
    private Button _pageSetupButton; /// ページ設定ボタン
    
    private PrintDocument _document;          /// 印刷されるドキュメント
    private PageSetupDialog _pageSetupDialog; /// ページ設定ダイアログ
    private PrintDialog _printDialog;         /// 印刷ダイアログ

    this()
    {
        this.text = "Simple print"; // ウィンドウタイトルを設定
        this.size = Size(350, 300); // ウィンドウサイズを設定

        this._document = new PrintDocument(); // 印刷されるドキュメントを生成
        this._pageSetupDialog = new PageSetupDialog(_document); // 印刷するドキュメントを渡して生成
        this._printDialog = new PrintDialog(_document); // 印刷するドキュメントを渡して生成

        // ページ設定ボタンの設定
        with(_pageSetupButton = new Button())
        {
            parent = this;
            text = "ページ設定...";
            location = Point(50, 50);
            size = Size(100, 30);
            click ~= &doPageSetupDialog;
        }

        // 印刷ボタンの設定
        with(_printButton = new Button())
        {
            parent = this;
            text = "印刷...";
            location = Point(50, 100);
            size = Size(100, 30);
            click ~= &doPrintDialog;
        }
    }

    /// ページ設定ボタンをクリックしたとき
    private void doPageSetupDialog(Control sender, EventArgs e)
    {
        // ページ設定ダイアログの初期設定
        _pageSetupDialog.minMargins = new Margins(100, 100, 100, 100); // 1/100インチ単位。上下左右1インチを余白とする
        _pageSetupDialog.showNetwork = true;      // ネットワークボタンを表示(OSによっては表示されない)
        _pageSetupDialog.showHelp = true;         // ヘルプボタンを表示
        _pageSetupDialog.allowMargins = true;     // 余白を変更可能にする
        _pageSetupDialog.allowOrientation = true; // 用紙方向を変更可能にする
        _pageSetupDialog.allowPaper = true;       // 用紙を変更可能にする
        _pageSetupDialog.allowPrinter = true;     // プリンタボタンを表示(OSによっては表示されない)

        // ページ設定ダイアログを表示
        DialogResult r = _pageSetupDialog.showDialog();
        if (r == DialogResult.OK)
        {
            // ページ設定ダイアログで設定した内容を表示
            string msg = "[";
            msg ~= "minMargins: " ~ to!string(_pageSetupDialog.minMargins) ~ ", ";
            msg ~= "defaultPageSettings: " ~ to!string(_pageSetupDialog.document.printerSettings.defaultPageSettings) ~ "]";
            msgBox(msg, "doPageSetupDialog");
        }
    }

    /// 印刷ボタンをクリックしたとき
    private void doPrintDialog(Control sender, EventArgs e)
    {
        // 印刷ダイアログの初期設定
        _printDialog.allowSomePages = true; // 「ページ指定」を入力可能にする
        _printDialog.showHelp = true; // ヘルプボタンを表示

        // ダイアログで選択された印刷範囲ごとにドキュメントの状態に応じたページ範囲を設定する
        _printDialog.document.printRange ~= (PrintDocument doc, PrintRangeEventArgs e) {
            final switch (e.printRange.kind)
            {
            case PrintRangeKind.ALL_PAGES:
                // 印刷ダイアログで「すべて」を選択したときのページ範囲を設定
                e.printRange.addPrintRange(PrintRange(1, 2));
                break;
            case PrintRangeKind.SELECTION:
                // 印刷ダイアログで「選択した部分」を選択したときのページ範囲を設定
                e.printRange.addPrintRange(PrintRange(1, 1));
                break;
            case PrintRangeKind.CURRENT_PAGE:
                // 印刷ダイアログで「現在のページ」を選択したときのページ範囲を設定
                e.printRange.addPrintRange(PrintRange(2, 2));
                break;
            case PrintRangeKind.SOME_PAGES:
                // 印刷ダイアログで「ページ指定」を選択したとき
                // ダイアログからページ範囲をもらうのでここでは何も書かない
            }
        };

        // 印刷範囲の決定後、全体の印刷開始前に呼ばれる
        _printDialog.document.beginPrint ~= (PrintDocument doc, PrintEventArgs e) {
            // msgBox("プリンタドライバへの印刷指示を開始");
        };

        // 各ページのページ設定を決定する前に呼ばれる
        _printDialog.document.queryPageSettings ~= (PrintDocument doc, QueryPageSettingsEventArgs e) {
            // 印刷しようとしているページのページ設定をデフォルトから変更する
            // ここでe.pageSettingsを変更しても次のページには影響しない

            // NOTE: まだ印刷開始後に用紙方向を変更することはできない。このタイミングでResetDC()が必要
            // if (e.currentPage == 2)
            //     e.pageSettings.landscape = true; // 2ページ目だけ用紙を横向きにしたい
        };

        // 各ページの印刷をするときに呼ばれる
        _printDialog.document.printPage ~= (PrintDocument doc, PrintPageEventArgs e) {
            Graphics g = e.graphics;
            int dpiX = e.pageSettings.printerResolution.x; // dpi単位
            int dpiY = e.pageSettings.printerResolution.y; // dpi単位

            // すべてのページに余白を描く
            Rect marginRect = Rect(
                e.marginBounds.x * dpiX / 100, // e.marginBoundsは1/100dpi単位
                e.marginBounds.y * dpiY / 100,
                e.marginBounds.width * dpiX / 100,
                e.marginBounds.height * dpiY / 100);
            g.drawRectangle(new Pen(Color.green, 10), marginRect);

            if (e.currentPage == 1) // 1ページの印刷内容を描画
            {
                string str =
                    "PrintDcoument.DocumentName: " ~ to!string(doc.documentName) ~ "\n\n" ~
                    "PrintDcoument.defaultPageSettings: " ~ to!string(doc.printerSettings.defaultPageSettings) ~ "\n\n" ~
                    "PrintDcoument.printerSettings: " ~ to!string(doc.printerSettings) ~ "\n\n" ~
                    "PrintPageEventArgs.pageSettings: " ~ to!string(e.pageSettings) ~ "\n\n" ~ 
                    "PrintPageEventArgs.pageBounds: " ~ to!string(e.pageBounds) ~ "\n\n" ~
                    "PrintPageEventArgs.marginBounds: " ~ to!string(e.marginBounds);
                Rect paramPrintRect = Rect(
                    e.marginBounds.x * dpiX / 100, // e.marginBoundsは1/100dpi単位
                    e.marginBounds.y * dpiY / 100,
                    e.marginBounds.width * dpiX / 100,
                    e.marginBounds.height * dpiY / 100
                );
                g.drawText(
                    str,
                    new Font("MS Gothic", 8/+pt+/ * dpiX / 72),
                    Color.black,
                    paramPrintRect
                );

                e.hasMorePage = true; // 次のページがあるのでtrue
            }
            else if (e.currentPage == 2) // 2ページの印刷内容を描画
            {
                Rect redRect = Rect(1 * dpiX, 1 * dpiY, 1 * dpiX, 1 * dpiY); // 1×1インチの正方形
                redRect.offset(marginRect.x, marginRect.y); // 余白の分だけ座標をオフセット(要改良)
                g.fillRectangle(new SolidBrush(Color.red), redRect);

                Rect blueRect = Rect(dpiX, dpiY, 3 * dpiX, 3 * dpiY); // 3×3インチの正方形
                blueRect.offset(marginRect.x, marginRect.y); // 余白の分だけ座標をオフセット(要改良)
                g.drawRectangle(new Pen(Color.blue, 10), blueRect);

                Rect textRect = Rect(1 * dpiX, 1 * dpiY, 1 * dpiX, 1 * dpiY); // 1×1インチの正方形
                textRect.offset(marginRect.x, marginRect.y); // 余白の分だけ座標をオフセット(要改良)
                g.drawText(
                    "ABCDEあいうえお",
                    new Font("MS Gothic", 12/+pt+/ * dpiX / 72), // 1ポイントは1/72インチ
                    Color.black,
                    textRect
                );

                Rect purpleRect = Rect(3 * dpiX, 3 * dpiY, 1 * dpiX, 1 * dpiY); // 1×1インチの正方形
                purpleRect.offset(marginRect.x, marginRect.y); // 余白の分だけ座標をオフセット(要改良)
                g.drawEllipse(new Pen(Color.purple, 10), purpleRect);

                Pen pen = new Pen(Color.black, 10);
                enum lineNum = 20;
                for (int x; x < lineNum; x++)
                {
                    g.drawLine(
                        pen,
                        marginRect.x + cast(int)(x / 4.0 * dpiX),
                        e.marginBounds.y * dpiY / 100,
                        marginRect.x + cast(int)((lineNum - x - 1)/4.0 * dpiX),
                        e.marginBounds.bottom * dpiY / 100);
                }

                e.hasMorePage = false; // 次のページがないのでfalse
            }
        };

        // 印刷が始まる前に、プリンタドライバへの印刷指示が終わった時点で呼ばれる(非同期処理)
        _printDialog.document.endPrint ~= (PrintDocument doc, PrintEventArgs e) {
            // msgBox("印刷を指示しました");
        };

        // 印刷ダイアログを表示
        DialogResult r = _printDialog.showDialog();
        if (r == DialogResult.OK)
        {
            // OKボタンを押した後の処理があれば書く
        }
    }
}

static this()
{
    Application.enableVisualStyles();
}

void main()
{
    Application.run(new MainForm());
}

今後の課題

  • 座標系の変換が自動でされるようにする。少なくともPrinterUnitConvertとかMarginsConverterとかのヘルパークラスを作って楽したい。
  • 途中のページから用紙方向を変えられるようにする。ResetDC()を呼ぶためにはDEVMODE構造体を別のところから持ってこないとできない。
  • PrintPreviewDialogを実装する。その前にPrintPreviewControlを実装するべきかもしれない。
  • 印刷(printing)モジュールの上に、タイムチャート描画ライブラリを実装する。

DFLのダウンロード

github.com