CPrintDialogの後始末

[戻る]
NOR 2012/02/01(水) 12:35:23
ダイアログベースのアプリケーションに、
独自の印刷機能を持たせようとしています。

CPrintDialog dlg(FALSE);
dlg.DoModal();

上記のように印刷ダイアログを出し、OKで閉じると、
dlg.m_pdの中にあるhDCやhDevModeやhDevNamesには有効な値が入りますが、
これらは誰が後始末するものなのでしょうか?

ネット上のサンプルを見ると、dlg.GetPrinterDC()の戻り値に対しては
DeleteDC()を行っているものが多いのですが、
hDevModeやhDevNamesに対してはなにもしていないようです。

ただ、MSDN内のCPrintDialog自体の解説を読むと、
GlobalFree()で解放する必要があるようにも思えます。

「これが正しい後始末」という正解をご存じのかたはいらっしゃいませんでしょうか。
よろしくお願いいたします。

仲澤@失業者 2012/02/01(水) 15:38:31
マニュアルに記載されているとおりでしょう。

つまり、CWinApp::OnFilePrintSetup()ではなく、
CPrintDialogを単体で使用した場合で、hDevModeとhDevNamesをNULLに
しとくと勝手にメモリが確保されるので、使い終わったら
GlobalFree()で開放してねってことですね。
PRINTDLG構造体の説明に該当する部分があるようです。

プリンタは半恒常的なデバイスなので、確保したままアプリを続行し
機会があれば、再利用するのがすじですね。
なので、CWinAppも終了時にそれを開放するわけです。
従って、我々も、それにならうべきなのかもしれません。

NOR 2012/02/01(水) 16:34:26
仲澤@失業者さん、情報ありがとうございます。
念のため確認させていただきたいのですが、

http://www.crimson-systems.com/tips/t066a.htm
http://rarara.cafe.coocan.jp/cgi-bin/lng/vc/vclng.cgi?print+200603/06030011.txt
ネット上のサイトや、この掲示板の過去ログでも、
上記のようにDeleteDC()だけが行われている例があり、
過去ログではそのことについてはどなたも指摘されていなかったのですが、
DeleteDC()のあとに

GlobalFree(dlg.m_pd.hDevMode);
GlobalFree(dlg.m_pd.hDevNames);

も入れるのが正しい後始末ということになるのでしょうか?

NOR 2012/02/01(水) 18:20:04
すみません、さらにもう一つ追記させていただきます。

CPrintDialog::GetDeviceName()の実装を見てみたところ、

  LPDEVNAMES lpDev = (LPDEVNAMES)GlobalLock(m_pd.hDevNames);
  return (LPCTSTR)lpDev + lpDev->wDeviceOffset;

となっていて、GlobalLock()をしたままの状態で関数を抜けていました。

ところが、
http://msdn.microsoft.com/ja-jp/library/k12kcz48%28v=vs.80%29.aspx
の使用例を見ても、GlobalUnlock()をしてません。

これはなぜ問題無いのでしょうか。

2012/02/02(木) 00:59:48
本当ですね。
呼んだ側でGlobalUnlockしろということなんでしょうかね。
AfxCreateDCなど、dlgprnt.cppには他にもリークがありますね。

仲澤@失業者 2012/02/02(木) 11:06:10
探してみましたが、残念ながら自分の手元にはCPrintDialogを
使ったコードがありませんでした。
SDKのPrintDlg()を使った自前のコードがあったので見てみると、
PRINTDLGのhDevModeもhDevNamesも自前で用意してましたorz。
まあ、これはCPrintDialog固有の問題だという認識なのでかまい
ません。

>も入れるのが正しい後始末ということになるのでしょうか?

基本的な手順として、GlobalFree()する場合は、まずGlobalFlags()して、
必要ならGlobalUnlock()するのが常識でしょう。
また、プリンターデバイスの情報の取得や操作は「重い処理」として、
捉えたほうが良いですね。つまりCPrintDialogのインスタンスは、
アプリケーションのグローバルであるほうが効率的だということです。
メモリを開放しなければ、ユーザが2度目の印刷を行うときに
前回の設定が残ることが期待できますし、開放はアプリケーション
終了時で十分ですよね。

>これはなぜ問題無いのでしょうか。

自分には問題点が認識できませんでした。ポインタを返すのには
GlobalLock()は必要不可欠です。また、ロック中のメモリを開放する
手段は提供されています。だれが何回ロックするかわからないので、
当然ですよね。どういった問題を認識しましたか(質問)。

NOR 2012/02/02(木) 12:17:07
> つまりCPrintDialogのインスタンスは、
> アプリケーションのグローバルであるほうが効率的だということです。

CPrintDialogのコンストラクタには、
動作モードや親ウィンドウを指定する必要がありますし、
複数の場所から出すのにはちょっと再利用しづらくありませんか?

> どういった問題を認識しましたか(質問)。

MSDNのCPrintDialog::GetDeviceName()の使用例では、
GetDeviceName()など使ったあとにGlobalFree()をしていませんし、
解説にも「内部でGlobalLock()してるから、使う側でGlobalUnlock()してね」
といった記述もありません。
むしろ「GlobalUnlock()しちゃダメ」な前提でもあるのではないかと思いました。

仲澤@失業者 2012/02/02(木) 15:29:57
>複数の場所から出すのにはちょっと再利用しづらくありませんか?

なるほど、もっともCWinAppはそれほど悩んでいないようなので
それ専用に設計されたのかもしれませんね。

>GetDeviceName()など使ったあとにGlobalFree()をしていませんし、
>解説にも「内部でGlobalLock()してるから、使う側でGlobalUnlock()してね」
>といった記述もありません。

これは多分必要ない「はず」だったからでしょう。GetDeviceName()の戻り値は
スタック上のCStringで、参照でもポインタでもありません。
単なるコピーです。つまりGlobalUnlock()はコード可能な状態なわけで、
妄想すると、それを忘れたんじゃないかという疑いがあります。

gak 2012/02/02(木) 18:31:40
> GlobalFree(dlg.m_pd.hDevMode);
> GlobalFree(dlg.m_pd.hDevNames);
> も入れるのが正しい後始末ということになるのでしょうか?
yes.

以下私見。
CPrintDialog が内部で確保したメモリであっても何故 CPrintDialog 自身が解放しない
かというと、恐らく一般的な「印刷」機能に依った実装を優先したためだと思われる。

例えばあるアプリで以下のような処理を行うとする

1、印刷の設定で使用するプリンタを「標準で使用するプリンタ」でないプリンタを選択
2、印刷の設定で「用紙サイズ」を標準設定の「A4」から「B5」に変更
3、印刷の設定で「給紙方法」を標準設定の「トレイ1」から「手差し」に変更
4、印刷実行して印刷処理終了
5、再度印刷処理を行う

上記の「5」段階を実行した際、一般的なアプリでは先の「1」〜「3」で設定された内容
は維持される事が望まれる。もっと言えば、少なくともそのアプリが起動中は「印刷設
定」した情報は維持される事が望まれる。

ではその「設定された内容」というのが具体的にどれかというと hDevNames(使用するプ
リンタの情報) と hDevMode(用紙etcの情報) になる。
なので、仮に CPrintDialog がデストラクタ等で hDevNames と hDevMode を勝手に捨て
ちゃう様な動作をしちゃうと「馬鹿。それまだ使うよ!」となりかねない。

まぁそんなこんなで MS の人達としては「hDevNames、hDevMode DC等の生死管理は
CPrintDialog で行わない」事をベターな解としたのではなかろうか。
加えるなら、それだと不親切だとも考えたので CWinApp等 に OnFilePrintSetup()、
DoPrintDialog() 等の印刷関係の機能を実装し、一般的な利用をするだけならば
CPrintDialog なんてクラスを直接触る必要ない実装を採ったのではなかろうか。

> むしろ「GlobalUnlock()しちゃダメ」な前提でもあるのではないかと思いました。
API仕様に則るならば GlobalUnlock() すべき。だけど GlobalUnlock() しなくても実害
はほぼゼロ。Lock されていようがいまいが GlobalFree() で解放できる仕様になってい
るから。
もう CPrintDialog 関しては割り切って、名より実を取ったのかも。

PATIO 2012/02/02(木) 21:06:05
私もgakさんのお話はありえる話だと思いました。
但し、何処まで行ってもここでやり取りされている内容は
利用者が状況から想像しているにすぎないので
本当の意味できちんとした答えが欲しいのであれば、
Microsoftに訊くしかないと思います。

但し、Microsoftが納得の行く答えを提示してくれるかはわかりませんから
だめもとでと言う話になると思いますけれど。

VisualStudio(MFCが使えるEdtion)のライセンスをお持ちなら
Microsoft Connectあたりで尋ねてみるのも手かなと言う気がしますけど、
あそこで尋ねるのにインシデントとか要りましたっけ?

ITO 2012/02/04(土) 10:53:02
>あそこで尋ねるのにインシデントとか要りましたっけ?
「不具合問い合わせ」ということで質問しないとインシデントがいると思います。
 不具合でないと、
 「これは不具合でなく仕様です。これ以降は別途インシデントが必要になります。」
 という話になると思います。

NOR 2012/02/06(月) 13:46:36
[[解決]]
>> GlobalFree(dlg.m_pd.hDevMode);
>> GlobalFree(dlg.m_pd.hDevNames);
>> も入れるのが正しい後始末ということになるのでしょうか?
> yes.

明確な回答ありがとうございます。
印刷ダイアログを単独で使う場合の、ハンドルを残さない後始末としては、

  if (dlg.GetPrinterDC() != NULL) {
    DeleteDC(dlg.GetPrinterDC());
  }
  if (dlg.m_pd.hDevMode != NULL) {
    GlobalFree(dlg.m_pd.hDevMode);
  }
  if (dlg.m_pd.hDevNames != NULL) {
    GlobalFree(dlg.m_pd.hDevNames);
  }

というようなものになるということですね。
CPrintDialog::GetDevMode()などで中途半端にカプセル化してる割には、
結局m_pd.hDevModeを直接操作しなくてはいけないのですか。

前述のサイトや過去ログの例でもとりあえず動いてしまうので、
この手の解放し忘れのミスにはなかなか気づきませんよね。
GlobalAlloc()やGlobalFree()を使うメモリに関しても、
newやdeleteのように標準で解放し忘れを教えてくれればよいのですけど。

>> むしろ「GlobalUnlock()しちゃダメ」な前提でもあるのではないかと思いました。
> API仕様に則るならば GlobalUnlock() すべき。だけど GlobalUnlock() しなくても実害
> はほぼゼロ。Lock されていようがいまいが GlobalFree() で解放できる仕様になってい
> るから。

CPrintDialog::GetDeviceName()にGlobalUnlock()が無いのは、
MS側のうっかりの可能性が高いということになりますか。

CPrintDialogに関してはもう何年もそのままでしょうし、
それでこれまで大きな問題になっていないのであれば、
信じてしまったほうが楽になれるかもしれません。

そうすれば、CPrintDialog::GetDevMode()のほうも
使う側でGlobalUnlock()を呼ばなくてもよいことになりますし。

> VisualStudio(MFCが使えるEdtion)のライセンスをお持ちなら
> Microsoft Connectあたりで尋ねてみるのも手かなと言う気がしますけど、

当初の質問であったGlobalFree()が必要なのは間違いないでしょうけど、
GlobalUnlock()のほうはなぜなのか聞いてみたいところではありますね。

古いスレッドには追加発言できません。