Перевірка форм з допомогою регулярних виразів в MFC, Різне, Програмування, статті

На цей раз я вирішив скористатися своєю колонкою, щоб описати одне цікаве додаток, який я створив із застосуванням бібліотеки 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 для іншої моєї статті в цьому номері.


Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*