|
|
|||||||||||||||||||||||||||||
|
Проверка форм с помощью регулярных выражений в MFCИсточник: CodingClub Пол Дилация (Paul DiLascia)
На этот раз я решил воспользоваться своей колонкой, чтобы описать одно интересное приложение, которое я создал с применением библиотеки RegexWrap (о ней читайте мою статью в этом номере). RegexForm - это система проверки форм для MFC на основе регулярных выражений. Данное приложение было главной причиной, побудившей меня реализовать библиотеку RegexWrap. Но поскольку многие детали не относятся к самим регулярным выражениям, здесь имеет смысл обсудить RegexForm, а не RegexWrap. Одно из важнейших применений регулярных выражений на практике - проверка правильности пользовательского ввода. Регулярные выражения здорово упрощают проверку ZIP-кодов, телефонных номеров, номеров кредитных карт, т.е. всех видов информации, с которыми нам часто приходится иметь дело в жизни. Одно регулярное выражение может заменить десятки и даже сотни строк процедурного кода. Поддержка таких выражений была встроена в языки программирования под UNIX и Web вроде Perl с самого начала, но в Windows или MFC она появилась лишь с созданием .NET Framework (если не считать библиотек от сторонних поставщиков). Так что теперь, когда .NET предоставляет полную библиотеку регулярных выражений, почему бы не задействовать ее в MFC-приложениях? И благодаря моей библиотеке RegexWrap, о которой рассказывается в основной статье, вам не понадобятся даже Managed Extensions или /clr. В MFC уже есть механизм для проверки ввода в диалогах - Dialog Data Exchange (DDX) и Dialog Data Validation (DDV). С технической точки зрения, DDX передает данные между «экраном» и вашим объектом диалога, тогда как DDV проверяет эти данные. DDX начинает работать, когда вы вызываете UpdateData из обработчика OnOK своего диалога: // Пользователь нажал OK:void CMyDialog::OnOK() { UpdateData(TRUE); // получить данные диалога ...} UpdateData - виртуальная CWnd-функция, которую можно переопределить в диалоге. Ее булев аргумент сообщает, как копировать информацию - с экрана в объект диалога или наоборот. [Вы можете вызвать UpdateData(FALSE) из OnInitDialog для инициализации своего диалога.] Стандартная реализация в CWnd создает объект CDataExchange и передает его другой виртуальной функции, DoDataExchange, которую, как предполагается, вы переопределяете для вызова конкретных DDX-функций, чтобы передать данные индивидуальным полям данных (data members): void CMyDialog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Text(pDX, IDC_NAME, m_name); DDX_Text(pDX, IDC_AGE, m_age); ... // и т. д.} Здесь IDC_NAME и IDC_AGE - идентификаторы элементов управления «поле ввода» (edit controls), а m_name и m_age - поля данных типа CString и int соответственно. DDX_Text копирует то, что пользователь ввел в качестве Name и Age, в m_name и m_age (попутно перегруженная версия приводит содержимое Age к типу int). DDX-функциям известно, что делать, так как CDataExchange::m_bSaveAndValidate равно TRUE при копировании с экрана в диалог и FALSE в обратном случае. В MFC есть целый набор DDX-функций для всех видов данных и типов элементов управления. Например, у DDX_Text имеется минимум дюжина перегруженных версий для копирования и преобразования текстового ввода в различные типы данных вроде CString, int, double, COleCurrency и др. Есть даже DDX_Check для преобразования состояния флажка в целое значение и DDX_Radio, которая делает то же самое применительно к кнопкам-переключателям (radio buttons). DDX-функции передают данные, тогда как DDV-функции проверяют их. Скажем, чтобы ограничить имя пользователя 35 символами, вы могли бы написать: // В CMyDialog::DoDataExchangeDDX_Text(pDX, IDC_NAME, m_sName); // получить или // установить значениеDDV_MaxChars(pDX, m_sName, 35); // проверить А чтобы сузить допустимый возраст от 1 до 120 лет: // Поле m_age имеет тип intDDX_Text(pDX, IDC_AGE, m_age);DDV_MinMaxInt(pDX, m_age, 1, 120); DDV в отличие от DDX довольно примитивен. Репертуар проверок в MFC весьма ограничен. Вы можете указывать предельное число символов в текстовом поле и накладывать ограничения по минимальным и максимальным значениям различных типов. Это, конечно, хорошо, но как быть, если нужно проверять ZIP-коды или телефонные номера? Для этого в MFC нет ничего. Вы должны писать свои DDV-функции. Когда я в первом приближении реализовал проверку на основе регулярных выражений, от меня потребовалось написать лишь одну функцию наподобие: void DDV_Regex(CDataExchange* pDX, CString& val, LPCTSTR pszRegex){ if (pDX>m_bSaveAndValidate) { CMRegex r(pszRegex); if (!r.Match(val).Success()) { pDX>Fail(); // генерирует исключение } }} Это позволяет легко проверять ввод с применением регулярных выражений, например: // В CMyDialog::DoDataExchangeDDX_Text(pDX, IDC_ZIP, m_zip);DDV_Regex(pDX, m_zip,_T("^\d{5}(\d{4})?$")); Совсем неплохо для четырех строк кода. (Конечно, если у вас есть RegexWrap, иначе вам придется напрямую вызывать Framework-класс Regex, используя управляемые расширения.) DDV_Regex отлично работает в MFC-схеме DDX/DDV, но, начав добавлять очередные поля, я быстро обнаружил некоторые существенные недостатки DDX/DDV. Вот лишь один пример. Каждая DDV-функция выводит окно с сообщением об ошибке и генерирует исключение, если с полем что-то неладно, так что при наличии пяти неверно заполненных полей пользователь получит целых пять окон сообщений - это перебор! Кроме того, мне не хотелось «зашивать» регулярное выражение в вызов DDV. Для проверки нового поля нужно добавить очередную переменную-член и дополнительный код в DoDataExchange, которая очень скоро разбухнет так, что мало не покажется: DDX_Text(pDX, IDC_FOO,...);DDV_Mumble(pDX, ...)DDX_Text(pDX, IDC_BAR,...);DDV_Bletch(...)... // и т. д. для 14 строк Зачем писать процедурные инструкции для описания правил проверки, которые в принципе являются статическими? Одна из пяти моих заповедей программирования - избегай процедурного кода. А другая: одна таблица лучше тысячи строк кода. Несомненно, вы догадались, к чему я клоню. В итоге я написал свою систему проверки полей в диалогах; она основана на правилах, а значит, опирается на таблицу. Моя система работает поверх DDX, но не обращается к DDV и имеет куда более дружелюбный UI. Она проста в использовании и, конечно, выполняет проверку через регулярные выражения. Все это инкапсулировано в класс CRegexForm, применимый в любом MFC-диалоге. Естественно, я написал и тестовую программу, демонстрирующую, как работает этот класс. На первый взгляд TestForm выглядит тривиальным MFC-приложением на основе диалога. В его главном окне несколько полей ввода: Zip Code, SSN (Social Security number) (номер карточки социального страхования), Phone Number и др. Но стоит начать работу с TestForm, как тут же осознаешь, что под капотом этой программы скрыто куда больше, чем кажется. Если вы перейдете в поле ввода клавишей Tab, TestForm покажет подсказку, где описывается, что именно вы можете ввести в это поле (рис. 1). Когда вы вводите недопустимый символ (например букву в поле Phone Number), TestForm отбрасывает этот символ и подает звуковой сигнал. Нажав клавишу Enter или кнопку OK, вы получите сообщение (наподобие показанного на рис. 2) с описанием всех полей с недопустимыми значениями. Все ошибки появляются в одном окне сообщения, а не каждая в отдельном. После этого, когда пользователь перейдет в одно из таких полей, TestForm вновь выведет сообщение об ошибке - теперь уже только о той, которая связана с данным полем (рис. 3), - поэтому пользователям не нужно запоминать, что говорилось в первоначальном сообщении; программа сама напоминает о конкретной ошибке по мере исправления полей с неправильными значениями. Если неверно значение лишь одного поля, TestForm пропускает первое окно сообщения. Рис. 1. Подсказки TestForm Рис. 2. Множество неверно заполненных полей Рис. 3. Петрек? Не согласен! Всю эту магию CRegexForm берет на себя. Вам остается лишь использовать ее, а это несложно. Во-первых, вы должны определить свою форму. Вот как это делается в TestForm (в файле MainDlg.cpp): // Карта формы/полейBEGIN_REGEX_FORM(MyRegexForm) RGXFIELD(IDC_ZIP,RGXF_REQUIRED,0) RGXFIELD(IDC_SSN,0,0) RGXFIELD(IDC_PHONE,0,0) RGXFIELD(IDC_TOKEN,0,0) RGXFIELD(IDC_PRIME,RGXF_CALLBACK,0) RGXFIELD(IDC_FAVCOL,0,CMRegex::IgnoreCase)END_REGEX_FORM() Макросы определяют статическую таблицу, которая описывает каждое поле ввода. В большинстве случаев вам хватит идентификатора элемента управления, но я предусмотрел место для флагов и RegexOptions. Например, в TestForm поле Zip Code является обязательным (RGXF_REQUIRED), поле Prime Number использует обратный вызов (об этом - чуть позже), а для Favorite Columnist (IDC_FAVCOL) указана CMRegex::IgnoreCase, которая делает это поле нечувствительным к регистру букв. Глядя на таблицу, у вас может возникнуть вопрос, а где же сами регулярные выражения? Отвечаю: в файле ресурсов. Для каждого идентификатора поля или элемента управления CRegexForm ожидает передачи ресурсной строки с тем же идентификатором. Ресурс-ная строка состоит из пяти подстрок, разделенных символом «новая строка» (« »). Общий формат таков: «Name Regex LegalChars Hint ErrMsg». Вот строка для IDC_ZIP: "Zip Code ^\d{5}(\d{4})?$ [\d] ##### or #########" Первая подстрока, «Zip Code», - это имя поля. Вторая - «^d{5}(-d{4})?$» - регулярное выражение для проверки поля Zip Code. (В ресурсной строке надо набирать два обратных слэша, чтобы указать обратный слэш регулярного выражения.) Третья подстрока является еще одним регулярным выражением, описывающим допустимые символы. Для Zip Code это «[d-]», разрешающее цифры и дефис. Если ваше поле не накладывает ограничений на символы, можете опустить LegalChars, набрав « », что указывает на пустую подстроку. Четвертая подстрока - символы формата (#), выводимые в подсказке. Наконец, можно предоставить пятую подстроку - сообщение об ошибке, отображаемое, когда поле содержит недопустимое значение. Для поля Zip Code сообщения об ошибке нет, поэтому CRegexForm формирует таковое в виде «Should be xxx», где xxx заменяется подсказкой. «Should be» - другая ресурсная строка (подробности чуть позже). Из всех подстрок обязательной является только первая (имя поля). Зачем хранить всю эту информацию в ресурсных строках вместо того, чтобы закодировать ее прямо в карте полей? Одна из причин такова: если бы вся эта информация была в карте, код стал бы слишком громоздким. Гораздо аккуратнее вынести подобные строки из кода. И поскольку у макросов не может быть необязательных параметров, вам понадобилось бы по несколько макросов вроде RGXFIELD3, RGXFIELD4 и RGXFIELD5 в зависимости от того, сколько аргументов вы хотели бы использовать. Как вам такая перспектива? Но самая важная причина для переноса информации в ресурсные строки - стремление упростить локализацию. Переводчики могут переводить строки и создавать ресурсные DLL для разных языков. Даже регулярные выражения сами по себе могут потребовать перевода (ZIP-коды выглядят иначе в других странах, например в Великобритании или Ботсване), поэтому они тоже выносятся в файл ресурсов. Пока я с вами, позвольте мне заметить, насколько легко разбирать эти подстроки с помощью регулярных выражений. В MFC есть 26-строчная функция AfxExtractSubString для разбора подстрок документа (document substrings), а CRegexForm, используя CMRegex, делает это одной строкой кода! CString str;str.LoadString(nID);vector<CString> substrs = CMRegex::Split(str, _T(" ")); Теперь substrs[i] является i-той подстрокой, и, если вас интересует, сколько их всего, просто вызовите substrs.size(). Я определенно рад, что создал оболочку функции Split для возврата STL-вектора. Определив карту полей с использованием BEGIN/END_REGEX_FORM и составив ресурсные строки, вы должны создать экземпляр CRegexForm в своем диалоге и инициализировать его: // В OnInitDialogm_rgxForm.Init(MyRegexForm, this, IDS_MYREGEXFORM, MYWM_RGXFORM_MESSAGE); Естественно, CRegexForm требуются карта полей и указатель на ваш диалог; второй и третий аргументы - это еще одна ресурсная строка и идентификатор сообщения обратного вызова (callback message ID). Как и строки индивидуальных полей, инициализирующая строка состоит из нескольких подстрок, разделенных символами « ». В TestForm IDS_MYREGEXFORM выглядит так: «Error: %s Required Should be: %s Bad Value». Первая подстрока - «Error: %s» - префикс ошибки. CRegexForm использует ее для вывода «Error: xxx», где xxx - сообщение об ошибке. Вторая подстрока - «Required» - слово или фраза, используемая, когда поле является обязательным (RGXF_REQUIRED). Третью подстроку, «Should be: %s», я, собственно, уже описывал. С ее помощью CRegexForm генерирует сообщение об ошибке «Should be: xxx», где xxx - подсказка по данному полю. Последняя подстрока, «Bad Value», является универсальным сообщением об ошибке, которым CRegexForm пользуется в том случае, если для поля не задано ни подсказки, ни конкретного сообщения об ошибке. Пользователи ни в коем случае не должны увидеть это сообщение, потому что вы не забудете определить подсказку или конкретное сообщение об ошибке для каждого поля, правда? Последний аргумент для Init - MYWM_RGX-FORM_MESSAGE - определенный в приложении идентификатор сообщения обратного вызова, который позволяет CRegexForm взаимодействовать с вашим приложением и выполнять нестандартные проверки, требующие процедурного кода. Если вам нужно использовать математические алгоритмы или проверять фазы луны при контроле ввода, установите RGXF_CALLBACK в поле флагов, и CRegexForm будет посылать вашему диалогу сообщение обратного вызова (callback message) с кодом уведомления RGXNM_VALIDATEFIELD, когда наступит время для соответствующей проверки. TestForm использует обратный вызов для проверки поля Prime Number; все детали показаны на рис. 4. CRegexForm работает с DDX, используя собственные внутренние объекты CString, поэтому вам не надо определять член диалога для каждого текстового поля. Вам остается лишь вызвать CRegexForm для передачи данных: void CMyDialog::DoDataExchange(CDataExchange* pDX){ CDialog::DoDataExchange(pDX); m_rgxForm.DoDataExchange(pDX);} При инициализации CRegexForm создает массив защищенных структур FLDINFO - по одной на каждое поле в вашей карте. Одним из членов FLDINFO является FLDINFO::val - объект CString, в котором хранится текущее значение поля. Внутренне CRegexForm использует DDX_Text с этим CString. Чтобы получить или установить значения внутренних полей, вызывайте CRegexForm::GetFieldValue или SetFieldValue соответ-ственно; обе функции идентифицируют поле по идентификатору элемента управления: m_rgxForm.SetFieldValue(IDC_ZIP,_T("10025")); CRegexForm интерпретирует все значения как текст и хранит их в объектах CString, но предоставляет методы GetFieldValInt и GetFieldValDouble, позволяющие получить значение как int или double. Для остальных типов вам придется самостоятельно выполнять преобразования или пользоваться DDX-функциями MFC в DoDataExchange. В TestForm есть кнопка Populate, обработчик которой вызывает CRegexForm::SetFieldValue для заполнения формы образцами данных, показанных на рис. 3. В целом, CRegexForm для идентификации полей использует идентификаторы элементов управления. У него есть методы GetFieldName, GetFieldHint и GetFieldError, которые возвращают имя поля, подсказку и код ошибки, - все они принимают в качестве параметра идентификатор элемента управления. Я продемонстрировал вам, как создать карту полей, подготовить ресурсные строки, инициализировать CRegexForm и подключить его через DDX. Остается лишь обсудить, как происходит проверка пользовательского ввода. Это делается при нажатии кнопки OK: void CMyDialog::OnOK(){ UpdateData(TRUE); // копирование "экран">диалог int nBad = m_rgxForm.Validate(); if (nBad>0) { m_badFields = m_rgxForm.GetBadFields(); ... }} UpdateData обращается к MFC-механизму DDX, который вызывает DoDataExchange вашего диалога. В свою очередь DoDataExchange вызывает CRegexForm::DoDataExchange, который копирует пользовательский ввод в свои внутренние структуры FLDINFO. Далее CRegexForm::Validate проходит по полям, вызывая CMRegex::Match для проверки каждого из них на соответствие его регулярному выражению. Если значение поля недопустимо, CRegexForm устанавливает код ошибки RGXERR_NOMATCH в структуре FLDINFO (или код RGXERR_MISSING, когда обязательное для заполнения поле оказывается пустым). Validate возвращает число полей, значения которых недопустимы. Если таковые поля есть, вы можете вызвать CRegexForm::GetBadFields, чтобы получить массив (STL-вектор) идентификаторов этих полей. Далее вы перебираете массив и получаете код ошибки и сообщение об этой ошибке. Именно это делает CMainDlg в TestForm, создавая окно сообщения наподобие показанного на рис. 2. Если недопустимо значение лишь одного поля, CMainDlg вызывает CRegexForm::ShowBadField, чтобы выделить это поле и вывести сообщение об ошибке, как на рис. 3. Если значения всех полей допустимы, TestForm показывает окно сообщения со списком введенных значений (рис. 5). В реальном приложении нужно было бы скопировать эти значения в место их назначения. Полный исходный код CMainDlg::OnOK приведен на рис. 6. Отделяя проверку данных от обмена ими, CRegexForm дает больший контроль над вашим UI и позволяет избежать появления «зашитых» в MFC сообщений об ошибках. Рис. 5. Введенные данные Я упомянул итоговое окно. Оно полностью обрабатывается классом CRegexForm; вам нужно лишь вызвать CRegexForm::SetFeedBackWindow. Для сообщений об ошибках можно выбрать подходящий цвет. CRegexForm также берет на себя подсказки. По умолчанию он выводит подсказку по полю всякий раз, когда пользователь переходит в новое поле нажатием клавиши Tab (рис. 1). Для отключения подсказок вызовите CRegexForm::SetShowHints(FALSE). А чтобы вновь включить их, вызовите SetShowHints(TRUE, nDelay, nTimeout), где nDelay - задержка перед выводом подсказки в миллисекундах (по умолчанию - 250), а nTimeout задает, сколько времени в миллисекундах подсказка отображается на экране (по умолчанию - 0, т. е. всегда). CRegexForm автоматически убирает подсказку, когда пользователь переключается на другой элемент управления (EN_KILLFOCUS). В TestForm используется функция SetShowHints, с помощью которой реализован флажок, включающий или отключающий подсказки (рис. 1). Есть еще одна функциональность, по поводу которой я долго сомневался, стоит ли о ней упоминать. CRegexForm поддерживает немедленную проверку. Я не советую пользоваться этой возможностью, так как на мой взгляд это портит GUI, однако бывают ситуации, когда нельзя давать пользователю переключаться на другое поле, пока в предыдущее не введена допустимая информация. На этот случай предусмотрен флаг RGXF_IMMED, или же вы можете вызвать SetValidateImmed для немедленной проверки всех полей. В TestForm имеется флажок, который включает или отключает функцию немедленной проверки. Включив его, вы сами убедитесь, почему немедленная проверка - не лучшая идея. А что у нас там с ограничениями по числу символов и по минимальным/максимальным значениям (min/max)? Проверка min/max - как раз то, что регулярные выражения делать не позволяют. И хотя «.{0,35}» - регулярное выражение, описывающее все строки, длина которых не может превышать 35 символов, на самом деле вам нужен EM_LIMITTEXT, чтобы ограничить длину текста в поле ввода и чтобы этот элемент управления подавал звуковой сигнал, когда пользователь набирает в нем слишком много символов. Поскольку я терпеть не могу, когда в системе проверки форм недостает какой-то функциональности, уже поддерживаемой MFC, я ввел концепцию «псевдорегулярных выражений». Например, регулярное выражение для IDC_AGE (поля Age) - «rgx:min-max:int:1:120,maxchars:3». Очевидно, что это не истинно регулярное, а псевдорегулярное выражение, которое CRegexForm распознает и интерпретирует. Универсальный формат выглядит так: «rgx:expr,expr,…ex-pr», где каждое выражение описывает свое ограничение. На данный момент поддерживаются только два выражения: «minmax:type:minval:maxval» (где type - тип int или double) и «maxchars:maxval». CRegexForm особым образом разбирает эти выражения и использует EM_LIMITTEXT для maxchars - так же, как DDV_MinMaxInt. Детали см. в полном исходном коде, который можно скачать с сайта MSDN Magazine. Как и в случае ресурсных строк, регулярные выражения резко упрощают разбор и этих «псевдовыражений». Я показал вам все, что умеет делать CRegexForm. А как он это делает? На полное описание у меня не хватает места, но я обрисую картину в целом. CRegexForm использует мой CSubclassWnd для создания подкласса вашего диалога. Для тех, кто не знает, замечу, что CSubclassWnd - это класс, который был написан мной давным-давно и который с помощью механизма создания подклассов в Windows перехватывает сообщения, адресованные другому окну. Самое интересное в CSubclassWnd заключается в том, что он позволяет создать подкласс MFC-окна, не вставляя новый класс в вашу иерархию. Я мог бы сделать CRegexForm производным от CDialog, но тогда вам пришлось бы наследовать свой диалог от CRegexForm. И что бы вы тогда делали, если бы уже реализовали собственный CBetterDialog, производный от CDialog? Тогда вам понадобилось бы как-то вкраивать мой класс CRegexForm, и еще неизвестно, чем бы это кончилось. Это один из крупных недостатков MFC: она использует наследование в реализации поддержки подклассов, из-за чего иерархия классов точно отражает систему подклассов в Windows. Но никакой нужды в этом нет, и по большей части предпочтительнее писать подключаемые (plug-in) классы наподобие CRegexForm, который создает подкласс вашего диалога, не заставляя вас перекраивать всю иерархию классов. Для меня CSubclassWnd стал настолько незаменим, что без него я просто не могу программировать! CRegexForm с помощью CSubclassWnd перехватывает EN_KILLFOCUS и EN_SETFOCUS для скрытия и вывода подсказок, а также EN_CHANGE для очистки состояния ошибки поля, как только пользователь вводит что-то еще. Подсказки реализованы в самом CRegexForm на основе моего класса CPopupText, впервые описанного в колонке за сентябрь 2000 г. (msdn.microsoft.com/msdnmag/issues/0900/c). Чтобы предотвратить ввод недопустимых символов, CRegexForm устанавливает другую производную от CSubclassWnd ловушку для каждого поля ввода, для которого задано регулярное выражение LegalChars. Этот вложенный класс, CRegexForm::CEditHook, перехватывает сообщения WM_CHAR, посылаемые полю ввода, и «глотает» любые недопустимые символы с подачей звукового сигнала, вызывая MessageBeep. Все детали см. в полном исходном коде RegexWrap для другой моей статьи в этом номере.
|
|