Что можно сгрузить

Что можно посмотреть

FileOpenDlg

Тестовый пример FileOpenDlg представляет из себя проект Visual C++ версии 6.0 SP5.

FileOpenDlg позволяет демонстрировать четыре разновидности диалога открытия файлов Windows:

  1. Диалог открытия файла в стиле Проводника, созданный на основе стандартного шаблона FILEOPEN.DLG. Не содержит никакого кода, предназначен только для демонстрации стандартного шаблона.
  2. Стандартный диалог. Демонстрирует стандарный диалог, создаваемый функцией GetOpenFileName без какой-либо модификации.
  3. Стандартный диалог с подцепленной пустой процедурой ловушки.
  4. Измененный диалог. В режиме таблицы выводится нестандартное количество колонок с нестандартной информацией.

Вы можете посмотреть исходные тексты FileOpenDlg в архиве FileOpenDlg.zip.


Структура стандартного диалога открытия файла

Структура диалога

Чтобы продемонстрировать что представляет из себя стандартный диалог открытия файла Windows, я написал тестовый пример Что можно посмотреть, который Вы можете использовать.

По-видимому, в основу создания диалога положен ресурс, поставляемый вместе с Microsoft Visual C++ в файле FILEOPEN.DLG. Во всяком случае он описан как шаблон по умолчанию (default template) для диалогов Open и Save As в документе Platform SDK: Windows User Interface: Customizing Common Dialog Boxes: Custom Templates. Однако там же оговорено, что этот шаблон не используется для целей модификации упомянутых диалогов. Я включил этот ресурс в тестовый проект FileOpenDlg сделав единственное изменение - я сделал видимым большой элемент списка [9] в центре диалога (в шаблоне он по умолчанию скрыт). Создаваемый на основе этого ресурса диалог выглядит следующим образом:

Диалог открытия файла, созданный на основе ресурса из FILEOPEN.DLG

Рис 1. Диалог открытия файла, созданный на основе ресурса из FILEOPEN.DLG

В Platform SDK описаны идентификаторы (их символьные эквиваленты определены в файле DLGS.H) и назначение элементов управления диалога открытия файла в стиле "Проводника":

Таблица 1. Документированные идентификаторы элементов управления стандартного диалога

 | Идентификатор элемента | Описание
1 | chx1 | Флажок "Open as read-only"
2 | cmb1 | Выпадающий список с перечислением фильтров типа файла
3 | stc2 | Метка элемента cmb1 "Files of type:"
4 | cmb2 | Выпадающий список файловой системы, позволяющий пользователю выбирать диски и папки
5 | stc4 | Метка элемента cmb2 "Look in:"
6 | cmb13 | Выпадающий список, который содержит имя текущего файла, позволяет пользователю ввести имя открываемого файла и выбрать файл, который был открыт недавно. Используется в ранних приложениях стиля "Проводник" или приложениях Windows 2000. Сравнить с edt1
7 | edt1 | Элемент редактирования, который содержит имя текущего файла, позволяет пользователю ввести имя открываемого файла. Сравнить с cmb13
8 | stc3 | Метка элементов cmb13 или edt1 "File name:"
9 | lst1 | Список, отображающий содержимое текущей директории
10 | stc1 | Метка элемента lst1
11 | IDOK | Кнопка "Open"
12 | IDCANCEL | Кнопка "Cancel"
13 | pshHelp | Кнопка "Help"

Номера на Рис. 1 и последующих соответствуют номерам элементов управления в левой колонке этой таблицы. На Рис. 1 метка [10] не видна - она содержит пустую строку. Кроме того, отсутствует выпадающий список [6] - вместо него используется поле редактирования [7]. Элементом [9] является многоколоночный список который, как мы увидим ниже, на самом деле не используется.

Вот как выглядит иерархия окон этого диалога.

Структура окон диалога Рис. 1

Рис 2. Структура окон диалога Рис. 1

Стандартный диалог открытия файла Windows создается вызовом GetOpenFileName. Параметры диалога передаются в структуре OPENFILENAME. Это можно сделать, например, с помощью следующего кода:

    const int fn_buflen = MAX_PATH;
    TCHAR sFileName[fn_buflen] = _T("");
    OPENFILENAME ofn = { sizeof(OPENFILENAME) };
    ofn.hwndOwner = m_hWnd;
    ofn.hInstance = _Module.GetModuleInstance();
    ofn.lpstrFilter = _T("All Files *.*\0*.*\0");
    ofn.lpstrFile = sFileName;
    ofn.nMaxFile = fn_buflen;

    GetOpenFileName(&ofn);

и в этом случае выглядит следующим образом:

Стандартный диалог открытия файла

Рис 3. Стандартный диалог открытия файла

Иерархия окон для этого диалога выглядит так:

Структура окон диалога Рис. 3

Рис 4. Структура окон диалога Рис. 3

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

HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Comdlg32

может содержать значения

Имя значения | Значение | Описание
NoPlacesBar | 1 | Скрывает панель инструментов [+3]
NoFileMRU | 1 | Скрывает список недавно открытых файлов
NoBackButton | 1 | Скрывает кнопку "Назад"

Эти значения самоописательны. Единственное, что стоит пояснить - после установки значения NoFileMRU в диалоге Рис. 3 выпадающий список [6] замещается окном редактирования [7].

Кроме того, видимостью Places Bar можно управлять с помошью структуры OPENFILENAME, если Вы имеете соответствующую версию Platform SDK и установите _WIN32_WINNT равным 0x0500 или более. При этом могут, однако, возникнуть проблемы с другими версиями ОС, в т.ч. NT. Довольно замысловатая техника обхода этих проблем изложена в статье Paul DiLascia "Windows 2000 File Dialog Revisited".

Из остальных изменений перечислим следующие:

  • Кнопка "Help" скрыта. Вместо нее используется значек "?" в заголовке окна.
  • Добавлена панель инструментов [+2} на место скрытой метки [10].
  • Вместо элемента списка [9] (напомню, что он изначально является скрытым, я сделал его видимым искусственно) для отображения списка файлов предполагается использовать элемент ListView [+1]. Он, к тому же, не вставлен в диалог напрямую, а имеет своим предском окно класса SHELLDLL_DefView.

Поскольку нам придется заниматься модификацией отображения списка файлов, все что касается ListView [+1] и его окружения имеет особый интерес. К сожалению, информация о том, что представляет из себя SHELLDLL_DefView практически отсутствует. Кое-что можно узнать из двух статей Дино Эспозито (Dino Esposito. "Active Desktop: The Shell and Beyond" и "A Brand New Web Look for Your Folders"). По-видимому, это окно элемента управления Shell DefView с CLSD={1820fed0-473e-11d0-a96c-00c04fd705a2}.

Добавим пустую hook-процедуру

namespace empty {
UINT APIENTRY hookProc(HWND, UINT, WPARAM, LPARAM)
{
    return 0;
}
}; // namespace empty

с помошью кода

    const int fn_buflen = MAX_PATH;
    TCHAR sFileName[fn_buflen] = _T("");
    OPENFILENAME ofn = { sizeof(OPENFILENAME) };
    ofn.hwndOwner = m_hWnd;
    ofn.hInstance = _Module.GetModuleInstance();
    ofn.lpstrFilter = _T("All Files *.*\0*.*\0");
    ofn.lpstrFile = sFileName;
    ofn.nMaxFile = fn_buflen;
    ofn.Flags = OFN_EXPLORER | OFN_ENABLEHOOK;
    ofn.lpfnHook = empty::hookProc;

    GetOpenFileName(&ofn);

И снова диалог изменится:

Диалог открытия файла с пустой hook-процедурой

Рис 5. Диалог открытия файла с пустой hook-процедурой

Иерархия окон этого диалога выглядит следующим образом.

Структура окон диалога Рис. 5

Рис 6. Структура окон диалога Рис. 5

Добавление hook-процедуры приводит к следующим эфектам:

  • Окно Places Bar исчезает, а выпадающий список FileMRU замещается на окно редактирования.
  • Создается дочерний диалог [+4].

Дочерний диалог [+4] был создан несмотря на то, что не был указан какой-либо модифицированный шаблон диалога с помошью OPENFILENAME.lpTemplateName. Этот диалог служит окном, обслуживаемым hook-процедурой, и позволяющим передавать ей сообщения и оповещения.

Теперь можно перейти к исследованию поведения и модификации диалога.

См. далее:

  • Поведение стандартного диалога открытия файла
  • Модификация диалога
  • Что можно посмотреть


Поведение стандартного диалога открытия файла

Поведение диалога

Нашей задачей является изменние информации о файлах, предоставляемой стандартным элементом управления ListView [+1] диалога на Рис. 5 в режиме Report (Таблица). Переключение режимов ListView осуществляется с помошью выпадающего списка на панели иструментов [+2] диалога (в более ранних версиях этого диалога перевлючение осуществлялось с помошью отдельных кнопок для каждого режима: большие и малые значки, список, таблица).

Чтобы что-то сделать, нам нужен доступ к окну стандартного диалога и окну ListView. Для этого нужно, как и ранее, использовать hook-процедуру, но теперь уже не пустую:

namespace cust {
UINT APIENTRY hookProc(HWND hWnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    UINT uiRet = 0;
    switch (uiMsg)
    {
    case WM_NOTIFY:
        return onNotify(hWnd, wParam, lParam);
    }
    return uiRet;
}
}; // namespace cust

с помошью кода

    const int fn_buflen = MAX_PATH;
    TCHAR sFileName[fn_buflen] = _T("");
    OPENFILENAME ofn = { sizeof(OPENFILENAME) };
    ofn.hwndOwner = m_hWnd;
    ofn.hInstance = _Module.GetModuleInstance();
    ofn.lpstrFilter = _T("All Files *.*\0*.*\0");
    ofn.lpstrFile = sFileName;
    ofn.nMaxFile = fn_buflen;
    ofn.Flags = OFN_EXPLORER | OFN_ENABLEHOOK;
    ofn.lpfnHook = cust::hookProc;

    GetOpenFileName(&ofn);

Нас интересуют некоторые оповещения, посылаемые стандартным диалогом hook-процедуре:

namespace cust {
UINT APIENTRY onNotify(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
    UINT uiRet = 0;
    OFNOTIFY *pOfn = (OFNOTIFY *)lParam;
    switch (pOfn->hdr.code)
    {
    case CDN_INITDONE:
        return onInitDone(hWnd);
    case CDN_FOLDERCHANGE:
        return onFolderChange(hWnd);
    case CDN_SELCHANGE:
        return onSelChange(hWnd);
    ..........
    }
    return uiRet;
}
}; // namespace cust

Здесь hWnd - дочерний далог [+4] из Рис. 6. Зная его можно найти окно стандартного диалога с помошью GetParent(hWnd).

Что известно об элементе управления ListView [+1] из Рис. 6? Прежде всего, он не является составной частью шаблона стандартного диалога и его id отсутствует среди документированных идентификаторов - он создается динамически где-то в процессе инициализации диалога. Далее, он принадлежит оконному классу "SysListView32", является потомком окна клсса "SHELLDLL_DefView" и это единственные дочерние окна стандартного диалога, принадлежащие этим классам. Этого достаточно, чтобы найти окно ListView. Для этого служит функция:

HWND APIENTRY getListView(HWND hWnd)
{
    HWND hWndSHListView = FindWindowEx(hWnd, NULL, _T("SHELLDLL_DefView"), NULL);
    if (!hWndSHListView)
        return NULL;
    return FindWindowEx(hWndSHListView, NULL, _T("SysListView32"), NULL);
}

Первым оповещением CDN_*, которое получает hook-процедура, является CDN_INITDONE. В коде onNotify с ним связана функция onInitDone:

namespace cust {
UINT APIENTRY onInitDone(HWND hWnd)
{
    UINT uiRet = 0;
    ATLASSERT(!getListView(GetParent(hWnd)));
    ..........
    return uiRet;
}
}; // namespace cust

Предложение ATLASSERT в onInitDone удостоверяет, что в момент получения оповещения CDN_INITDONE элемент управления ListView еще не создан.

Следующее оповещение, получаемое hook-процедурой, является CDN_FOLDERCHANGE. В коде onNotify с ним связана функция onFolderChange:

namespace cust {
UINT APIENTRY onFolderChange(HWND hWnd)
{
    UINT uiRet = 0;

    // Get ListView control.
    HWND hWndLVprev = g_lv.Attach(getListView(GetParent(hWnd)));
    // Previous ListView Window is not the same as the current.
    ATLASSERT(HWND(g_lv) && hWndLVprev != HWND(g_lv));
    // Previous ListView window is destroyed.
    ATLASSERT(!hWndLVprev || !IsWindow(hWndLVprev));
    ..........
    return uiRet;
}
}; // namespace cust

Доступ к элементу управления ListView здесь осуществляется с помошью класса ovp::CListView, который инкапсулирует нужные методы этого элемента. Метод Attach подключает глобальный экземпляр этого класса g_lv к найденному объекту окна элемента.

Первое редложение ATLASSERT в этом фрагменте удостоверяет, что в момент получения оповещения CDN_FOLDERCHANGE элемент управления ListView уже существует. Таким образом, он создан в промежутке между оповещениями CDN_INITDONE и CDN_FOLDERCHANGE.
Оповещение CDN_FOLDERCHANGE посылается когда пользователь меняет просматриваемый каталог. При этом предыдущее окно всякий раз уничтожается и вместо него создается новое, так что hWndLVprev != HWND(g_lv).

Обычно за оповещением CDN_FOLDERCHANGE следует оповещение CDN_SELCHANGE. Оповещение CDN_SELCHANGE посылается только в том случае, если текущая директория содержит поддиректории или файлы, соответствующие установленному фильтру. Если, например, текущая директория пуста, опвещения CDN_SELCHANGE не последует.

Мы собираемся модифицировать представление информации в ListView в стиле Report (Таблица). Поэтому нам важно уметь обращаться со стилями представления ListView. В документации определены 4 таких стиля: LVS_ICON, LVS_SMALLICON, LVS_LIST и LVS_REPORT. Пользователь изменяет стили представления ListView c помошью выпадающего списка на панели инструментов [+2] (мы опускаем стиль Thumbnales, появившийся в современных версиях Windows. Это особый стиль и ведет себя иначе, чем основные стили).
Программно получать и устанавливать эти стили можно с помошью функций getViewStyle и setViewStyle класса ovp::CListView.

Если ListView создается впервые в данном диалоге, он получает стиль LVS_LIST, что удостоверивает следующий фрагмент:

namespace cust {
UINT APIENTRY onFolderChange(HWND hWnd)
{
    ..........
    // First ListView presentation style is always LVS_LIST,
    // but latest may be different.
    ATLASSERT(hWndLVprev || g_lv.getViewStyle() == LVS_LIST);
    ..........
}
}; // namespace cust

Последущие экземпляры ListView наследуют установленный пользователем стиль предыдущего экземпляра ListView (за исключением стиля Thumbnails).

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

Однако момент, когда добавляются колонки (видимые только в стиле LVS_REPORT), зависит от стиля ListView, в котором его получает обработчик onFolderChange, и от версии диалога.

  • В предшествующих версиях диалога колонки всегда добавлялись к ListView до получения оповещения CDN_FOLDERCHANGE независимо от стиля ListView.
  • В текущей версии диалога колонки добавляются до получения оповещения CDN_FOLDERCHANGE только в том случае, если ListView имеет стиль LVS_REPORT. В противном случае создание колонок откладывается до того момента, когда пользователь переключит ListView в этот стиль.

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

Теперь можно перейти к модификации диалога.

См. далее:

  • Модификация диалога
  • Что можно посмотреть


Модификация стандартного диалога открытия файла

Модификация диалога

На Рис. 5 в окне ListView отображены четыре колонки со свойствами файлов и директорий: имя, размер, тип и дата модификации. Нужно убрать три последних колонки и вместо них добавить другие, содержащие некоторую другую информацию. Чтобы не усложнять задачу, мы не будем извлекать информацию из файлов как это делал я при решении задачи, где я столкнулся с обсуждаемой здесь проблемой (там для этого приходилось осуществлять довольно сложную интерпретацию файлов определенного типа). Вместо этого мы создадим две новые колонки и назовем их Сol 1 и Col 2 и будем заполнять их строками "Col1 Item Text" и "Col2 Item Text".

Вся эта работа осуществляется в обработчике оповещения CDN_FOLDERCHANGE onFolderChange и в подключаемой процедуре окна ListView wpLvFunc:

namespace cust {

ovp::CListViewCommCtrl g_lv; // ListView Control Wrapper class.
WNDPROC g_wpOldLV = NULL;

LONG FAR PASCAL wpLvFunc(HWND hWnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    if (uiMsg == LVM_INSERTCOLUMNA || uiMsg == LVM_INSERTCOLUMNW)
    { // Skip all ListView Insert Column messages.
        return DefWindowProc(hWnd, uiMsg, wParam, lParam);
    }
    if (uiMsg == WM_DESTROY)
    { // Unsubclass ListView on window destuction.
        WNDPROC wpOldLV = g_wpOldLV;
        g_wpOldLV = NULL;
        SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)wpOldLV);
        return CallWindowProc(wpOldLV, hWnd, uiMsg, wParam, lParam);
    }
    return CallWindowProc(g_wpOldLV, hWnd, uiMsg, wParam, lParam);
}

UINT APIENTRY onFolderChange(HWND hWnd)
{
    ATLASSERT(!g_wpOldLV);
    UINT uiRet = 0;

    // Get ListView control.
    HWND hWndLVprev = g_lv.Attach(getListView(GetParent(hWnd)));
    // Previous ListView Window is not the same as the current.
    ATLASSERT(HWND(g_lv) && hWndLVprev != HWND(g_lv));
    // Previous ListView window is destroyed.
    ATLASSERT(!hWndLVprev || !IsWindow(hWndLVprev));
    // First ListView presentation style is always LVS_LIST,
    // but latest may be different.
    ATLASSERT(hWndLVprev || g_lv.getViewStyle() == LVS_LIST);

    int nColCount = g_lv.getColumnCount();
    if (nColCount > 0)
    {
        // Rename 0 column
        g_lv.renameColumn(0, TEXT("File Name"));

        // Remove columns but 0
        for (int iCol = nColCount - 1;iCol > 0;iCol--)
            g_lv.deleteColumn(iCol);

        nColCount = 1;
    }

    // Insert new ListView Columns.
    if (!nColCount)
        g_lv.insertColumn(0, 0, TEXT("File Name"));
    g_lv.insertColumn(1, 1, TEXT("Col 1"));
    g_lv.insertColumn(2, 2, TEXT("Col 2"));

    if (!nColCount)
        // Subclass ListView Control to restrict column addition
        g_wpOldLV = (WNDPROC)SetWindowLong(HWND(g_lv), GWL_WNDPROC, (DWORD)wpLvFunc);

    // Add values to new Columns
    int ind = -1;
    while (true)
    {
        ind = g_lv.getNextItem(ind);
        if (ind < 0)
             break;

        g_lv.setItemText(ind, 1, "Col1 Item Text");
        g_lv.setItemText(ind, 2, "Col2 Item Text");
    }

    // Set column widths
    LONG nlvStyle = g_lv.getViewStyle();
    if (nlvStyle != LVS_REPORT)
        g_lv.setViewStyle(LVS_REPORT);
    g_lv.setColumnWidth(0);
    g_lv.setColumnWidth(1);
    g_lv.setColumnWidth(2);
    if (nlvStyle != LVS_REPORT)
        g_lv.setViewStyle(nlvStyle);

    return uiRet;
}
}; // namespace cust

В начале этого обработчика объект окна ListView связывается с глобальным экземпляром класса ovp::CListView, который инкапсулирует методы элемента ListView. Последующие проверки ATLASSERT обсуждались в предыдущем разделе и имеют исключительно демонстрационное значение.

Метод getColumnCount позволяет получить текущее число колонок ListView. В ранних весиях диалога оно всегда было > 0, однако в современной версии это не так. Поэтому далее следует проверка, имеются ли вообще какие-то колонки, и, если да, первая из них переименовывается, а последующие удаляются. Этот код будет выполнен в том случае, если перед переходом в новую директорию предшествующий элемент ListView имел стиль LVS_REPORT, или в старых версиях диалога.

Следующая группа предложений добавляет нужные колонки. Далее, если к моменту вызова onFolderChange ListView уже имел колонки, нам не о чем беспокоиться - диалог не будет добавлять колонки до следующей смены директории. Если же это не так, нужно запретить возможное последующее добавление ненужных колонок. Это делается с помошью подклассирования окна ListView процедурой wpLvFunc. Эта процедура пропускает к исходной оконной процедуре ListView все сообщения, за исключением LVM_INSERTCOLUMNA и LVM_INSERTCOLUMNW, блокируя тем самым добавление колонок. Процедура самостоятельно отключается от окна при получении сообщения WM_DESTROY.

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

Последнее что нужно сделать - отрегулировать ширину колонок. Для этого необходимо временно переключиться в стиль Report элемента ListView, после чего можно установить какие-то подходящие значения. В нашем случае для установки ширины колонок использована константа LVSCW_AUTOSIZE_USEHEADER.

В результате получаем диалог следующего вида:

Модифицированный диалог открытия файла

Рис 7. Модифицированный диалог открытия файла

Этот диалог работает как с современной, так и со старыми версиями comdlg32.dll багодаря проверкам числа колонок в обработчике onFolderChange.

См. далее:

  • Что можно посмотреть