Теперь, когда вы научились управлять формой дерева, мы продолжим развитие приложения. Используя клавишу Delete, удалите все ресурсы типа Bitmap. Удалите также глобальное объявление структуры TVINSERTSTRUCT. Теперь мы покажем, что можно обходиться и без ее помощи. Уберите весь учебный код, следующий после строки m_plmgList = new CImageList, и вставьте новый, так, чтобы функция приобрела вид:
void CLeftView::OnInitialUpdate()
{
CTreeView::OnInitialUpdate();
::SetWindowLongPtr(m_Tree.m_hWnd, GWL_STYLE, GetWindowLong(m_Tree.m_hWnd, GWL_STYLE)|TVS_HASLINES I TVS_HASBUTTONSITVS_LINESATROOT|TVS_SHOWSELALWAYS);
//====== Создаем новый список изображений
m_pImgList = new CImageList;
//====== Связываем его с системным списком изображений
GetSvsImqList () ;
//====== Получаем имена логических дисков
char s [1024] ;
DWORD size = ::GetLogicalDriveStrings (1024, s);
if (Isize) // В случае отказа
return; // уходим молча
//=== Сканируем текст и вставляем новые узлы дерева
for (char *pName = s; *pNarae; pName += strlen(pName)+1)
Addltem (TVI_ROOT, pName);
}
Функция GetSysimgList, которую мы создадим чуть позже, получает от системы список системных значков и связывает его с деревом. Начать показ файлового дерева мы решили с демонстрации всех логических дисков, имеющихся в операционной системе в данный момент. API-функция GetLogicalDriveStrings заполняет строку текста, в которую она помещает перечень всех присутствующих в операционной системе логических дисков. Строка имеет особый формат: она состоит из нескольких подстрок, завершающихся нулем, например:
a:\0c:\0d:\00
Обратите внимание на то, что признаком конца перечня являются два нулевых байта. Первый завершает подстроку, а второй — всю строку. Используя эту особенность, мы создали цикл for (), в котором подстроки — имена логических дисков, сначала выявляются, а затем используются для вставки в дерево узлов, соответствующих логическим дискам. Функция Addltem, которую создадим позже, определяет индекс значка, соответствующего вставляемой сущности (диск, папка или файл), и создает в дереве новый узел с соответствующим ему изображением.
Теперь займемся созданием вспомогательных функций, которые понадобились при разработке функции OninitialUpdate. Введите в файл LeftView.cpp реализацию функции GetSysimgList, объявление которой уже существует в файле интерфейса LeftView.h класса CLef tview:
void CLeftView::GetSysImgList()
{
SHFILEINFO info;
// Попытка получить описатель системного списка значков
HIMAGELIST hlmg = (HIMAGELIST)
::SHGetFilelnfо("С:\\",0, Sinfo,sizeof (info), SHGFI_SYSICONINDEX | SHGFI_SMALLICON);
//=== Приписываем описатель системного списка
//=== изображений объекту CImageList
if (Ihlmg || !m_pImgList->Attach(hlmg))
{
MessageBox(0,"He могу получить System Image List!");
return; }
//=== Связывание списка с элементом управления деревом
m_Tree.SetlmageList(m_pImgList, TVSIL_NORMAL);
}
Функция SHGetFilelnfo позволяет получить информацию о каком-либо объекте файловой системы. Последний параметр уточняет смысл вопроса. Определяем его с помощью битовых констант SHGFI_SYSICONINDEX и SHGFI_SMALLICON, которые означают, что мы интересуемся индексами значков в системном списке и нам нужны маленькие значки. Вы помните, что Windows поддерживает значки двух типов: большие (32x32) и маленькие (16x16). Результатом вызова функции будет описатель (handle) всего списка значков, который мы затем должны связать с элементом m_Tree. Но сначала требуется прикрепить (attach) Windows-описатель списка к объекту класса CimageList, адрес которого мы храним в переменной m_pImgList.
Понятие прикрепить описатель (attach a handle) вы будете встречать достаточно часто, программируя в рамках MFC, но значительно реже, чем разработчики, базирующиеся на платформе SDK (Software Development Kit), которые не пользуются классами MFC. Вместо этого они используют многочисленные структуры и прямо вызывают функции API из программы на языке С или C++. При этом им иногда приходится писать в 5-10 раз больше кода. Итак, понятие прикрепить описатель означает примерно следующее: дать объекту класса ту функциональность, которой обладает Windows-объект, обычно описываемый структурой и адресуемый с помощью описателя (handle). Внутри многих классов MFC скрыто существуют Windows-описатели, которые должны быть правильно инициализированы. Часто, но не всегда, это делается без нашего участия. Иногда мы должны предпринять какие-то действия для инициализации описателя. В данном случае это можно сделать прямым присвоением, например m_pimgList->m_hImageList = himg; но такой способ менее надежен, так как в нем непосредственно запоминается какой-то адрес памяти. Содержимое по этому адресу система может изменить в результате наших же манипуляций с объектами, и тогда мы получим проблему под названием «Irreproducible Bug» (невоспроизводимая ошибка). Точнее будет сказать трудновоспроизводимая ошибка — самый неприятный тип ошибок, для борьбы с которыми идут в ход все средства (даже AssertValid и Dump). Значительно надежнее использовать метод Attach класса CimageList, так как в этом случае система будет следить за перемещениями структур, адресуемых описателем. При этом работает класс CHandleMap и его метод SetPermanent, которые, к сожалению, не документированы.
Связывание списка с объектом m_Tree производит функция SetlmageList, последний параметр которой (TVSIL_NORMAL) говорит о том, что тип списка обычный, то есть состоит из двух изображений. Альтернативным выбором является TVSIL_STATE, справку о нем вы получите самостоятельно, если захотите. Поместите следующий код в файл LeftView.cpp. Он вставляет в дерево новый элемент с изображением, которое ему соответствует:
void CLeftView::AddItem (HTREEITEM h, LPCTSTR s)
{
SHFILEINFO Info;
int len = sizeof(Info);
//=== Добываем изображение (маленький значок)
::SHGetFileInfo (s, 0, SInfo, len, SHGFI_ICON
| SHGFI_SMALLICON); int id = Info.ilcon;
//=== Добываем изображение в выбранном состоянии
::SHGetFileInfo (s,0,Slnfo,len,
SHGFI_ICON | SHGFI_OPENICON | SHGFI_SMALLICON);
int idSel = Info.ilcon;
//====== Копируем параметр в рабочую строку
CString sName(s);
//=== Отсекаем лишние символы (сначала в конце строки)
if (sName.Right(1) == '\\')
sName.SetAt (sName.GetLength() - 1, '\0');
//====== Затем в начале строки
int iPos = sNarae.ReverseFind('\\') ;
if (iPos != -1)
sName = sNarne.Mid(iPos + 1) ;
//=== Вставляем узел в дерево
HTREEITEM hNew = m_Tree.InsertltemfsName,id,idSel,h);
//====== Вставляем пустой узел
if (NotErapty(s))
m_Tree.Insertltem("", 0, 0, hNew);
}
Функция SHGetFilelnf о вызывается дважды, так как от системы надо получить два индекса изображений: для объекта файловой системы в обычном состоянии и для него же в выбранном состоянии. Метод Insertltem класса CTreeCtrl вставляет узел в дерево. Его параметры задают:
Вставляемый в дерево логический диск надо проверить на наличие вложенных сущностей и вставить внутрь данного узла дерева хотя бы один элемент, когда диск не пуст. Если этого не сделаеть, то в дереве не будет присутствовать маркер (+), с помощью которого пользователь раскрывает узел.
При проверке диска (функция NotEmpty) мы не сканируем его далеко вглубь, а просто проверяем на наличие хотя бы одной папки. Если диск имеет хотя бы одну папку, то вставляем внутрь соответствующего ей узла пустой элемент (Insertltem ("", 0, 0, h)), который дает возможность впоследствии раскрыть (expand) данный узел. Затем, когда пользователь действительно его раскроет, мы обработаем это событие и удалим пустой элемент. Вместо него наполним раскрытую ветвь реальными сущностями. Этот прием обеспечивает постепенное наполнение дерева по сценарию, определяемому действиями пользователя.
Примечание 1
Примечание 1
Сначала я написал рекурсивную функцию анализа и заполнения всего файлового дерева при начальном запуске приложения. Оказалось, что эта процедура занимает 5-7 минут, в течение которых приложение выглядит мертвым. Правда, после нее дерево раскрывает свои ветви мгновенно, так как оно уже хранит информацию обо всех своих ветвях. В выбранном варианте работы с деревом вновь раскрываемые ветви вносят некоторую задержку, но после схлопывания (collapse) какой-либо ветви ее повторное раскрытие происходит быстро, так как информация уже имеется в дереве, точнее в элементе CTreeCtrl Другим вариантом решения проблемы является параллельное сканирование файлового дерева в другом потоке приложения.
Операция отсечения лишних символов нам понадобилась для того, чтобы из длинного файлового пути выделить только имя папки, которое должно появится в дереве справа от bitmap-изображения объекта — узла дерева. Мы решили показывать в дереве, в левом окне приложения, только папки. Файлы этих папок будут изображены в виде картинок в другом, правом, окне. Картинкой я называю содержимое документа в виде его чертежа — многоугольника (для простоты). Показывать будем только те файлы, которые соответствуют документам нашего приложения. Если вы помните, они должны иметь расширение mgn, как это было определено на этапе работы с мастером AppWizard.
При усечении строки неоходимо использовать знание структуры файлового пути и методы класса cstring. Сначала отсекаем символ ' \' справа от имени папки, затем все символы слева от него. Существует и другой способ, использующий функцию _splitpath, справку по которой я рекомендую получить самостоятельно. В настоящий момент развития приложения строка sName может содержать только одно из имен логических дисков и большая часть кода работает вхолостую, но чуть позже, когда мы будем иметь дело с длинными файловыми путями, он заработает полностью.
Для того чтобы оживить дерево в его начальном состоянии, осталось добавить код функции NotEmpty, которая проверяет текущий узел (файловый адрес папки) на наличие в нем вложенных папок и возвращает true в случае успеха и false, в случае если папка пуста или в ней присутствуют только файлы.
Примечание 2
Примечание 2
Здесь важно отметить, что даже в пустой или вновь созданной папке всегда присутствуют два объекта файловой системы. Это так называемые «точки», или каталоги с именами: "." и "..". Они, возможно, знакомы вам со времен использования команд DOS.
В библиотеке MFC имеется класс CFileFind, который умеет обнаруживать в папке любые объекты файловой системы. Если объекту такого класса, который обнаружил объект «точка», задать вопрос isDirectory (), то он ответит утвердительно. Тот же ответ будет получен и на другой вопрос isDots (). Другим объектам файловой системы, настоящим папкам и файлам, соответствуют другие ответы на эти же вопросы. Папки отвечают на первый вопрос утвердительно, а на второй отрицательно. Простым файлам нет смысла задавать второй вопрос, так как они отвечают отрицательно на первый. Для них актуален другой вопрос isHidden (), на который утвердительно отвечают файлы с Windows-атрибутом hidden. Его можно использовать для управления показом файлов. В случае если папка содержит только такие файлы, то мы будем считать, что она пуста. Если в папке есть и другие, то в их числе могут быть и mgn-файлы наших документов. В этом случае мы будем считать, что папка не пуста. С учетом сказанного строим алгоритм и функцию проверки файлового адреса:
bool CLeftView::NotErapty(CString s)
{
//====== Параметр s содержит текущий файловый путь
//====== Объект класса, умеющего искать нечто в папке
CFileFind cff;
//====== Дополняем путь маской *.* или \*.*
s += s.Right(l) == '\\' ? "*.*" : "\\*.*";
BOOL bFound = cff.FindFile(s);
//====== Цикл поиска настоящих объектов
while (bFound)
{
bFound = cff.FindNextFile(); //====== Это папка?
if (cff . IsDirectory () && ! cf f. IsDots () )
return true; //====== Это файл?
if (!cff.IsDirectory() SS !cff.IsHidden())
return true;
}
//====== He найдены объекты, достойные внимания
return false;
}
Отметьте, что цикл while не будет продолжительным, так как выход из него происходит при обнаружении первой же настоящей папки или файла. Запустите приложение, устраните возможные ошибки и убедитесь в том, что дерево с изображениями дисков действительно появляется в левом окне. При раскрытии узлов дерева, соответствующих «не пустым» дискам, появляется только одно изображение, которое определяется нулевым индексом системного списка (Рисунок 5.2). Вы помните, что в «непустые» узлы мы вставляли нулевые элементы. Рекомендуем с