Учебник

Теоретический материал.

Глава II

  • Работа с файлами

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

  • Диалог выбора файлов

    Имеется два практически идентичных диалога выбора имени файла: открыть файл для чтения GetOpenFileName()и для записи GetSaveFileName():
    
    BOOL APIENTRY GetOpenFileNameW(LPOPENFILENAMEW); BOOL APIENTRY GetSaveFileNameW(LPOPENFILENAMEW);
    				
    Аргумент функций — указатель на переменную типа OPENFILENAMEW. Поля этой структуры приведены в табл. 2.1. Все функции, обеспечивающие диалог, описаны в файле включений commdlg.h.
    Таблица 2.1. Описание полей структуры OPENFILENAMEW
    Тип поля Имя Описание
    DWORD lStructSize Размер структуры в байтах
    HWND hwndOwner Дескриптор окна-владельца
    HINSTANCE hInstance Дескриптор приложения
    LPCWSTR lpstrFilter Фильтр из пар строк — в кодировке Unicode
    LPWSTR lpstrCustomFilter Пользовательский фильтр — в кодировке Unicode
    DWORD nMaxCustFilter Размер фильтра в символах
    DWORD nFilterIndex Смещение в буфере фильтра
    LPWSTR lpstrFile Указатель на строку с именем файла — в кодировке Unicode
    DWORD nMaxFile Размер буфера для имени файла
    LPWSTR lpstrFileTitle Указатель на строку с выбранным именем файла — в кодировке Unicode
    DWORD nMaxFileTitle Размер буфера для имени выбранного файла
    LPCWSTR lpstrInitialDir Указатель на строку начального каталога — в кодировке Unicode
    LPCWSTR lpstrTitle Указатель на строку с заголовком окна — в кодировке Unicode
    DWORD Flags Флаг: OFN_ALLOWMULTISELECT — допускается выбор нескольких файлов;
    OFN_HIDEREADONLY — переключатель Read Only не выводится;
    OFN_READONLY — только для чтения;
    OFN_OVERWRITEPROMPT — генерируется сообщение при записи существующего файла
    WORD nFileOffset Смещение имени файла в строке полного имени
    WORD nFileExtension Смещение расширения имени файла
    LPCWSTR lpstrDefExt Указатель на строку с расширением имени файла по умолчанию — в кодировке Unicode
    LPARAM lCustData Данные для функции-перехватчика
    LPOFNHOOKPROC lpfnHook Указатель на функцию-перехватчик
    LPCWSTR lpTemplateName Шаблон диалогового окна — в кодировке Unicode
    Таким образом, для организации диалога выбора файла необходимо заполнить по- ля структуры OPENFILENAME и вызвать одну из функций GetOpenFileName() или GetSaveFileName(), после их успешной работы в поле lpstrFileполучим указатель на строку с полным именем выбранного файла.
  • Простой просмотрщик файлов

    
    #include 
    #include 
    #include 
    #include 
    const int LineHeight = 16;//Высота строки текста + межстрочное расстояние
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
    int wmId; PAINTSTRUCT ps;
    HDC hdc;
    static TCHAR name[256] = _T("");; static OPENFILENAME file; std::ifstream in;
    std::ofstream out;
    static std::vector v; std::vector::iterator it; std::string st;
    int y;
    switch (message)
    {
    case WM_CREATE:
    file.lStructSize = sizeof(OPENFILENAME); file.hInstance = hInst;
    file.lpstrFilter = _T("Text\0*.txt"); file.lpstrFile = name;
    file.nMaxFile = 256; file.lpstrInitialDir = _T(".\\"); file.lpstrDefExt = _T("txt"); break;
    case WM_COMMAND:
    wmId = LOWORD(wParam); switch (wmId)
    {
    case ID_FILE_NEW :
    if (!v.empty()) std::vector().swap(v); InvalidateRect(hWnd, NULL, TRUE);
    break;
    case ID_FILE_OPEN :
    file.lpstrTitle = _T("Открыть файл для чтения"); file.Flags = OFN_HIDEREADONLY;
    if (!GetOpenFileName(&file)) return 1; in.open(name);
    while (getline(in,st)) v.push_back(st); in.close(); InvalidateRect(hWnd,NULL,1);
    break;
    case ID_FILE_SAVE :
    
    
    file.lpstrTitle = _T("Открыть файл для записи"); file.Flags = OFN_NOTESTFILECREATE;
    if (!GetSaveFileName(&file)) return 1; out.open(name);
    for (it = v.begin(); it != v.end(); ++it) out << *it << '\n'; out.close();
    break;
    case IDM_EXIT: DestroyWindow(hWnd); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break; case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    for (y = 0, it = v.begin(); it < v.end(); ++it, y += LineHeight) TextOutA(hdc, 0, y, it->data(), it->length());
    EndPaint(hWnd, &ps); break;
    case WM_DESTROY: PostQuitMessage(0); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
    }
    				
    Начнем с того, что добавим к существующему меню три пункта. Для этого перейдем на подложку Resourse View рабочей области и в выпадающем меню File введем три пункта &New, &Open, &Save (см. рис. 1.22). Если не вводить идентификаторы пунктов меню, то мастер присвоит им значения по умолчанию: ID_FILE_NEW,ID_FILE_OPEN, ID_FILE_SAVE. Затем создадим сепаратор (линию-разделитель): для этого достаточно ввести вместо имени дефис (-) или отметить флаг Separator в окне свойств. После чего "отбуксируем" пункт меню Exit в конец списка.
    ПРИМЕЧАНИЕ
    Чтобы вновь не открывать окно свойств для каждого пункта меню, достаточно прикрепить его к "холсту" верхней кнопкой с изображением канцелярской кнопки.
    При выборе пункта меню будет сгенерировано сообщение WM_COMMAND, а в младшее слово wParam помещено значение соответствующего идентификатора. Наша задача заключается в добавлении к оконной функции обработчиков этих сообщений.
    Однако предварительно необходимо решить, как мы будем хранить строки прочитанного файла.
    Можно в стиле классического С читать из файла построчно и выделять память для хранения каждой строки, а указатели на строки хранить в одномерном массиве. Но поскольку мы не знаем заранее размер файла, то нам придется либо запасать большой массив указателей, на всякий случай (что, тем не менее, приведет к ограничению размера файла), либо предварительно определить его размер, а затем выделить необходимую память. Первый вариант для нас совершенно неприемлем, поскольку он абсолютно не профессионален, второй же вариант потребует читать файл 2 раза, что вряд ли улучшит эффективность программы.
    Однако вспомним, что в составе стандартной библиотеки шаблонов STL (Standard Template Library) имеется набор контейнеров, которые достаточно просто разрешат наши проблемы. Выберем самый простой контейнер vector, где строки будем хранить в виде экземпляров стандартного класса string, что существенно упростит работу.
    Текстовый файл будем читать, используя потоковые классы ввода/вывода.
    Итак, добавляем в заголовок приложения операторы включения соответствующих библиотек commdlg.h, vector, string, fstream.
    Здесь первая строчка необходима для подключения функций файлового диалога, а три следующих файла включений содержат определения классов: vector, string, ifstream, ofstream.
    ПРИМЕЧАНИЕ
    Все потоковые классы и библиотека STL определены в стандартной области имен std, однако подключение всего пространства имен директивой "using namespace std" не является хорошим решением. Для ускорения компиляции рекомендуется явно указывать область видимости для каждой переменной, например, std::vector v;
    Переменные, значение которых должно сохраниться после выхода из оконной функции, мы объявим как static:
    • name — символьный массив для хранения имени открываемого файла;
    • file — переменная типа структуры OPENFILENAME;
    • v — вектор для данных типа string.
    Остальные переменные могут быть автоматическими, потому что нет необходимости сохранять их значение между вызовами оконной функции. Большую часть полей структуры file (см. табл. 2.1) разумно заполнить при обработке сообщения WM_CREATE, поскольку это сообщение обрабатывается только один раз при создании окна.
    ПРИМЕЧАНИЕ
    Поскольку переменная file описана как static, то нулевые значения можно не присваивать — эти поля и так будут инициированы нулями при создании переменной.
    При выборе команды меню New мы должны очистить вектор и окно вывода.
    Наиболее простой способ для этого — метод clear(), однако, очистив вектор, метод не освободит выделенной под него памяти, что не совсем хорошо. Даже если мы воспользуемся методом v.resize(0), то и это не станет решением проблемы — он не сможет уменьшить размер выделенной памяти, поскольку предназначен лишь для увеличения размера контейнера. Существует довольно элегантный прием для решения этой проблемы:
    
    std::vector().swap(v);
    				
    Здесь создается временный "пустой" вектор для переменных типа string и тут же обменивается с вектором v. Если вспомнить, что метод swap() обменивает не толь- ко содержимое контейнеров, но и их размеры, то становится ясно, что в результате мы получим пустой вектор v и временный вектор, содержащий предыдущий набор данных, который автоматически уничтожается деструктором. Разумеется, если вектор и так "пустой", то очищать его нет смысла, поэтому мы используем условный оператор:
    
    if (!v.empty()) . . .
    				
    Теперь, после того как вектор очищен, нужно перерисовать окно, как обычно обратившись к функции InvalidateRect(). По команде Open мы заполним лишь два поля переменной
    
    file: file.lpstrTitle = _T("Открыть файл для чтения");
    file.Flags = OFN_HIDEREADONLY;
    				
    где первое поле обеспечит вывод заголовка диалогового окна, а второе поле можно было бы и не заполнять, поскольку OFN_HIDEREADONLY означает лишь то, что переключатель Read Only не выводится. При обращении к функции GetOpenFileName() происходит вывод диалогового окна (рис. 2.1), где мы имеем возможность выбрать файл для чтения. Если работа функции завершилась успешно, то в символьном массиве name мы получим полное имя файла и сможем открыть его методом open() потокового класса.

    Рис. 2.1. Диалоговое окно выбора имени файла для чтения

    Теперь файл построчно читаем в память. Лучше всего воспользоваться перегруженной для класса string функцией getline(), которая принимает в качестве параметров входной поток и переменную типа string. Функция читает из потока in строку и помещает ее в переменную st, причем конструктор класса string сам позаботится о выделении памяти. Далее, методом v.push_back(st) строка добавляется в конец контейнера. Возвращаемым значением функции getline() является входной поток in, поэтому цикл работает, пока весь файл не будет прочитан. При достижении конца файла функция getline() возвращает NULL и цикл прекращается.
    Теперь входной поток нужно закрыть методом close() и перерисовать окно.
    Обработка команды Save осуществляется аналогично. Мы также переопределяем значение двух полей lpstrTitleи Flags, изменяя заголовок диалогового окна и флаг открытия окна OFN_NOTESTFILECREATE, который позволяет выбрать имя существующего файла для замены.
    При записи файла мы воспользовались диалогом GetSaveFileName(), который отличается от предыдущего только тем, что кнопка в диалоговом окне называется Сохранить вместо Открыть (рис. 2.2).
    Для записи в файл содержимого контейнера организуем цикл от начала до конца контейнера по итератору it. Добавим в конце каждой строки символ перевода строки '\n'.

    Рис. 2.2. Диалог выбора имени файла для записи

    Теперь осталось организовать вывод содержимого контейнера в окно при обработке сообщения WM_PAINT. Мы можем выводить содержимое контейнера в окно по итератору it. Нужно только добавить переменную целого типа y для позиционирования строк текста по вертикали. Будем увеличивать эту переменную на каждом шаге цикла на LineHeight — этого достаточно для шрифта по умолчанию.
    Функция TextOutA() не требует задания С-строки, ей достаточно символьного масcива типа char*, однако класс string имеет метод data(), возвращающий указатель на массив символов. Размер строки текста получим методом length().
    ПРИМЕЧАНИЕ
    Поскольку приложение ориентировано на чтение файлов с однобайтной кодировкой, то мы должны явно указать суффикс "A" функции TextOutA(), что будет означать использование типа char* в качестве указателя строки.
    Если все сделано правильно, то мы получим программу, читающую текстовый файл, выводящую его в окно и имеющую возможность сохранить его копию. Приложение хотя и работает, но имеет ряд существенных недостатков:
    1. Если весь текст не помещается в окне, часть его будет утеряна.
    2. "Исчезают" символы, не имеющие графического образа, в частности символы табуляции.

    Решение указанных проблем требует модернизации программы.
    Для обработки символов табуляции проще всего заменить функцию вывода текста на TabbedTextOutA():
    
    TabbedTextOutA(hdc, 0, y, it->data(), it->length(), 0, NULL, 0);
    				
    Начальные параметры имеют тот же смысл, а три последних параметра определяют позиции табуляции. По умолчанию (нулевые значения параметров), символ табуляции разворачивается до средней ширины 8 символов.

    Организация скроллинга

  • Скроллинг или прокрутку можно установить как свойство окна, добавив флаги горизонтального и вертикального скроллинга WS_VSCROLL и WS_HSCROLL в стиль создаваемого окна функции CreateWindow().
    
    hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW| WS_VSCROLL | WS_HSCROLL,
    CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
    				
    Однако всю логику обслуживания полос прокрутки мы должны организовать самостоятельно (листинг 2.2). Используем в качестве основы предыдущую задачу, листинг 2.1, добавив сюда скроллинг.
    
    #include 
    #include 
    #include 
    #include 
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lPa- ram)
    {
    PAINTSTRUCT ps;
    HDC hdc;
    static TCHAR name[256] = _T("");; static OPENFILENAME file; std::ifstream in;
    std::ofstream out;
    static std::vector v; std::vector::iterator it; std::string st;
    int y, k;
    static int n,length,sx,sy,cx,iVscrollPos,iHscrollPos,COUNT,MAX_WIDTH; static SIZE size = {8, 16}; //Ширина и высота символа
    switch (message)
    {
    case WM_CREATE:
    file.lStructSize = sizeof(OPENFILENAME); file.hInstance = hInst;
    file.lpstrFilter = _T("Text\0*.txt"); file.lpstrFile = name;
    file.nMaxFile = 256; file.lpstrInitialDir = _T(".\\"); file.lpstrDefExt = _T("txt"); break;
    case WM_SIZE:
    sx = LOWORD(lParam); sy = HIWORD(lParam); k = n - sy/size.cy;
    if (k > 0) COUNT = k; else COUNT = iVscrollPos = 0; SetScrollRange(hWnd, SB_VERT, 0, COUNT, FALSE); SetScrollPos (hWnd, SB_VERT, iVscrollPos, TRUE);
    k = length - sx/size.cx;
    if (k > 0) MAX_WIDTH = k; else MAX_WIDTH = iHscrollPos = 0; SetScrollRange(hWnd, SB_HORZ, 0, MAX_WIDTH, FALSE); SetScrollPos(hWnd, SB_HORZ, iHscrollPos, TRUE);
    break;
    case WM_VSCROLL : switch(LOWORD(wParam))
    {
    case SB_LINEUP : iVscrollPos--; break; case SB_LINEDOWN : iVscrollPos++; break;
    case SB_PAGEUP : iVscrollPos -= sy / size.cy; break;
    
    case SB_PAGEDOWN : iVscrollPos += sy / size.cy; break;
    case SB_THUMBPOSITION : iVscrollPos = HIWORD(wParam); break;
    }
    iVscrollPos = max(0, min(iVscrollPos, COUNT)); if (iVscrollPos != GetScrollPos(hWnd, SB_VERT))
    {
    SetScrollPos(hWnd, SB_VERT, iVscrollPos, TRUE); InvalidateRect(hWnd, NULL, TRUE);
    }
    break;
    case WM_HSCROLL : switch(LOWORD(wParam))
    {
    case SB_LINEUP : iHscrollPos--; break; case SB_LINEDOWN : iHscrollPos++; break; case SB_PAGEUP : iHscrollPos -= 8; break; case SB_PAGEDOWN : iHscrollPos += 8; break;
    case SB_THUMBPOSITION : iHscrollPos = HIWORD(wParam); break;
    }
    iHscrollPos = max(0, min(iHscrollPos, MAX_WIDTH)); if (iHscrollPos != GetScrollPos(hWnd, SB_HORZ))
    {
    SetScrollPos(hWnd, SB_HORZ, iHscrollPos, TRUE); InvalidateRect(hWnd, NULL, TRUE);
    }
    break;
    case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_FILE_NEW :
    if (!v.empty()) std::vector().swap(v); n = length = 0;
    SendMessage(hWnd, WM_SIZE, 0, sy << 16 | sx); InvalidateRect(hWnd, NULL, TRUE);
    break;
    case ID_FILE_OPEN :
    file.lpstrTitle = _T("Открыть файл для чтения"); file.Flags = OFN_HIDEREADONLY;
    if (!GetOpenFileName(&file)) return 1; in.open(name);
    while (getline(in,st))
    {
    if (length < st.length()) length = st.length(); v.push_back(st);
    
    
    }
    in.close();
    n = v.size();
    SendMessage(hWnd, WM_SIZE, 0, sy << 16 | sx); InvalidateRect(hWnd, NULL, TRUE);
    break;
    case ID_FILE_SAVE :
    file.lpstrTitle = _T("Открыть файл для записи"); file.Flags = OFN_NOTESTFILECREATE;
    if (!GetSaveFileName(&file)) return 1; out.open(name);
    for (it = v.begin(); it != v.end(); ++it) out << *it << '\n'; out.close();
    break;
    case IDM_EXIT: DestroyWindow(hWnd); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break;
    case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps); for(y=0,it=v.begin()+iVscrollPos;it!=v.end()&&ylength())
    TabbedTextOutA(hdc,0,y,it->data()+iHscrollPos,it->length()- iHscrollPos,0,NULL,0);
    EndPaint(hWnd, &ps); break;
    case WM_DESTROY: PostQuitMessage(0); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
    }
    				
    Первое, что необходимо сделать для создания полос прокрутки — задать диапазон и установить позицию движка. Две функции SetScrollRange() и SetScrollPos() позволят решить эту задачу:
    
    BOOL WINAPI SetScrollRange(HWND hWnd, int nBar, int nMinPos, int nMaxPos, BOOL bRedraw);
    int WINAPI SetScrollPos(HWND hWnd, int nBar, int nPos, BOOL bRedraw);
    				
    • hWnd — дескриптор окна;
    • nBar — идентификатор полосы прокрутки, здесь может принимать значение
    • SB_HORZ или SB_VERT;
    • nMinPos, nMaxPos — минимальное и максимальное значение позиции движка;
    • nPos — текущая позиция движка;
    • bRedraw — перерисовка полосы прокрутки, если значение поля TRUE, и нет пере- рисовки, если FALSE.
    Удобнее всего установить параметры прокрутки в сообщении WM_SIZE, поскольку это сообщение обрабатывается при создании окна, и будет генерироваться системой при каждом изменении его размера.
    Диапазон вертикальной прокрутки установим равным количеству прочитанных строк файла n за вычетом одной экранной страницы. Это желательно сделать, чтобы в конце текста не появлялось пустой страницы. Количество строк, помещающихся на одной странице, мы вычислим, поделив размер окна по вертикали на высоту строки sy/size.cy. Высоту окна получим здесь же, в сообщении WM_SIZE, извлекая старшее слово lParam, а геометрические параметры шрифта зададим при описании переменной:
    
    static SIZE size = {8, 16};
    				

    Таким будет размер символа шрифта по умолчанию, как мы выяснили, исследуя системные шрифты в главе 1 (см. листинг 1.13). Однако с такой арифметикой могут быть проблемы. Действительно, при создании окна переменная n равна 0, поскольку она объявлена как статическая и выражение:
    
    k = n - sy/size.cy;
    				
    будет отрицательным. Такой же результат будет и при открытии файла, который целиком помещается в одном окне. Поэтому мы в следующей строке скорректируем значение переменной COUNT, которая и будет определять диапазон вертикального скроллинга.
    
    if (k > 0) COUNT = k; else COUNT = iVscrollPos = 0;
    				
    Заодно присваиваем нулевое значение переменной iVscrollPos, являющейся индексом текущей позиции вертикального скроллинга.
    Теперь установим диапазон и позицию скроллинга: SetScrollRange(hWnd, SB_VERT, 0, COUNT, FALSE); SetScrollPos (hWnd, SB_VERT, iVscrollPos, TRUE);
    ПРИМЕЧАНИЕ
    Если минимальное и максимальное значения позиции движка совпадают, то полоса прокрутки в окне не выводится.
    Параметры горизонтального скроллинга устанавливаются аналогично.
    Очевидно, что при открытии приложения, когда количество строк n и максимальная длина строки length равны нулю, полос прокрутки не будет, они должны появиться лишь при открытии файла достаточно большого размера.
    Немного модифицируем обработчик сообщения WM_CREATE, добавив в поле фильтра еще один шаблон:
    
    file.lpstrFilter = _T("Text\0 *.txt\0Все файлы\0 *.*");
    				
    Для корректной обработки команды New добавим две строки текста:
    
    n = length = 0;
    SendMessage(hWnd, WM_SIZE, 0, sy << 16 | sx);
    				
    Здесь мы обнулим переменные, описывающие количество строк файла и максимальную длину строки, после чего сгенерируем сообщение WM_SIZE при помощи функции SendMessage(), что вызовет переопределение параметров скроллинга:
    
    LRESULT WINAPI SendMessageW(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lPa- ram);
    				
    ПРИМЕЧАНИЕ
    Этот прием генерации сообщения мы используем для экономии кода. Можно конечно оформить этот код отдельной функцией, но хотелось показать и эту возможность.
    Функция SendMessage() обеспечивает передачу сообщения окну с дескриптором hWnd, дополнительная информация передается через wParam и lParam. Причем функция синхронная и возвращает управление только после обработки сообщения. Возвращаемое значение зависит от сообщения.
    Поскольку в сообщении WM_SIZE мы получаем размеры окна в переменной lParam, то здесь мы должны самостоятельно сформировать его значение. В старшее слово lParam нужно поместить высоту окна — это мы сделаем операцией логического сдвига sy<<16, а ширину окна в младшее слово добавим операцией логического сложения. wParam здесь не используется, поэтому запишем 0.
    При обработке команды Open вычислим максимальное количество символов в строке, эта информация нам понадобится для определения горизонтального скроллинга. Добавим в цикл чтения файла строку:
    
    if (length < st.length()) length = st.length();
    				

    По завершении чтения файла в переменной length будет храниться размер строки максимальной длины. Количество строк файла получим как размер контейнера:
    
    n = v.size().
    				
    Перед тем как перерисовать окно, сгенерируем сообщение WM_SIZE для переустановки параметров скроллинга.
    Теперь мы должны организовать логику работы с полосами прокрутки.
    Любые действия с полосами прокрутки вызывают генерацию сообщения: WM_VSCROLL для вертикального иWM_HSCROLL для горизонтального скроллинга. В младшем слове wParam передается идентификатор команды. Мы будем обрабатывать лишь следующий набор команд:
        SB_LINEUP — щелчок мыши на стрелке вверх (влево);
        SB_LINEDOWN — щелчок мыши на стрелке вниз (вправо);
        SB_PAGEUP — щелчок мыши внутри полосы выше (левее) движка;
        SB_PAGEDOWN — щелчок мыши внутри полосы ниже (правее) движка;
        SB_THUMBPOSITION — отпускание движка после его буксировки.
    ПРИМЕЧАНИЕ
    При рассмотрении горизонтального скроллинга логичнее было бы использовать символи- ческие константы SB_LINELEFT, SB_LINERIGHT, SB_PAGELEFT, SB_PAGERIGHT; они эквивалентны приведенным выше и представляют собой целые числа: 0, 1, 2, 3.
    Для отслеживания позиции движков полос прокрутки используем две статические переменные целого типа iVscrollPos — для вертикального скроллинга и iHscrollPos — для горизонтального. По командам SB_LINEUP и SB_LINEDOWN уменьшим или увеличим значение переменной на 1. По командам SB_PAGEUP и SB_PAGEDOWN, означающим перелистывание на страницу для горизонтального скроллинга, уменьшим или увеличим переменную iVscrollPos на количество помещающихся в окне строк, которое мы вычислим, поделив вертикальный размер окна на высоту строки: sy/size.cy. Для горизонтального скроллинга будем смещать переменную iHscrollPos на размер табуляции, равный 8 символам.
    Последней обработаем команду SB_THUMBPOSITION, которая будет сгенерирована после отпускания движка по завершении его буксировки. Мы можем получить позицию движка из старшего слова wParam: iVscrollPos = HIWORD(wParam).
    Но если этим ограничиться, то будет возможно либо отрицательное значение, либо значение, превышающее максимальный размер скроллинга. Во избежание подобной ситуации воспользуемся следующей конструкцией:
    
    iVscrollPos = max(0, min(iVscrollPos, COUNT));
    				

    max() и min() — макросы, определенные в файле включений windows.h для вычисления максимума и минимума двух арифметических выражений.
    Теперь мы можем быть уверены, что переменная iVscrollPos не выйдет за пределы отрезка [0; COUNT].
    Затем проверяем, изменилась ли позиция движка?
    
    if (iVscrollPos != GetScrollPos(hWnd, SB_VERT))
    				
    Сравним значение переменной iVscrollPos с возвращаемым значением функции GetScrollPos() и, если значения не совпадают, перерисуем полосу скроллинга с новым положением движка:
    
    SetScrollPos(hWnd, SB_VERT, iVscrollPos, TRUE);
    				

    После чего перерисуем содержимое окна, вызывая функцию InvalidateRect(). Для горизонтального скроллинга выполним аналогичные действия.
    Нам осталось рассмотреть организацию вывода текста в сообщении WM_PAINT. Выводом будут управлять две переменные iVscrollPos и iHscrollPos, определяющие позиции скроллинга. Будем начинать вывод текста со строки с индексом iVscrollPos, а горизонтальное смещение определим индексом iHscrollPos.
    В цикле for(...) блок начальной инициализации будет выглядеть так:
    
    y = 0, it = v.begin() + iVscrollPos;
    				

    Переменная y, как и ранее, используется для вычисления y-координаты выводимой строки текста, а итератор it, которым мы выбираем строки текста из контейнера, сместим на iVscrollPos позиций. Для контейнера vectorэта операция допустима и итератор сейчас показывает на строку текста с индексом iVscrollPos.
    Рассмотрим, как записано условие:
    
    it < v.end() && y < sy
    				
    Цикл завершится, если будет достигнут конец контейнера или у-координата строки выйдет за пределы окна.
    Приращение координат:
    
    ++it, y += size.cy
    				
    Итератор it увеличивается на 1, а y-координата на высоту строки. Осталось вывести строку текста с учетом обработки табуляций:
    
    TabbedTextOutA(hdc,0,y, it->data()+iHscrollPos, it->length()-iHscrollPos, 0, NULL, 0);
    				

    Выводим строки текста со смещением iHscrollPos, размер строки уменьшается на эту же величину: it->length()-iHscrollPos.
    ПРИМЕЧАНИЕ
    Здесь мы поступаем не совсем корректно, поскольку выводим строки до конца, не заботясь о том, что часть строки может выйти за пределы окна. Это несколько ухудшает эффективность программы, но упрощает ее логику.
  • Панель инструментов

    Чтобы получить максимально полное представление о возможностях Windows, можно создать для нашей задачи инструментальную панель, которая позволит выполнять команды меню нажатием на соответствующую кнопку. Для трех пунктов меню New, Open и Save создадим панель инструментов со стандартными для этих команд кнопками. Традиционный путь создания панели инструментов заключается в вызове функции CreateToolbarEx(), прототип которой помещен в файл включений
    
    commctrl.h. HWND WINAPI CreateToolbarEx(HWND hWnd, DWORD ws, UINT wID, int nBitmaps, HINSTANCE hBMInst, UINT wBMID, LPCTBBUTTON lpButtons,int iNumButtons,int dxButton, int dyButton, int dxBitmap, int dyBitmap,UINT uStructSize);
    				
    13 параметров этой функции имеют следующее значение:
    1. HWnd — описатель родительского окна;
    2. Ws — флаги стиля окна;
    3. wID — идентификатор дочернего окна панели инструментов;
    4. nBitmaps — число изображений в битовом образе;
    5. hBMInst — дескриптор ресурса битового образа для загрузки;
    6. wBMID — идентификатор ресурса битового образа;
    7. lpButtons — указатель на массив структур TBBUTTON, которые содержат информацию о кнопках;
    8. iNumButtons — число кнопок инструментальной панели;
    9. dxButton — ширина кнопок в пикселах;
    10. dyButton — высота кнопок в пикселах;
    11. dxBitmap — ширина, в пикселах, кнопки битового образа;
    12. dyBitmap — высота, в пикселах, кнопки битового образа;
    13. uStructSize — размер структуры TBBUTTON.
    Информация о кнопках размещается в массиве структур TBBUTTON. struct TBBUTTON
    
    {
    int iBitmap; // индекс изображения кнопки
    int idCommand; // идентификатор команды, соответствующей кнопке BYTE fsState; // определяют начальное состояние и
    BYTE fsStyle; // стиль кнопки BYTE bReserved[2]; // резерв
    DWORD dwData; // определенное приложением значение int iString; // индекс текстовой метки кнопки
    };
    				
    Для описания включаемых в приложение кнопок в Windows имеется два набора готовых для использования битовых образов. Первый набор содержит изображения 15 кнопок, соответствующих командам меню File и Edit, откуда мы и позаимствуем необходимые изображения. Идентификаторы кнопок вполне отражают их назначение:
    
    STD_CUT, STD_COPY, STD_PASTE, STD_UNDO, STD_REDOW, STD_DELETE, STD_FILENEW, STD_FILEOPEN, STD_FILESAVE, TD_PRINTPRE, STD_PROPERTIES, STD_HELP, STD_FIND, STD_REPLACE, STD_PRINT.
    
    				
    1. Добавим файл включения директивой:
      
      #include 
      				
    2. На глобальном уровне опишем 3 кнопки:
      
      TBBUTTON tbb[] ={
      {STD_FILENEW, ID_FILE_NEW, TBSTATE_ENABLED, TBSTYLE_BUTTON,0,0,0,0},
      {STD_FILEOPEN, ID_FILE_OPEN,TBSTATE_ENABLED, TBSTYLE_BUTTON,0,0,0,0},
      {STD_FILESAVE, ID_FILE_SAVE,TBSTATE_ENABLED, TBSTYLE_BUTTON,0,0,0,0}
      };
      				
      Строка описания: имя кнопки, ее идентификатор, константа TBSTATE_ENABLED, которая означает, что кнопка активна; стиль кнопки стандартный — TBSTYLE_BUTTON, остальные 4 параметра здесь не используются, и мы заполним их нулями. Поскольку мы выбрали для идентификаторов кнопок те же значения, что и для пунктов меню, нажатие на кнопку панели инструментов генерирует те же сообщения, что и команды меню — WM_COMMAND со значением идентификатора соответствующего пункта меню в младшем слове wParam. Таким образом, нет необходимости писать код для обработки сообщения о нажатии на кнопку панели инструментов, так как эти сообщения будут обработаны тем же кодом, что и команды меню.
    3. Опишем дескриптор панели инструментов:
      
      static HWND hwndToolBar;
      
      				
    4. Теперь вызовем функцию CreateToolbarEx() и передадим ей необходимые параметры. Это можно сделать в сообщении WM_CREATE.
      
      hwndToolBar = CreateToolbarEx(hWnd, WS_CHILD | WS_VISIBLE | CCS_TOP,1, 0,HINST_COMMCTRL,IDB_STD_SMALL_COLOR,tbb,3,0,0,0,0,sizeof(TBBUTTON));
      				
      Стиль окна определяется логической суммой трех констант: WS_CHILD — окно является дочерним; WS_VISIBLE — окно отображается автоматически (если не включить этот стиль, то необходимо использовать функцию ShowWindow()); CCS_TOP — окно размещается сверху под строкой меню, причем размер по вертикали устанавливается исходя из размеров кнопок, а горизонтальный размер устанавливается по ширине родительского окна.
      Следующий параметр — идентификатор окна панели инструментов, присваиваем ему номер 1. Далее 0, поскольку для стандартного ресурса HINST_COMMCTRL этот параметр не используется. Идентификатор ресурса выберем IDB_STD_SMALL_COLOR для "маленьких" (16 16) кнопок. Далее описывается tbb — указатель на массив структур TBBUTTON и количество элементов –– 3. Следующие 4 поля для стандартных элементов управления не используются и заполняются нулевым значением, а последним параметром стоит размер структуры sizeof(TBBUTTON).
      ПРИМЕЧАНИЕ
      Следует иметь в виду, что для хранения изображения всех элементов панели инструментов используется один битовый образ, а извлечение необходимого образа происходит по индексу.
    5. Необходимо предусмотреть изменение размеров панели инструментов при изменении размеров окна, иначе панель инструментов будет выглядеть не очень привлекательно. Лучше всего сделать это при обработке сообщения WM_SIZE посылкой сообщения TB_AUTOSIZE:
      
      SendMessage(hwndToolBar, TB_AUTOSIZE, 0, 0);
      
      				
      что приведет к автоматической корректировке размера панели инструментов, и она гарантированно будет занимать всю верхнюю строку.
    6. Теперь осталось решить последнюю проблему: дело в том, что панель инструментов, являясь дочерним окном, лежит внутри клиентской области окна и при выводе текста перекроет его верхнюю часть. Можно решить эту проблему, сместив начальную точку вывода текста. При обработке сообщения WM_SIZE вычислим высоту панели инструментов при помощи функции GetWindowRect(), кото- рая возвращает координаты окна относительно рабочего стола:
      
      GetWindowRect(hwndToolBar, &rt); size_Toolbar = rt.bottom - rt.top;
      
      				
      переменные rt и size_Toolbar предварительно определим в оконной функции
      
      static int size_Toolbar; RECT rt;
      				
      В сообщении WM_PAINT при начальной инициализации цикла переменной y за- дадим значение:
      
      for(y = size_Toolbar,...),
      				
      при этом первая строка текста будет смещена ниже панели инструментов. Однако в этом случае необходимо также скорректировать и размах скроллинга по вертикали, поскольку он зависит от размеров окна. В сообщении WM_SIZE отре- дактируем строку:
      
      k = n - (sy - size_Toolbar)/size.cy;
      				
    7. Мы все сделали правильно, но при компиляции будет выдано сообщение об ошибке:
      
      error LNK2001: unresolved external symbol imp CreateToolbarEx@52
      
      				
      Дело в том, что библиотека, в которой находится тело функции CreateToolbarEx(), по умолчанию не подключается к проекту. Необходимо сделать это "вручную". Откроем окно свойств проекта в диалоговом окнеSolution Explorer. На подложке Linker | Input в окне редактирования Additional Dependencies добавляем к списку библиотечных файлов comctl32.lib.

    Рис. 2.3. Диалоговое окно настройки параметров проекта

    ПРИМЕЧАНИЕ
    Имя файла comctl32.lib выглядит не очень красиво, но это наследие операционной системы MS-DOS, где имя файла не превышало 8 символов.
    Теперь компиляция должна пройти успешно. На рис. 2.4 показан пример работы созданного приложения.

    Рис. 2.4. Работа приложения с панелью инструментов

  • Выбор шрифтов

    Можно и далее наращивать функциональные возможности программы просмотра файлов. Следующее расширение позволит программе произвольно выбирать шрифт для вывода текста. Воспользуемся имеющимся в системе стандартным диалогом выбора шрифта.
    Для обращения к диалоговому окну выбора шрифта необходимо описать две переменные типа CHOOSEFONT и LOGFONT, где первая обеспечивает взаимодействие с функцией диалога, а вторая требуется для хранения информации о выбранном шрифте. Обе структуры имеют большое количество полей и определены в файлах включений commdlg.h и wingdi.h.
    Финальный текст оконной функции приведен в листинге 2.3, здесь же обсудим лишь новые элементы. Начнем с описания переменных:
    
    static LOGFONT lf; static CHOOSEFONT cf; static HFONT hfont;
    				
    Нам потребуется определить лишь 4 поля структуры CHOOSEFONT. Сделаем это в сообщении
    
     WM_CREATE: cf.lStructSize = sizeof(CHOOSEFONT);
    cf.Flags = CF_EFFECTS | CF_INITTOLOGFONTSTRUCT | CF_SCREENFONTS;
    
    cf.hwndOwner = hWnd; cf.lpLogFont = &lf;
    				
    Первое поле содержит размер структуры CHOOSEFONT. Следующие поля определяют флаги и дескриптор родительского окна, а последнее поле хранит указатель на переменную типа LOGFONT. Flags задает режим работы диалогового окна:
    • CF_EFFECTS — позволяет пользователю определять зачеркнутый, подчеркнутый шрифт, а также цвет текста;
    • CF_INITTOLOGFONTSTRUCT — используется структура LOGFONT;
    • CF_SCREENFONTS — выводится список только экранных шрифтов, поддерживаемых системой.
    Теперь можно строить обращение к функции диалога
    
    ChooseFont(): BOOL APIENTRY ChooseFont(LPCHOOSEFONT);
    				
    Функция принимает указатель на переменную типа CHOOSEFONT и возвращает TRUE при успешной работе диалога. Добавим в выпадающее меню Оформление пункт Шрифт с идентификатором ID_FONT и построим его обработчик.
    ПРИМЕЧАНИЕ
    Перед открытием окна Menu нужно установить в поле свойств Language — Русский (Россия).
    После выбора шрифта мы, чтобы "не замусорить" память, уничтожим предыдущий шрифт, если он существовал, при помощи конструкции if (hfont) ... Затем создадим шрифт функцией CreateFontIndirect(). В качестве параметра функция принимает указатель на структуру LOGFONT и заполняет ее поля в ходе диалога. Теперь необходимо определить метрику текста: нам нужна высота шрифта и средняя ширина символа. Определить эти параметры можно обращением к функции GetTextMetrics(), которую мы уже рассматривали в главе 1. Однако предварительно необходимо получить контекст устройства hdc и выбрать созданный шрифт текущим. Среднюю ширину символа мы получим из поля tmAveCharWidth структуры TEXTMETRIC, которую также нужно предварительно описать:
    
    TEXTMETRIC tm;
    				
    Высоту строки дает выражение:
    
    tm.tmHeight + tm.tmExternalLeading.
    				
    Осталось переопределить параметры скроллинга, послав сообщение
    
    WM_SIZE: SendMessage(hWnd, WM_SIZE, 0, sy << 16 | sx);
    				
    и перерисовать окно вызовом функции InvalidateRect().
    В сообщении WM_PAINT необходимо добавить код для переключения на выбранный шрифт и установить его цвет.
    И последний штрих. Практически все современные манипуляторы типа "мышь" имеют среднее колесико, которое обычно используется для прокрутки текста. Было бы уместно и нам воспользоваться этой возможностью, тем более, что это довольно просто. Прокрутка колесика мыши порождает сообщение WM_MOUSEWHEEL, а в старшем слове wParam возвращается количество шагов колесика, умноженное на константу WHEEL_DELTA (значение константы 120). Нам достаточно поделить это значение на данную константу, и мы получим количество шагов. Позаботимся о том, чтобы не выйти за границы скроллинга, установим новую позицию движка и перерисуем окно.
    
    case WM_MOUSEWHEEL:
    iVscrollPos -= (short)HIWORD(wParam)/WHEEL_DELTA; iVscrollPos = max(0, min(iVscrollPos, COUNT)); SetScrollPos(hWnd, SB_VERT, iVscrollPos, TRUE); InvalidateRect(hWnd, NULL, TRUE);
    break;
    				
    ПРИМЕЧАНИЕ
    Принципиально важно задать явное преобразование типа (short)HIWORD(wParam), иначе отрицательное значение в старшем слове wParam будет интерпретироваться как очень большое положительное число.
    
    #include 
    #include 
    #include 
    #include 
    #include  TBBUTTON tbb[] =
    {
    {STD_FILENEW, ID_FILE_NEW, TBSTATE_ENABLED, TBSTYLE_BUTTON,0,0,0,0},
    {STD_FILEOPEN, ID_FILE_OPEN,TBSTATE_ENABLED, TBSTYLE_BUTTON,0,0,0,0},
    {STD_FILESAVE, ID_FILE_SAVE,TBSTATE_ENABLED, TBSTYLE_BUTTON,0,0,0,0}
    };
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
    PAINTSTRUCT ps;
    HDC hdc;
    static TCHAR name[256] = _T("");; static OPENFILENAME file; std::ifstream in;
    std::ofstream out;
    static std::vector v; std::vector::iterator it;
    
    
    std::string st; int y, k;
    static int n,length,sx,sy,cx,iVscrollPos,iHscrollPos,COUNT,MAX_WIDTH; static SIZE size = {8, 16 };
    static HWND hWndToolBar; static int size_Toolbar; RECT rt;
    static LOGFONT lf; static CHOOSEFONT cf; static HFONT hfont; TEXTMETRIC tm;
    switch (message)
    {
    case WM_CREATE:
    file.lStructSize = sizeof(OPENFILENAME); file.hInstance = hInst;
    file.lpstrFilter = _T("Text .txt\0 *.txt\0Все файлы\0 *.*"); file.lpstrFile = name;
    file.nMaxFile = 256; file.lpstrInitialDir = _T(".\\"); file.lpstrDefExt = _T("txt");
    hWndToolBar = CreateToolbarEx(hWnd,WS_CHILD|WS_VISIBLE|CCS_TOP,1,0, HINST_COMMCTRL,IDB_STD_SMALL_COLOR,tbb,3,0,0,0,0,sizeof(TBBUTTON));
    cf.lStructSize = sizeof(CHOOSEFONT);
    cf.Flags = CF_EFFECTS | CF_INITTOLOGFONTSTRUCT | CF_SCREENFONTS;
    cf.hwndOwner = hWnd; cf.lpLogFont = &lf; break;
    case WM_SIZE:
    sx = LOWORD(lParam); sy = HIWORD(lParam);
    k = n - (sy - size_Toolbar)/size.cy;
    if (k > 0) COUNT = k; else COUNT = iVscrollPos = 0; SetScrollRange(hWnd, SB_VERT, 0, COUNT, FALSE); SetScrollPos (hWnd, SB_VERT, iVscrollPos, TRUE);
    k = length - sx/size.cx;
    if (k > 0) MAX_WIDTH = k; else MAX_WIDTH = iHscrollPos = 0; SetScrollRange(hWnd, SB_HORZ, 0, MAX_WIDTH, FALSE); SetScrollPos(hWnd, SB_HORZ, iHscrollPos, TRUE); SendMessage(hWndToolBar, TB_AUTOSIZE, 0, 0); GetWindowRect(hWndToolBar, &rt);
    size_Toolbar = rt.bottom - rt.top; break;
    
    
    case WM_MOUSEWHEEL:
    iVscrollPos -= (short)HIWORD(wParam)/WHEEL_DELTA; iVscrollPos = max(0, min(iVscrollPos, COUNT)); SetScrollPos(hWnd, SB_VERT, iVscrollPos, TRUE); InvalidateRect(hWnd, NULL, TRUE);
    break;
    case WM_VSCROLL : switch(LOWORD(wParam))
    {
    case SB_LINEUP : iVscrollPos--; break; case SB_LINEDOWN : iVscrollPos++; break;
    case SB_PAGEUP : iVscrollPos -= sy/size.cy; break; case SB_PAGEDOWN : iVscrollPos += sy/size.cy; break;
    case SB_THUMBPOSITION : iVscrollPos = HIWORD(wParam); break;
    }
    iVscrollPos = max(0, min(iVscrollPos, COUNT)); if (iVscrollPos != GetScrollPos(hWnd, SB_VERT))
    {
    
    }
    break;
    SetScrollPos(hWnd, SB_VERT, iVscrollPos, TRUE); InvalidateRect(hWnd, NULL, TRUE);
    case WM_HSCROLL : switch(LOWORD(wParam))
    {
    case SB_LINEUP : iHscrollPos--; break; case SB_LINEDOWN : iHscrollPos++; break; case SB_PAGEUP : iHscrollPos -= 8; break; case SB_PAGEDOWN : iHscrollPos += 8; break;
    case SB_THUMBPOSITION : iHscrollPos = HIWORD(wParam); break;
    }
    iHscrollPos = max(0, min(iHscrollPos, MAX_WIDTH)); if (iHscrollPos != GetScrollPos(hWnd, SB_HORZ))
    {
    
    }
    break;
    SetScrollPos(hWnd, SB_HORZ, iHscrollPos, TRUE); InvalidateRect(hWnd, NULL, TRUE);
    case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_FILE_NEW :
    if (!v.empty()) std::vector().swap(v);
    
    
    n = length = 0;
    SendMessage(hWnd, WM_SIZE, 0, sy << 16 | sx); InvalidateRect(hWnd,NULL,TRUE);
    break;
    case ID_FILE_OPEN :
    file.lpstrTitle = _T("Открыть файл для чтения"); file.Flags = OFN_HIDEREADONLY;
    if (!GetOpenFileName(&file)) return 1; in.open(name);
    while (getline(in,st))
    {
    if (length < st.length()) length = st.length(); v.push_back(st);
    }
    in.close();
    n = v.size();
    SendMessage(hWnd, WM_SIZE, 0, sy << 16 | sx); InvalidateRect(hWnd,NULL,1);
    break;
    case ID_FILE_SAVE :
    file.lpstrTitle = _T("Открыть файл для записи"); file.Flags = OFN_NOTESTFILECREATE;
    if (!GetSaveFileName(&file)) return 1; out.open(name);
    for (it = v.begin(); it != v.end(); ++it) out << *it << '\n'; out.close();
    break; case ID_FONT :
    if(ChooseFont(&cf))
    {
    if (hfont) DeleteObject(hfont); hfont = CreateFontIndirect(&lf); hdc = GetDC(hWnd); SelectObject(hdc, hfont); GetTextMetrics(hdc, &tm); size.cx = tm.tmAveCharWidth;
    size.cy = tm.tmHeight + tm.tmExternalLeading; ReleaseDC(hWnd, hdc);
    SendMessage(hWnd, WM_SIZE, 0, sy << 16 | sx); InvalidateRect(hWnd, NULL, TRUE);
    }
    break;
    case IDM_EXIT: DestroyWindow(hWnd); break;
    
    
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break;
    case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps); if (hfont)
    { SelectObject(hdc, hfont); SetTextColor(hdc, cf.rgbColors);
    }
    for (y = size_Toolbar, it = v.begin() + iVscrollPos; it != v.end() && y < sy; ++it, y += size.cy)
    if (iHscrollPos < it->length())
    TabbedTextOutA(hdc, 0, y, it->data()+iHscrollPos, it->length()-iHscrollPos, 0, NULL, 0);
    EndPaint(hWnd, &ps); break;
    case WM_DESTROY:
    if (hfont) DeleteObject(hfont); PostQuitMessage(0);
    break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
    }
    
    				
    Пожалуй, это все, что можно было сделать на данном этапе (рис. 2.5).

    Рис. 2.5. Окно финальной версии программы-просмотрщика текстовых файлов


    Однако мы должны отдавать себе отчет, что не все проблемы нашли удовлетвори- тельное решение:
    1. Так, максимальная длина строки, необходимая для организации горизонтально- го скроллинга, определена не совсем корректно при наличии в тексте символов табуляции.
    2. Нам пришлось выводить текст до конца строки, даже если он выходит за грани- цу окна, поскольку решение этой проблемы доступными на данном уровне средствами требует слишком много усилий.
    3. Горизонтальный скроллинг будет работать удовлетворительно для моноширин- ных шрифтов, но для TrueType, шрифтов, когда ширина символов различна, строки текста будут "плавать" при скроллинге, и мы можем потерять "хвосты" длинных сток.
    Если первые две проблемы можно как-то решить, написав дополнительный код, то в последнем случае у нас пока нет подходящего механизма.
    Наиболее просто эти проблемы разрешаются при использовании виртуальных окон, о чем мы будем говорить в главе 4.
  • Чтение и запись файлов в библиотеке Win32 API

    В Win32 API имеется свой набор функций, обеспечивающих работу с файлами. Мы не будем долго задерживаться на этом вопросе, поскольку нас пока вполне устраивает библиотека потоковых классов, обеспечивающая достаточно хороший сервис. Однако для полноты картины посмотрим, как можно создать программу чтения и записи текстовых файлов, используя имеющийся набор API-функций (листинг 2.4).
    Нам понадобятся 3 функции:
    1. Для открытия или создания файла используется функция:
      
      HANDLE WINAPI CreateFileW(
      LPCWSTR lpFileName, //имя файла
      DWORD dwDesiredAccess, //способ доступа
      DWORD dwShareMode, //совместный доступ LPSECURITY_ATTRIBUTES lpSecurityAttributes, //атрибуты доступа DWORD dwCreationDisposition, //проверка существования
      DWORD dwFlagsAndAttributes, //атрибуты файла
      HANDLE hTemplateFile); //дескриптор временного файла
      				
    2. Читать из файла будем функцией:
      
      BOOL WINAPI ReadFile(
      HANDLE hFile, //дескриптор файла
      LPVOID lpBuffer, //буфер в памяти
      DWORD nNumberOfBytesToRead, //максимальное число байтов
      LPDWORD lpNumberOfBytesRead, //указатель переменной, возвращающей
      //количество фактически прочитанных байтов LPOVERLAPPED lpOverlapped); //указатель на структуру OVERLAPPED
      				
    3. Пишем в файл функцией:
      
      BOOL WINAPI WriteFile(
      HANDLE hFile, //дескриптор файла
      LPCVOID lpBuffer, //буфер в памяти
      DWORD nNumberOfBytesToWrite, //число записываемых байтов LPDWORD lpNumberOfBytesWritten, //указатель переменной,
      //возвращающей количество фактически записанных байтов
      LPOVERLAPPED lpOverlapped); // указатель на структуру OVERLAPPED 
      				
    Более подробную информацию об этих функциях можно почерпнуть в MSDN (Microsoft Developer Network), а функцию CreateFile() мы рассмотрим подробнее в главе 6.
    
    #include 
    const DWORD MaxLength = 0x7fff;
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
    PAINTSTRUCT ps;
    HDC hdc;
    static TCHAR name[256] = _T("");; static OPENFILENAME file;
    DWORD result;
    static HANDLE hFile;
    static char text[MaxLength]; static int sx, sy;
    static DWORD nCharRead; RECT rt;
    switch (message)
    {
    case WM_CREATE:
    file.lStructSize = sizeof(OPENFILENAME); file.hInstance = hInst;
    file.lpstrFilter = _T("Text\0*.txt\0Все файлы\0*.*"); file.lpstrFile = name;
    file.nMaxFile = 256; file.lpstrInitialDir = _T(".\\"); file.lpstrDefExt = _T("txt"); break;
    case WM_SIZE:
    sx = LOWORD(lParam); sy = HIWORD(lParam);
    
    break;
    case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_FILE_NEW : nCharRead = 0;
    InvalidateRect(hWnd, NULL, TRUE); break;
    case ID_FILE_OPEN :
    file.lpstrTitle = _T("Открыть файл для чтения"); file.Flags = OFN_HIDEREADONLY;
    if (!GetOpenFileName(&file)) return 1;
    hFile = CreateFile(name, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    ReadFile(hFile, text, MaxLength, &nCharRead, NULL); CloseHandle(hFile);
    if (nCharRead == MaxLength)
    {
    MessageBox(hWnd, _T("Слишком большой файл"),
    _T("Неудачное открытие файла"), MB_YESNO | MB_ICONHAND); return 0;
    }
    InvalidateRect(hWnd, NULL,TRUE); break;
    case ID_FILE_SAVE :
    file.lpstrTitle = _T("Открыть файл для записи"); file.Flags = OFN_NOTESTFILECREATE;
    if (!GetSaveFileName(&file)) return 1;
    hFile = CreateFile(name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    WriteFile(hFile, text, nCharRead, &result, NULL); CloseHandle(hFile);
    break;
    case IDM_EXIT: DestroyWindow(hWnd); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break;
    case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps); SetRect(&rt, 0, 0, sx, sy);
    DrawTextA(hdc, text, nCharRead, &rt, DT_LEFT); EndPaint(hWnd, &ps);
    break;
    
    case WM_DESTROY: PostQuitMessage(0); break;
    default: return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
    }
    
    				
    Диалог открытия файла для чтения и записи позаимствуем у предыдущей задачи (см. листинг 2.1), оттуда же возьмем обработчики сообщений WM_CREATE и WM_SIZE. Открываем файла для чтения функцией CreateFile():
    
    hFile = CreateFile(name, GENERIC_READ, 0, NULL, OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL, NULL);
    				
    Первый параметр name — полное имя файла. GENERIC_READ означает, что файл открывается для чтения. Следующий параметр 0 — совместного доступа к файлу нет. NULL — атрибуты доступа наследуются от порождающего процесса. OPEN_EXISTING — проверка существования файла. FILE_ATTRIBUTE_NORMAL — файл с "нормальными" атрибутами (т. е. файл не объявлен, как скрытый, системный и т. д.) и последний параметр NULL— временный файл не создается.
    Возвращаемое значение функции hFile — дескриптор файла. Здесь было бы умeстно проверить, насколько удачно завершилась операция открытия файла. Если файл не смог открыться, функция возвращает 0.
    Читаем файл одним блоком функцией ReadFile(): ReadFile(hFile, text, MaxLength, &nCharRead, NULL);
    Здесь hFile — дескриптор открытого файла. text — указатель символьного массива, куда считывается максимально MaxLength байтов. В переменной nCharRead мы получим фактическое число прочитанных байтов. Последний параметр NULL означает, что нет перекрытия читаемого блока данных.
    Здесь мы для упрощения задачи ограничились максимальным размером файла в MaxLength = 0x7fff байтов. Вообще-то функция может читать блоки размером до 64 К. Смысл нашего ограничения будет понятен, когда мы рассмотрим элементы управления в главе 3.
    Если же размер файла окажется больше, то будет прочитан блок в MaxLength байтов, а nCharRead вернет это же значение. В этом случае мы в диалоговом окне выводим сообщение "Слишком большой файл".
    При записи открываем файл также функцией CreateFile():
    
    hFile = CreateFile(name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    				

    где вторым параметром стоит идентификатор GENERIC_WRITE, означающий, что файл открыт для записи, а параметр CREATE_ALWAYS означает, что создается новый файл, существующий файл будет уничтожен. Записываем в файл также одним блоком функцией WriteFile():
    
    WriteFile(hFile, text, nCharRead, &result, NULL);
    				
    где все параметры имеют тот же смысл, что и для функции ReadFile(). Нам пришлось описать переменнуюresult, в которую возвращается количество записанных байтов. Эту переменную можно использовать для контроля правильности записи. Если блок данных записан на диск, то ее значение совпадает с nCharRead.
    Как при чтении, так и при записи, закрываем файл функцией CloseHandle().
    Поскольку в этой задаче мы работаем с символьным массивом, а не С-строкой, то в меню New достаточно обнулить число прочитанных байтов и перерисовать окно:
    
    nCharRead = 0;
    				
    При выводе содержимого файла в сообщении WM_PAINT возникают определенные сложности. Дело в том, что мы прочитали файл как байтовый массив, имеющий ту же структуру, что и текстовый файл, в частности в конце строки присутствует пара символов — '\r\n'. Можно, конечно, "разобрать" файл на строки и выводить в окно построчно, но можно поступить проще, воспользовавшись функцией DrawText(), которая и предназначена для вывода текстового массива такой струк- туры. Предварительно только нужно создать прямоугольник размером с окно:
    
    SetRect(&rt, 0, 0, sx, sy);
    				
    Текст выведем следующей конструкцией:
    
     DrawTextA(hdc, text, nCharRead, &rt, DT_LEFT);
    				
    где мы установили выравнивание текста влево.
    Мы не будем останавливаться на вопросах организации скроллинга. Предложим эту задачу для самостоятельной работы.
  • Вопросы к главе

    1. Формат функций GetOpenFileName(), GetSaveFileName().
    2. Скроллинг как свойство окна. Настройка параметров скроллинга.
    3. Обработка сообщений WM_VSCROLL и WM_HSCROLL.
    4. Генерация сообщения функцией SendMessage().
    5. Панель инструментов, структура TBBUTTON и функция CreateToolbarEx().
    6. Как производится корректировка размеров панели инструментов?
    7. Диалог выбора шрифта, функция ChooseFont().
    8. API-функции для чтения и записи файла.
  • Задания для самостоятельной работы

    1. Дополнить фильтр выбора имени файла возможностью выбора файлов с расши- рением dat и без расширения имени.
    2. В листинге 2.2 обработать сообщение о перемещении движка полосы скроллин- га SB_THUMBTRACK и колесика мыши.
    3. Заменить контейнер vector, использовавшийся для хранения текстовых строк (листинг 2.3), на контейнер list.
    4. Дополнить программу просмотра текстового файла (листинг 2.3) пунктом меню для чтения текстового файла в кодировке DOS и Windows. Реализовать переко- дировку текста для корректного вывода в окне.
    5. Сделать неактивными пункт меню Save и кнопку Сохранить на панели инстру- ментов при загрузке приложения и по команде New. После открытия файла командой Open активизировать кнопку и пункт меню Save.
    6. Организовать скроллинг для задачи просмотра текстового файла, листинг 2.4. Дополнить задачу панелью инструментов и меню выбора шрифтов.