Здравствуйте, Дорогие друзья! Мы продолжаем создание собственного оконного приложения на WinApi C++ и сегодняшняя статья будет, наверное, самой интересной из всего цикла. Статьи я не публиковал давно, так уж получилось, поэтому показов теперь у меня будет кот наплакал, однако унывать я не собираюсь. Вперед! К новому уровню программирования!
Перед тем как мы начнем, напомню значение некоторых терминов:
- параметр – некоторая переменная, с участием которой выполняется та или иная операция внутри функции. Значение параметра задается в скобочках в момент вызова функции;
- директория – то же самое, что «папка» и «каталог»;
- дескриптор – идентификатор, название;
- поток – работа отдельной части приложения, выполняющей конкретную операцию;
В прошлой статье мы запланировали сделать так, чтобы наша программа либо сохраняла новую карточку, если ВИН-код введен оператором впервые, либо открывала карточку из базы, если ВИН-код в ней уже есть. На первый взгляд может показаться, что реализовать подобную задачу очень сложно. На самом же деле нет. Нам будет достаточно создать всего одну функцию. Давайте подумаем как эта функция будет работать. При вводе какого-либо символа в текстовое поле «VIN-код» программа будет считывать этот символ в небольшой буфер. Скажем, 24 байт нам хватит для этого за глаза (можно и меньше, но я привык делать массивы кратными восьми, Вам советую делать также). Так как все автомобили в нашей базе классифицируются именно ВИН-кодом, то приложению не составит труда сравнить считанную из строки последовательность символов со всеми имеющимися в базе названиями директорий (ведь именно папки носят имена ВИН-кодов). Если совпадение будет найдено, кнопка в нижнем правом углу окна программы приобретет вид «загрузить» или «открыть». Если же совпадений не будет, то на кнопке появится надпись «создать». Также сделаем так, чтобы при обнаружении полного совпадения ВИН-кода, данные клиента (то есть ФИО) автоматически считывались из соответствующего файлика и подгружались в нужные текстовые поля программы. Не будем торопиться. Все наши действия должны быть нам понятны. Для начала давайте рассмотрим как считывается информация из файла. В окне программы создадим кнопку с именем «Считать»:
В файл resources.h помещаем соответствующие значения дескриптора окошка кнопки и ее идентификатора (То есть HWND ReadBtnи #define btnRead12). Компилируем, запускаем приложение:
При нажатии на нее, ожидаемо, ничего не происходит. Сделаем, чтобы происходило. Создадим новую функцию. Назовем ее ReadFromFile().
Создадим буфер в который будем записывать считанную информацию. Назовем его RBuf и выделим под него 256 байт.
Для считывания текста из файла используется функция ReadFile(). Найдем ее в справочнике:
У функции пять параметров. Первый, hFile– дескриптор устройства (простыми словами – структура, в которой указан путь к файлу, из которого считывается информация, параметры доступа и т.д.). Второй, lpBuffer – буфер, в который будет считываться текст (наш массив RBuf). Третий, nNumberOfBytesToRead – количество байт в считываемом тексте. Четвертый, lpNumberOfBytesRead– указатель на переменную, которой присвоится значение, равное количеству считанных байт. Пятый, lpOverlapped– указатель на структуру OVERLAPPED. С этой структурой мы разберемся позже. Пока что же данный параметр мы укажем как NULL.
Создадим локальную переменную типа DWORD, в которую будет записываться количество считанных байт. Назовем ее NOfBytesR. Значение ей присваивать не нужно.
Для примера выведем информацию из файла «E:\C_Project\Databank\ABCDEFG0123456789\file.txt». Сначала зададим этот путь явно. Создаем HANDLEдля параметра hFile. Подобное мы делали в третьей статье цикла. Вот тут:
Скопируем эту строчку в нашу новую функцию и заменим первый параметр функции CreateFile() на «E:\C_Project\Databank\ABCDEFG0123456789\file.txt».
Как и обещал в третьей статье, разберем сигнатуру функции CreateFile() более подробно. Первый параметр (lpFileName), как Вы уже знаете – это путь к файлу.
Второй параметр (dwDesiredAcces) – параметр доступа к файлу (чтение или запись, мы указывали и то, и другое).
Третий параметр (dwShareMode) – режим доступа к файлу. Мы указали 0. Поэтому, когда открыто наше приложение мы не можем открыть файл, созданный нами вне программы. Система будет жаловаться на то, что файл используется другим процессом. Помимо нуля данный параметр может принимать три значения:
- FILE_SHARE_DELETE – разрешить сторонним процессам удалять содержимое файла. Поясняю: мы записали в файлик данные клиента, тут же открыли его в блокноте, выделили текст и стерли его. Система на нас из-за этого ругаться не будет.
- FILE_SHARE_READ – разрешить сторонним процессам читать содержимое файла при работе с ним. Поясняю: мы сохранили данные в файл и тут же открываем его в блокноте или в любом другом редакторе, не закрывая наше приложение. Система не скажет нам, что файл используется другим процессом и позволит его открыть.
- FILE_SHARE_WRITE – ну тут и пояснять не надо: разрешить запись для сторонних процессов. Открыли файл, дописали нужного и ненужного, благополучно сохранили и закрыли.
Как видите, ничего сложного, идем дальше.
Четвертый параметр (lpSecurityAttributes) – указатель на структуру SECURITY_ATTRIBUTES (данная структура содержит дескриптор безопасности). Найдем ее в справочнике:
Атрибуты у данной структуры следующие:
- nLength– размер структуры в байтах. Если вдруг нам понадобится указывать его, так и запишем: sizeof(Имя структуры);
- lpSecurityDescriptor– указатель на структуру SECURITY_DESCROPTOR; Сюда мы углубляться не будем. Скажу только, что изменять параметры в данной структуре напрямую (то есть своими ручками) не следует, для этих целей имеются специальные функции.
- bInheritHandle– переменная типа bool, которая показывает будет ли дескриптор наследоваться при создании нового процесса.
Возвращаемся к структуре SECURITY_ATTRIBUTES, а точнее к ее значению в функции CreateFile(). Мы присваивали ей значение 0. Пусть таким оно остается и впредь.
Пятый параметр функции CreateFile() (dwCreationDisposition) – действие, которое можно выполнить с файлом. Существует несколько значений (между собой их комбинировать нельзя!):
- CREATE_ALWAYS – создать новый файл;
- CREATE_NEW – создать файл, если он не существует;
- OPEN_ALWAYS – открыть файл (если файл не существует, то он будет создан);
- OPEN_EXISTING – открыть файл, если он существует;
- TRUNCATE_EXISTING – занулить (очистить) указанный файл. Данный атрибут будет работать только с указанным в dwDesiredAcces значения GENERIC_WRITE.
Шестой параметр (dwFlagsAndAttributes) – атрибуты и флаги. Здесь могут быть указаны следующие атрибуты:
- FILE_ATTRIBUTE_ARCHIVE – файл может быть архивирован;
- FILE_ATTRIBUTE_ENCRYPTED – файл может быть зашифрован;
- FILE_ATTRIBUTE_HIDDEN – файл скрыт из каталогов;
- FILE_ATTRIBUTE_NORMAL – содержит все «атрибуты по умолчанию»;
- FILE_ATTRIBUTE_OFFLINE – файл хранится автономно, то есть физически в конкретном месте он отсутствует;
- FILE_ATTRIBUTE_READONLY – файл доступен только для чтения;
- FILE_ATTRIBUTE_SYSTEM – файл используется только системой;
- FILE_ATTRIBUTE_TEMPORARY – файл используется как временное хранилище.
Также здесь могут употребляться следующие флаги:
- FILE_FLAG_BACKUP_SEMANTICS – файл создается для резервного копирования;
- FILE_FLAG_DELETE_ON_CLOSE – файл должен быть удален после закрытия его дескрипторов;
- FILE_FLAG_NO_BUFFERING – не использовать системное кеширование при открытии файла;
- FILE_FLAG_OPEN_NO_RECALL – открыть файл, но не переносить его на локальный диск;
- FILE_FLAG_OPEN_REPARSE_POINT – точки повторного анализа (тема о-очень длинная и не очень нам сейчас полезная, ее рассматривать не будем);
- FILE_FLAG_OVERLAPPED – файл создается для асинхронного ввода-вывода;
- FILE_FLAG_POSIX_SEMANTICS – доступ к файлу обуславливается в соответствии с POSIX(простыми словами: способы взаимодействия с программой в зависимости от системы компьютера);
- FILE_FLAG_RANDOM_ACCESS – доступ к файлу случайный;
- FILE_FLAG_SESSION_AWARE – файл может быть открыт только с помощью сведений о текущем сеансе;
- FILE_FLAG_SEQUENTIAL_SCAN – доступ к файлу предоставляется последовательно: от начала к концу;
- FILE_FLAG_WRITE_THROUGH – записывать файл непосредственно на диск.
Седьмой параметр (hTemplateFile) – допустимый дескриптор файла шаблона. Можно указать NULL (так как при открытии существующего файла этот параметр игнорируется).
Видите, сколько много ненужной важной информации скрывалось от нас «под капотом» у функции CreateFile?
Итак, мы быстро пробежали по всем параметрам и теперь можем с легкостью указать те из них, которые понадобятся нам при открытии файла. Запишем их по пунктам:
1. hFile – «E:\\C_Project\\Databank\\ABCDEFG0123456789\\file.txt».
В прошлой статье я говорил почему нужно использовать именно двойную косую, а не одинарную.
2. dwDesiredAcces– GENERIC_READ. Нам нужно только чтение, записывать мы ничего не собираемся.
3. dwShareMode- FILE_SHARE_READ. Мы должны обеспечить доступ сторонних процессов к файлу во время его считывания.
4. lpSecurityAttributes– 0 или NULL. Дескриптор безопасности по умолчанию, дочерними процессами он наследоваться не будет.
5. dwCreationDisposition– OPEN_EXISTING. Открываем файл, если он существует, пустые файлы нам создавать не надо.
6. dwFlagsAndAttributes – FILE_ATTRIBUTES_NORMAL. Атрибуты задаем по умолчанию.
7. hTemplateFile - 0.CreateFile() всё равно проигнорирует этот параметр при открытии файла.
Готово! Передаем параметры в функцию CreateFile():
Отлично! Теперь мы можем смело использовать функцию ReadFile():
Обратите внимание! В качестве третьего параметра в данной функции необходимо указать конкретное число байт, а не количество символов в уже существующем массиве (то есть sizeof() тут не проканает)!
Выведем текст через диалоговое окно:
Добавляем объявление функции ReadFromFile() в файл resources.h и переходим в обработчик команд. Здесь нам нужно вызвать функцию ReadFromFile() при нажатии на кнопку «Считать».
Проверяем: компилируем, запускаем приложение, жмем «Считать»:
А теперь давайте попробуем вывести в диалоговом сообщении только первую строчку. Как это сделать? Знай мы сколько конкретно символов в каждой строке, то могли бы разделить текст на строчки, аналогично тому, как мы его объединяли, когда записывали в файл. Но фамилии, имена и отчества по длине различны, а значит поделить текст на равные части и каждую из них записать как отдельную строку не получится… Сумбурное объяснение. Поясняю наглядно:
Оператор «\0» используется для обозначения конца текста в файле (По сути, именно он записан в тех ячейках, где у нас Nan). Программа посимвольно «вытаскивает» из файла текст и записывает его в буфер. Когда она «упирается» в этот символ, завершает считывание, так как понимает, что дальше «осмысленного» текста уже не будет.
Видите, строчки различные по длине, а значит делить текст на строки по определенному количеству байт не получится. И как тогда быть? Просто! Каждая строка (кроме последней) завершается переносом курсора на новую строку («\r\n»). Предположим, что мы считали текст из файла в массив и хотим узнать что лежит в первой строке. Для этого мы начинаем поочередно, один за другим, выписывать символы из большого массива в маленький, пустой, пока не наткнемся на оператор «\r». Наткнулись? Отлично! Завершаем копирование символов и сообщаем, что операция выполнена.
То есть нам достаточно создать цикл, который будет копировать данные из одного массива в другой, а условием для выхода из этого цикла будет совпадение символа в первом массиве с «\r». Попробуем реализовать это в программе:
Цикл while() выполняется только до тех пор, пока условие в его скобках правдиво. То есть в нашем случае, он будет выполняться, пока в ячейке с номером k массива RBufне обнаружится символ «\r». Если вдруг кто-то не понял, поясняю: программа начинает цикл. На этот момент переменная k имеет значение 0. Программа проверяет правдиво ли условие в скобках цикла while, то есть действительно ли RBuf[0] не равно «\r». Не равно? Хорошо! Из ячейки RBuf[0] в ячейку miniBuf[0] скопируется символ. Затем переменная kувеличится на единицу и мы вернемся в начало цикла, где с символом «\r» будет сравниваться уже ячейка RBuf[1], а данные будут копироваться из RBuf[1] в miniBuf[1]. И так далее, пока в RBuf[k] не обнаружится «\r».
Проверим, работает ли наша программа:
Как видим, все прекрасно работает. Хмм… Что ж, усложним задачу: пусть от нас требуется узнать что лежит не в первой строке, а во всех, кроме первой. Сделать это не сложно. Давайте еще раз посмотрим на Рисунок 15, а именно на строчки 461, 462 и 465. Здесь фигурирует переменная k, которую мы используем для перебора ячеек массива RBuf. Выполнение цикла в нашей программе останавливается, когда k-тая ячейка этого массива будет содержать в себе символ «\r». Мы знаем, что следом за этим символом будет стоять символ «\n». Почему? Потому что при записи текста в файл сами туда его поместили. То есть мы знаем, что в ячейке RBuf[k] лежит символ «\r», в следующей за ним, то есть в RBuf[k+1] будет лежать «\n», а дальше… дальше, начиная с ячейки RBuf[k+2] будет записан весь остальной текст. Для наглядности выведем два сообщения: в первом пусть также будет выводиться первая строка из текста, а во втором – весь остальной текст. Просто создадим еще один цикл и перепишем символы из RBuf в miniBuf после того, как будет выведено первое сообщение.
Итак, компилируем, запускаем приложение, жмем «Считать»:
Жмем ОК в открывшемся диалоговом окне и, о чудо:
Открывается окошко, в котором содержится весь текст, считанный из файла, за исключением первой строки. Также мы можем отделить и вторую строку от третьей. Главное не забыть «сбросить» значение переменной p в ноль:
Внимательно посмотрите на строчки 470 и 473. Перед началом второго цикла мы переходим в ячейку массива, где лежит символ «\n», если мы этого не сделаем, то цикл прекратится, не успев начаться, ведь в ячейке RBuf[k] после завершения первого цикла будет лежать символ «\r», который прописан в условии выхода из этого (второго) цикла. Посмотрим, что получится? Компилируем, запускаем программу и жмем «Считать»:
Всё бы ничего, если бы небольшое «но»: рисунок 21 б) очень хорошо иллюстрирует существенную недоработку нашего скрипта: в массиве miniBuf при копировании во втором цикле остались лишние символы из первого. Почему? Потому что мы его не затерли. Очистить массив можно используя функцию memset(). С ней мы уже встречались в первой статье цикла. У нее довольно простой синтаксис:
memset(указатель на массив, заменяющий символ, кол-во заменяемых символов);
Очистим массив miniBufпосле первого цикла (после того, как выведем в диалоговом окне фамилию):
Обратите внимание: в качестве второго параметра в функцию memset() мы передали ноль. Почему? Не будет ли это ошибкой? Отвечаю на этот вопрос: не будет. Так как у нас массив состоит из переменных типа char, то 0 будет интерпретироваться в нем как NULL, то есть не как символьный ноль, а как пустое место. Компилируем, запускаем приложение, кликаем на «Считать»:
Теперь всё работает, как и должно. Еще более усложним задачу. Пусть при нажатии на кнопку «Считать», текст из файла теперь не выводится в диалоговых окнах, а печатается в текстовых окошках «Фамилия», «Имя» и «Отчество». Помните, как в третьей статье мы устанавливали текст в окошки edit с помощью функции SetWindowText(). Напомню Вам синтаксис:
SetWindowText(окошко, текст).
Ничего сложного. Итак, убираем функции MessageBox() из функции ReadFromFile() и заменяем их на SetWindowText().
Компилируем, запускаем приложение. Чтобы проверить работает ли наша функция сделаем следующее: укажем вместо шаблонных ФИО другие, произвольные, скажем Петров Юрий Григорьевич, в поле VIN-номер введем тот ВИН-код, который прописан у нас в программе (пока что), то есть «ABCDEFG01234567890»:
Делаем мы это, чтобы в файле, откуда мы считываем данные, эти самые данные изменились с шаблонных. Жмем сохранить, закрываем приложение. Тут же вновь запускаем приложение. Оно откроется с данными по умолчанию:
Нажмем «Считать»:
Что мы видим? А видим мы небольшую ошибку в работе нашей функции: в окошко «Отчество» записался лишний символ. Ожидаемо. Частично виновата эта строчка:
Дело в том, что когда при переборе символов в буфере RBuf обнаруживается символ «\r», в массив miniBuf записывается уже «левый» символ из ячейки RBuf[k+2]. Исправить это очень просто: заранее инкрементируем kдо нужных значений перед вторым и третьим циклами, а в циклах уберем сдвиги на 1 и 2 байта:
Проверяем: компилируем код, запускаем приложение, жмем «Считать»:
Как видим, ничего не меняется. Почему? Ведь мы всё делаем правильно! Я не зря сказал, что циклы виноваты в этом лишь «частично». Для того, чтобы Вы точно поняли что происходит, давайте после последнего цикла выведем сообщение с полным текстом, считанным из файла:
Проверяем, что у нас получится:
Ух-ты, как неожиданно! Вот оно откуда «уши торчат» оказывается!.. Но всё же почему? Всё дело вот в этой строчке:
Здесь мы создаем массив RBuf. Как думаете, какие у него в этот момент внутри символы? Верно: мусор! И при считывании информации из файла, она ложилась поверх этого мусора, иногда возникал сбой и мы наблюдали непонятно какие символы в конце третьей строки. Исправляем! При создании массива делаем все его ячейки пустыми:
Ну что, проверим, ушла ли ошибка? Компилируем, запускаем, жмем «Считать»:
Отлично! Мусор из массива RBuf ушел и наша функция ReadFromFile() заработала как и должна была: хорошо. Теперь можно затереть строчку с вызовом сообщения со считанным из файла текстом (заодно поправим комментарии) и вновь усложнить нашу задачу. Пусть теперь нам нужно не просто считать текст из файла и загрузить его в поля «Фамилия», «Имя» и «Отчество» по нажатию кнопки, а сделать это из той папки, название которой будет указано в поле VIN-номер. Как мы можем это сделать? Идем по шагам.
1. Разумеется, сначала нам нужно считать текст из поля VIN-номер;
2. Проверить, есть ли папка с таким именем в директории Databank;
3. Если папка есть, то считать данные из файла file.txt;
4. Если такой папки нет, то нужно вывести сообщение с ошибкой.
Третий пункт нужно расписать более подробно: перед считыванием текста мы должны создать путь к файлу file.txt. То есть «срастить» путь к папке Databank с названием папки и с именем файла. Мы делали подобную операцию при записи текста в файл в предыдущей статье.
Если с третьим пунктом всё более-менее понятно, то со вторым, думаю, не очень. Как вообще можно узнать есть ли папка с таким-то именем в директории? Вручную что ли открыть и посмотреть?! Нет. Помните, в начале статьи я говорил, что сегодня будет очень интересно. Именно эту часть я и имел ввиду. Сейчас мы с Вами научимся изучать содержимое папок, не открывая Проводник.
Узнать о вложенных директориях можно с помощью функции FindFirstFile(). Давайте найдем ее в справочнике:
Первый параметр тут – путь к папке, которую мы хотим проверить (папка, в которой мы будем искать лежащие в ней папки), второй – указатель на структуру WIN32_FIND_DATA, атрибутам которой присваиваются значения, несущие информацию о найденных в директории папках. Посмотрим, что из себя представляет эта структура:
Как же тут много всего! Начинаем разбираться что тут за что отвечает:
1. Атрибут dwFileAttributes– атрибуты файла, то есть его описание. Их очень много. Если нужно, смотрите тут: https://learn.microsoft.com/ru-ru/windows/win32/fileio/file-attribute-constants.
2. ftCreationTime– тут у нас хранится структура, в которой содержатся сведения о том, когда файл или папка были созданы.
3. ftLastAccessTime– в этой структуре находится информация о том, когда в последний раз файл открывался или записывался.
4.ftLastWriteTime – Здесь хранится информация о том, когда в последний раз файл перезаписывался.
5. nFileSizeHigh– Размер файла в байтах.
6. nFileSizeLow– Аналогично.
Размер файла вычисляется по формуле:
FileSize= nFileSizeHigh*(MAXDWORD + 1) + nFileSizeLow
MAXDWORDздесь – максимально возможное значение переменной типа DWORD. Про переменные DWORD мы с Вами не говорили раньше. Чтобы Вам было понятнее и чтобы Вы не путались впредь, поясняю: DWORD – это всего лишь unsigned long int. То есть просто беззнаковый расширенный int. Значение MAXDWORD равно 4294967295.
7. dwReserwed0 – тег точки повторного изменения. Применяется только при соблюдении определенных условий.
8. dwReserwed1 – Зарезервировано.
9. cFileName[MAX_PATH] – Имя файла. Собственно то, что мы и собираемся искать.
10. cAlternateFileName– Альтернативное имя файла (имя файла не превышает восьми символов, отделено от расширения точкой, расширение состоит из трех символов, не содержит в себе пробелов).
11. dwFileTipe– Не используется.
12. dwCreatorTipe– Не используется.
13. wFinderFlags– Не используется.
То есть суть функции FindFirstFile() такова: при вызове, она переходит в указанную директорию и заполняет структуру WIN32_FIND_DATA данными файлов или папок, которые там обнаружит.
Давайте посмотрим как это всё работает. Сделаем так, чтобы, помимо всего прочего, при нажатии на кнопку «Считать» выскакивало диалоговое окошко со списком, присутствующих в директории Databank папок. Чтобы реализовать подобное нужно всего лишь создать структуру типа WIN32_FIND_DATA, в которую будут загружаться данные о папках и затем передать указатель на эту структуру в функцию FindFirstFile() при ее вызове из обработчика команд. Структуру создадим глобальную (то есть в файле resources.h) и назовем ее, скажем, FolderInform. Создаем:
Возвращаемся в файл main.cpp. Переходим к функции ReadFromFile(). Отступаем от последней операции пару строчек и вызываем функцию FindFirstFile(), а затем сообщение, с помощью которого выведем название одной из папок, обнаруженных в директории Databank. Первым параметром мы должны передать в функцию путь к папке. Он будет таким: «E:\\C_Project\\Databank\\*». Символ «*» показывает программе, что нужно искать результат в папке, название которой предшествует ей, то есть в «Databank». Поиск файлов в директории – это всегда новый процесс. Чтобы мы могли им управлять, нам нужно дать ему имя. Назовем его, пожалуй FindFolder:
Компилируем, запускаем приложение и жмем «Считать».
Очень интересно. Текста в появившемся сообщении нет, зато есть точка. Что бы это могло значить? Сейчас поясню: точка говорит нам о том, что папка не пуста и о том, что она вообще существует (то есть, по сути, точка – это папка Databank, знаю, звучит непонятно, но, поверьте, немного поработаете с FindFirstFile() и это для Вас станет нормой). Но мы ведь и так об этом знаем! Где текст? Где названия папок, которые там лежат? А они всё там же! Просто мы их не можем считать за один раз: функция FindFirstFile() инициализирует считывание информации о файлах, лежащих в директории, но не обо всех сразу! Она не выводит список вложенных папок, а делает это итеративно. Поясняю простыми словами. Мы должны проверить каждый файл по отдельности. Чтобы продолжить поиск папок (или файлов), в WinApi есть отдельная функция. И имя ей FindNextFile(). Заглянем в справочник:
Как видите, ничего сложного: первый параметр – хэндл, который мы создавали для FindFirstFole() (помните, я говорил, что он нам пригодится), второй параметр – структура WIN32_FIND_DATA, то есть FolderInform.
Компилируем, запускаем, жмем кнопку «Считать»:
О! Прогресс! Теперь у нас не одна точка, а две! Это значит, что наша папка Databank лежит не в корневой папке, то есть сама папка лежит в папке. Вызовем FindNextFile() еще раз:
Компилируем, смотрим, что получится:
Ну вот: и буковки, и циферки, и никаких точек. Это говорит о том, что цели своей мы достигли. Только вдумайтесь: наша программа заглянула в указанную директорию и показала нам название одной из папок, которая там хранится. Нужно больше? Что ж, давайте последовательно выведем имена всех папок в диалоговом окне. Для того, чтобы подобное реализовать, воспользуемся циклом while. Условием для него будет сама функция FindNextFile(). Дело в том, что если функция завершается успешно, то она возвращает ненулевое значение, в противном случае мы получим ноль. Кажется я не затрагивал эту тему в прошлых статьях, но: если в условии, совершенно для любого оператора (для цикла for, для цикла while, для ветвления if-else) будет находиться ноль, то это будет равнозначно значению false. Если в условии будет получаться значение, отличное от нуля, то мы получим true. Внутри цикла мы будем построчно заполнять массив, который создадим заранее:
Вызывать функцию из условия цикла на самом деле – очень распространенная практика. Думаю, впоследствии мы с Вами всё чаще будем сталкиваться с подобным. Компилируем, смотрим, что у нас получится:
Заглянем в Проводник, в папку Databank:
Как говорится, найдите три отличия. Разумеется трех нету, но два есть точно: это точки. Может показаться, что они лишние в этом списке. На самом деле нет. Более того, в некотором роде, они могут быть нам полезны! Где и для чего? Сейчас мы этот вопрос рассматривать не будем, однако, возможно, позже мы к нему вернемся. В настоящее время нас должен интересовать другой вопрос: как убрать точки из списка? Даю подсказку: «длина имени папки». Имена папок в этой директории – это ВИН-коды. А они, как мы знаем, семнадцатизначные. Папка VIN – это тестовая папка, созданная нами в образовательных целях, никакой значимости она не имеет. Тем не менее, давайте всё же сделаем наш код более универсальным. Пусть программа отфильтровывает только имена «.» и «..». То есть пропускает только те имена, в которых больше двух символов. Применяем внутри цикла условие if:
Обратите внимание: мы закомментировали строку495, в которой копировали первый элемент в массив, чтобы избежать появления в нашем списке точки.
Компилируем, запускаем, жмем «Считать»:
Отлично! Всё работает. Никаких точек в списке не наблюдается. Продолжаем двигаться дальше. Теперь сделаем так, чтобы при введении символа в строку VIN, наша программа выполняла поиск в базе ВИН-кодов (то есть имен вложенных в директорию Databank папок) и показывала нам подсказки – возможные варианты кодов. Для того, чтобы было интереснее работать, насоздаем новых вымышленных клиентов:
Имена некоторых папок похожи. Это сделано специально (для чистоты эксперимента), ведь иногда ВИН-коды авто могут отличаться друг от друга всего одним символом. Сначала реализуем следующее: пусть при вводе очередного символа запускается поиск вложенных папок в директории Databank и их список выводится в диалоговом окошке. Временно закомментируем строки с 455 по 490, то есть код, который считывает текст из файла и помещает его в текстовые поля «Фамилия», «Имя», «Отчество».
Теперь при вызове функции ReadFromFile() будет только выполняться перебор папок в указанной директории. Мы с Вами уже умеем обрабатывать сообщение от текстового окна, которое оно отправляет при вводе в него с клавиатуры символа. Мы ознакомились с этим когда писали функцию StarInEdit(), заменяющую звездочки в окошке на вводимые символы и обратно. Перейдем в обработчик команд. Здесь мы вызывали указанную функцию:
Сразу же после StarInEdit() вызовем функцию ReadFromFile():
Компилируем, запускаем приложение, в строку VIN-номер вводим любой символ с клавиатуры:
Наша функция работает, но список в диалоговом окошке нам мало интересен в том плане, что через него нельзя взаимодействовать с программой. Сделаем так, чтобы при вводе символа в поле VIN-номер, имена папок отображались не в диалоговом окне, а скажем в виде выпадающего списка под строкой VIN-номер (под текстовым полем, в котором по умолчанию вписаны звездочки). Давайте рассуждать. Сам по себе, на пустом месте, текст появиться не может, то есть мы должны будем создать окно, в которое будет вписан этот текст. Это во-первых. Во-вторых, это окно должно давать нам возможность взаимодействовать с программой через текст, который будет в него выведен (то есть мы кликнули на соответствующую строчку в нем и программа на это как-то отреагировала). Ну и, наконец, в третьих – в это окно программа должна будет поместить все имена папок, расположенных в директории Databank (пока что именно всех, сортировкой займемся чуть позже), при этом оно не должно быть слишком большим, чтобы не перекрыть собой кнопки. Самым оптимальным вариантом будет создать окно типа «Listbox». Окно данного типа может иметь полосу прокрутки, а также предоставляет пользователю возможность взаимодействовать с программой через введенный в него текст. Заполнить его мы сможем через цикл while(), который ранее использовали для заполнения массива BufFolder:
Мы просто заменим функцию strcat() на функцию добавления в окно listboxновой строки. На первый взгляд может показаться, что всё просто, но это не так. Основная проблема состоит в создании окна. Мы не знаем когда и какого размера его создавать. Поясняю. Предположим, что у нас в папке лежат девять папок. У каждой из них имя начинается с символов «ХТ23». После них названия различаются, то есть у первой имя «ХТ230200989777647», у второй «ХТ231447596839502», у третьей «ХТ230800983375715» и т.д. Мы можем создать listbox высотой в две или три строки и полосой прокрутки и поместить в него названия всех этих папок. Тут проблем не возникнет. Но что если в директории Databank будет лежать всего одна папка? Согласитесь, listbox с двумя пустыми строками будет смотреться как минимум глупо. Предлагаю следующий вариант решения:
1. При вводе символа в поле VIN-номер создать окно listboxвысотой в три строки;
2. Считать в listboxимена папок;
3. Спросить у listboxсколько в нем строчек;
4. Если строк в listbox меньше трех, то перестроить listbox, изменив его высоту на две строки;
5. Если строка в listbox всего одна, перестроить listbox, изменив его высоту на одну строку.
Писать код будем постепенно, детально прорабатывая каждый шаг из пяти перечисленных. Начнем с первого: при вводе данных в поле VIN-номер создадим под ним окошко типа listbox. Стоит заметить, что при вводе каждого последующего символа, окно будет перестраиваться снова и снова. Однако, это нам только на руку. Почему? Позже сами увидите! Итак, нам нужно создать новое окно. А это значит нам понадобится новая глобальная переменная типа HWND. Назовем ее lbVIN, сокращенно от ListBoxVIN. Прописываем ее в файле resources.h. Создавать окошко мы будем в функции ReadFromFile() сразу после вызова функции FirstFindFile().
Координаты х=390 и у=245 мы взяли из функции создания текстового поля VIN-номер (только сместили окно вниз на 20 пиксел, то есть на высоту окна VIN-номер). Создать окно без параметров hWndParrent и hInstanceмы не сможем, поэтому добавляем их в заголовок функции ReadFromFile(). То есть, если не учитывать закоментированный ранее участок кода, эта функция будет иметь следующий вид:
Правим описание функции в файле resources.h и убираем вызов функции из меню кнопки «Считать» в обработчике команд. Также правим вызов функции в обработчике в момент ввода символа в текстовое окошко:
Компилируем, запускаем приложение, вводим любой символ в поле VIN-номер:
Первый шаг выполнен: окно типа listboxсоздается. Мы специально указали атрибут LBS_STANDARD среди прочих при создании окна, чтобы оно было очерчено со всех сторон тонкой линией. В противном случае оно сливалось бы с окошком VIN-номер и его сложно было бы интерпретировать как окно-подсказку.
Переходим ко второму шагу. Напоминаю. Здесь нам нужно считать в созданное окно имена папок, хранящиеся в директории Databank. Записать строку в окно listboxпроще простого: достаточно отправить ему сообщение с текстом, который нужно поместить в эту строку и кодовым словом «LB_ADDSTRING». Сообщение мы будем отправлять из цикла, в котором раньше заполняли массив BufFolder.
Обязательно приводим строку с именем папки к типу LPARAM, иначе компилятор на нас заругается. Компилируем, запускаем, в поле VIN-номер вводим любой символ:
Ну вот: данные выводятся в окошко-подсказку. Более того: содержимое окошка можно легко перелистывать. Смело можно утверждать, что второй шаг выполнен!
Переходим к третьему шагу. Теперь нам нужно узнать сколько в окошке-подсказке заполненных строчек. Зачем? Пожалуй я Вам покажу наглядно. Временно удалим из директории Databank все папки кроме одной:
Теперь запустим наше приложение и введем символ в поле VIN-номер:
Некрасиво. Две пустые строки портят впечатление о нашей программе. Решить данную проблему можно двумя способами: спросить у окна lbVIN сколько в нем заполненных строк, а затем перестроить его, либо ввести в цикл, в котором мы заполняем окно именами папок, переменную-счетчик, и в зависимости от ее значения, корректировать размер окна. Как первый, так и второй способ применимы в данном случае. Выберем первый. Он для нас наиболее предпочтителен, так как в будущем нам предстоит сортировка списка в окошке lbVIN, и, как следствие, постоянное увеличение или уменьшение его размеров. Узнать сколько строк в listboxможно отправив ему сообщение с командным словом «LB_GETCOUNT». Выглядеть это будет приблизительно так:
Внести изменения в уже созданное окно можно с помощью функции SetWindowPos(). Найдем ее в справочнике:
Первый параметр здесь – дескриптор окна, параметры которого мы хотим откорректировать.
Второй параметр – дескриптор окна, которое расположено перед данным окном. То есть дескриптор окна, поверх которого «на мониторе» лежит окно, параметры которого мы будем корректировать. Также здесь допускается использовать одно из следующих значений:
- HWND_BOTTOM – Если предыдущий параметр является дескриптором самого верхнего окна, то оно переместится в нижнюю часть всех окон. То есть как бы «поднырнет» под остальные;
- HWND_TOP – Окно расположится в верхней области порядка, то есть встанет поверх всех остальных окон;
- HWND_TOMPOST – Окно расположится поверх всех окон, которые не будут являться верхними;
- HWND_NOTOMPOST – Окно смещается с первой позиции и устанавливается за всеми верхними окнами.
Третий и четвертый параметр – это координаты окна. Если откорректируем эти значения, то с легкостью сможем подвинуть окошко, даже не касаясь его курсором.
Пятый параметр – ширина окна.
Шестой параметр – высота окна. Как раз ее-то мы и будем менять.
Седьмой параметр – флаг, определяющий положение окна и его размер. Значения тут могут быть следующие:
Другим окном в процессе перерисовки lbVINне перекроется, поэтому по Zнам его положение менять не нужно. Рамка вокруг окна уже отрисована, скрывать, отображать и уж тем более деактивировать нам окно не нужно. Остается только один допустимый параметр, который мы можем тут указать: SWP_NOMOVE, так как смещать окно мы не планируем. Добавляем функцию SetWindowPos() в наш код:
Что здесь вообще будет происходить? Внимание на строчку 507. Здесь мы узнаем сколько строк в окне lbVINзаполнены текстом. Затем переходим к условию (строка 508). Если количество строк больше нуля, но меньше трех, то программа должна выполнить действие, описанное следом в фигурных скобках. Иначе – проигнорировать это действие и выполнять следующую операцию. В фигурных скобках у нас функция SetWindowPos(). Обратите внимание на то, как мы задали высоту окна: CountLBVIN*20. То есть количество строк мы умножаем на высоту одной строки. Для одной строки это будет 20, а для двух строк 40. По сути мы решаем проблему одной строчкой кода.
Компилируем, смотрим, что получилось:
После ввода символа в строку VIN-номер, под ней появилось окно со списком имен папок, хранящихся в папке Databank. Как видим, две пустые строки из окна удалились. Нужно проверить, не будет ли сбоить программа, если в списке будет две строки. Открываем Проводник, заходим в директорию Databank и добавляем сюда одну новую папку:
Теперь запустим нашу программу и введем любой символ в строку VIN-номер:
Пустая строка в окне lbVIN отсутствует. Это очень хорошо, ведь это означает, что наш код работает как надо. Однако радоваться рано. Нужно проверить, не будет ли условие, которое мы прописали в функцию, мешать созданию полноценного, трехстрочного, окна lbVIN. Переходим в папку Databank и возвращаем туда все удаленные ранее папки:
Запускаем наше приложение и смотрим как программа отреагирует на ввод символа в поле VIN-номер:
Функция работает корректно, а это значит мы выполнили пункты четыре и пять. То есть вновь можно перейти к более сложной задаче.
Мы сделали так, чтобы при вводе символов в поле VIN-номер, под ним появлялось окно-подсказка со всеми возможными ВИН-кодами, которые хранятся в базе нашей программы. А теперь вдумайтесь: пользователь вводит букву L, а ему в подсказке выпадает всё, начиная от A до Z. Понимаете, куда я клоню? Верно: нам нужно сделать сортировку ВИН-кодов. Программа должна показывать в окне подсказке только те номера, которые частично совпадают с введенным в поле VIN-номер. То есть, если мы введем букву А, то в окошке должны всплыть все ВИН-коды, которые начинаются с А и никакие другие! Давайте подумаем как это можно сделать.
Рассуждаем: мы должны знать какие символы введены в строку VIN-номер. Без этого нам не с чем будет сравнивать имена папок, лежащих в Databank. То есть мы должны будем считать текст из текстового поля VIN-номер. Затем нужно будет удалить из считанной строки звездочки (чтобы остались только буквы и цифры). После чего можно будет заполнять окошко-подсказку, сравнивая при этом имена папок со считанными из окна VINсимволами. Считывать текст из окошек типа edit мы уже умеем. Для этого применяется функция GetWindowText().
Параметр hEdit, то есть дескриптор окошка VIN-номер, мы зададим в функцию ReadFromFile() при ее вызове. Если это сделать изнутри функции явно, программа неверно обработает наш запрос и ничего не считает. Теперь мы должны отсечь часть строки, в которой находятся звездочки. В этом нам поможет функция strncpy(). В отличие от функции strcpy(), она не просто копирует текст из одного массива в другой, но и усекает его на указанное количество символов. Синтаксис у нее простой:
strncpy(массив назначения, исходный массив, кол-во копируемых символов).
Ничего сложного. Исходный массив – это BufEditVIN. Массив назначения создадим перед вызовом функции GetWindowText(). Назовем его BufSimVIN. Также выделим ему 24 байта памяти. Теперь решим вопрос с количеством копируемых символов. Окошко VIN-номер мы сделали так, что звездочки при вводе символов в него, всегда находятся справа от курсора. То есть позиция курсора на единицу больше количества введенных в окно символов. Поясняю: мы ввели два символа, курсор стоит на месте потенциального третьего, ввели пять символов – на месте шестого. То есть мы можем узнать сколько символов было введено в окно просто спросив у него, где в настоящее время находится курсор и отняв от полученного ответа единицу. Напоминаю: чтобы узнать у окна типа edit позицию курсора, нужно отправить ему сообщение с кодовым словом EM_GETSEL. Обычно эта команда возвращает начальную и конечную позиции выделенного текста, но в случае если текст не выделен, она просто вернет позицию курсора. Проверим, будет ли корректно работать наша программа, если сделать всё так как мы описали. Как всегда вызовем диалоговое окошко. На этот раз поместим в него текст после того, как уберем из строки лишние символы:
Компилируем код, проверяем что получилось:
Наша функция работает некорректно. Где мы могли допустить ошибку? Отвечаю: нигде. Наш код верный. Но почему же тогда наша функция не убрала лишние символы из текста? Давайте посмотрим сюда:
Мы последовательно вызываем две функции: StarInEdit() и ReadFromFile(). По факту, компилятор будет видеть на месте этих строчек сами функции с подставленными в них значениями параметров. Мы получаем позицию курсора через сообщение, так? Так. И сообщение отправляется главному окну с помощью функции SendMessage(). Напомню: сообщения SendMessage() выполняются обработчиком сразу после получения, а вот сообщения PostMessage() становятся в очередь и ждут, когда обработчик выполнит все операции и сможет их обработать. У нас две функции, в которых есть SendMessage() и PostMessage(). Покажу «стыковку» этих функций вот так:
Не совсем верно, ведь программа выполняет и другие операции, помимо отправки сообщений, но сейчас для меня главное, чтобы Вы поняли суть очередности обработки сообщений от дочерних окошек. Смотрите: выполнив все предыдущие операции, обработчик получает сообщениеEM_SETSEL через SendMessage(), строчка 365, он обрабатывает его тут же, без очереди, и выделяет символ, находящийся в конце строки VIN. Затем он получает сообщение EM_REPLACESEL, также отправленный ему с помощью SendMessage(). Программа удаляет выделенный символ, и, внимание: наш курсор теперь размещен в конце строки. Затем мы отправляем обработчику команду EM_SETSEL, то есть вернуть курсор в начало строки, установить его справа от введенного ранее символа. И всё было бы хорошо. НО! Сообщение встает в очередь таких же сообщений, а обработчик переходит к сообщению EM_GETSEL, которое мы отправляем ему уже из функции ReadFromFile(), потому что оно отправлено с помощью функции SendMessage()! То есть мы находим позицию курсора еще до того, как он вернется на нее, в то время, когда он стоит в конце строки. Казалось бы, в чем проблема: отправим сообщение EM_GETSEL через функцию PostMessage() и проблема решится сама собой. Не решится. Если мы это сделаем, то сообщение встанет в очередь и будет обработано только после всех других, «срочных», сообщений. И, скорее всего, в лучшем случае, мы получим пустой массив BufSimVIN. Что же тогда делать? Я предлагаю схитрить: давайте вызовем нашу функцию ReadFromFile() только тогда, когда сообщение EM_SETSEL, отправленное из функции StarInEdit() с помощью PostMessage() уже будет обработано. Но как нам узнать, что это произошло? А очень просто: мы добавим в очередь сообщений команду, которая вызовет функцию ReadFromFile(). Если не поняли, поясняю. У нас имеются два типа сообщений: одни выполняются обработчиком команд мгновенно – это сообщения, отправленные через SendMessage(), и есть сообщения, которые выполняются по очереди – это те, которые были отправлены через PostMessage(). Нам нужно, чтобы функция ReadFromFile() начала обрабатываться только после того, как команда EM_SETSEL, отправленная из функции StarInEdit, будет уже обработана. То есть только после того, как курсор займет нужную позицию в строке VIN. Чтобы это сделать, мы создадим собственную команду и отправим ее обработчику команд с помощью PostMessage(). Так как отправим мы ее ПОСЛЕ отправки EM_SETSEL, то и в очередь сообщений она встанет ПОСЛЕ нее. И нам неважно, будут ли еще команды в очереди между ними или нет, главное, что EM_SETSEL успеет обработаться. Пробуем реализовать свою задумку. Создаем команду. Назовем ее CallFunc1 (сокращенно от «Вызвать Функцию №1»). Присвоим этой команде идентификатор 300. Так и пишем в resources.h:
#define CallFunc1 300
Затем возвращаемся в main.cpp и идем в обработчик команд. Здесь мы заменяем вызов функции ReadFromFile() из обработчика команд на отправку сообщения с созданной нами командой через PostMessage():
Теперь обработаем нашу команду. Для этого перейдем в switch(Message) и разместим здесь case для CallFunc1:
Сообщение CallFunc1 теперь не может быть обработано раньше, чем сообщение EM_SETSEL, потому как стоит в очереди позже него, а значит и функция ReadFromFile() будет вызвана только после того, как курсор в окошке VIN вернется на нужную нам позицию. Проверяем: компилируем, запускаем приложение, в поле VIN-номер вводим любой символ:
И, кажется, что всё отлично, но…
Дело в том, что после создания окошка-подсказки, программа инициирует повторный вызов функции ReadFromFile(). И на этот раз в массив BufSimVINвновь считывается полная строка из BufEditVIN. Для того, чтобы избежать двойного вызова ReadFromFile() добавим небольшое условие в наш код:
Создадим глобальную интовую переменную IterVINи с помощью нее будем отслеживать сколько раз программа пытается вызвать нашу функцию. Вызов ДО построения окошка lbVIN мы проигнорируем, а вот на второй вызов мы отреагируем и отправим окну hwnd сообщение с командой CallFunc1. Проверим, корректно ли будет работать наша функция:
После нажатия на кнопку ОК, диалоговое окно закрывается, появляется окошко-подсказка, функция ReadFromFile() повторно не вызывается. Эту проблему мы решили. Теперь можно вернуться к сортировке списка в окне listbox. Чтобы отсортировать допустимые имена папок от недопустимых мы будем сравнивать части их названий с введенными в поле VIN символами. В этом нам поможет функция lstrcmpi(). Ей мы уже ранее пользовались, когда отслеживали ввод ФИО в обработчике команд. Прежде чем сравнивать строки нам нужно сделать их одинаковыми по длине. То есть если мы ввели в поле VIN один символ, то и сравнивать мы его должны только с первым символом имен папок. Ввели два символа – в именах папок также нужно учитывать только два первых символа и т.д. Переходим в функцию ReadFromFile(). Здесь создадим новый массив на 24 ячейки, в который будем класть урезанные имена папок из директории Databank. Назовем его AbbName. Теперь сделаем так, чтобы после считывания имени папки из директории Databank, наша программа «урезала» его до необходимого количества символов, а затем сравнила с той строкой, что хранится в массиве BufSimVIN, то есть с тем, что мы ввели в окно VIN. В случае, если строчки совпадут, программа должна будет добавить соответствующее имя папки в окошко-подсказку lbVIN:
Обратите внимание на строку 526: функция lstrcmpi(), в случае соответствия сравниваемых строк, возвращает значение 0. С помощью восклицательного знака мы инвертируем этот ноль, превращая его в логическую единицу, тем самым заставляя программу выполнить код, находящийся в фигурных скобках при данном операторе if.
Теперь наша функция сначала проверяет подходит ли имя папки в директории по длине, затем сравнивает его с введенной частью имени, и только потом добавляет в список подсказок в окно lbVIN. Проверим, будет ли работать наша программа:
Как видим, программа работает, но не вполне корректно: позади окна-подсказки при повторном вводе символов в окно VIN, возникает «паразитное» окошко. Почему так происходит? Посмотрите на рисунок 85. Все дело в строке 519. При каждом вызове функции ReadFromFile(), а она вызывается каждый раз, когда мы вводим или стираем символ в окне VIN, программа строит трехстрочное окно-подсказку. Делает она это даже если оно уже существует. Как с этим бороться? С помощью условия. Перед тем, как строить окно lbVIN, программа должна проверить, а вдруг оно уже построено? Узнать, существует ли окно, можно с помощью функции IsWindow(). Найдем ее в справочнике:
Если окно, дескриптор которого мы передаем в функцию, существует, то функция вернет ненулевое значение. Если же окно не существует, то IsWindow() вернет ноль. Отсюда следует, что нам нужно сделать так, чтобы, если окна lbVIN не существует, программа его создала, в противном случае – проигнорировала строку с его созданием и перешла к следующей операции.
Проверим. Компилируем код, запускаем приложение, вводим несколько символов в строку, а затем стираем часть из них:
И вновь, наша функция работает лишь частично: окно lbVIN не создается вновь при каждом введенном или стертом символе, но в нем остаются старые строки. Исправляем этот недочет. Добавляем в наше условие оператор else. Нам нужно удалять старые строки из окна-подсказки перед каждым его заполнением. Чтобы это сделать, нам достаточно просто отправить окну lbVIN сообщение с командой «LB_RESETCONTENT»:
Компилируем, запускаем, проверяем:
И вот, наконец, мы учли все нюансы и наша функция работает как надо. Продолжить, пожалуй, нам придется в следующей статье, так как Дзен не позволяет прикреплять к одной публикации больше ста картинок. А без иллюстраций, сами понимаете, никуда. На заключительном изображении приведу код из файла main.cpp (может быть даже получится прикрепить и картинку с кодом из resources.h). Постараюсь добавить к нему как можно больше информативных комментариев. Надеюсь, что сегодняшняя статья была Вам полезна. Поощрите лайком, если это так: буду знать, что кому-то мой труд да помог. Спасибо, что читаете. Удачи в учебе и труде!
PS: Рисунки с кодом искажаются Дзеном из-за размеров и выглядят после загрузки приблизительно так: (Рисунок 92 и Рисунок 93), поэтому код к этой и последующей статьям, буду загружать отдельно. Вот ссылка на код к этой статье: https://dzen.ru/a/aOM4xMAP8h58Y0UL.