...ing logging 4.0

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

DFL: 印刷プレビューダイアログのサンプルコード

印刷プレビューダイアログ PrintPreviewDialog が最低限動くようになったのでGitHubで公開しました。

前回記事で述べたPrintDialogとPageSetupDialogの実装ではコモンダイアログを呼び出していますが、PrintPreviewDialogは自分で実装しました。

ツールバーアイコン

ツールバーのアイコンは自作しましたが、ボタンのビットマップをDFLに埋め込むためにリソールファイルを使うのが面倒だったので、下の方法で手抜きしています。

ImageList _imageList = new ImageList;
_imageList.imageSize = Size(32,32);
_imageList.transparentColor = Color.red;
import std.path;
string bmpPath = dirName(__FILE__) ~ r"\image\previewprintdialog_toolbar.bmp";
_imageList.images.addStrip(new Bitmap(bmpPath));
_toolBar.imageList = _imageList;

印刷範囲の設定

今回のアップデートでhasMorePageプロパティをなくしました。 WinFormsではPrintDocument.print()の呼び出し後に印刷終了をユーザが指示する仕組みですが、 DFLでは印刷する前にPrintRange構造体の配列を作って印刷範囲を設定する方法に絞りました。

PrintRangeひとつで(fromPage,toPage)が表現されて、(1,2)なら1ページ目と2ページ目が印刷範囲になります。 (1,1)なら1ページ目だけが印刷範囲です。 (1,3)なら1ページ目、2ページ目及び3ページ目が印刷範囲です。 このPrintRangeの配列を作るので、[(1,3),(4,4),(7,8)]のようになり、この場合は1、2、3、4、7、8ページが印刷範囲になります。

PrintDocument.print()を呼び出すと、印刷範囲の先頭から順に、 printPageイベントハンドラの引数でページ番号が与えられるので、 そのページ番号に応じてユーザが描画します。

印刷範囲が最後までスキャンされれば印刷処理は自然に終わりますが、 e.cancelプロパティを使えば印刷処理の途中で中断することも引き続き可能です。

その他の変更点

座標系の変換を間違えてばかりだったので、変換を一括して請け負うPrinterUnitConvertを実装しました。

サンプルコード

前回の記事に印刷プレビュー部分を追加しただけなので長いですがそのまま記載します。

import dfl;
import std.conv;

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

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

        this._document = new PrintDocument(); // 印刷されるドキュメントを生成
        this._document.printRange ~= &doPrintRange; // ダイアログで選択された印刷範囲ごとにドキュメントの状態に応じたページ範囲を設定する
        this._document.beginPrint ~= &doBeginPrint; // 印刷範囲の決定後、全体の印刷開始前に呼ばれる
        this._document.queryPageSettings ~= &doQueryPageSettings; // 各ページのページ設定を決定する前に呼ばれる
        this._document.printPage ~= &doPrintPage; // 各ページの印刷をするときに呼ばれる
        this._document.endPrint ~= &doEndPrint; // 印刷が始まる前に、プリンタドライバへの印刷指示が終わった時点で呼ばれる(非同期処理)

        this._pageSetupDialog = new PageSetupDialog(_document); // 印刷するドキュメントを渡して生成
        this._printDialog = new PrintDialog(_document); // 印刷するドキュメントを渡して生成
        this._previewDialog = new PrintPreviewDialog(_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;
        }

        // 印刷プレビューボタンの設定
        with(_previewButton = new Button())
        {
            parent = this;
            text = "印刷プレビュー...";
            location = Point(50, 150);
            size = Size(100, 30);
            click ~= &doPrintPreview;
        }
    }

    /// ページ設定ボタンをクリックしたとき
    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)
    {
        // 印刷ダイアログを表示
        DialogResult r = _printDialog.showDialog();
        if (r == DialogResult.OK)
        {
            // OKボタンを押した後の処理があれば書く
        }
    }

    /// 印刷プレビューボタンをクリックしたとき
    private void doPrintPreview(Control sender, EventArgs e)
    {
        // 印刷プレビューダイアログを表示
        DialogResult r = _previewDialog.showDialog();
        if (r == DialogResult.OK)
        {
            // OKボタンを押した後の処理があれば書く
        }
    }

    ///
    private void doPrintRange(PrintDocument doc, PrintRangeEventArgs e)
    {
        final switch (e.printRange.kind)
        {
        case PrintRangeKind.ALL_PAGES:
            // 印刷ダイアログで「すべて」を選択したときのページ範囲を設定
            e.printRange.addPrintRange(PrintRange(1, 8));
            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:
            // 印刷ダイアログで「ページ指定」を選択したとき
            // ダイアログからページ範囲をもらうのでここでは何も書かない
        }
    }

    ///
    private void doBeginPrint(PrintDocument doc, PrintEventArgs e)
    {
        // msgBox("プリンタドライバへの印刷指示を開始");
    }

    ///
    private void doQueryPageSettings(PrintDocument doc, QueryPageSettingsEventArgs e)
    {
        // 印刷しようとしているページのページ設定をデフォルトから変更する
        // ここでe.pageSettingsを変更しても次のページには影響しない

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

    ///
    private void doPrintPage(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
            );
        }
        else if (e.currentPage >= 2 || e.currentPage <= 8) // 2-8ページの印刷内容を描画
        {
            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);
            }
        }
    }

    ///
    private void doEndPrint(PrintDocument doc, PrintEventArgs e)
    {
        // msgBox("印刷を指示しました");
    }
}

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

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

今後の課題

  • PrintPreviewDialogで、Fitモードでないときにスクロールバーを表示できるようにする。
  • PrintPreviewDialogで、Fitモードのときにウィンドウの横幅に対してもFitするようにする。
  • 途中のページから用紙方向を変えられるようにする。 用紙方向を変えるためにはResetDC()を呼べばいいが、 そのためにはDEVMODE構造体を別のところから持ってこないとできない。
  • 印刷(printing)モジュールの上に、タイムチャート描画ライブラリを実装する。

DFLのダウンロード

github.com