...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

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

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

DFL: 既定プリンターの給紙方法を取得する

年末から、DFLに印刷機能を追加しようとしている。 GDI Print APIという古いAPIを使っているので日本語の資料は色々あるけど、先は長そうだ。 まずは、APIの使い方を調べるため、ページ設定ダイアログと印刷ダイアログを表示したり、印刷するには印刷できたが、 印刷スケールが滅茶苦茶だったりして、まだまともに動作していない。 とりあえず、WinFormsのPageSetupDialogクラスに相当する実装が動くようにしよう。

さて、ページ設定ダイアログにある給紙方法の選択コンボボックスでの選択結果を取得する方法がよく分からなくて詰まっていたが、 なんとか動くようになったので、一旦、ここにまとめておく。 DeviceCapabilities()にDC_BINSやDC_BINNAMESをセットして返ってくる給紙方法番号や給紙方法文字列が、めんどくさい仕様だった。

ページ設定ダイアログで選択された給紙方法番号は、DEVMODE.dmDefaultSourceで得られる。 この番号は、次のとおり定義されている(wingdi.dから引用)。

// DEVMODE.dmDefaultSource
enum : short {
    DMBIN_ONLYONE = 1,
    DMBIN_UPPER   = 1,
    DMBIN_LOWER,
    DMBIN_MIDDLE,
    DMBIN_MANUAL,
    DMBIN_ENVELOPE,
    DMBIN_ENVMANUAL,
    DMBIN_AUTO,
    DMBIN_TRACTOR,
    DMBIN_SMALLFMT,
    DMBIN_LARGEFMT,
    DMBIN_LARGECAPACITY, // = 11
    DMBIN_CASSETTE   = 14,
    DMBIN_FORMSOURCE,
}
enum : short {
    DMBIN_FIRST = DMBIN_UPPER,
    DMBIN_LAST = DMBIN_FORMSOURCE,
    DMBIN_USER = 256,
}

給紙方法番号は、上記のシステム定義のほか、DMBIN_USER(256)以上の番号で、プリンタごとに定義されている。 うちのプリンターの場合は、5種類の給紙方法のうち、4つがユーザー定義で、1つがシステム定義だった。

DeviceCapabilities()にDC_BINSとWORD型の配列バッファを与えると、配列バッファに給紙方法番号が得られる。 つまり、要素が給紙方法番号を表している

[276, 7, 277, 257]

のような配列が得られる。 また、配列の要素数は関数の戻り値で得られる。

DeviceCapabilities()にDC_BINNAMESと文字配列のバッファを与えると、配列バッファに給紙方法文字列が得られる。 この文字列の仕様がめんどくさい。 それぞれの文字列は24文字ごとに書き込まれていて、 1つの文字列は最大24文字までで、null終端されている。 ただし、1つの文字列が24文字のときは、null終端がない。 よって、文字列ごとにwstring型に分割して代入するために、null終端されているのが何文字目かを調べて、文字列長を修正する手間がかかった。

これで、給紙方法番号と給紙方法文字列の対応が得られた。 2つの配列の同じインデックス番号同士の要素が、それぞれ組になる。 値は嘘だが次のようなイメージ。

[276, 7, 277, 257]
["自動", "手差し", "上トレイ", "下トレイ"]

さらにめんどくさいことに、次は、dmDefaultSourceと同じ値(上記の277とか)を、 給紙方法番号配列から検索して、そのインデックス番号(上記なら2)を得る。

給紙方法文字列の配列をそのインデックス番号で引くと、 ページ設定ダイアログに表示されているものと同じ文字列が得られる。

ということで、とりあえず動いていそうなコードは以下のとおり。

PaperSource createPaperSource(HGLOBAL hDevMode, HGLOBAL hDevNames)
{
    DEVMODE* pDevMode = cast(DEVMODE*)GlobalLock(hDevMode);
    scope(exit)
        GlobalUnlock(pDevMode);
    DEVNAMES* pDevNames = cast(DEVNAMES*)GlobalLock(hDevNames);
    scope(exit)
        GlobalUnlock(pDevNames);
    
    // Get printer basic settings.
    string deviceName = fromUnicodez(pDevMode.dmDeviceName.ptr);
    string outputPort = fromUnicodez(cast(wchar*)(cast(ubyte*)pDevNames + pDevNames.wOutputOffset * wchar.sizeof));

    // Get default paper source kind.
    PaperSourceKind sourceKind = {
        if (pDevMode.dmDefaultSource <= DMBIN_LAST) // System defined paper source.
            return cast(PaperSourceKind)pDevMode.dmDefaultSource;
        else if (pDevMode.dmDefaultSource >= DMBIN_USER) // User defined paper source.
            return PaperSourceKind.CUSTOM;
        else
            assert(0);
    }();

    // Get number of paper sources.
    int sourceNum = DeviceCapabilities(toUnicodez(deviceName), "", DC_BINS, null, pDevMode);
    WORD[] sourceBuffer = new WORD[sourceNum];
    DeviceCapabilities(toUnicodez(deviceName), "", DC_BINS, cast(wchar*)sourceBuffer.ptr, pDevMode);
    WORD[] sourceList;
    for (int i = 0; i < sourceNum; i++)
        sourceList ~= sourceBuffer[i];

    // Get name of paper sources.
    enum BINNAME_MAX_LENGTH = 24;
    wchar[] sourceNamesBuffer = new wchar[BINNAME_MAX_LENGTH * sourceNum];
    DeviceCapabilities(toUnicodez(deviceName), toUnicodez(outputPort), DC_BINNAMES, sourceNamesBuffer.ptr, pDevMode);
    // Reference: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-devicecapabilitiesw
    // Value: DC_BINNAMES
    // Meaning: Retrieves the names of the printer's paper bins.
    //          The pOutput buffer receives an array of string buffers.
    //          Each string buffer is 24 characters long and contains the name of a paper bin.
    //          The return value indicates the number of entries in the array.
    //          The name strings are null-terminated unless the name is 24 characters long.
    //          If pOutput is NULL, the return value is the number of bin entries required.
    wstring[] sourceNameList;
    for (int i = 0; i < sourceNum; i++)
    {
        wchar* w = cast(wchar*)(cast(ubyte*)sourceNamesBuffer + i * BINNAME_MAX_LENGTH * wchar.sizeof);
        int end = -1;
        for (int j = 0; j < BINNAME_MAX_LENGTH; j++)
        {
            if (w[j] == '\0')
            {
                end = j;
                break;
            }
        }
        if (end == -1) // Null terminal is not found.
            sourceNameList ~= w[0..BINNAME_MAX_LENGTH].dup; // TODO: Is it correct?
        else
            sourceNameList ~= w[0..end].dup; // Contains null terminal.
    }

    // Get paper source name.
    // Search index of paper source.
    wstring sourceName = {
        int index = -1;
        for (int i = 0; i < sourceNum; i++)
        {
            if (sourceList[i] == pDevMode.dmDefaultSource)
            {
                index = i;
                break;
            }
        }
        if (index != -1)
            return sourceNameList[index];
        else
            return "no name"w;
    }();
    
    return new PaperSource(sourceKind, to!string(sourceName));
}

備考

  • toUnicodez()の実装は、ここには載っていない。DFLの関数なので、省略している。
  • hDevModeとhDevNamesは、上位の関数内で生成、開放すること。 生成は、ページ設定ダイアログを開いたらシステムがやってくれるので、実際には開放処理だけ書けばいい。

DFL: RichTextBoxのサンプルコード

RichTextBox(リッチエディットコントロール)のサンプルコードです。

全文を貼ると長くなるのでソースはリンク先を参照してください。

サンプルを起動すると下図のテキストが表示されます。

ツールバーには以下のボタンがあり、それぞれRichTextBoxクラスのメソッドに対応しています。

  • Bold: 太字にする。
  • UnderLine: 下線を引く。
  • Font: フォントを設定する。
  • BaseUp: ベースラインを上げる。
  • BaseDown: ベースラインを下げる。
  • F.Color: 文字色を変更する。
  • B.Color: 背景色を変更する。
  • ^X: 上付き文字にする。
  • _X: 下付き文字にする。
  • GetText: テキストを取得してメッセージボックスで表示する。
  • InsText: [Insert Text] というテキストをカーソル位置に挿入する。
  • GetRtf: 選択範囲のRTFデータを取得してメッセージボックスで表示する。
  • GetSelNum: 選択範囲の文字数を取得してメッセージボックスで表示する。
  • SelNum(5): カーソル位置から5文字分を範囲選択する。

テキストを範囲選択後、ツールバーのボタンを使って色々な書式を設定したものが冒頭の画像になります。 メッセージボックスは、全文選択してからGetRtfボタンを押したときのものです。

構造体のアラインメントの問題

1か月悩んだことを書いておきます。

バグったときに構造体のアラインメントを気にしないといけないのは辛い・・・。

1. リンク修飾の範囲が正しく得られない

リッチエディットコントロールの標準機能により、URLの自動検出機能を有効にすると画像のようにURL部分がリンク修飾されます。 ここをクリックするとEN_LINK通知コードが発行されるので、これをウィンドウプロシージャで捕まえるとリンク部分のテキストが得られます。

しかし、本来ならば、ENLINK構造体のメンバのCHARRANGE構造体のメンバであるcpMinとcpMaxで範囲が得られるはずなのですが、なぜか正しい範囲が得られません。 どうしても解決できないのでVisualStudioを使いC++でEN_LINK通知コードを捕まえる処理を書いて挙動を確認してみると、こっちではちゃんと正しい範囲が得られました。

そこで、C++とDのEN_LINK構造体のサイズを比較してみると、C++では52バイト、Dでは56バイトでした。

WPARAMやLPARAMを別の型にキャストして使うのは、Windowsプログラミングではよく出てくる形ですが、DでLIBやDLLを使う場面では、構造体サイズが異なることによってまともに動かないことがあるようです。

下図のように、ENLINK構造体にalign(1):を追記することで構造体サイズがC++と一致し、バグは解消しました。

2. EM_STREAMOUT / EM_STREAMIN メッセージを送ると例外で落ちる

リッチエディットコントロールからRTFを取得する方法はいくつかあるようです。 DFLでは元々EM_STREAMOUTメッセージを使う方法が使われていました。 しかし、動作確認をしてみると全然動いておらず、実行時に謎の例外で落ちます。

こちらも色々と原因を探っていましたが、C++とDでEDITSTREAM構造体のサイズを比較すると異なっており、前述と同じアラインメントの問題でした。

下図のように、EDITSTREAM構造体にalign(1):を追記することで構造体サイズがC++と一致し、バグは解消しました。

変更点

今回のRichTextBoxの改修に伴い、読み込むリッチエディットコントロールのバージョンが3.0以下(Riched20.dll)から4.1以上(Msftedit.dll)に変更されています。

DFLのダウンロード

github.com

PCを新調した

OSが入ったHDDをSSDにクローンしたけどブートができなくて焦った~。 ブートマネージャーが旧Dドライブ(Windows Vista入り)になってて、旧Cドライブと旧Dドライブにブートローダーがあって、デュアルブートしていたらしい。 一度全部のHDDを外してSSDだけにして、Windows10インストールディスクでコマンドプロンプトを開いてbcdbootコマンドを使ってSSDにブートファイルを構成しなおしたら起動してよかった。

  • OS: Windows 10 Pro 64bit (もうしばらく10で頑張る)
  • CPU: AMD Ryzen 7 5700X (これまでIntelだったので初めてのAMD)
  • CPUクーラー: DEEPCOOL AK400 WH R-AK400-WHNNMN-G-1
  • メモリ: crucial CT2K16G4DFRA32A DDR4 PC4-25600 16GB 2枚組
  • マザーボード: GIGABYTE B550 AORUS ELITE V2
  • グラフィックボード: ASUS Dual GeForce RTX 3060 Ti OC Edition 8GB GDDR6X DUAL-RTX3060TI-O8GD6X
  • SSD: WD BLACK SN770 NVMe SSD WDS100T3X0E 1TB (新C:)
  • HDD: SATA 6Gb/s 2TB WDC WD20EFRX 2TB (旧C:→新I:)
  • HDD: SATA 6Gb/s 3TB WDC WD30EZRX 3TB (旧D:)
  • HDD: SATA/300 500GB ST3500320AS 500GB (旧E:)
  • ディスプレイ: Acer VG280Kbmiipx (2台)
  • 電源: ANTEC NeoECO Gold NE750G
  • ケース: ANTEC SOLO black (流用)
  • USBフロントパネル: GRAUGEAR G-MP01CR

とりあえず以前に旧PCに使ったベンチマークを同じソフトで。 今時のベンチマークソフトで試さないと全然分からないだろうけど。

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
CrystalMark 2004R3 [0.9.126.451] (C) 2001-2008 hiyohiyo
                                  Crystal Dew World [http://crystalmark.info/]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

------------------------------------------------------------------------------
CrystalMark Result
------------------------------------------------------------------------------
   Display Mode : 2560 x 1440 32bit (ClearType)

    CrystalMark :  638529

[ ALU ]            164655
      Fibonacci :   33747
      Napierian :   45219
   Eratosthenes :   28807
      QuickSort :   56860
[ FPU ]             77169
        MikoFPU :    5739
     RandMeanSS :   44831
            FFT :   17901
     Mandelbrot :    8676
[ MEM ]            198714
           Read : 35241.00 MB/s ( 35241)
          Write : 70353.71 MB/s ( 70353)
     Read/Write : 64343.90 MB/s ( 64343)
          Cache : 287556.78 MB/s ( 28755)
[ HDD ]            107404
           Read : 3346.49 MB/s ( 22232)
          Write : 4074.46 MB/s ( 25872)
 RandomRead512K : 2655.20 MB/s ( 18776)
RandomWrite512K : 2779.68 MB/s ( 19398)
 RandomRead 64K :  692.22 MB/s (  8961)
RandomWrite 64K : 1333.05 MB/s ( 12165)
[ GDI ]             20956
           Text :   12130
         Square :     987
         Circle :    3297
         BitBlt :    4542
[ D2D ]             24434
   Sprite    10 :  690.86 FPS  (    69)
   Sprite   100 :  633.94 FPS  (   633)
   Sprite   500 :  436.31 FPS  (  2181)
   Sprite  1000 :  406.71 FPS  (  4067)
   Sprite  5000 :  162.16 FPS  (  8108)
   Sprite 10000 :   93.76 FPS  (  9376)
[ OGL ]             45197
  Scene 1 Score :   24599
  Lines (x1000) : (5721918)
  Scene 1  CPUs : (   1024)
  Scene 2 Score :   20598
Polygons(x1000) : (1447265)
  Scene 2  CPUs : (    512)

------------------------------------------------------------------------------
System Information
------------------------------------------------------------------------------
             OS : Windows NT6.2 Business  [6.2 Build 9200]
   Display Mode : 2560 x 1440 32bit 60Hz
         Memory : 32691 MB
        DirectX : 12.0
------------------------------------------------------------------------------
CPU
------------------------------------------------------------------------------
       CPU Name : AMD Unknown CPU
  Vendor String : AuthenticAMD
    Name String : AMD Ryzen 7 5700X 8-Core Processor             
       CPU Type : Original OEM processor
Number(Logical) : 16
         Family : F
       FamilyEx : 0A
     Generation : F
          Model : 1
       Stepping : 2
           APIC : 00
        Feature : MMX SSE SSE2 SSE3 SSSE3 SSE4.1 SSE4.2 SSE4A MMX+ NX AMD-V AMD64
          Clock : 3393.61 MHz
      Data Rate :    HT

     L1 I-Cache :   32 KB
     L1 D-Cache :   32 KB
       L2 Cache :  512 KB [Full:3393.61 MHz]
       L3 Cache : 32768 KB
------------------------------------------------------------------------------
Device
------------------------------------------------------------------------------
        ChipSet : AMD Unknown
          North : AMD Unknown
          South : AMD Unknown
          Video : NVIDIA Unknown
 IDE Controller : 
------------------------------------------------------------------------------
HDD
------------------------------------------------------------------------------
Type Size    Model                                  ( Buffer Mode )     
SATA 2000.3GB WDC WD20EFRX-68EUZN0                     
SATA 801.5GB WDC WD30EZRX-00D8PB0                     
SATA 500.1GB ST3500320AS                              

ビットマップのコピー・アンド・ペースト関連リンク集

ブックマーク整理のためここへ吐き出す。

ビットマップ関係

ビットマップファイルフォーマット

BMP(ビットマップ)のファイルフォーマット

ビットマップファイルフォーマット | イメージングソリューション

BMPファイルのフォーマット

ビットマップファイルの操作

クリップボードからビットマップの保存方法について - プログラマ専用SNS ミクプラ

DIB→DDB変換【Windowsプログラミング研究所】

DDB→DIB変換【Windowsプログラミング研究所】

DIBSectionを作る【Windowsプログラミング研究所】

DIBをBMPファイルに保存する【Windowsプログラミング研究所】

BMPファイルを32ビットDIBSectionとして読み込む【Windowsプログラミング研究所】

BMPファイルを32ビットDIBとして読み込む【Windowsプログラミング研究所】

32ビットDIBを作る【Windowsプログラミング研究所】

DIBをBMPファイルに保存する【Windowsプログラミング研究所】

CreateDIBSectionによる汎用ビットマップ

HBITMAPをビットマップファイルに保存

Microsoft

ビットマップ ヘッダーの種類 - Win32 apps | Microsoft Learn

デバイスに依存しないビットマップ - Win32 apps | Microsoft Learn

イメージの保存 - Win32 apps | Microsoft Learn

イメージのキャプチャ - Win32 apps | Microsoft Learn

BITMAPINFO (wingdi.h) - Win32 apps | Microsoft Learn

BITMAPINFOHEADER (wingdi.h) - Win32 apps | Microsoft Learn

BITMAP (wingdi.h) - Win32 apps | Microsoft Learn

GetDIBColorTable 関数 (wingdi.h) - Win32 apps | Microsoft Learn

GetObject 関数 (wingdi.h) - Win32 apps | Microsoft Learn

IDataObjectとクリップボード関係

Implementing IDataObject

Implementing IDataObject - Catch22

非実在ファイルのD&Dで複数ファイルを送り出す - イグトランスの頭の中

非同期ドロップ処理フリースレッドマーシャラー + CFSTR_FILEDESCRIPTORで複数ファイル (SHCreateMemStream) · GitHub

クリップボード | C# プログラミング解説

システム - .NET Tips (VB.NET,C#...)

Copying bitmap to the Clipboard

Qt 4.8: QOleDataObject Class Reference

RichEditコントロールからテキストを取得する

DFLのRichTextBox用にせっかく書いたけど不要だった・・・ここで供養する。

// この辺のインポートが必要だけど競合するので選択インポートする。
private import dfl.internal.winapi;
private import core.sys.windows.richedit : GETTEXTEX, GETTEXTLENGTHEX, GTL_CLOSE;
final @property Dstring getText() // getter
{
    GETTEXTLENGTHEX getLength;
    getLength.codepage = 1200; // Unicode
    getLength.flags = GTL_CLOSE;
    int textLength = cast(int)sendMessage(handle, EM_GETTEXTLENGTHEX, cast(WPARAM)&getLength, 0);

    GETTEXTEX getText;
    getText.cb = textLength + cast(uint)wchar.sizeof; // Add Utf16 null terminater.

    // dmd同梱のrichedit.dには定義されていなくて古いもよう。
    // Flags for the GETEXTEX data structure
    enum GT_DEFAULT = 0;
    enum GT_USECRLF = 1;
    enum GT_SELECTION = 2;
    enum GT_RAWTEXT = 4;
    enum GT_NOHIDDENTEXT = 8;
    getText.flags = GT_DEFAULT; // GT_RAWTEXT | GT_SELECTION;
    getText.codepage = 1200; // Unicode
    getText.lpDefaultChar = null;
    getText.lpUsedDefChar = null;

    ubyte[] buf = new ubyte[getText.cb];
    int copiedLength = cast(int)sendMessage(handle, EM_GETTEXTEX, cast(WPARAM)&getText, cast(LPARAM)buf.ptr);
    return fromUnicode(cast(wchar*)buf.ptr, copiedLength);
}

参考文献