Microsoft Print to PDF で C/C++ コードから出力ファイル名を指定する方法

Windows 10 標準装備の仮想プリンタ Microsoft Print to PDF ですが、作成されるPDFファイル名をアプリケーションプログラムのコードから指定できないか探していたところ、以下を見つけました。

How to programmatically print to PDF file without prompting for filename in C# using the Microsoft Print To PDF printer that comes with Windows 10

https://stackoverflow.com/questions/31896952/how-to-programmatically-print-to-pdf-file-without-prompting-for-filename-in-c-sh

C/C++ から Windwos API を直接呼び出している場合は、CreateDC の第3パラメータ DOCINFO 構造体の3つ目のメンバ lpszOutput にデフォルトの "PORTPROMPT:" ではなく出力したいファイル名を指定すればいいです。

    DOCINFO di = { sizeof(di), "テストDocName" };
    di.lpszOutput = "C:\\HOGE\\HOGEHOGE.PDF";

OWLNext の場合は、TPrinter インスタンスメンバ関数 GetData を呼び出して取得する TPrintDialog::TData クラスインスタンスに SetDevNames で設定するときの第3パラメータに、出力したいファイル名を指定すればいいです。OWLNext はそのデータを元に TCreatedDC::TCreatedDC にて、Windows API 関数 CreateDC を呼び出すようになっています。

    TPrinter printer;
    printer.GetData()->SetDevNames("winspool", "Microsoft Print to PDF", "C:\\HOGE\\HOGEHOGE.PDF");


VC141 + OWLNext6.44.2 で動作するサンプルコードができましたので載せておきます。

// <pdf.cpp>
// Copyright 2018 Perce-neige https://perce-neige.hateblo.jp/
// Copyright を削除しない限り、ご自由にお使いください

#define _USE_MATH_DEFINES       // <cmath> にて M_PI を定義してもらう
#define _OWLPCH

#include <owl/pch.h>
#pragma hdrstop

#include <owl/printer.h>        // owl::TPrintout
#include <owl/printdia.h>       // owl::TPrintDialog
#include <owl/applicat.h>       // owl::TApplication
#include <owl/framewin.h>       // owl::TFrameWindow
#include <owl/menu.h>           // owl::TMenu

#include <windows.h>

#include <algorithm>            // std::min
#include <cmath>                // std::sin
#include <memory>               // std::unique_ptr
#include <string>               // std::string

#include "pdf.rh" // リソースID定義

class TMyPrintOut : public owl::TPrintout {
public:
    TMyPrintOut(const char* title) : owl::TPrintout(title) {}

    // 1ページだけ
    void GetDialogInfo(int& minPage, int& maxPage, int& selFromPage, int& selToPage)
        { minPage = maxPage = selFromPage = selToPage = 1; }
    bool HasPage(int pageNumber) { return pageNumber == 1; }
    
    void BeginPage(owl::TRect& clientR);
    void PrintPage(int page, owl::TRect& bandRect, unsigned flags);

};

void TMyPrintOut::BeginPage(owl::TRect& clientRect)
{
    DC->SetMapMode(MM_ANISOTROPIC);
    DC->SetViewportExt(PageSize);
    // 印字可能サイズ [mm]
    owl::TSize printableSize(DC->GetDeviceCaps(HORZSIZE), DC->GetDeviceCaps(VERTSIZE));
    // スケーリング: これで以降論理単位が 0.1mm となる
    owl::TSize windowExtt(printableSize.cx * 10, printableSize.cy * 10);
    DC->SetWindowExt(windowExtt);
    //
    clientRect.left = clientRect.top = 0;
    clientRect.right = clientRect.left + windowExtt.cx;
    clientRect.bottom = clientRect.top + windowExtt.cy;
}

// サンプルのページ描画
void TMyPrintOut::PrintPage(int /*page*/, owl::TRect& /*bandRect*/, unsigned /*flags*/)
{
    owl::TRect clientRect;
    BeginPage(clientRect);
    DC->MoveTo(clientRect.left, clientRect.bottom / 2);
    DC->LineTo(clientRect.right, clientRect.bottom / 2);
    DC->MoveTo(clientRect.right / 2, clientRect.bottom);
    DC->LineTo(clientRect.right / 2, clientRect.top);
    double r = std::min(clientRect.right, clientRect.bottom) / 4;
    // カージオイド を (3/2)π 回転したもの
    // 媒介変数 t を -π <= t <= π で動かす
    int n = int(M_PI * r);
    for (int i = -n; i <= n; i++) {
        double t = i * M_PI / n;
        double c = std::cos(t);
        double s = std::sin(t);
        int dx = int(r * (1 + c) * (-s));
        int dy = int(r * (1 + c) * c);
        int x = int(clientRect.right  / 2 + dx);
        int y = int(clientRect.bottom / 2 + dy);
        if (i == -n)
            DC->MoveTo(x, y);
        else
            DC->LineTo(x, y);
    }
}

class TMyFrame : public owl::TFrameWindow {
public:
    TMyFrame(const char* name) : owl::TFrameWindow(0, name) {}

private:
    void CmOutputPDF();
    
    DECLARE_RESPONSE_TABLE(TMyFrame);
};

DEFINE_RESPONSE_TABLE1(TMyFrame, owl::TFrameWindow)
    EV_COMMAND(CM_OUTPUTPDF, CmOutputPDF),
END_RESPONSE_TABLE;

void TMyFrame::CmOutputPDF()
{
    try {
        std::string driver = "winspool";
        std::string device = "Microsoft Print to PDF";
        std::string port = "C:\\WORK\\PDFOWL.PDF"; // ### 出力PDFファイル名
        //
        owl::TPrinter printer;
        printer.GetData()->SetDevNames(driver.c_str(), device.c_str(), port.c_str());
        TMyPrintOut printOut("PDF出力テスト");
        printer.Print(this, printOut, false);
    }
    catch (const std::exception& x) {
        MessageBox(x.what(), "エラー", MB_OK | MB_ICONERROR);
    }
    catch (...) {
        MessageBox("想定外の例外です。", "エラー", MB_OK | MB_ICONERROR);
    }
}

class TMyApp : public owl::TApplication {
public:
    TMyApp(const char* title) : owl::TApplication(title), Title(title) {}

private:
    void InitMainWindow() {
        std::unique_ptr<owl::TFrameWindow> mainWindow(new TMyFrame(Title.c_str()));
        mainWindow->SetMenuDescr(owl::TMenuDescr(MAIN_MENU, 1, 0, 0, 0, 0, 0));
        SetMainWindow(mainWindow.release());
    }

    std::string Title;

};

int OwlMain(int /*argc*/, char** /*argv*/)
{
    TMyApp("PDF出力テスト").Run();
    return 0;
}
// <pdf.rh>

#define MAIN_MENU       10000
#define CM_OUTPUTPDF    10100
// <pdf.rc>

#include "pdf.rh"

#include <owl/window.rh>
#include <owl/except.rc>
#include <owl/printer.rc>

MAIN_MENU MENU {
          POPUP "ファイル(&F)"
          {
                MENUITEM "PDF出力(&P)", CM_OUTPUTPDF
                MENUITEM "終了(&X)", CM_EXIT
          }
}

CMakeLists.txt は https://perce-neige.hateblo.jp/entry/2018/10/25/214023 にあります。

自前のインサートイテレータ

BCC582 (C++Builder 2006) では問題なかった下記のような自前のインサートイテレータがあります。

template <class X>
class MyInsertIterator {
public:
    MyInsertIterator(X& x) : x_(&x) {};
    MyInsertIterator& operator =  (const Foo& v) { x_->Add(v); return *this; }
    MyInsertIterator& operator *  ()    { return *this; }
    MyInsertIterator& operator ++ ()    { return *this; }
    MyInsertIterator& operator ++ (int) { return *this; }
protected:
    X* x_;
};

template <class X>
inline MyInsertIterator<X> MyInserter(X& x)
{
    return MyInsertIterator<X>(x);
}

これを std::copy で使っているのですが、VC141 (Visual Studio 2017 の Visual C++) でデバッグ用ビルドすると warning C4996 と error C2794 が報告されます。

まず error C2794 対策、

エラーメッセージは

'iterator_category': 'std::iterator_traits<_Iter>' の基底クラスの直接的または間接的なメンバーではありません。

ですが、イテレータの特性が取得できないみたいです。そこで MyInsertIterator クラス定義内に以下を追加します。

public:
    using iterator_category = std::output_iterator_tag;
    using difference_type = void;
    using value_type = void;
    using pointer = void;
    using reference = void;
    using container_type = X;

つづいて warning C4996 対策、

警告メッセージは

'std::copy::_Unchecked_iterators::_Deprecate': Call to 'std::copy' with parameters that may be unsafe - this call relies on the caller to check that the passed values are correct. To disable this warning, use -D_SCL_SECURE_NO_WARNINGS. See documentation on how to use Visual C++ 'Checked Iterators'

ということで、マクロ _SCL_SECURE_NO_WARNINGS を定義するとか、pragma で warning C4996 を無効にしてしまう方法もありますが、他への影響を最小限にするため、MyInsertIterator クラス定義の後に以下を追加してイテレータを checked の扱いにしてしまいます。

template<class _Container>
struct std::_Is_checked_helper<MyInsertIterator<_Container> > : public std::true_type {};

最後に、全体のコード例を載せて起きます。

長くなりそうだったので X::Add(Foo) の代わりに X::push_back(int) になってます。

#include <iostream>
#include <algorithm>
#include <iterator>
#include <vector>

using namespace std;

template <class X>
class MyInsertIterator {
public:
    using iterator_category = output_iterator_tag;
    using difference_type = void;
    using value_type = void;
    using pointer = void;
    using reference = void;
    using container_type = X;
    MyInsertIterator(X& x) : x_(&x) {};
    MyInsertIterator& operator =  (const typename X::value_type& v) { x_->push_back(v); return *this; }
    MyInsertIterator& operator *  ()    { return *this; }
    MyInsertIterator& operator ++ ()    { return *this; }
    MyInsertIterator& operator ++ (int) { return *this; }
protected:
    X* x_;
};

template <class X>
struct _Is_checked_helper<MyInsertIterator<X> > : public true_type {};

template <class X>
inline MyInsertIterator<X> MyInserter(X& x)
{
    return MyInsertIterator<X>(x);
}

int main()
{
    vector<int> v = { 2, 3, 5, 7, 11, 13 };
    vector<int> w;
    copy(v.begin(), v.end(), MyInserter(w));
    cout << "w:";
    for (auto& i : v) {
        cout << ' ' << i;
    }
    cout << endl;
}

cmake で bcc582 と vc141 一気にビルド

1本の CMakeLists.txt と数本の短いバッチフィルで、2つのコンパイラ bcc582 と vc141 をそれぞれ使って、さらにそれぞれの debug バージョンと release バージョンを一気にビルドするためのメモです。

今後付け足していく予定です。

ディレクトリ構造

. ; ソース, CMakeLists.txt, バッチファイル, 改名したEXEファイルをコピーしてくる
|
+-- build ; ビルド ディレクトリ
     |
     +-- bcc582d ; bcc582 debug バージョン用 ビルド ディレクトリ
     |
     +-- bcc582 ; bcc582 release バージョン用 ビルド ディレクトリ
     |
     +-- vc141 ; vc141用 ビルド ディレクトリ

環境変数

コンパイラのインストールディレクトリを設定しておく

BCC582=C:\PROGRA~1\Borland\BDS\4.0
VC141=C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build

バッチファイル

build.bat

@echo off
cmd /c build_bcc582.bat
cmd /c build_vc141.bat
cp --update --no-target-directory build/bcc582d/foo.exe       ./foo_bcc582d.exe
cp --update --no-target-directory build/bcc582/foo.exe        ./foo_bcc582.exe
cp --update --no-target-directory build/vc141/release/foo.exe ./foo_vc141.exe
cp --update --no-target-directory build/vc141/debug/foo.exe   ./foo_vc141d.exe

build_bcc582.bat

@echo off
PATH=%BCC582%\Bin;%PATH%
mkdir build\bcc582d
cd build\bcc582d
cmake -DCMAKE_BUILD_TYPE=Debug -G "Borland Makefiles" ../..
make
cd ..\..
mkdir build\bcc582
cd build\bcc582
cmake -DCMAKE_BUILD_TYPE=Release -G "Borland Makefiles" ../..
make -DNDEBUG
cd ..\..

build_vc141.bat

@echo off
call "%VC141%\vcvarsall.bat" x86
mkdir build\vc141
cd build\vc141
cmake ../..
for %%a in (*.vcxproj) do msbuild %%a /m /p:Configuration=Debug
for %%a in (*.vcxproj) do msbuild %%a /m /p:Configuration=Release
cd ..\..

CMakeLists.txt

cmake_minimum_required(VERSION 3.11)
project(foo CXX)
add_executable(foo main.cpp)

main.cpp テスト用サンプル

#include <iostream>
int main()
{
    using namespace std;
#ifdef NDEBUG
    cout << "Release(NDEBUG defined)" << endl;
#else
    cout << "DEBUG(NDEBUG not defined)" << endl;
#endif
#ifdef __BORLANDC__
    cout << "BORLANDC: " << hex << __BORLANDC__ << endl;
#endif    
#ifdef _MSC_VER
    cout << "MSC: " << _MSC_VER << endl;
#endif    
    return 0;
}

Windows で ionice

Windows で、ビルド(特にリンク)とかディスクアクセスが多い処理をさせている最中、他の作業をやろうとしてもディスクアクセスが遅くてイライラすることがありませんか?

CPUの優先度を下げるのは start /low や cygwin の nice を使えばできますが、IOの優先度を下げるコマンドは無いみたいです。

Windows Vista から Windows API 関数 SetPriorityClass の2つめの引数にPROCESS_MODE_BACKGROUND_BEGIN, PROCESS_MODE_BACKGROUND_END が追加されています。

以下の挟まれた部分のコードが、低いIO優先度で実行されます。

SetPriorityClass(GetCurrentProcess(), PROCESS_MODE_BACKGROUND_BEGIN);
// ... 低いIO優先度で実行される
SetPriorityClass(GetCurrentProcess(), PROCESS_MODE_BACKGROUND_END);

これを利用して、I/O優先度を下げてコマンドを実行するコマンドを作ってみました。以下をコンパイルして ionice.exe を作ります。

linux の同じ名前のコマンドのようなオプション引数は対応してません。

一応 VC141 (Visual Studio 2017 の Visual C++) と BCC582 (C++Builder 2006) で確認してあります。

#include <windows.h>
#include <process.h>
#include <iostream>

// 定義がなかったら定義する
#ifndef PROCESS_MODE_BACKGROUND_BEGIN
#define PROCESS_MODE_BACKGROUND_BEGIN 0x00100000
#endif

#ifndef PROCESS_MODE_BACKGROUND_END
#define PROCESS_MODE_BACKGROUND_END 0x00200000
#endif

// _spanvp は BCCだと spanvp 引数・戻り値は同じ
#ifdef __BORLANDC__
#define _spawnvp spawnvp
#endif

int main(int argc, char** argv)
{
    using namespace std;
    if (argc < 2) {             // 引数無し
        cout << "書式: ionice <コマンドライン...>" << endl;
        return 0;
    }
    //
    if (!SetPriorityClass(GetCurrentProcess(), PROCESS_MODE_BACKGROUND_BEGIN)) {
        cerr << "SetPriorityClass(*, PROCESS_MODE_BACKGROUND_BEGIN) エラー: " << GetLastError() << endl;
        cerr << "でも続行します" << endl;
    }
    int result = _spawnvp(P_WAIT, argv[1], argv+1);
    if (!SetPriorityClass(GetCurrentProcess(), PROCESS_MODE_BACKGROUND_END)) {
        cerr << "SetPriorityClass(*, PROCESS_MODE_BACKGROUND_END) エラー: " << GetLastError() << endl;
        cerr << "でも続行します" << endl;
    }
    return result;
}

使い方

例1

        make nantoka

を低い IO 優先度で実行したい場合に

        ionice make nantoka

例2

場合によっては(バッチファイルとか?) cmd.exe を噛まさないと動かないかもしれません。

        build nantoka

を低い IO 優先度で実行したい場合に

        ionice cmd /c build nantoka

Emacs で C++ Builder 付属の grep で検索してジャンプ

WindowsGNU Emacs 25.2.1 を使っていますが、cygwin の egrep だと shift-jis の2バイト文字が検索できないので、C++ Builder 付属の grep を使って検索をして、検索結果からジャンプできるようにしてみました。

~/.emacs に以下を入れます。

(load "grep")                           ; grep-regexp-alist が defconst されてるせいか先に読み込まないと上手く変更できない
(setq grep-use-null-device nil)         ; Windows では使わない

;; bcc の grep を使用する
(setq grep-command "grep -o -n -i ")
;; -o は unix っぽい形式(これが無いと解釈できない)
;; -n は行番号付加 (これが無いと解釈できない)
;; -i は大文字小文字区別しない
;; -d はディレクトリを下る

;; bcc の grep の出力も解釈できるように
;;
;; unix grep : <file>:<lineno>:<text>
;; bcc grep -on :
;;   ファイルの最初: <file>:<lineno:%-7d> <text>
;;   それ以外:       <file>  <lineno:%-7d> <text>
;;
(setq bcc-grep-regexp-alist
      '(("^\\(.*?[^/\n]\\)[: ][ \t]*\\([1-9][0-9]*\\)\\([ \t]*:\\)?"
         1 2
         ((lambda ()
            (when grep-highlight-matches
              (let* ((beg (match-end 0))
                     (end (save-excursion (goto-char beg) (line-end-position)))
                     (mbeg (text-property-any beg end 'font-lock-face grep-match-face)))
                (when mbeg
                  (- mbeg beg)))))
          .
          (lambda ()
            (when grep-highlight-matches
              (let* ((beg (match-end 0))
                     (end (save-excursion (goto-char beg) (line-end-position)))
                     (mbeg (text-property-any beg end 'font-lock-face grep-match-face))
                     (mend (and mbeg (next-single-property-change mbeg 'font-lock-face nil end))))
                (when mend
                  (- mend beg)))))))))
(setq grep-regexp-alist bcc-grep-regexp-alist)

ブログスタート

主に C++Windows のソフトウェア開発を行っています。そこで調べたことや気づいたことを書いて行こうと思います。

現在使っているのは Borland C++ Builder 2006、もう12年も前のコンパイラです。フレームワークは OWLNext 6.30.4 で、OWL 2.0 の頃から使ってます。

VC141 (Visual Studio 2017 の Visual C++) + OWLNext 6.44.2 への移行を画策していますが、既存プロジェクトの移植がなかなか進みません。

C++ の他に用があるのは Perl 5 と、あと Emacs Lisp です。

Lisp だとコードをコンパクトに書けるので、エディタのマクロとしてだけでなく、実験用にも使用してます。