MDI-додаток на Visual C погляд зсередини - Програмні продукти
У своїй статті я хочу розповісти про ключові моменти створення додатків з використанням мультидокументного інтерфейсу (Multiple Document Interface, MDI) бібліотеки Microsoft Foundation Classes (MFC) компанії Microsoft.
У багатьох підручниках з MFC створення MDI-додатків приділяється недостатньо уваги. Зазвичай обмежуються описом кроків його створення за допомогою AppWizard та прикладом роботи з текстовим документом: відкриття документа у кількох дочірніх вікнах, редагування та збереження.
Складніша справа, якщо створюється MDI проект, в якому дочірні вікна повинні відображати різні дані (наприклад, з таблиць баз даних) і водночас надавати можливість користувачеві вводити та коригувати дані. Ці вікна повинні мати набір GUI (Graphic User Interface) елементів: кнопки (Button), поля введення (EditBox), списки (ListBox) та ін.
Бібліотека MFC підтримує додатки двох принципово різних типів - з урахуванням однодокументного (Single Document Interface, SDI) і MDI інтерфейсу. У SDI-програми всього одне вікно і завантажити одночасно можна лише один документ. Додатково для роботи з документом можливе використання модальних та немодальних діалогів. Хорошим прикладом програми SDI є програма Notepad. На відміну від SDI, MDI додаток має кілька так званих дочірніх вікон, кожне з яких працює з окремим документом. Прикладом MDI програми може бути MS Word.
Головне вікно MDI програми нагадує робочий стіл, на якому розміщуються різні документи (дочірні вікна). Ці вікна з документами можна згорнути в значки всередині головного вікна, розмістити по-різному на «столі» (каскадом, мозаїкою та ін.).
Базове MDI додаток легкостворюється за допомогою AppWizard. При запуску останнього у діалоговому вікні AppWizard - Step 1 тип програми MDI встановлено за замовчуванням (рис. 1).

Мал. 1. Перше діалогове вікно AppWizard під час створення MFC програми.
Процес запуску SDI та MDI додатків багато в чому однаковий. Об'єкт-додаток похідного від CWinApp класу має перевизначену функцію-член InitInstance. Ця функція відрізняється від аналогічної функції SDI програми. Ця функція починається з виклику AddDocTemplate. Це виглядає так:
CMultiDocTemplate* pDocTemplate; pDocTemplate = новий CmultiDocTemplate( IDR_XXTYPE, RUNTIME_CLASS(CMyDoc), RUNTIME_CLASS(CChildFrame), RUNTIME_CL e);
У додатках SDI існує один клас вікна-рамки і один об'єкт цього класу і AppWizard генерує клас з ім'ям CMainFrame, похідний від CFrameWnd. У MDI додатку є два класи вікна-рамки і безліч об'єктів-рамок.
MDI програма має клас дочірнього вікна CСhildFrame і при кожному відкритті нового документа програма створює новий екземпляр цього класу для відображення документа всередині головного вікна.

Мал. 2. Взаємозв'язок вікна-рамки та вікна відображення в MDI додатку.
У SDI-програмі об'єкт CMainFrame обрамляє програму та містить об'єкт «вид». У MDI-програмі ці дві речі розділені: в InitInstance створюється об'єкт CMainFrame, а всередині об'єкта CChildFrame міститься вікно відображення. Код, що генерується AppWizard, має вигляд:
CMainFrame* pMainFrame = новий CMainFrame; if (!pMainFrame->LoadFrame(IDR_MAINFRAME)) return FALSE; m_pMainWnd = pMainFrame; pMainFrame->ShowWindow(m_nCmdShow); pMainFrame->UpdateWindow();
Елемент даних m_pMainWnd належить класуCWinApp. Функція InitInstance надає цьому елементу даних покажчик на основне вікно-рамку. Тому, якщо такий покажчик знадобиться, ми можемо отримати доступ до m_pMainWnd через глобальну функцію AfxGetApp.
У MDI-додатку існують два види основного меню: один вид використовується при відкритті лише основного вікна-рамки (без відкритих дочірніх вікон), другий – при відкритті хоча б одного дочірнього вікна. При цьому використовуються два окремих ресурси меню: IDR_MAINFRAME і IDR_TESTTYPE (де TEST є назвою програми). Ось як виглядають ці два ресурси з розбивкою на підрядки:
IDR_MAINFRAME "Test" // заголовок основного вікна програми IDR_TESTTYPE \n // заголовок вікна програми (з'являється в заголовку // дочірнього вікна) Test\n // це ім'я документа за умовчанням Test\n // ім'я типу документа Test Files (*.dat)\n // опис та фільтр для типу документа .dat\n // розширення для файлів документів цього типу >Test.Document\n // ідентифікатор типу файлу в реєстрі Test Document // опис типу файлу в реєстрі
Якщо подивитися на файл ресурсу Test.rc, ми побачимо, що ці підрядки об'єднані в один довгий рядок. Заголовок програми береться з ресурсу IDR_MAINFRAME і при відкритті документа до цього заголовка додається ім'я файлу цього документа.
Під час створення порожнього документа MDI-додаток викликає OnFileNew. Основне вікно-рамка вже створена, тому OnFileNew викликає функцію OpenDocumentFile класу CwinApp. При цьому відбувається таке:
- Функція OnFileNew створює об'єкт-документ, але поки що не завантажує його дані
- Створюється об'єкт класу CChildFrame для дочірнього вікна-рамки та формується вікно. Меню DR_MAINFRAME замінюється на меню IDR_TESTTYPE.
- Створюється об'єкт "вигляд" та формуєтьсявікно відображення (але це вікно поки що не відображається).
- Встановлюється зв'язок між переліченими вище об'єктами.
- Для об'єкта-документа викликається функція-член OnNewDocument.
- Для об'єкта "вигляд" викликається віртуальна функція-член OnInitialUpdate.
- Для об'єкта "дочірня рамка" викликається віртуальна функція-член ActivateFrame, щоб вивести на екран вікно-рамку та вікно відображення.
Розглянемо створення MDI-додатку практично. Запускаємо Microsoft Visual Studio та створюємо новий проект з ім'ям Test. Зі списку запропонованих типів проектів виберемо тип MFC AppWizard (exe). AppWizard запропонує вам пройти шість кроків для створення проекту. На першому кроці залишаємо за замовчуванням тип створюваної програми Multiple Documents, як зображено на рис. 1. На наступних чотирьох кроках залишаємо параметри за замовчуванням. На шостому етапі базовий клас CView змінимо на CFormView, як зображено на рис. 3. Натиснувши клавішу Finish, AppWizard автоматично створить каркас MDI-програми.

Мал. 3. Крок 6. Зміна базового класу програми CFormView.
Якщо ви не припускаєте відразу завантажувати який-небудь документ при запуску програми, тоді можна видалити ресурс меню IDR_MAINFRAME, а IDR_TESTTYPE перейменувати в IDR_MAINFRAME. У цьому випадку необхідно також додати наступний код до розділу ініціалізації класу CTestApp:
// Parse command line for standard shell commands, DDE, file open CCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo);
//Don't show a new MDI child window during startup if (cmdInfo.m_nShellCommand == CCommandLineInfo::FileNew) cmdInfo.m_nShellCommand = CCommandLineInfo::FileNothing; // Цим кодом ми повідомляємо програми не відкривати нове вікно при //старті програми. //Розташування повідомлень, спрямованих на лінію комбінації if (!ProcessShellCommand(cmdInfo)) return FALSE;
Змінимо властивості (Properties) пункту меню New на Test, ID_FILE_NEW на ID_OPEN_TEST, а Open ... на Data і ID_FILE_OPEN на ID_OPEN_DATA, як показано на рис. 4.

Мал. 4. Властивості нового пункту меню Test.
Додамо два діалогові ресурси (два дочірні вікна). Стиль цих вікон необхідно встановити, як показано на рис. 5.

Мал. 5. Властивості дочірніх діалогових вікон.
У списку ресурсів String Table необхідно створити два ідентифікатори: IDR_TESTOPEN та IDR_DATAOPEN. Зразкові властивості цих ресурсів показано на рис. 6.

Мал. 6. Властивості дочірніх діалогових вікон.
На створені два вікна можна помістити необхідні елементи керування (EditBox, ListBox, Radio Buttons, Check Buttons та ін.).
Тепер потрібно визначити класи для нових вікон. Для першого вікна це буде клас CTestClass, а для другого – CDataClass. Зверніть увагу, що за промовчанням ClassWizard пропонує базовий клас для діалогу CDialog. Наші вікна успадковані від базового класу CFormView.
void CTestApp::OnOpenTest() OpenNewDoc("TestOpen"); >
void CTestApp::OnOpenData() OpenNewDoc("DataOpen"); >
MDI-програма підтримує множинні шаблони документів. Для цього виконується виклик AddDocTemplate для кожного із шаблонів:
CMultiDocTemplate* pDocTemplateTest; pDocTemplateTest = New CMultiDocTemplate( IDR_TESTOPEN, RUNTIME_CLASS(CTestDoc), RUNTIME_CLASS(CChildFrame), // класокна> // клас дочірнього вікна AddDocTemplate(pDocTemplateTest);
CMultiDocTemplate* pDocTemplateData; pDocTemplateData = newCMultiDocTemplate( IDR_DATAOPEN, RUNTIME_CLASS(CTestDoc), RUNTIME_CLASS(CChildFrame), // клас вікна-рамки RUNTIME_CLASS(CDataOpen)); // клас дочірнього вікна AddDocTemplate(pDocTemplateData);
При викликі New з меню File каркас програм виводить на екран список шаблонів, надаючи можливість вибрати необхідний шаблон по імені, заданому в рядковому ресурсі (підрядок типу документа). Під час виконання програми об'єкт-документ зберігає список об'єктів (активних шаблонів документів). Перебирати цей список дозволяють функції-члени GetFirstDocTemplatePosition та GetNextDocTemplate класу CWinApp:
BOOL CTestApp::OpenNewDoc(const CString& strTarget) CString strDocName; CDocTemplate* pSelectedTemplate; POSITION pos = GetFirstDocTemplatePosition(); while (pos! CDocTemplate*) GetNextDocTemplate(pos); pSelectedTemplate->GetDocString(strDocName, CDocTemplate::docName); if (strDocName == strTarget) // вибирається з рядкового ресурсу шаблону pSelectedTemplate-& (NULL); return TRUE; > > return FALSE;
Завантаження та збереження документів в MDI-додатку здійснюється за аналогією з SDI, але з двома відмінностями: коли документ завантажується, з диска створюється новий об'єкт-документ, а при закритті останнього дочірнього вікна відображення документа - руйнується.
Запустивши програму, ми побачимо два активні підпункти меню з меню File. Вибравши їх, завантажаться два документи (дочірні вікна) зі своїм набором елементів керування та відображення.
На закінчення варто відзначити, що створення MDI-додатків не складає особливих труднощів. А в результаті отримуємо зручний у використанні додаток, що має сучасний зовнішній вигляд і відповідає встановленим світовимстандартів. Успіхів вам у програмуванні!