Поддержка нескольких расширений файлов в MFC-приложениях

Как создать MFC-приложение с поддержкой нескольких расширений файлов

Вступление

Я уже довольно много времени пишу приложения с использованием классов MFC. Это очень хорошая и удобная библиотека классов, плюс к ней добавлен MFC AppWizard, автоматически генерирующий шаблон приложения, куда остаётся вписать только ваш собственный код. И всё же у библиотеки MFC есть один очень существенный минус: если вам нужно сделать что-то, заранее в этой библиотеке не предусмотренное, то приходится тратить очень много времени и сил, изворачиваться, пытаясь вылезти из тех рамок, в которые нас запихнула MFC. Данная статья посвящена созданию приложений, которые работают с несколькими типами файлов.

C чего начнём? А начнём мы, как и следует, с точной и чёткой формулировки задачи. В этой статье мы будем создавать однооконное (SDI) приложение, с одним типом документа, с одним типом представления, но с поддержкой файлов нескольких расширений. Простым примером может служить текстовый редактор: скажем, файлы txt и log - они оба текстовые и должны обрабатываться и отображаться одинаково. Может быть и другой вариант: файлы содержат одни и те же данные, но в разных форматах... В общем, вариантов довольно много. Понятно, что у каждого могут быть свои требования, поэтому я постараюсь сделать задачу как можно более общей.

Итак, мы будем создавать программу, которая:

  1. Умеет открывать и сохранять файлы нескольких форматов, при этом в диалоге сохранения можно будет выбрать один из следующих типов:
    1. Все поддерживаемые типы (*.ex1; *.ex2; *.ex3)
    2. Первый тип (*.ex1)
    3. Второй тип (*.ex2)
    4. Третий тип (*.ex3)
    5. Все файлы (*.*)

    В диалоге сохранения будут все те же варианты, кроме самого первого.

  2. Регистрирует на себя открытие файлов с такими расширениями по двойному клику.
  3. По желанию, при регистрации создаёт или не создаёт запись в контекстном меню "Создать".
  4. Каждому из указанных трёх расширений сопоставляет свою иконку.

Приступаем к выполнению Как несложно догадаться, первым шагом будет создание нового приложения. Здесь надо сделать небольшое лирическое отступление. Дело в том, что решение поставленных задач будет немного отличаться в зависимости от того, в какой среде происходит создание проекта. Поэтому я буду вести параллельно две линии для систем Visual Studio 6.0 и Visual Studio .NET 2003, указывая различающиеся места.

Итак, запускаем AppWizard, называем наше приложение SDI. На первом шаге выбираем тип - Single document, ставим галочку Document/View architecture support, английский язык. Второй шаг: базы данных не нужны, ставим None. Третий: контейнеры не нужны, None. Четвёртый: Здесь ставим галочки по желанию, я оставил всё как есть по умолчанию. Нажимаем кнопку Advanced и вводим параметры нового приложения:

File extension:ex1
File type ID:SDI.Document
Main frame caption:SDI
Doc type name:SDIDoc
File new name (short name):Unnamed
Filter name:SDI Files (*.ex1)
File new name (long name):Unnamed file

Далее можно сразу жать Finish, более-менее важных настроек дальше нет. Компилируем наш чистый проект, и запускаем его. Можно ничего в программе не делать, регистрация файлов происходит автоматически при запуске.

Сейчас я хочу сделать ещё одно отступление, посвящённое более подробному разбору того, что мы только что вписывали. Здесь будет рассказано, как файлы регистрируются в системе, и что при этом пишется в реестр. Если кому-то уже всё это известно, следующий параграф можно пропустить.

Регистрация файлов в системе Я предполагаю здесь, что читатель уже знаком с тем, что такое реестр и как с ним работать. Если нет - этому посвящено довольно много литературы, поэтому рассказывать здесь основы я не буду.

Для того, чтобы Windows умела работать с файлом, задавать ему иконку, менять его контекстное мемню и т.д., необходимо завести в реестре идентификатор файла. Именно его мы указали в поле File type ID. Это по сути название нашего типа файла с точки зрения системы. Все типы файлов хранятся в реестре в ветви [HKEY_CLASSES_ROOT] под своим же названием. Давайте посмотрим в ветвь нашего нового типа файла: [HKEY_CLASSES_ROOT\SDI.Document]. В этой ветви параметр (По умолчанию) (в английских версиях Windows - (Default)) содержит название типа файла каким он предстаёт пользователю: откройте любую папку на диске, вызовите контекстное меню, и в нём подменю Создать: вы увидите среди прочих некий "Unnamed file" - ничего не напоминает? Именно это значение мы вписали в поле File new name (long name), и оно же находится в параметре (По умолчанию) ветви [HKEY_CLASSES_ROOT\SDI.Document].

Хорошо, идём дальше. Разворачиваем эту веточку и видим два подраздела: DefaultIcon и shell.Второй нам не нужен, а вот первый в том же параметре (По умолчанию) по умолчанию содержит иконку, сопоставленную данному типу файла.

Что ж, как описывается тип файла, мы разобрали. Осталось узнать ещё немного: как же тип файла связывается с расширением? А очень просто. В том же разделе реестра [HKEY_CLASSES_ROOT] перечислены все расширения - это разделы, начинающиеся с точки. Найдём там и наше расширение - .ex1 (точка важна!). В параметре (По умолчанию) этого раздела мы видим как раз идентификатор типа, к которому прикреплено данное расширение, а именно - наш SDI.Document.

И осталось указать на последнюю деталь, которая нам понадобится в будущем: это наличие подраздела ShellNew в разделе [HKEY_CLASSES_ROOT\.ex1]. Именно из-за наличия этого подраздела у нас и появляется новый тип файла в меню Создать. Попробуйте удалить или переименовать ShellNew, и вы увидите, что наш "Unnamed file" исчезнет из этого меню.

Продолжим Итак теперь мы знаем смысл трёх из семи заполненных полей. Остальные поля не имеют отношения к реестру, а касаются только самой программы. А именно: Main frame caption - это просто заголовок основного окна приложения; Filter name - фильтр, появляющийся в диалогах открытия и сохранения файлов; Doc type name - имя нашего типа с точки зрения программы (если бы у нас программа поддерживала несколько разных типов документов, то при создании нового выводился бы диалог с предложением выбрать тип создаваемого документа. Doc type name - это название, появляющееся в том списке. В нашем приложении его не будет); File new name (short name) - имя нового докумемнта по умолчанию (в многооконных приложениях при создании нового пустого документа его имя создаётся из этого параметра плюс порядковый номер).

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

Добавление нескольких расширений На данный момент программа поддерживает только одно расширение - EX1. Для того, чтобы добавить нужные нам EX2 и EX3, откроем строковые ресурсы нашей программы и посмотрим на строку с идентификатором IDR_MAINFRAME. Я здесь не буду досконально описывать формат этой строки - большая часть её - это те строки, о которых я уже рассказал, если что-то неясно - вперёд в MSDN! :) Нам сейчас важна следующая часть этой строки: \nSDI Files (*.ex1)\n.ex1\n. Изменим её следующим образом: \nSDI Files (*.ex1; *.ex2; *.ex3)\n.ex1;.ex2;.ex3\n, откомпилируем и запустим наше приложение. В окнах открытия и сохранения мы видим эти самые фильтры и они работают! Неужели так просто? Увы, не совсем... Посмотрите в реестр: наш тип файла связан с весьма любопытным расширением .ex1;.ex2;.ex3. Не слишком-то это похоже на правильную регистрацию нужных нам типов файлов...

И вот теперь начинаются танцы с бубном, которые должны привести нас к тому, что мы хотим получить.

Итак, у нас проблемы с регистрацией расширений. За регистрацию отвечает метод CWinApp::RegisterShellFileTypes(). Однако зайдя внутрь этого метода, мы видим, что единственное осмысленное действие, которое он выполняет, это вызов

m_pDocManager->RegisterShellFileTypes(bCompat);

А вот уже метод CDocManager::RegisterShellFileTypes(BOOL bCompat) как раз и выполняет всю работу по регистрации форматов. Значит, именно его нам надо переопределять.

Создаём новый класс CDocManagerEx, отнаследованный от CDocManager. Теперь в файле SDI.cpp добавляем строчку

#include "DocManagerEx.h"

а в методе InitInstance() сразу после строчки

LoadStdProfileSettings(); // Load standard INI file options (including MRU)

дописываем строчку

m_pDocManager = new CDocManagerEx;

Теперь нам нужно переопределить метод CDocManagerEx::RegisterShellFileTypes(BOOL bCompat). Самый простой способ сделать нормальную реализацию - это скопировать её из исходников MFC.

Внимание: в MFC 4.2 в этом методе есть небольшой глючок. В приаттаченном проекте он исправлен (ничего серьёзного, просто регистрация файлов происходила не совсем при тех условиях, что должна бы происходить, судя по логике кода). В MFC 7.1 эта ошибка исправлена.

В этом методе используются несколько констант и функций, которые в нашем файле не определены, поэтому их надо перенести из того же файла, откуда мы взяли вышеприведённый код (это файл docmgr.cpp). Эти определения находятся в самом начале этого файла. Для нашего кода нужны все константы и функция _AfxSetRegKey(...). Кроме того, нужно подключить файл afxpriv.h (я сделал это в файле stdafx.h), иначе код не будет компилироваться.

Начнём теперь разбираться с нашей RegisterShellFileTypes. Этот метод для каждого зарегестрированного в приложении шаблона документов (он у нас всего один) создаёт в реестре запись с соответствующим идентификатором, регистрирует открытие файлов на свой EXE-файл и устанавливает связь между расширением файла и типом. Если bCompat == TRUE, то попутно ещё регистрируются на тот же EXE-файл команды печати, добавляется запись об иконке для данного типа файлов и прописывается ключик ShellNew для появления нового типа файла в подменю Создать.

Для наших целей самым удобным вариантом будет создать отдельный метод для регистрации одного конкретного расширения. Объявим его как

bool RegisterSingleFileType(CDocTemplate* pTemplate, CString FilterExt, int IconNum, BOOL bCompat, BOOL bShellNew);

Здесь я ввёл следующие параметры:

  • pTemplate: шаблон документа
  • FilterExt: расширение файла (.ex1)
  • IconNum: номер иконки для этого расширения
  • bCompat: просто переносим этот параметр из RegisterShellFileTypes
  • bShellNew: флажок - надо ли создавать запись в меню Создать

В этот метод можно перенести чуть ли не всё содержимое метода RegisterShellFileTypes, нужно только убрать цикл, перебирающий все шаблоны документа, и добавить условие проверки флага bShellNew. Далее, вставляем там код, модифицирующий имя типа файла (это нужно, чтобы сделать разные иконки), ещё пара косметических добавлений, и метод готов.

Теперь осталось подправить RegisterShellFileTypes. Оставляем там цикл по шаблонам документов (на будущее, вдруг понадобится...), вставляем инкрементирующийся счётчик номера иконки и в этом цикле вызываем RegisterSingleFileType с нужными параметрами. Осталось только добавить ещё две иконки для нужных типов файлов, да решить для себя - нужно ли добавлять этот тип файлов в меню Создать. Я решил эту запись не добавлять. Если кому-то она очень нужна, я думаю, читатель с этой архисложной задачей справится самостоятельно ;-)

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

Сначала мы подправим ещё один метод - CSingleDocTemplate::MatchDocType(). В нашем варианте задачи это, в общем-то, необязательно. Метод MatchDocType() отвечает за определение типа открываемого файла. У нас тип один (не путать с расширением!), поэтому особой разницы не будет. Но если кто-то захочет модифицировать программу так, чтобы она поддерживала несколько типов документов, то этот метод будет полезен.

Для этого нам нужно создать свой класс, отнаследованный от CSingleDocTemplate. Назовём его CMyDocTemplate, пропишем использование конструктора базового класса и добавим метод

virtual Confidence MatchDocType(LPCTSTR lpszPathName, CDocument*& rpDocMatch);

Реализацию берём из исходников MFC и подправляем так, чтобы расширение файла сравнивалось не с шаблонной строкой вида ".ex1;.ex2;.ex3", а по отдельности с каждым из расширений.

Для того, чтобы проект компилился, необходимо объявить в файле MyDocTemplate.cpp функцию

BOOL AFXAPI AfxComparePath(LPCTSTR lpszPath1, LPCTSTR lpszPath2);

Далее, нужно добавить заголовочный файл shlwapi.h (лучше всего это сделать в файле stdafx.h) и подключить библиотеку shlwapi.lib. После всего этого идём в файлик SDI.cpp, пишем там в начале

#include "MyDocTemplate.h"

а в методе InitInstance() ищем строчки

CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CSDIDoc), RUNTIME_CLASS(CMainFrame), // main SDI frame window RUNTIME_CLASS(CSDIView)); AddDocTemplate(pDocTemplate);

и заменяем CSingleDocTemplate на CMyDocTemplate.

Так, отлично. Немножко передохнём, и снова в путь. Уже совсем чуть-чуть осталось.

За работу с диалогом выбора файла отвечает метод CDocManager::DoPromptFileName(). Класс CDocManagerEx у нас уже есть, поэтому прямо в нём переопределяем метод

virtual BOOL DoPromptFileName(CString& fileName, UINT nIDSTitle, DWORD lFlags, BOOL bOpenFileDialog, CDocTemplate* pTemplate);

В реализации, как обычно, взятой из исходного кода MFC, ищем то место, где добавляется фильтр *.* (это место помечено соответствующим комментарием), и дописываем перед этим наш код:

if (!bOpenFileDialog) // In "Save As..." dialog we must remove strFilter.Empty(); // the first filter - "SDI Files (*.ex1; *.ex2; *.ex3") strFilter += "SDI-1 Files (*.ex1)"; strFilter += (TCHAR)'\0'; strFilter += _T("*.ex1"); strFilter += (TCHAR)'\0'; strFilter += "SDI-2 Files (*.ex2)"; strFilter += (TCHAR)'\0'; strFilter += _T("*.ex2"); strFilter += (TCHAR)'\0'; strFilter += "SDI-3 Files (*.ex3)"; strFilter += (TCHAR)'\0'; strFilter += _T("*.ex3"); strFilter += (TCHAR)'\0';

Для того, чтобы этот метод работал, нам ещё необходимо добавить функцию _AfxAppendFilterSuffix(). Её определение находится в том же файле, что и реализация методов класса CDocManager. Копируем её оттуда и подправляем в соответствии с тем, что у нас сейчас в основном шаблоне несколько расширений. Кстати говоря, эту функцию нужно править только в MFC 4.2. MFC версии 7.1 уже содержит код, который правильно обрабатывает шаблоны с несколькими расширениями.

Ну и последний штрих - добавление кода, который сделает нам правильное расширение у безымянного файла:

CString strExt; if (pTemplate->GetDocString(strExt, CDocTemplate::filterExt) && !strExt.IsEmpty()) { ASSERT(strExt[0] == '.'); int pos = fileName.Find(strExt); if (pos != -1) fileName = fileName.Left(pos) + '.' + strDefault; }

Этот код добавляется почти в самом конце метода DoPromptFileName(), перед тем, как переменная fileName будет использована.

Результаты Ну вот и всё! Наконец-то наша программа готова. Самые нетерпеливые, я думаю, уже откомпилировали и запустили полученный проект, убедившись, что всё теперь работает именно так, как и намечалось (или не убедившись, что маловероятно, но, конечно, полностью не исключено). К этой статье присоединены два проекта - один для Visual Studio 6.0, второй - для Visual Studio .NET 2003. Различия там незначительны, но они есть.

Надеюсь, эта статья оказалась вам полезной. Если остались какие-то вопросы или найдены ошибки, то прошу - в комментарии или же лично, по электронной почте.

Ссылки на проекты: Проект для Visual Studio 6.0 (35 кб) Проект для Visual Studio .NET 2003 (56 кб)



Опубликовал admin
14 Апр, Среда 2004г.

Комментарии

Огромное спасибо, замечательная статья, только вот ссылочки на исходники уже битые Ж(




Программирование для чайников.