Общие замечания

Как правило, «пользовательские» диалоговые окна в MFC состоят из ресурса диалогового окна, создаваемого обычным образом, и связанного с ним класса C++, порождаемого от CDialog. Заметим, что ClassWizard способен колдовать только над теми диалоговыми окнами, ресурсы которых имеют численные идентификаторы (и вообще существенно ограничивает степень свободы программиста при их разработке, о чем еще будет сказано ниже).

Класс CDialog

Заголовочный файл #include <afxwin.h>
Непосредственный предок класс CWnd
Объектная модель DYNAMIC

Вначале мы рассмотрим «собственные» методы класса CDialog.

Конструирование (construction)

CDialog(
   LPCTSTR pszResName,
   CWnd* pParentWnd = NULL
);
CDialog(
   UINT uResID,
   CWnd* pParentWnd = NULL
);

Связывает класс C++ с заданным ресурсом диалогового окна, имеющим строковое имя pszResName или численный идентификатор uResID. Параметр pParentWnd определяет будущее окно-владелец конструируемого диалогового окна («будущее» — так как вызов конструктора еще не создает самого окна); если значение этого параметр задается равным NULL, то владельцем диалогового окна станет главное окно приложения.

Обе эти формы конструктора применяются при создании модальных диалоговых окон и, как правило, вызываются из конструктора потомка CDialog с указанием конкретного имени или идентификатора ресурса. Например:

// файл SampleDialog.h

class CSampleDialog: public CDialog
{
public:
   CSampleDialog(CWnd* pParentWnd = NULL);
...
};
// файл SampleDialog.cpp

CSampleDialog::CSampleDialog(CWnd* pParentWnd):
CDialog(IDD_SAMPLE, pParentWnd)
{
   ...
}

Существует также «защищенный» (protected) конструктор

CDialog(void);

предназначенный для создания немодальных диалоговых окон, который обязательно должен быть переопределен в классе-потомке.

Инициализация (initialization)

Используется только для немодальных диалоговых окон.

BOOL Create(
   LPCTSTR pszResName,
   CWnd* pParentWnd = NULL
);
BOOL Create(
   UINT uResID,
   CWnd* pParentWnd = NULL
);

Данный метод создает немодальное диалоговое окно на основании ресурса со строковым именем pszResName или численным идентификатором uResID и отображает его на экране. Окно-владелец создаваемого диалогового окна определяется значением параметра pParentWnd; если это значение задано равным NULL, то владельцем становится главное окно приложения. При успешном создании окна метод возвращает ненулевое значение.

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

Операции (operations)

virtual int DoModal(void);

С функциональной точки зрения этот метод является аналогом функции Win32 API DialogBox — он отображает на экране модальное диалоговое окно, блокируя до его закрытия окно-владелец, заданное при вызове конструктора. Если диалоговое окно создать не удалось (например, по причине неверно заданного идентификатора ресурса или использования элементов управления, оконные классы которых не зарегистрированы в системе), метод DoModal возвращает значение -1; в противном случае возвращается определяемый программистом «результат завершения» диалога, значение которого совпадает, как правило, с идентификатором кнопки, нажатой пользователем для закрытия диалогового окна.

void EndDialog(
   int nResult
);

Уничтожает модальное диалоговое окно, назначая «результат завершения» равным nResult — именно это значение и вернет метод DoModal в вызывающую функцию. Заметим, что данный метод должен использоваться только внутри обработчиков сообщений диалогового окна. И второе замечание: для уничтожения немодальных диалоговых окон необходимо использовать унаследованный от класса CWnd метод DestroyWindow, причем вызывать его можно не только из обработчиков сообщений, но и «извне».

void NextDlgCtrl(void) const;

Активизирует, передавая ему фокус ввода, следующий (относительно имеющего этот фокус в момент вызова) элемент управления в диалоговом окне, имеющий стиль WS_TABSTOP.

void PrevDlgCtrl(void) const;

Активизирует, передавая ему фокус ввода, предыдущий (относительно имеющего этот фокус в момент вызова) элемент управления в диалоговом окне, имеющий стиль WS_TABSTOP.

void GotoDlgCtrl(
   CWnd* pWnd
) const;

Активизирует, передавая ему фокус ввода, элемент управления определяемый параметром pWnd.

void SetDefID(
   UINT uID
);

Делает кнопку, имеющую идентификатор uID кнопкой по умолчанию, назначая ей стиль BS_DEFPUSHBUTTON. Заметим, что вызов этого метода не всегда должным образом изменяет внешний вид предыдущей кнопки по умолчанию; для преодоления этого эффекта рекомендуется послать ей сообщение BM_SETSTYLE.

DWORD GetDefID(void) const;

Возвращает идентификатор кнопки по умолчанию. Если такая кнопка в диалогом окне отсутствует, метод возвращает значение 0. В противном случае, младшее слово результата содержит ее идентификатор, а старшее — значение DC_HASDEFID. Таким образом, получение этого идентификатора может выглядеть так:

DWORD dwDefID = GetDefID();
if (dwDefID != 0 && HIWORD(dwDefID) == DC_HASDEFID)
{
   // кнопка по умолчанию есть
   TRACE(_T("ID of the default push button is %u.\n"), LOWORD(dwDefID));
}
else
{
   // кнопки по умолчанию нет
   TRACE(_T("There is no default push button.\n"));
}

Виртуальные методы (overridables)

virtual void DoDataExchange(
   CDataExchange* pDX
);

Этот метод предназначен для реализации обмена данными с диалоговым окном, который включает в себя инициализацию элементов управления перед отображением окна на экране и сохранение введенных пользователем значений после его закрытия. В этом же методе может выполняться и проверка корректности пользовательского ввода при попытке закрытия диалогового окна «командой» с идентификатором IDOK. Объект класса CDataExchange, указатель на который передается через параметр pDX, содержит информацию о «направлении» обмена и несколько вспомогательных методов, используемых MFC в процессе его выполнения и проверки корректности ввода пользователя.

Заметим, что реализация метода DoDataExchange, наследуемая классом CDialog от своего непосредственного предка — класса CWnd, не выполняет абсолютно никаких действий; тем не менее, ее вызов из DoDataExchange классов-потомков CDialog присутствует во всех примерах, приводимых в MSDN. Отметим также, что данный метод не должен вызываться приложением явно; вместо этого, необходимо пользоваться методом

BOOL UpdateData(
   BOOL bSaveAndValidate = TRUE
);

задавая параметр bSaveAndValidate равным FALSE для инициализации элементов управления и TRUE — для сохранения введенных пользователем значений и проверки их корректности. Более подробно механизмы обмена данными и проверки корректности пользовательского ввода рассматриваются ниже в соответствующих частях данной темы.

virtual BOOL OnInitDialog(void);

Вызывается в ответ на сообщение WM_INITDIALOG. Реализация этого метода вызывает UpdateData для инициализации элементов управления (через соответствующее обращение к DoDataExchange) и центрирует диалоговое окно относительно его владельца, задаваемого при вызове конструктора. Если этот метод перекрывается, то в нем необходимо обязательно вызвать базовую реализацию.

virtual void OnOK(void);

Вызывается в ответ на «команду» с идентификатором IDOK или нажатие пользователем клавиши Enter. Реализация этого метода вызывает UpdateData для сохранения пользовательского ввода и проверки его корректности (через соответствующее обращение к DoDataExchange). Если результат этой проверки оказывается успешным, то диалоговое окно закрывается с «результатом завершения», равным IDOK; в противном случае, «поверх» него выводится окно с соответствующим сообщением об ошибке, после закрытия которого пользователь может исправить недопустимое значение.

virtual void OnCancel(void);

Вызывается в ответ на «команду» с идентификатором IDCANCEL или нажатие пользователем клавиши Esc. Реализация этого метода закрывает диалоговое окно с «результатом завершения», равным IDCANCEL.

«Прямое» обращение к элементам управления

Помимо перечисленных выше, класс CDialog наследует от своего непосредственного предка (класса CWnd) несколько методов, предназначенных для «прямого» обращения к расположенными на диалоговом окне элементами управления.

CWnd* GetDlgItem(
   int nID
) const;
void GetDlgItem(
   int nID,
   HWND* phWnd
) const;

Первый вариант этого метода возвращает указатель на объект, соответствующий элементу управления с идентификатором nID; если такого элемента нет, метод возвращает значение NULL. Заметим, что этот объект может быть временным (MFC автоматически уничтожает такие объекты, когда очередь сообщений приложения пуста), поэтому запоминать полученный указатель для «долгосрочного» использования не следует.
Второй вариант метода записывает дескриптор окна элемента управления с идентификатором nID в переменную по адресу phWnd; если такого элемента нет, то в эту переменную записывается значение NULL.

void CheckDlgButton(
   int nID,
   UINT fuCheck
);

Назначает состояние флажка или радиокнопки с идентификатором nID в соответствии с параметром fuCheck, который может принимать одно из следующих значений:

BST_UNCHECKED
неотмеченное;
BST_CHECKED
отмеченное;
BST_INDETERMINATE
неопределенное (данный вариант применим только для флажков, имеющих стиль окна BS_3STATE или BS_AUTO3STATE).
UINT IsDlgButtonChecked(
   int nID
);

Возвращает значение BST_UNCHECKED, BST_CHECKED или BST_INDETERMINATE, которое соответствует текущему состоянию флажка или радиокнопки с идентификатором nID — отмеченному, неотмеченному или неопределенному.

void CheckRadioButton(
   int idcFirst,
   int idcLast,
   int idcCheck
);

Метод позволяет установить в отмеченное состояние заданную радиокнопку в группе, автоматически автоматически переводя текущую отмеченную радиокнопку из этой группы в неотмеченное состояние. Параметры idcFirst и idcLast определяют идентификаторы соответственно первой и последней кнопок группы, а параметр idcCheck — идентификатор радиокнопки, состояние которой необходимо установить в отмеченное.

int GetCheckedRadioButton(
   int idcFirst,
   int idcLast
);

Возвращает идентификатор текущей отмеченной радиокнопки в группе, первая и последняя кнопка которой задаются идентификаторами idcFirst и idcLast соответственно.

void SetDlgItemText(
   int nID,
   LPCTSTR pszSrc
);

Метод назначает элементу управления с идентификатором nID текст в соответствии со строкой pszSrc.

int GetDlgItemText(
   int nID,
   LPTSTR pszDest,
   int cchMax
) const;
int GetDlgItemText(
   int nID,
   CString& strDest
) const;

Первый вариант этого метода копирует текст элемента управления с идентификатором nID в строку pszDest; через параметр cchMax необходимо передать максимально допустимую длину текста с учетом завершающего нуля. Второй вариант метода аналогичен, но текст копируется в объект класса CString, ссылка на который передается через параметр strDest. Оба варианта метода возвращают длину скопированной строки без учета завершающего нуля.

void SetDlgItemInt(
   int nID,
   UINT uValue,
   BOOL fSigned = TRUE
);

Метод преобразует целое число uValue в строковое представление и назначает получившийся текст элементу управления с идентификатором nID. Если значение параметра fSigned отлично от нуля, то число интерпретируется как имеющее знак, а в противном случае — как беззнаковое.

UINT GetDlgItemInt(
   int nID,
   BOOL* pfSuccess = NULL,
   BOOL fSigned = TRUE
) const;

Метод преобразует текст элемента управления с идентификатором nID в число и возвращает получившееся значение; при этом в переменную по адресу pfSuccess (если этот адрес отличен от NULL) записывается ненулевое значение в случае успешно выполненного преобразования, либо 0 — в случае ошибки. Если значение параметра fSigned отлично от нуля, то текст интерпретируется как строковое представление числа со знаком, а в противном случае — как беззнакового. Заметим, что при ошибках преобразования данный метод возвращает нулевое значение, поэтому использование параметра pfSuccess является единственным надежным способом определить, был ли весь текст элемента управления успешно преобразован в число.

Обмен данными с диалоговым окном

Механизм, предназначенный для осуществления этого обмена, носит официальное наименование dialog data exchange (сокращенно — DDX) и состоит из набора глобальных функций DDX_*, которые необходимо вызывать в реализации метода DoDataExchange. Строго говоря, DDX предлагает даже не один, а два способа обмена данными.

Первый способ заключается в связывании с элементами управления так называемых «переменных-значений». При вызове метода UpdateData для инициализации элементов управления (то есть, с параметром bSaveAndValidate равным FALSE) текущие значения этих переменных будут использоваться для назначения текста полей ввода, установки состояния флажков, выделения строк в списках, etc. При вызове же UpdateData для сохранения и проверки пользовательского ввода (то есть, с параметром bSaveAndValidate равным TRUE) в эти переменные будут скопированы текущие «значения» связанных с ними элементов управления — «текст полей ввода, состояние флажков…» Заметим, что в подавляющем большинстве случаев такие «переменные-значения» являются полями класса-потомка CDialog, инкапсулирующего соответствующее диалоговое окно.

Рассмотрим небольшой пример. Допустим, мы связали некую целочисленную переменную с флажком, имеющим идентификатор IDC_CHECK_CONFIRM. Если к моменту вызова метода OnInitDialog эта переменная имеет значение BST_CHECKED, то при первоначальном отображении диалогового окна на экране данный флажок будет находиться в отмеченном состоянии. После того, как пользователь снимет с флажка отметку и закроет диалоговое окно нажатием на кнопку OK, наша переменная будет иметь значение BST_UNCHECKED.

Все функции DDX, предназначенные для реализации данного способа, имеют следующий обобщенный прототип:

void DDX_содержательная_часть_имени(
   CDataExchange* pDX,
   int nID,
   тип_переменной& refValue
);

и отличаются, как нетрудно видеть, только содержательной частью имени и типом переменной, ссылка на которую передается через параметр refValue и которая будет связана с элементом управления, имеющим идентификатор nID. Через параметр pDX необходимо передать именно тот указатель на объект класса CDataExchange, который был получен методом DoDataExchange.

Ниже приведена таблица, в которой перечислены полные имена функций DDX и типы переменных, используемые каждой из них для обмена данными; последняя колонка содержит описание этих данных и вид элемента управления, с которым используется та или иная функция.

Имя функции Тип
переменной
Описание данных
DDX_CBIndex int Индекс строки, выделенной в выпадающем списке.
DDX_CBString CString Текст строки, выделенной в выпадающем списке. При инициализации элемента управления выделяется первая строка, начинающаяся с символов, содержащихся в связанной переменной.
DDX_CBStringExact CString Текст строки, выделенной в выпадающем списке. При инициализации элемента управления выделяется строка, в точности соответствующая содержанию связанной переменной; если такой строки в списке нет — не выделяется ничего.
DDX_Check int Состояние флажка (BST_UNCHECKED, BST_CHECKED или BST_INDETERMINATE).
DDX_DateTimeCtrl CTime
COleDateTime
Дата/время, которые выбраны в элементе управления Date and Time Picker, преобразованные к одному из перечисленных типов.
DDX_LBIndex int Индекс строки, выделенной в списке.
DDX_LBString CString Текст строки, выделенной в списке. При инициализации элемента управления выделяется первая строка, начинающаяся с символов, содержащихся в связанной переменной.
DDX_LBStringExact CString Текст строки, выделенной в списке. При инициализации элемента управления выделяется строка, в точности соответствующая содержанию связанной переменной; если такой строки в списке нет — не выделяется ничего.
DDX_MonthCalCtrl CTime
COleDateTime
Дата, которая выбрана в элементе управления Month Calendar, преобразованная к одному из перечисленных типов.
DDX_Radio int Индекс отмеченной радиокнопки в группе. Для корректной работы данной функции первая (и только первая) радиокнопка группы и следующий за этой группой элемент управления должны иметь стиль WS_GROUP (флажок Group на вкладке General диалогового окна Properties в редакторе ресурсов); при этом переменная должна быть связана с первой радиокнопкой группы.
DDX_Scroll int Текущая позиция ползунка в элементе управления Scroll Bar.
DDX_Slider int Текущая позиция ползунка в элементе управления Trackbar.
DDX_Text BYTE
short
int
UINT
long
DWORD
CString
float
double
COleCurrency
COleDateTime
Текст поля ввода, преобразованный к одному из перечисленных типов.

Второй способ обмена данными, предоставляемый DDX, заключается в использовании одной единственной универсальной функции

void DDX_Control(
   CDataExchange* pDX,
   int nID,
   CWnd& refWnd
);

связывающей элемент управления, имеющий идентификатор nID, с переменной, которая является объектом класса-потомка CWnd. В библиотеке MFC реализованы классы CComboBox, CButton, CDateTimeCtrl, etc, инкапсулирующие все основные элементы управления; установив соответствующую связь, приложение может манипулировать ими, используя методы этих классов. Например:

// файл SampleDialog.h

class CSampleDialog: public CDialog
{
...
public:
   virtual BOOL OnInitDialog(void);
protected:
   virtual void DoDataExchange(CDataExchange* pDX);
...
public:
   CButton m_checkConfirm;
...
};
// файл SampleDialog.cpp

BOOL CSampleDialog::OnInitDialog(void)
{
   CDialog::OnInitDialog();
   m_checkConfirm.EnableWindow(FALSE);
}

void CSampleDialog::DoDataExchange(CDataExchange* pDX)
{
   DDX_Control(pDX, IDC_CHECK_CONFIRM, m_checkConfirm);
   ...
}

Заметим, что оба рассмотренных выше способа не являются взаимоисключающими — с одним и тем же элементом управления могут быть связаны как «переменная-значение», так и объектная переменная. Это позволяет, в частности, избежать вызовов метода UpdateData в случае, когда требуется узнать или изменить состояние только одного элемента управления. При использовании подобного приема рекомендуется выполнить все вызовы DDX_Control (поскольку они подменяют оконные функции соответствующих элементов управления) до обращения к остальным функциям DDX

Проверка корректности пользовательского ввода

Для осуществления такой проверки предназначен механизм, называемый в официальной документации dialog data validation (сокращенно — DDV). Подобно DDX он состоит из набора глобальных функций DDV_*, которые необходимо вызывать в реализации метода DoDataExchange. Однако, в отличие от DDX, функции DDV взаимодействуют не с элементами управления, а с теми «переменными-значениями», которые с этими элементами связаны. По этой причине (и в силу других особенностей реализации механизмов обмена данными и их проверки) вызов функции DDV должен следовать непосредственно за соответствующим вызовом функции DDX.

Таким образом, в корректной реализация метода DoDataExchange должна выполняться следующая последовательность действий:

  1. Вызов метода DoDataExchange, унаследованного от непосредственного предка.
  2. Последовательный вызов функций DDX_Control для связывания требуемых элементов управления с соответствующими объектными переменными.
  3. Последовательный вызов пар функций DDX/DDV, первая из которых связывает требуемый элемент управления с соответствующей «переменной-значением», а вторая — проверяет значение этой переменной на допустимость. Естественно, что если такая проверка не требуется, вызов функции DDV может быть опущен.

Ниже подробно рассматриваются функции DDV; сразу оговорим, что первым параметром в любую из них необходимо передавать именно тот указатель на объект класса CDataExchange, который был получен методом DoDataExchange.

void DDV_MaxChars(
   CDataExchange* pDX,
   CString const& strValue,
   int cchMax
);

Предназначена для проверки и ограничения количества символов в строке strValue; если это количество превосходит cchMax, то значение будет считаться некорректным. В подавляющем большинстве случаев, эта функция используется в паре с DDX_Text; при этом соответствующему полю ввода посылается сообщение EM_LIMITTEXT, ограничивающее его «емкость» заданным количеством символов.

void DDV_MinMaxByte(
   CDataExchange* pDX,
   BYTE bValue,
   BYTE bMin,
   BYTE bMax
);

Предназначена для проверки значения bValue на принадлежность заданному диапазону, нижняя граница которого определяется параметром bMin, а верхняя — параметром bMax. В подавляющем большинстве случаев, эта функция используется в паре с DDX_Text для тех полей ввода, в которых задаются не строковые, а числовые значения.

Существует еще несколько аналогичных функций, предназначенных для проверки численных значений различных типов на принадлежность заданному диапазону; их имена вместе с именами типов проверяемых величин приведены в таблице ниже:

Имя функции Тип величины
DDV_MinMaxDWord DWORD
DDV_MinMaxInt int
DDV_MinMaxLong long
DDV_MinMaxUnsigned unsigned
DDV_MinMaxFloat float
DDV_MinMaxDouble double

Как и DDV_MinMaxByte, каждая из этих функций используется в паре с DDX_Text для проверки корректности содержимого полей ввода.

void DDV_MinMaxDateTime(
   CDataExchange* pDX,
   CTime& timeValue,
   const CTime* pMinTime,
   const CTime* pMaxTime
);

Предназначена для проверки даты/времени timeValue на принадлежность заданному диапазону, нижняя граница которого определяется объектом класса CTime, адресуемым параметром pMinTime, а верхняя — параметром pMaxTime. Эта функция используется в паре с DDX_DateTimeCtrl применительно к элементам управления Date and Time Picker.

void DDV_MinMaxDateTime(
   CDataExchange* pDX,
   COleDateTime& odtValue,
   const COleDateTime* pMinTime,
   const COleDateTime* pMaxTime
);

Данный вариант функции полностью аналогичен предыдущему, за исключением того, что оперирует величинами типа COleDateTime.

void DDV_MinMaxMonth(
   CDataExchange* pDX,
   CTime& timeValue,
   const CTime* pMinTime,
   const CTime* pMaxTime
);

Как и DDV_MinMaxDateTime, эта функция проверяет указанную дату на принадлежность заданному диапазону; отличие состоит в том, что она используется в паре с DDX_MonthCalCtrl применительно к элементам управления Month Calendar.

void DDV_MinMaxMonth(
   CDataExchange* pDX,
   COleDateTime& odtValue,
   const COleDateTime* pMinTime,
   const COleDateTime* pMaxTime
);

Данный вариант функции полностью аналогичен предыдущему, за исключением того, что оперирует величинами типа COleDateTime.

void DDV_MinMaxSlider(
   CDataExchange* pDX,
   DOWRD dwValue,
   DOWRD dwMin,
   DOWRD dwMax
);

Предназначена для проверки того, находится ли текущая позиция ползунка, значение которой передается через параметр dwValue, в диапазоне от dwMin до dwMax. Эта функция используется в паре с DDX_Slider применительно к элементам управления Trackbar.

Если в процессе сохранения и проверки данных пользовательский ввод в одном из элементов управления оказывается некорректным, то этот процесс прерывается и после закрытия пользователем окна с сообщением об ошибке «виноватый» элемент управления получает фокус ввода. Заметим, что те же самые проверки выполняются и при инициализации элементов управления, однако в этом случае сообщения об ошибках (менее информативные) выводятся в окно отладчика и метод DoDataExchange продолжает свою работу обычным образом.

Тексты выдаваемых пользователю сообщений об ошибках ввода хранятся в ресурсах MFC и при желании могут быть переопределены. Например, сообщение о выходе целого числа за пределы требуемого диапазона имеет идентификатор AFX_IDP_PARSE_INT_RANGE и содержит текст «Please enter an integer between %1 and %2.», причем на место форматирующих последовательностей «%1» и «%2» будут автоматически подставлены нижняя и верхняя границы этого диапазона. Если приложение предоставляет свой собственный строковый ресурс с таким же идентификатором, то для формирования сообщения об ошибке будет использоваться именно он. Текст этой строки может быть произвольным, но он обязательно должен содержать те же самые форматирующие последовательности.

Еще раз подчеркнем, что проверка корректности пользовательского ввода производится после переноса данных из элемента управления в связанную с ним переменную; причем, если этот процесс будет прерван где-то на середине, часть связанных переменных получит новые значения, а часть будет по-прежнему сохранять старые. Именно по этой причине и рекомендуется связывать элементы управления только с полями класса-потомка CDialog, соответствующего тому диалоговому окну, которое содержит эти элементы — «внешняя сторона» будет находиться в корректном состоянии вне зависимости от того, допускает пользователь ошибки при вводе информации или нет:

void CSampleApp::OnChangeData(void)
{
   CSampleDialog dlgSample;
   ...   // копируем подлежащие изменению данные из полей объекта-приложения
   ...   // в соответствующие поля объекта-диалога
   if (dlgSample.DoModal() == IDOK)
   {
      // весь(!) пользовательский ввод корректен - можно копировать новые
      // данные обратно в поля объекта-приложения
      ...
   }
}
обновлено
29.03.2006
 
Проверка PR и ТИЦ