WTL для MFC программистов. Часть VIII

Наборы страниц свойств и мастера

Автор: Michael Dunn
Перевод: Алексей Кирюшкин
The RSDN Group

Источник: The Code Project
Опубликовано: 01.10.2003
Версия текста: 1.0

Введение
WTL классы наборов страниц свойств
Методы CPropertySheetImpl
WTL классы страниц свойств
Методы CPropertyPageWindow
Методы CPropertyPageImpl
Обработка уведомлений
Создание набора страниц свойств
Самый простой набор свойств из всех известных
Создание полезной страницы свойств
Создание улучшенного класса набора свойств
Создание мастера
Добавление других страниц, работа с DDV
Другие соображения по UI
Центровка набора страниц свойств
Добавление иконок к страницам набора
Далее

Демонстрационный проект

Введение

Наборы страниц свойств (Property sheets) были популярным способом представления опций еще до того, как Windows 95 сделали их одним из стандартных элементов управления. Мастера на основе набора страниц свойств часто используются для руководства пользователем при установке программного обеспечения или при решении других сложных задач. WTL обеспечивает хорошую поддержку при создании обоих типов окон свойств, а также позволяет использовать все возможности диалогов, рассмотренные нами ранее, например DDX и DDV. В этой статье я продемонстрирую создание обычного набора свойств, мастера, а также обработку событий и уведомлений набором свойств.

WTL классы наборов страниц свойств

При реализации набора страниц свойств объединяются возможности двух классов – CPropertySheetWindow и CPropertySheetImpl. Оба они определены в заголовочном файле atldlgs.h. CPropertySheetWindow – класс интерфейса окна (производный от CWindow), CPropertySheetImpl имеет карту сообщений и фактически реализует окно. Аналогичная ситуация имеет место быть с базовыми ATL-классами окон, когда вместе используются CWindow и CWindowImpl.

CPropertySheetWindow реализует обертки для различных PSM_* сообщений, например, SetActivatePageByID() является оберткой для PSM_SETCURSELID. CPropertySheetImpl управляет структурой PROPSHEETHEADER и массивом HPROPSHEETPAGES. В CPropertySheetImpl также есть методы для установки некоторых полей PROPSHEETHEADER и удаления и добавления страниц. Используя переменную-член этого класса m_psh можно обратиться непосредственно к структуре PROPSHEETHEADER,.

И, наконец, есть еще CPropertySheet – специализация CPropertySheetImpl, которую вы можете использовать, если не собираетесь настраивать набор свойств.

Методы CPropertySheetImpl

Далее рассмотрены самые важные методы CPropertySheetImpl. Так как множество методов являются только обертками вокруг оконных сообщений, я не буду приводить исчерпывающий список, однако вы можете найти его в файле atldlgs.h.

CPropertySheetImpl(_U_STRINGorID title = (LPCTSTR) NULL,
                   UINT uStartPage = 0, HWND hWndParent = NULL)

Конструктор CPropertySheetImpl позволяет определить несколько общих настроек сразу же, так что позже вам не придется вызывать другие методы для их установки. Параметр title определяет текст для заголовка набора свойств. _U_STRINGorID – это служебный класс WTL, который позволяет устанавливать этот параметр, используя LPCTSTR или идентификатор ресурса. К примеру, обе из этих строк будут работать:

  CPropertySheetImpl mySheet ( IDS_SHEET_TITLE );
  CPropertySheetImpl mySheet ( _T("My prop sheet") );

если IDS_SHEET_TITLE – идентификатор строки в таблице строк. uStartPage – индекс (начиная с 0) страницы, которая должна быть активной при появлении набора свойств. hWndParent устанавливает родительское окно для набора.

BOOL AddPage(HPROPSHEETPAGE hPage)
BOOL AddPage(LPCPROPSHEETPAGE pPage)

Добавляет вкладку к набору. Если страница уже создана, вы можете передать ее дескриптор (HPROPSHEETPAGE) в первом варианте этого метода. Более обычный путь состоит в использовании второго варианта. В этом случае вы передаете структуру PROPSHEETPAGE (которую можно получить из CPropertyPageImpl, рассматриваемого ниже), а CPropertySheetImpl создаст страницу и будет управлять ею для вас.

BOOL RemovePage(HPROPSHEETPAGE hPage)
BOOL RemovePage(int nPageIndex)

Удаляет страницу из набора. В качестве параметра вы можете передать дескриптор этой страницы или ее индекс (считая от 0).

BOOL SetActivePage(HPROPSHEETPAGE hPage)
BOOL SetActivePage(int nPageIndex)

Устанавливает активную страницу в наборе. Вы можете передать в этот метод дескриптор или индекс (считая от 0) страницы, которую нужно сделать активной. Этот метод можно вызывать перед созданием набора, чтобы установить, какая страница будет активной, когда набор страниц свойств впервые станет видимым.

void SetTitle(LPCTSTR lpszText, UINT nStyle = 0)

Устанавливает текст заголовка набора свойств. nStyle может быть 0 или PSH_PROPTITLE. Если его значение - PSH_PROPTITLE, то к набору добавляется бит стиля, который заставляет слова "Properties for" предшествовать тексту, преданному в параметре lpszText.

void SetWizardMode()

Устанавливает стиль PSH_WIZARD, который превращает набор страниц свойств в мастера. Метод нужно вызывать до того, как набор станет видимым.

void EnableHelp()

Устанавливает стиль PSH_HASHELP, добавляющий к набору кнопку Help.

ПРИМЕЧАНИЕ

Обратите внимание, что вы также должны вызвать метод EnableHelp() для каждой страницы, имеющей справку, чтобы кнопка Help не только появилась, но и была активной.

INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow())

Создает и показывает модальное окно набора свойств. Положительное возращаемое значение говорит об успешном завершении метода, полное описание возвращаемых значений можно посмотреть в описании API-функции PropertySheet(). Если происходит ошибка и набор свойств не может быть создан, DoModal() возвращает -1.

HWND Create(HWND hWndParent = NULL)

Создает и показывает немодальное окно набора свойств и возвращает дескриптор его окна. Если происходит ошибка и набор не может быть создан Create() возвращает NULL.

WTL классы страниц свойств

Классы WTL, инкапсулирующие работу со страницами свойств подобны классам набора окон свойств. Есть класс оконного интерфейса CPropertyPageWindow, и класс реализации CPropertyPageImpl. CPropertyPageWindow очень мал и содержит главным образом сервисные функции, которые вызывают методы в родительском наборе страниц свойств.

CPropertyPageImpl является наследником CDialogImplBaseT, так как страница строится на основе ресурса диалога. Это означает, что все возможности диалогов WTL также доступны для страниц свойств, как, например, DDX и DDV. CPropertyPageImpl имеет два основных назначения: управляет структурой PROPSHEETPAGE (хранимой в переменной-члене m_psp) и обрабатывает PSN_* уведомления. Для очень простых вкладок можно использовать класс CPropertyPage. Но он подходит только для страниц, которые не взаимодействуют с пользователем, типа страницы About или страницы Введение в мастере.

Вы также можете создать страницы содержащие элементы управления ActiveX. Для начала добавьте #include <atlhost.h> в stdafx.h. При реализации страницы свойств используйте класс CAxPropertyPageImpl вместо CPropertyPageImpl. Для простых страниц, содержащих элементы управления ActiveX, можно использовать CAxPropertyPageImpl вместо CPropertyPage.

Методы CPropertyPageWindow

Самый важный метод CPropertyPageWindow - GetPropertySheet():

CPropertySheetWindow GetPropertySheet()

Этот метод получает HWND родительского окна [окна набора] и подключает CPropertySheetWindow к этому HWND. Новый объект CPropertySheetWindow возвращается вызывавшему.

Обратите внимание, что в данном случае всего лишь создается временный объект, а не возвращается указатель или ссылка на фактический CPropertySheet или CPropertySheetImpl. Это важно, если вы используете ваш собственный класс, производный от CPropertySheetImpl и хотите получить доступ к его данным.

Оставшиеся члены CPropertyPageWindow только вызывают функции CPropertySheetWindow – обертки PSM_* сообщений:

BOOL Apply()
void CancelToClose()
void SetModified(BOOL bChanged = TRUE)
LRESULT QuerySiblings(WPARAM wParam, LPARAM lParam)
void RebootSystem()
void RestartWindows()
void SetWizardButtons(DWORD dwFlags)

Например, в классе, производном от CPropertyPageImpl вы можете вызвать:

  SetWizardButtons ( PSWIZB_BACK | PSWIZB_FINISH );

вместо:

CPropertySheetWindow wndSheet;
 
  wndSheet = GetPropertySheet();
  wndSheet.SetWizardButtons ( PSWIZB_BACK | PSWIZB_FINISH );

Методы CPropertyPageImpl

CPropertyPageImpl управляет структурой PROPSHEETPAGE, которой соответствует public член m_psp. CPropertyPageImpl также имеет оператор приведения к PROPSHEETPAGE*, так что вы можете передавать CPropertyPageImpl методам, которые принимают LPROPSHEETPAGE или LPCROPSHEETPAGE, например таким как CPropertyPageImpl::AddPage().

Конструктор CPropertyPageImpl позволяет установить заголовок страницы – текст, который будет размещен на закладке страницы:

CPropertyPageImpl(_U_STRINGorID title = (LPCTSTR) NULL)

Если вам необходимо создать страницу вручную, вместо того, чтобы позволить сделать это набору, вы можете вызвать метод Create():

HPROPSHEETPAGE Create()

Create() только вызывает CreatePropertySheetPage() с m_psp в качестве параметра. Вам может понадобится вызов Create(), если вы добавляете страницы к набору после того, как набор уже создан, или если вы создаете страницу, которая будет передана другому набору, не под вашим управлением (например, см. Расширение оболочки для добавления новых страниц в набор свойств файлов).

Существуют три метода для установки заголовка страницы:

void SetTitle(_U_STRINGorID title)
void SetHeaderTitle(LPCTSTR lpstrHeaderTitle)
void SetHeaderSubTitle(LPCTSTR lpstrHeaderSubTitle)

Первый метод меняет текст на закладке страницы. Два других используются в мастерах Wizard97-стиля (PSH_WIZARD97) для установки текста в заголовке выше области страницы.

Методы SetHeaderTitle и SetHeaderSubTitle доступны только при _WIN32_IE >= 0x0500

void EnableHelp()

Устанавливает флаг PSP_HASHELP в m_psp для разблокирования кнопки Help, когда активна данная страница.

Обработка уведомлений

CPropertyPageImpl имеет карту сообщений, которая обрабатывает WM_NOTIFY. Если код уведомления – PSN_*, OnNotify() вызывает обработчик данного специфического уведомления. Вызовы делаются с использованием виртуальных функций, так что обработчики могут быть легко перегружены в производных классах.

Существует два набора обработчиков уведомлений из-за изменения дизайна при переходе от WTL 3 к WTL 7. В WTL 3 обработчики уведомлений возвращали значения, отличные от значений, возвращаемых сообщениями PSN_*. Например, обработчик PSN_WIZFINISH из WTL 3 имеет вид:

    case PSN_WIZFINISH:
        lResult = !pT->OnWizardFinish();
    break;

ожидалось, что OnWizardFinish() возвратит TRUE, чтобы позволить мастеру завершиться, или FALSE, чтобы воспрепятствовать закрытию мастера. Это однако сломалось, когда стандартные элементы управления IE5 получили возможность возвращать дескриптор окна из обработчика PSN_WIZFINISH, для передачи этому окну фокуса. Приложения WTL 3 не могли использовать эту возможность, так как все значения, отличные от нуля, обрабатывались одинаково.

В WTL 7 OnNotify() не изменяет ни каких значений, возвращаемых из обработчиков PSN_*. Обработчики могут возвратить любое допустимое по документации значение и поведение при этом будет корректным.

Однако, для обратной совместимости, обработчики WTL 3 все еще присутствуют и используются по умолчанию в WTL 7.

Чтобы использовать обработчики WTL7 вы должны добавить к stdafx.h, перед включением atldlgs.h следующую строку:

#define _WTL_NEW_PAGE_NOTIFY_HANDLERS

Так как при написании нового кода нет никакой причины не использовать обработчики WTL 7, обработчики WTL 3 здесь рассматриваться не будут.

CPropertyPageImpl имеет обработчики по умолчанию для всех уведомлений, так что вы можете перекрывать только те, что необходимы в вашей программе. Вот обработчики по умолчанию и их действия:

int OnSetActive() // разрешает странице стать активной
BOOL OnKillActive() // разрешает станице стать неактивной
int OnApply() // возвращает PSNRET_NOERROR при успешном завершении операции "Применить "
void OnReset() // no action
BOOL OnQueryCancel() // разрешает операцию отмены
int OnWizardBack() // переход к предыдущей странице
int OnWizardNext() // переход к следующей странице
INT_PTR OnWizardFinish() // разрешает завершение мастера
void OnHelp() // no action
BOOL OnGetObject(LPNMOBJECTNOTIFY lpObjectNotify) // no action
int OnTranslateAccelerator(LPMSG lpMsg) // возвращает PSNRET_NOERROR чтобы показать, что сообщение не было обработано
HWND OnQueryInitialFocus(HWND hWndFocus) // возвращает NULL для установки фокуса на первый элемент управления в порядке перехода по табуляции.

Создание набора страниц свойств

Теперь, когда наш тур по классам закончен, необходима программа для иллюстрации, как их использовать. Демонстрационный проект для этой статьи – простое SDI приложение, которое показывает изображение в клиентской области и заполняет фон цветом. Изображение и цвет могут быть изменены через диалог – набор страниц свойств и мастер (который я опишу позже).

Самый простой набор свойств из всех известных

После создания SDI проекта WTL AppWizard-ом, мы можем начать создание набора свойств для диалога About. Начнем с изменения стилей диалога About, созданного для нас мастером, чтобы он мог работать как страница набора свойств.

Первым делом удалим кнопку OK, так как она не имеет смысла на странице набора. На вкладке Styles измените Style на Child и Border на Thin, оставьте пока свойство Title bar помеченным. На вкладке More Styles установите свойство Disabled.

На втором шаге создадим окно набора свойств в обработчике OnAppAbout(). Мы можем сделать это используя нерасширяемые CPropertySheet и CPropertyPage:

LRESULT CMainFrame::OnAppAbout(...)
{
CPropertySheet sheet ( _T("About PSheets") );
CPropertyPage<IDD_ABOUTBOX> pgAbout;
 
    sheet.AddPage ( pgAbout );
    sheet.DoModal();
    return 0;
}

Результат будет следующим:


Создание полезной страницы свойств

Так как далеко не каждая страница так же проста, как диалог About и для большинства потребуется класс, производный от CPropertyPageImpl, рассмотрим его реализацию. Мы сделаем новую вкладку, содержащую параметры настройки для графики, отображаемой в клиентской области. Вот ресурс диалога:


Этот диалог имеет те же стили, что и страница About. Для продолжения работы со страницей нам нужен будет новый класс, CBackgroundOptsPage. Этот класс порожден от CPropertyPageImpl, так как это страница свойств и от CWinDataExchange, для использования DDX.

class CBackgroundOptsPage :
    public CPropertyPageImpl<CBackgroundOptsPage>,
    public CWinDataExchange<CBackgroundOptsPage>
{
public:
    enum { IDD = IDD_BACKGROUND_OPTS };
 
    // Конструктор
    CBackgroundOptsPage();
    ~CBackgroundOptsPage();
 
    // Карты
    BEGIN_MSG_MAP(CBackgroundOptsPage)
        MSG_WM_INITDIALOG(OnInitDialog)
        CHAIN_MSG_MAP(CPropertyPageImpl<CBackgroundOptsPage>)
    END_MSG_MAP()
 
    BEGIN_DDX_MAP(CBackgroundOptsPage)
        DDX_RADIO(IDC_BLUE, m_nColor)
        DDX_RADIO(IDC_ALYSON, m_nPicture)
    END_DDX_MAP()
 
    // Обработчики сообщений
    BOOL OnInitDialog ( HWND hwndFocus, LPARAM lParam );
 
    // Обработчики уведомлений страницы свойств
    int OnApply();
 
    // DDX переменые
    int m_nColor, m_nPicture;
};

Обратите внимание:

Метод OnApply() довольно прост, в нем вызывается DoDataExchange() для обновления DDX переменных, а затем возвращается код, показывающий можно или нет закрывать набор свойств:

int CBackgroundOptsPage::OnApply()
{
    return DoDataExchange(true) ? PSNRET_NOERROR : PSNRET_INVALID;
}

Добавим к меню главного окна пункт Tools|Options, вызывающий окно набора свойств. Обработчик этой команды создает набор свойств, как и до этого, но с добавлением новой страницы – CBackgroundOptsPage.

void CMainFrame::OnOptions ( UINT uCode, int nID, HWND hwndCtrl )
{
CPropertySheet sheet ( _T("PSheets Options"), 0 );
CBackgroundOptsPage pgBackground;
CPropertyPage<IDD_ABOUTBOX> pgAbout;
 
    pgBackground.m_nColor = m_view.m_nColor;
    pgBackground.m_nPicture = m_view.m_nPicture;
 
    sheet.m_psh.dwFlags |= PSH_NOAPPLYNOW;
 
    sheet.AddPage ( pgBackground );
    sheet.AddPage ( pgAbout );
 
    if ( IDOK == sheet.DoModal() )
        m_view.SetBackgroundOptions ( pgBackground.m_nColor,
                                      pgBackground.m_nPicture );
}

Конструктор CPropertySheet теперь имеет второй параметр - 0, означающий, что первоначально должна быть видима страница с индексом 0. Можно изменить 0 на 1, чтобы при появлении набора была активной страница About. Так как это – только демонстрационный код, я собираюсь быть ленивым и сделать переменные CBackgroundOptsPage, подключенные к радио-кнопкам. Главное окно будет только инициализировать их начальными значениями и считывать их значения, если пользователь нажмет кнопку OK в окне набора свойств.

Если пользователь нажимает OK DoModal() возвращает IDOK, и мы сообщаем виду о новой картинке и цвете фона. Вот пара скриншотов, иллюстрирующих различные установки:



Создание улучшенного класса набора свойств

Обработчик OnOptions() создает прекрасный набор свойств, но в нем есть и ужасная часть - код инициализации, там, где не должно быть участия CMainFrame. Наилучший путь состоит в том, чтобы создать класс, производный от CPropertyPageImpl для решения тех же задач.

#include "BackgroundOptsPage.h"
 
class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet>
{
public:
    // Construction
    CAppPropertySheet ( _U_STRINGorID title = (LPCTSTR) NULL, 
                        UINT uStartPage = 0, HWND hWndParent = NULL );

    // Maps
    BEGIN_MSG_MAP(CAppPropertySheet)
        CHAIN_MSG_MAP(CPropertySheetImpl<CAppPropertySheet>)
    END_MSG_MAP()

    // Property pages
    CBackgroundOptsPage         m_pgBackground;
    CPropertyPage<IDD_ABOUTBOX> m_pgAbout;
};

В этом классе мы инкапсулировали информацию о страницах набора и собственно, поместили их в набор.

В конструкторе осуществляется добавление страниц к набору и установка других необходимых флажков:

CAppPropertySheet::CAppPropertySheet ( _U_STRINGorID title, UINT uStartPage, 
                                       HWND hWndParent ) :
    CPropertySheetImpl<CAppPropertySheet> ( title, uStartPage, hWndParent )
{
    m_psh.dwFlags |= PSH_NOAPPLYNOW;

    AddPage ( m_pgBackground );
    AddPage ( m_pgAbout );
}

В результате обработчик OnOptions() становится несколько проще:

void CMainFrame::OnOptions ( UINT uCode, int nID, HWND hwndCtrl )
{
CAppPropertySheet sheet ( _T("PSheets Options"), 0 );

    sheet.m_pgBackground.m_nColor = m_view.m_nColor;
    sheet.m_pgBackground.m_nPicture = m_view.m_nPicture;

    if ( IDOK == sheet.DoModal() )
        m_view.SetBackgroundOptions ( sheet.m_pgBackground.m_nColor,
                                      sheet.m_pgBackground.m_nPicture );
}

Создание мастера

Создание мастера, и это не удивительно, подобно созданию окна свойств. Дополнительная работа требуется только для разрешения кнопок Back и Next; также как и в наборах свойств MFC, перегрузите OnSetActive() и вызывите SetWizardButtons(), чтобы разрешить соответствующие кнопки.

Мы начнем с простой страницы введения с идентификатором IDD_WIZARD_INTRO:


Заметьте, что страница не имеет никакого текста в заголовке. Так как все страницы мастера обычно имеют один и тот же заголовок, я предпочитаю установить текст в конструкторе CPropertySheetImpl и иметь страницы, использующие одну и ту же строку из ресурсов. В этом случае я могу заменить только одну строку и каждая страница отобразит изменение.

Эта страница реализована в классе CWizIntroPage:

class CWizIntroPage : public CPropertyPageImpl<CWizIntroPage>
{
public:
    enum { IDD = IDD_WIZARD_INTRO };

    // Конструктор
    CWizIntroPage();

    // Карты
    BEGIN_MSG_MAP(COptionsWizard)
        CHAIN_MSG_MAP(CPropertyPageImpl<CWizIntroPage>)
    END_MSG_MAP()

    // Обработчики уведомлений
    int OnSetActive();
};

Конструктор устанавливает текст страницы, используя идентификатор строки из ресурсов:

CWizIntroPage::CWizIntroPage() :
    CPropertyPageImpl<CWizIntroPage>( IDS_WIZARD_TITLE )
{
}

Строка IDS_WIZARD_TITLE ("PSheets Options Wizard") появится в заголовке мастера, когда эта страница будет текущей. OnSetActive() разрешает только кнопку Next:

int CWizIntroPage::OnSetActive()
{
    SetWizardButtons ( PSWIZB_NEXT );
    return 0;
}

Для реализации мастера мы создадим класс COptionWizard и пункт Tools|Wizard в меню главного окна. Конструктор COptionWizard подобен конструктору CAppPropertySheet, в котором устанавливаются необходимые биты стиля или флажки и добавляются страницы к набору:

class COptionsWizard : public CPropertySheetImpl<COptionsWizard>
{
public:
    // Конструктор
    COptionsWizard ( HWND hWndParent = NULL );

    // Карты
    BEGIN_MSG_MAP(COptionsWizard)
        CHAIN_MSG_MAP(CPropertySheetImpl<COptionsWizard>)
    END_MSG_MAP()

    // Страницы свойств
    CWizIntroPage m_pgIntro;
};

COptionsWizard::COptionsWizard ( HWND hWndParent ) :
    CPropertySheetImpl<COptionsWizard> ( 0U, 0, hWndParent )
{
    SetWizardMode();

    AddPage ( m_pgIntro );
}

Обработчик для Tools|Wizard будет такой:

void CMainFrame::OnOptionsWizard ( UINT uCode, int nID, HWND hwndCtrl )
{
COptionsWizard wizard;

    wizard.DoModal();
}

А это мастер в действии:


Добавление других страниц, работа с DDV

Чтобы сделать этот мастер полезным, добавим новую страницу для выбора цвета фона. На этой странице также будет чек-бокс для демонстрации обработки ошибки DDV и запрещения для пользователя продолжать работу с мастером. Вот новая страница с идентификатором IDD_WIZARD_BKCOLOR:


Реализация этой страницы находится в классе CWizBkColorPage. Вот относящиеся к ней части:

class CWizBkColorPage :
    public CPropertyPageImpl<CWizBkColorPage>,
    public CWinDataExchange<CWizBkColorPage>
{
public:
    // кое-что удалено для краткости . . .

    BEGIN_DDX_MAP(CWizBkColorPage)
        DDX_RADIO(IDC_BLUE, m_nColor)
        DDX_CHECK(IDC_FAIL_DDV, m_bFailDDV)
    END_DDX_MAP()

    // Обработчики уведомлений
    int OnSetActive();
    BOOL OnKillActive();

    // переменные DDX
    int m_nColor;

protected:
    int m_bFailDDV;
};

OnSetActive() работает также как на вводной странице, только разрешает обе кнопки - Back и Next. OnKillActive() – новый обработчик, он вызывает DDX и проверяет значение m_bFailDDV. Если оно равно TRUE (чек-бокс установлен), OnKillActive() препятствует переходу к следующей странице мастера.

int CWizBkColorPage::OnSetActive()
{
    SetWizardButtons ( PSWIZB_BACK | PSWIZB_NEXT );
    return 0;
}

int CWizBkColorPage::OnKillActive()
{
    if ( !DoDataExchange(true) )
        return TRUE;    // предотвращает деактивацию

    if ( m_bFailDDV )
        {
        MessageBox (
          _T("Error box checked, wizard will stay on this page."),
          _T("PSheets"), MB_ICONERROR );

        return TRUE;    // предотвращает деактивацию
        }

    return FALSE;       // разрешает деактивацию
}

Обратите внимание, что логика из OnKillActive() могла бы быть перемещена в OnWizardNext(), так как оба обработчика имеют возможность удерживать мастера на текущей странице. Различие же их в том, что OnKillActive() вызывается когда пользователей нажимает Back или Next, а OnWizardNext() – только когда пользователь нажимает Next. OnWizardNext() также используется для других целей; это может быть перемещение мастера не к следующей по порядку странице, если некоторые страницы должны быть пропущены.

Мастер в демонстрационном проекте имеет еще две страницы, CWizBkPicturePage и CWizFinishPage. Так как они подобны двум страницам, описанным выше, я не буду рассматривать их подробно, но вы можете посмотреть демонстрационный проект, чтобы узнать все детали.

Другие соображения по UI

Центровка набора страниц свойств

Заданное по умолчанию поведение наборов свойств и мастеров состоит в том, чтобы появляться около левого верхнего угла их родительского окна:


Это выглядит несколько неуклюже, но к счастью может быть исправлено. Первый подход, о котором я подумал, состоял в том, чтобы перегрузить CPropertySheetImpl::PropSheetCallback() и центрировать набор в этой функции. ОС вызывает эту функцию, когда набор создается, а WTL использует чтобы subclass-сировать окно набора. Так что наша первая попытка могла бы быть такой:

class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet>
{
//...
    static int CALLBACK PropSheetCallback(HWND hWnd, UINT uMsg, LPARAM lParam)
    {
    int nRet = CPropertySheetImpl<CAppPropertySheet>::PropSheetCallback (
                                                        hWnd, uMsg, lParam );
 
        if ( PSCB_INITIALIZED == uMsg )
            {
            // центрируем набор ... но как?
            }
 
        return nRet;
    }
};

Как вы можете видеть, нас постиг облом. PropSheetCallback() – статический метод, так что у нас нет доступа к this, чтобы обратиться к окну набора. OK, а как насчет копирования кода из CPropertySheetImpl::PropSheetCallback() и добавления к нему нашего собственного? Оставляя пока в стороне то, что это привязывает наш код к конкретной реализации WTL (что само по себе предупреждение о том, что это – нехороший подход) имеем следующий код:

class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet>
{
//...
    static int CALLBACK PropSheetCallback(HWND hWnd, UINT uMsg, LPARAM)
    {
        if(uMsg == PSCB_INITIALIZED)
        {
            // Код, скопированный из WTL и настроенный дял использования CAppPropertySheet
            // в качестве T:
            ATLASSERT(hWnd != NULL);
            CAppPropertySheet* pT = (CAppPropertySheet*)
                                        _Module.ExtractCreateWndData();
            // subclass-сируем окно набора
            pT->SubclassWindow(hWnd);
            // удаляем массив дескрипторов страниц
            pT->_CleanUpPages();
 
            // Далее наш собственный код:
            pT->CenterWindow ( pT->m_psh.hwndParent );
        }
 
        return 0;
    }
};

В теории все выглядит хорошо, однако, когда я попробовал реализовать это, позиция набора не изменилась. Очевидно, код стандартного элемента управления повторно позиционирует окно набора уже после нашего вызова CenterWindow().

Итак, разочаровавшись в красивом решении, полностью инкапсулированном в классе набора, я возвратился к имеющемуся набору, а его страницы помогут нам центрировать окно набора. Я добавил определяемое пользователем сообщение с именем UWM_CENTER_SHEET:

  #define UWM_CENTER_SHEET WM_APP

CAppPropertySheet обрабатывает это сообщение в своей карте сообщений:

class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet>
{
//...
    BEGIN_MSG_MAP(CAppPropertySheet)
        MESSAGE_HANDLER_EX(UWM_CENTER_SHEET, OnPageInit)
        CHAIN_MSG_MAP(CPropertySheetImpl<CAppPropertySheet>)
    END_MSG_MAP()
 
    // Обработчики сообщений
    LRESULT OnPageInit ( UINT, WPARAM, LPARAM );
 
protected:
    bool m_bCentered;  // устанавливается в false в конструкторе
};
 
LRESULT CAppPropertySheet::OnPageInit ( UINT, WPARAM, LPARAM )
{
    if ( !m_bCentered )
        {
        m_bCentered = true;
        CenterWindow ( m_psh.hwndParent );
        }
 
    return 0;
}

Далее, в методе OnInitDialog() каждой страницы это сообщение посылается окну набора:

BOOL CBackgroundOptsPage::OnInitDialog ( HWND hwndFocus, LPARAM lParam )
{
    GetPropertySheet().SendMessage ( UWM_CENTER_SHEET );
 
    DoDataExchange(false);
    return TRUE;
}

Флажок m_bCentered в наборе гарантирует, что реальные действия будут только при обработке первого сообщения UWM_CENTER_SHEET.

RSDN: Тот же эффект можно получить просто добавив в OnInitDialog первой активируемой страницы строку
GetPropertySheet().CenterWindow();
или вообще отказаться от использования для центровки классов страниц, а добавить вызов CenterWindow() в обработчик сообщения WM_SHOWWINDOW в классе, производном от CPropertySheetImpl (в данном примере CAppPropertySheet).

Добавление иконок к страницам набора

Чтобы использовать другие возможности набора и его страниц, не обернутые функциями-членами, вы должны будете обратиться непосредственно к соответствующим структурам: PROPSHEETHEADER m_psh в CPropertySheetImpl, или к PROPSHEETPAGE m_psp в CPropertyPageImpl.

Например, чтобы добавить иконку к странице "Background" нужно добавить флажок и установить пару других полей в структуре PROPSHEETPAGE:

CBackgroundOptsPage::CBackgroundOptsPage()
{
    m_psp.dwFlags |= PSP_USEICONID;
    m_psp.pszIcon = MAKEINTRESOURCE(IDI_TABICON);
    m_psp.hInstance = _Module.GetResourceInstance();
}

И вот результат:


Далее

В части 9 я рассмотрю сервисные классы WTL, а также обертки для объектов GDI и стандартных диалогов.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.