プロパティー シートとウィザード
-- 目次 --
  1. 前書き
  2. Step 1. とりあえず作ってみる
  3. Step 2.『適用』(Apply) ボタンに関して
  4. Step 3. ウィザードを使用したプログラムのの作成
1. 前書き

プロパティーシート (Property Sheet) とは、 Windows 上の各種ソフトなどでオプション設定するときに出現する複数ページからなる ダイアログでタブ (tab) をクリックすることで頁を変更することができます。 Windows では至る所でプロパティーシートに遭遇しますが、 Visual C++ でプログラムを組んでいるときには コントロールなどのプロパティーの設定で最もよくお目にかかるものです。 厳密には、プロパティーシートは複数のプロパティーページの入れ物の ことで、プロパティーページの方がダイアログです。

ウィザード (Wizard) とは Windows ソフトをインストールするときなどによく登場します。 Wizard の各ページはダイアログで、『戻る』、『次へ』のボタンがあって、 最後のページには『終了』 (or『完了』) のボタンが付いています。 Wizard も色々なページのスタイルがありますが、 ウィザードも実はプロパティーシートです。

プロパティーシートもウィザードも Visual C++ で非常に簡単に プログラムを作成することができます。全部を一度に書き上げようとすると かなり混乱することになりますが、 基本は簡単ですから少しづつ作成すれば驚くほど簡単に作成することが できます。

注意
「MFC アプリケーション ウィザード」は厳密にはプロパティーシートです。 Visual C++ では最近、プロパティーシートのこともウィザードと呼んでいますが、ここでは ウィザードとは『戻る』、『次へ』のボタンがあるダイアログの意味と考えてください。
Step 1. とりあえずつくってみる
プロジェクトの作成

次の条件でプロジェクトワークスペースを作成し、 「MFC アプリケーション ウィザード」では「アプリケーションの種類」 として「シングル ドキュメント」を選びます。(従って単に View クラスのプログラムです。)

プロジェクトの種類Visual C++ プロジェクト
テンプレート MFC アプリケーション
プロジェクト名 PropSample
メニュー項目の作成

「リーソース ビュー」の 「Menu」/「IDR_MAINFRAME」 を選び、メニューの項目を作成します。 どこに作っても構わないのですが、ここでは「ファイル」メニューの中に「プロパティー シート」という項目を作成します。 このメニュー項目を選択すると、プロパティーシートが表示されるようにすることが以下の目的です。

プロパティーページの作成

次に、プロパティーシートに挿入するプロパティーページを作成します。 全部で 3 ページ作成します。これはあとでプロパティーシートをウィザードに 変更したときに 3 ページないとウィザードらしくないからで、 面倒くさければ 2 ページ程度でも構いません。

リーソースに新規のダイアログ (IDD_DIALOG1) を挿入します。「OK」, 「キャンセル」 のボタンが付いていますが、これは必要ありませんから、マウスで指定して、 「DEL」キーで消去します。 このダイアログのプロパティーを次のように変更します。

Caption 1頁
Style
Border 細枠
System MenuFalse

Caption の文字はプロパティー頁のタブ (tab) (= つまみの部分) に表示される文字で、プロパティー頁はプロパティーシートの子ウィンドウとして、 実現されるため、「子」の指定をつけます。

ダイアログ全体をマウスで指定して、右クリックして表示されるメニューから「クラスの追加」を選びます。 すると「MFC クラス ウィザード」が表示されます。そこで次のように設定して、「完了」します。

クラス名 CPage1
基本クラスCPropertyPage

IDD_DIALOG1 が消えてしまいますから、「リソース ビュー」で「IDD_DIALOG1」をダブルクリックして、 表示し、そこに、Edit Control を 1 つ配置します (横に少し長く作ります)。 ID は IDC_EDIT1 の まま放置します。 このエディットボックスに変数を割り当てるため、エディットボックスを マウスで右クリックして表示されるメニューから「変数の追加」を選びます。 すると「メンバー変数追加ウィザード」が表示されますから、次のように設定して「完了」します。

カテゴリ Value
変数の種類CString
変数名 m_edit

1 ページ目と全く同様に同様に 2 頁、3 頁を作ります。クラス名は CPage2, CPage3 としますが、すべてに同じようにエディットボックスを配置して、 割り当てるメンバー変数名も全く同じ 「m_edit」 とします。 ちょっと雑かもしれませんがクラスが異なりますから、同じ変数名でも一向に構いません。

プロパティーシートの作成

以上のように作成したプロパティーページを収納する プロパティーシートを作成します。 そのために「クラス ビュー」で、プロジェクト名「PropSample」を 右クリックすると表示されるメニューから 「追加」/「クラスの追加」を選びます。 表示される「テンプレート」からMFC クラス を選ぶと「MFC クラス ウィザード」が登場します。 そこで次のように設定して「完了」します。

クラス名 CMySheet
基本クラスCPropertySheet

CMySheet から CPage1, CPage2, CPage3 にアクセスできるようにするために ヘッダーファイル MySheet.h の先頭に次のような書き込みをします。

MySheet.h への書き込み
#pragma once
#include "Page1.h"
#include "Page2.h"
#include "page3.h"
// CMySheet

class CMySheet : public CPropertySheet
{
    DECLARE_DYNAMIC(CMySheet)

public:
    CMySheet(UINT nIDCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0);
    CMySheet(LPCTSTR pszCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0);
    virtual ~CMySheet();

protected:
    DECLARE_MESSAGE_MAP()
public:
    CPage1 m_page1;
    CPage2 m_page2;
    CPage3 m_page3;
};

更に、プロパティーページをプロパティーシートに付け加えるために CMySheet の 2 つのコンストラクターに次のように書き込みをします。

MySheet.cpp への書き込み
CMySheet::CMySheet(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage)
    :CPropertySheet(nIDCaption, pParentWnd, iSelectPage)
{
    AddPage(&m_page1);
    AddPage(&m_page2);
    AddPage(&m_page3);
}

CMySheet::CMySheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage)
    :CPropertySheet(pszCaption, pParentWnd, iSelectPage)
{
    AddPage(&m_page1);
    AddPage(&m_page2);
    AddPage(&m_page3);
}
View ファイルへの書き込み

PropSampleView.h でプロパティーページの各頁 (1 、2、3 頁) の エディットボックスに表示される文字を保存する変数を宣言します。

PropSampleView.h への書き込み
// 属性
public:
    CPropSampleDoc* GetDocument() const;
    CString m_edit_p1;
    CString m_edit_p2;
    CString m_edit_p3;

この変数をコンストラクターで初期化します。

PropSampleView.cpp への書き込み
CPropSampleView::CPropSampleView()
{
    // TODO: 構築コードをここに追加します。
    m_edit_p1 = "1頁の文字列";
    m_edit_p2 = "2頁の文字列";
    m_edit_p3 = "3頁の文字列";
}

更に文字列をグラフィック画面に表示するために PropSampleView.cpp の OnDraw 関数に 次の書き込みをします。(引数の pDC がコメントアウトされていますから、忘れずに復活させておきます。)

OnDraw() 関数の修正と書き込み
void CPropSampleView::OnDraw(CDC* pDC)
{
    CPropSampleDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    if (!pDoc)
        return;

    // TODO: この場所にネイティブ データ用の描画コードを追加します。
    pDC->TextOut(20, 40, m_edit_p1);
    pDC->TextOut(20, 70, m_edit_p2);
    pDC->TextOut(20, 100, m_edit_p3);
}

まだプロパティーシートを使えるようにはなっていませんが、 タイプミスがあるかどうかのチェックのためにビルドしてみましょう。

さて View クラスから CMySheet クラスを使用するために PropSampleView.cpp の先頭で ヘッダーファイルをインクルードします。

PropSampleView.cppへの書き込み
// PropSampleView.cpp : CPropSampleView クラスの実装
//

#include "stdafx.h"
#include "PropSample.h"

#include "PropSampleDoc.h"
#include "PropSampleView.h"

#include "MySheet.h"

次にメニューからプロパティーシート関数を実行するために、「リソース ビュー」「Menu」/「IDR_MAINFRAME」をダブルクリックして、表示し、メニューの項目に 「プロパティーシート」を作っていましたから、その上で右クリックして表示されるメニューから 「イベント ハンドラーの追加」を選びます。「イベント ハンドラ ウィザード」が 表示されます。そこで次のように設定して「追加して編集」を選びます。

メッセージの種類COMMAND
クラスの一覧 CPropSampleView

「イベント ハンドラ」が作成されますから、次のように書き込みをします。

void CPropSampleView::On32771()
{
    // TODO : ここにコマンド ハンドラ コードを追加します。
    CMySheet mysheet("プロパティーシート", this, 0);
    mysheet.m_page1.m_edit = m_edit_p1;
    mysheet.m_page2.m_edit = m_edit_p2;
    mysheet.m_page3.m_edit = m_edit_p3;
    int result = mysheet.DoModal();
    if(result == IDOK)
    {
        m_edit_p1 = mysheet.m_page1.m_edit;
        m_edit_p2 = mysheet.m_page2.m_edit;
        m_edit_p3 = mysheet.m_page3.m_edit;
        Invalidate();
    }
}

以上で、プログラムが出来ました。なお CPropertySheet は次の構文で使用します。

CPropertySheet(タイトル文字列, 親ウィンドウへのポインタ, 最初に表示されるページ)

ビルドして実行します。 「ファイル」/「プロパティーシート」 から次のようなプロパティーシートが 立ち上がるはずです。

プロパティーページのエディットボックスの文字列を修正して、 OK を押すと、修正結果がグラフィック画面に反映されることを確認します。

Step 2. 『適用』(Apply) ボタンに関して

Step 1 のようにして作ったプロパティーシートをよく見てみると、『適用』ボタンがあります。 これはコードを適当に付け加えて、プロパティーページが修正されたときに、 『適用』ボタンを押すと、 修正内容が直ちに反映できるようにプログラムを組むためにあります。

『適用』(Apply) ボタンを消すには

CMySheet のコンストラクターに次のように書き込むと、 『適用』ボタンが消えます。

CMySheet への書き込み
CMySheet::CMySheet(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage)
    :CPropertySheet(nIDCaption, pParentWnd, iSelectPage)
{
    AddPage(&m_page1);
    AddPage(&m_page2);
    AddPage(&m_page3);
    m_psh.dwFlags |= PSH_NOAPPLYNOW;
}

CMySheet::CMySheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage)
    :CPropertySheet(pszCaption, pParentWnd, iSelectPage)
{
    AddPage(&m_page1);
    AddPage(&m_page2);
    AddPage(&m_page3);
    m_psh.dwFlags |= PSH_NOAPPLYNOW;
}

詳しいことは Visual C++ の help で CPropertySheet を調べてください。なお

m_psh.dwFlags |= PSH_NOAPPLYNOW

は、次の意味です。(| はビット or です。知らなければ C 言語の 教科書を調べてください。)

m_psh.dwFlags = m_psh.dwFlags | PSH_NOAPPLYNOW
『適用』(Apply) ボタンを有効にする

『適用』ボタンは、そもそもプロパティーページが修正されたときに、 『適用』ボタンを押すと、 修正内容が直ちに反映できるようにプログラムを組むためにあります。 このようにプログラムを組むのはかなり面倒なのですが、 ともかくやってみることにしましょう。

各プロパティーページで修正があったときに, 発送されるメッセージは EN_CHANGE です。これを受け取ることが できるのは各プロパティーページです。そのため、処理するための ルーチンは各ページごとに行わないといけません。そうすると、Step 1 での プログラムそのままでは部分的に不都合となります。各ページごとの 文字列をプロパティーシートクラス CMySheet のメンバー変数として定めているためです。 そこで、この変数を大局変数にして、各プロパティーページから アクセスが可能な状況にして、処理手順を各ページごとに記述することにします。

そこで各頁の文字列を保存するための変数を CPropSampleView のメンバー変数から削除します :

PropSampleView.h の修正
// 属性
public:
    CPropSampleDoc* GetDocument() const;
    
    CString m_edit_p1; <---- 消去する
    CString m_edit_p2; <---- 消去する
    CString m_edit_p3; <---- 消去する

このあとで、これらの変数を PropSampleView.cpp のヘッダーで 次のように大局変数として宣言します。

PropSampleView.cpp の修正
#include "stdafx.h"
#include "PropSample.h"

#include "PropSampleDoc.h"
#include "PropSampleView.h"

#include "MySheet.h"
#include ".\propsampleview.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif
CString m_edit_p1;  
CString m_edit_p2;  
CString m_edit_p3;  

更に、これらの変数を各頁ごとに使用するためには、 各ページのクラスのヘッダーで、 変数が外部宣言されていること (declared externally) を明示します。 Page1.cpp の場合であれば次のようにします。 Page2.cpp, Page3.cpp に関しても同様の修正を施します。

Page1.cpp の修正
// Page1.cpp : 実装ファイル
//

#include "stdafx.h"
#include "PropSample.h"
#include "Page1.h"

extern CString m_edit_p1;

更に、「Edit Control」が修正されたときに処理をする 「イベント ハンドラ」を作る必要があります。これも各頁ごとに処理する必要が あります。1 頁目の場合であれば、「リソース ビュー」から「IDD_DIALOG1」をダブルクリックして 1頁目のダイアログを表示して、更に「Edit Control」の上で右クリックして表示されるメニューから 「イベント ハンドラの追加」を選びます。ここで次のように設定されていることを確認して 「追加して編集」を選びます。

メッセージの種類 EN_CHANGE
クラス名 CPage1

作成された「イベント ハンドラ」の「フレーム ワーク」に次のような 書き込みをします。

OnEnChangeEdit1() の書き込み
void CPage1::OnEnChangeEdit1()
{
    // TODO :  これが RICHEDIT コントロールの場合、....
    .........
    .........
    

    // TODO :  ここにコントロール通知ハンドラ コードを追加してください。
    SetModified(TRUE);  
}

以上の書き込みも 2 ページ目、3 ページ目にも同様に行います。 これで一応『適用』(Apply) ボタンが有効になりますが、『適用』ボタンが押されたときの 処理が必要となります。 そこで「クラス ビュー」「CPage1」を指定して、「プロパティー」「オーバライド」ボタンをクリックし、「OnApply」を探します。

「OnApply」をクリックすると、右端に下向きの矢印が表示され、これをクリックすると「<追加>OnApply」 が表示されます。そのままこれを選択すると、OnApply() 関数のフレーム ワークが追加されます。

これに書き込みをする必要があります。これも各頁ごとにする必要がありますが、 1 ページ目の場合であれば次のように書き込みをします。

OnApply() の書き込み
BOOL CPage1::OnApply()
{
    // TODO : ここに特定なコードを追加するか、もしくは基本クラスを呼び出してください。
    m_edit_p1 = m_edit;
    CWnd* parent1 = (CWnd*)GetParent();
    CWnd* parent2 = (CWnd*)(parent1->GetParent());
    parent2->Invalidate();
    SetModified(FALSE);
    return CPropertyPage::OnApply();
}

以上で完成です。ビルドして実行してみることにします。

Step 3. ウィザードを使用したプログラムのの作成
プロパティーシートからの変更点

プロパティーシートをウィザードに変更するためには、作成するプロパティーシートの メンバー関数 SetWizardMode() を呼び出すだけで十分なのですが、 これだけだとすべてのページのボタンが

となってしまいます。最初のページには『戻る』は必要ありませんし、 また最後のページには『完了』がないといけません。そこで、各プロパティーページに 関数を追加する必要があります。 そこで「クラス ビュー」「CPage1」を指定して、「プロパティー」「オーバライド」ボタンをクリックし、「OnSetActive」を探します。

「OnSetActive」をクリックすると、右端に下向きの矢印が表示され、これをクリックすると「<追加>OnSetActive」 が表示されます。そのままこれを選択すると、OnSetActive() 関数のフレーム ワークが追加されます。 CPage2, CPage3 に関しても同様に OnSetActive 関数を追加します。

OnSetActive 関数はプロパティーページがアクティブになったときただ一度実行される関数で、 ここでプロパティーページの体裁を指定します。 ボタンは実はプロパティーシートのメンバーです。そのため、 ボタンの体裁を変えるためには、 各々の OnSetActive 関数に次のように書き込みをしなければなりません。

CPage1 の OnSetActive 関数への書き込み
BOOL CPage1::OnSetActive() 
{
    // TODO : ここに特定なコードを追加するか、もしくは基本クラスを呼び出してください。
    CPropertySheet* parent = (CPropertySheet*)GetParent();
    parent->SetWizardButtons(PSWIZB_NEXT);
    return CPropertyPage::OnSetActive();
}
CPage2 の OnSetActive 関数への書き込み
BOOL CPage1::OnSetActive() 
{
    // TODO : ここに特定なコードを追加するか、もしくは基本クラスを呼び出してください。
    CPropertySheet* parent = (CPropertySheet*)GetParent();
    parent->SetWizardButtons(PSWIZB_BACK | PSWIZB_NEXT);
    return CPropertyPage::OnSetActive();
}
CPage3 の OnSetActive 関数への書き込み
BOOL CPage1::OnSetActive() 
{
    // TODO : ここに特定なコードを追加するか、もしくは基本クラスを呼び出してください。
    CPropertySheet* parent = (CPropertySheet*)GetParent();
    parent->SetWizardButtons(PSWIZB_BACK | PSWIZB_FINISH);
    return CPropertyPage::OnSetActive();
}

更に Step 1 で 作成したプログラムの メニュー項目のイベント ハンドラへの書き込みを修正します。

On32771() 関数への書き込み
void CPropSampleView::On32771() 
{
    // TODO: この位置にコマンド ハンドラ用のコードを追加してください
    CMySheet mysheet("プロパティーシート", this, 0);
    mysheet.m_page1.m_edit = m_edit_p1;
    mysheet.m_page2.m_edit = m_edit_p2;
    mysheet.m_page3.m_edit = m_edit_p3;
    mysheet.SetWizardMode();
    int result = mysheet.DoModal();
    if(result == ID_WIZFINISH)
    {
        m_edit_p1 = mysheet.m_page1.m_edit;
        m_edit_p2 = mysheet.m_page2.m_edit;
        m_edit_p3 = mysheet.m_page3.m_edit;
        Invalidate();
    }
}

ウィザードボタンによる処理

Windows ソフトのインストーラーは一般にウィザード形式になっています。 インストーラーは必ずと言っていよいくらいにユーザー名や所属を要求してきます。 しかし面倒だからと言って空白のまま処理しようとしても先に進むことが出来ません。 こんなことも簡単にプログラムを組むことができます。 前項のウィザードプログラムの 2 ページ目の文字列が空白である場合には 先に進むことができないようにしてみましょう。

「クラス ビュー」「CPage2」を指定して、「プロパティ」「オーバライド」ボタンを押して 「OnWizardNext」を探します。

「OnWizardNext」をクリックすると、右端に下向きの矢印が現れます。これをクリックすると 「<追加>OnWizardNext」と書かれた項目が表示されます。これを選択すると OnWizardNext() のフレーム ワークが作成されます。ここに以下のような書き込みをします。

OnWizardNext() 関数への書き込み

LRESULT CPage2::OnWizardNext() 
{
    // TODO : ここに特定なコードを追加するか、もしくは基本クラスを呼び出してください。
    CEdit* p = (CEdit*) GetDlgItem(IDC_EDIT1);
    if( p->LineLength() == 0)
    {
        AfxMessageBox("文字列を入力してください");
        return -1;
    }
    return CPropertyPage::OnWizardNext();
}

このようにすると 2 ページ目の文字列が空白であれば 3 ページ目に進めなくなります。 ビルドして、確かめてみましょう。