Здравствуйте, Дорогие друзья! В прошлой статье мы с Вами остановились на том, что реализовали функцию, которая анализирует введенный в текстовое поле текст и создает окошко с подсказкой. Также мы учились считывать данные из файла и вносить его построчно в отдельные графы. Эта статья будет немного сложнее предыдущих, поэтому сосредоточьтесь! Если что-то не поймете сразу, лучше перечитайте несколько раз, пока смысл написанного не станет Вам понятен! Итак. Сегодня мы продолжим работать над функцией ReadFromFile(). Научим ее автоматически переходить в папку с именем, которое наиболее соответствует введенному в поле VIN-номер, и считывать оттуда информацию в графы Фамилия, Имя, Отчество. Также, не обещаю, но очень надеюсь, что в этой статье мы с Вами, наконец-то сделаем функцию для шифрования данных в файлах и персонализируем файлы, то есть создадим для каждого типа файлов свое расширение и иконку. Приступаем!
Наша работа завершилась на том, что мы сделали сортировку в окне lbVIN. На первый взгляд кажется, что функция, которую мы написали, работает корректно. Однако, это не так. Выполняем простой тест программы.
1. Введем в строку VINс клавиатуры несколько символов, которые не содержатся в именах папок в директории Databank. Напомню, сейчас там лежат вот такие папки:
И как же отреагирует на это наша программа?
Рисунок 2 – Некорректная реакция программы на введенные символы
Вот и первая наша недоработка: при вводе в строку VIN ВИН-номера, которого нет в базе программы, создается пустое трехстрочное окно lbVIN.
Ошибка: Создание окна lbVINпри вводе неизвестного ВИН-номера.
Вариант устранения:Разрушать окно lbVINпри несоответствии введенного ВИН тем, что имеются в базе.
2. Введем в строку VINВИН-номер, имеющийся в базе, а затем сотрем его:
При удалении символов (то есть при замещении их звездочками), окно lbVIN не закрывается. Более того, в него считываются все ВИН-номера из директории Databank.
Ошибка:Окно lbVINсохраняется при полном удалении ВИН-номера из строки VIN.
Вариант устранения:Разрушать окно lbVIN, если строка VIN полностью заполнена звездочками.
3. Начнем вводить корректный ВИН-номер в строку VIN, затем введем некорректный символ.
Ошибка:Окно lbVIN сохраняется при вводе ВИН-номера, отсутствующего в базе.
Вариант устранения: Разрушать окно lbVIN, если вводится незнакомый ВИН-номер.
Ошибки обнаружены, варианты устранения найдены. Переходим в main.cpp приступаем к работе. Недоработки будем устранять в порядке их обнаружения. Начнем с первой. Нам нужно разрушать окошко lbVIN, если в базе не обнаружится соответствующего вводимому ВИН-номера. Пролистываем код и в конце функции ReadFromFile(), создаем новое условие. Очень простое: если в окно lbVIN не было ничего записано, то есть если оно осталось пустым, разрушить его. Все указанные операции мы с Вами уже делали в прошлой статье. В случае чего, можете перечитать ее еще раз.
Обратите внимание: мы используем переменную CountLBVIN, созданную ранее. Нам незачем создавать новую, так как в ней уже хранится информация о количестве строк в окне lbVIN.
Компилируем, запускаем приложение, проверяем, корректно ли будет работать наша программа:
В директории Databank отсутствуют папки с именем, начинающимся с буквы «d». После ввода данного символа с клавиатуры в окно VIN, окошко lbVIN не создается. А если говорить точнее, то оно создается и тут же удаляется, а так как это происходит очень быстро, мы этого не замечаем. Данная модернизация программного кода также должна была избавить нас от ошибки №3, так как при вводе нового (отсутствующего в базе программы) ВИН-номера, окно lbVIN остается пустым, а значит условие, созданное нами должно выполняться. Проверяем:
Осталось «вылечить» ошибку №2. Сделать это не сложно: достаточно сравнить строку, находящуюся в окошке VIN с шаблонной строкой «*****************» и если они совпадают – удалить окно lbVIN. Наша функция ReadFromFile() устроена таким образом, что при каждом вводе или удалении символа из окна VIN она считывает из него текст. Считанная строка загружается в массив BufEditVIN. Для сравнения строк, как всегда, применим функцию lstrcmpi().
Не забываем сделать инверсию результата работы функции lstrcmpi(). Напоминаю, если сравниваемые строки одинаковые, то она возвращает ноль. Код в фигурных скобочках после оператора ifбудет работать только если в простых скобках возле if будет получаться «ПРАВДА», то есть:
if(если здесь правдивое условие, то код в фигурных скобках выполнится)
{
Код в фигурных скобках;
}
Компилируем, запускаем, проверяем:
Вот и всё, теперь можно идти дальше. Сделаем так, чтобы при вводе нескольких символов с клавиатуры в окно VINи появлении окна-подсказки lbVIN, можно было бы перейти из VIN в lbVINнажатием на клавишу «ВНИЗ». Зачем? Чтобы выбрать ВИН-номер из подсказки.
При нажатии на клавишу «ВНИЗ» или, как принято называть ее на английском, «Down», окну, которое на тот момент владеет фокусом ввода, системой отправляется сообщение «WM_KEYDOWN». Основная проблема заключается в том, что при вводе ВИН-номера, фокус ввода будет захвачен окном VIN. А тип этого окна – edit. Именно оно получит сообщение WM_KEYDOWN , а не окно hwnd. И обрабатывать оно будет это сообщение по-своему (сдвинет курсор в строке на один символ вправо).Что же нам делать? Как отследить тот момент, когда клавиша Down будет нажата? Поможет нам в этом хук. Уверен, что найдутся шутники, которые захотят спросить «Левой или правой?». Ни той, ни другой. Хук – это процедура перехвата событий. Называется она так от английского «hook» - крюк. То есть с помощью хука можно что-то поймать на крючок. И ловить мы будем сообщение WM_KEYDOWN, которое система отправит окну VIN. Когда я учился в универе, хукам нас не учили. Почему? У меня два варианта ответов: первый – подобные знания преподаватели посчитали «высокоуровневой магией» и решили, что простым смертным в их повседневной жизни, она вряд ли пригодится, второй – напротив, посчитали эту тему слишком простой для рассмотрения и предоставили нам самим во всем разобраться. В любом случае, мы с Вами от хуков бежать не будем. В полной мере с ними, конечно, тоже знакомиться не станем, но принцип работы их рассмотрим.
Помните, как в прошлой статье, мы считывали строку из окошка VIN, а затем «урезали» ее до определенного количества символов и выводили в диалоговом окне? Сначала функция, которую мы написали, работала некорректно. Помните почему? Из-за очередности выполнения команд окном hwnd. Также мы затронули тему «очередь сообщений». Нам пришлось отправить сообщение окну hwnd, поставить его в очередь, чтобы обработчик команд вызвал функцию ReadFromFile() только после того, как все сообщения, отправленные из функции StarInEdit() через PostMessage() будут обработаны. А теперь представьте, что мы могли бы отследить тот момент, когда окно VIN получило сообщение EM_SETSEL. Во-от! Если бы мы могли «подглядеть», какие сообщения получает окно VIN, то с легкостью бы поняли в какой момент можно вызывать функцию ReadFromFile(), зная, что курсор в этом окошке уже установлен на нужное место.
В текущей ситуации мы наблюдаем похожий случай: мы хотим знать, когда клавиша Down будет нажата, находясь при этом в окошке VIN. Поясняю для тех, кто еще не понял: если курсор в VIN будет моргать, то есть если это окошко будет выбрано, то все сообщения от клавиатуры будут адресованы именно ему. Никакое другое окно этих сообщений не получит. А нам очень нужно увидеть, что это за сообщения. Поэтому мы будем следить за окном VIN и заглядывать в каждое сообщение, которое ему будет приходить. Как только придет сообщение WM_KEYDOWN, мы заберем его себе и ни в коем случае не отдадим его окошку VIN, потому что мы хотим, чтобы программа выполнила ту операцию, которую мы запланируем, а не ту, что предопределена для окошка типа edit.
Фуф! Объяснил, как мог! Теперь можно и к хуку переходить. Нам нужна функция, которая позволяла бы отслеживать сообщения, которые получает от системы какое-либо окно. Такая функция есть. Называется она SetWindowsHookEx(). Найдем ее в справочнике:
Итак, у нас четыре идентификатора. Первый, типа int, задает какой тип будет у перехватчика. Этот параметр может принимать следующие значения:
Нам, ясное дело, нужно будет использовать значение WH_KEYBOARD.
Второй параметр – переменная с неизвестным нам типом данных HOOKPROC. Не стоит бояться нового, ведь это очень даже знакомое нам старое – указатель на процедуру перехватчика. Ключевое слово здесь «процедура».
В качестве значения третьего параметра указываем NULL. Поясняю почему: если присвоим такое значение, то перехватчик будет отслеживать сообщения в текущем потоке, либо в стороннем потоке, связанном с текущим.
Данный параметр – идентификатор потока, с которым будет связана процедура перехватчика. Если укажем здесь NULL, то отслеживаться будут все системные сообщения (а значит этого делать не стоит!).
С первым и третьим параметрами всё более-менее понятно. Третье тоже немного нам знакомо. Поясняю, почему я так говорю: потому что процедура – не что иное, как обработчик команд (Да-да, тот самый LRESULT CALLBACK). То есть, по сути, мы создадим второй обработчик команд для нашей программы, который будет обрабатывать только те сообщения, которые перехватит наш хук.
На очереди четвертый параметр. Сюда мы должны вписать идентификатор текущего процесса, в котором и должен «орудовать» перехватчик. Найти идентификатор процесса можно с помощью функции GetCurrentThreadId(). Найдем ее в справочнике:
Простыми словами, эта функция возвращает идентификатор потока, который ее вызвал. Как раз то, что нам нужно.
Где мы будем инициализировать хук? Для начала давайте вызовем SetWindowsHookEx() в WinMain(). То есть, по факту, перехватчик начнет работать сразу же после запуска приложения.
Приступим. Систематизируем все написанное выше, касательно применимости хука для решения наших задач. Мы создадим перехватчик, который будет отлавливать сообщения с командным словом WM_KEYDOWN в текущем процессе и обрабатывать его в отдельном, специально созданном для этого, обработчике команд.
Идем в WinMain(). Вызываем отсюда SetWindowsHookEx():
Само-собой нам нужно создать переменные LocalHook и HookProc. Идем в файл resources.h. Здесь выбираем место поудобнее и создаем переменную типа HHOOK LocalHook, а также размещаем заголовок для процедуры HookProc:
Имена, которые мы даем хуку (LocalHook) и процедуре (HookProc) – произвольные, такие, какие захотим. Говорю это на всякий случай, вдруг кто-то решит, что это какие-то ключевые слова и называть их нужно именно так.
Перехватчик создали, теперь можно сделать для него обработчик. Возвращаемся в main.cpp, пролистываем код до конца, отступаем строчек десять и пишем:
Обратите внимание на строчку 578. VK_DOWN – это код виртуальной клавиши Down. Коды пересылаются системой окну в wParam. Все существующие коды можно посмотреть здесь: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes.
Конечно же, для того, чтобы проверить работает ли наша программа, выводим диалоговое окно с сообщением (строка 580). Компилируем, запускаем, переходим в окошко VIN и жмем клавишу Down:
Сообщения два потому что кнопка Down была нажата и потом отпущена. С этим мы обязательно разберемся, но чуть позже, а пока что решим другую проблему: наш хук перехватывает сообщение о нажатии клавиши Downне только у окошка VIN, но и в принципе, у всего нашего приложения. Нас это не устраивает! Давайте вызывать SetWindowsHookEx() в обработчике команд WndProcпри совпадении lParamсообщения EN_SETFOCUS с дескриптором VIN:
Теперь перехватчик начнет работу только когда окно VIN захватит курсор. Можно считать, что половина дела сделана. Почему половина? Представьте: мы передали фокус ввода окну VIN, хук запустится и начнет отлавливать сообщения VK_DOWN «по всей программе», затем мы заберем фокус у окна VIN. Казалось бы, чего переживать, ведь дело сделано. Ан нет! Не сделано. Ведь хук продолжит работать и программа вновь станет некорректно реагировать на нажатие клавиши Down вне окна VIN. Чтобы этого избежать, нам нужно «отключить» хук в тот момент, когда у окна VIN будет отозван фокус ввода. Прекратить отлов сообщений конкретным хуком можно с помощью функции UnhookWindowsHookEx(). Синтаксис у нее простой: она имеет единственный параметр – HHOOK хука, который нужно остановить. Создадим в обработчике case, соответствующий сообщению EN_KILLFOCUS с lParam, равным VIN. Вызовем из него UnhookWindowsHookEx():
Проверяем. Компилируем, запускаем, переходим в окно VIN и нажимаем клавишу Down:
Превосходно! Выведено всего одно сообщение. Поясняю почему: когда появляется диалоговое окно, оно забирает фокус ввода у окна VIN и, благодаря коду, который мы добавили в строки 149 – 152 (Рисунок 17) хук прекращает работу до того момента, когда мы отпустим клавишу Down. Теперь, если не передавать фокус ввода окну VIN, программа будет реагировать на нажатие Down по умолчанию.
Отлично! С самым сложным разобрались. Теперь перейдем к более простому: при нажатии Down в тот момент, когда фокус ввода будет захвачен окном VIN, курсор должен перескочить из него в окно-подсказку lbVIN. Чтобы реализовать подобное, нам нужно заменить строчку кода, в которой вызывали MessageBox() на простое сообщение LB_SELITEMRANGE, отправляемое окну lbVIN посредством функции SendMessage(). В lParam нам нужно указать TRUE(true– побуждает выбрать строку, false наоборот, отменить выбор), в качестве LOWORD wParam передается индекс строки, с которой начинается выделение, в HIWORD – строка в списке, на которой выделение заканчивается.
Чтобы указать LOWORD и HIWORDв lParamсообщения, мы применяем специальный макрос MAKELPARAM. Если вдруг не сработает сообщение LB_SELITEMRANGE, можно использовать LB_SETCURSEL, указавв wParam сообщения индекс строки, которую нужно выделить, а в lParam- NULL.
А теперь внимание! Нам нужно понять откуда инициировать работу перехватчика. Текущее положение вызова функции SetWindowsHookEx() в коде нас явно не устраивает. Поясняю: сейчас хук вызывается при передаче фокуса окну VINи прекращает работать, когда фокус у этого окна мы забираем. Напомню, чтобы всё встало на свои места: в качестве первого параметра мы передали в функцию SetWindowsHookEx() значение WH_KEYBOARD, и теперь хук перехватывает все сообщения от клавиатуры, которые система направляет процессу нашего приложения и связанным с ним процессам. То, что мы обрабатываем нажатие Down в обработчике HookProc не значит, что остальные сообщения достигнут окна VIN. Более того, сообщение LB_SELITEMRANGE (Рисунок 19), на данный момент, также бесполезно по той простой причине, что окно lbVIN, которому мы отправляем это сообщение, не может быть создано, пока в окно VIN не будет введен хотя бы один символ.
Как нам вырваться из этой «петли»? Хитростью и коварством! В обработчике HookProc мы прописали условие только для Down. Остальные сообщения от клавиатуры перехватываются хуком и игнорируются обработчиком. Нам нужно, чтобы всё, кроме Down достигало окна VIN. Решим проблему в наглую: пусть, если хук перехватил не Down, сообщение пересылается окну VIN.
Теперь, если перехватчик поймает не Down, процедура HookProcвременно его отключит (под «его» имеется ввиду хук), симулирует отправку перехваченного сообщения окну VIN, а затем снова включит его. Делаем мы это для того, чтобы наша программа не зациклилась, поймав первое сообщение от клавиатуры.
Данный код также будет работать не вполне конкретно. Посмотрите на строку 606: мы вызываем перехватчик прямо из процедуры, которая ему принадлежит. А это значит, что обработчик при нажатии любой клавиши, кроме Down будет вызван дважды, и, соответственно, в окно VINзапишутся две одинаковые буквы. Чтобы этого избежать, нужно создать условие, согласно которому при повторном вызове процедуры перехватчика, она будет игнорировать полученное сообщение с кодовым словом. Создадим глобальную переменную типа Bool. Назовем ее IsKeyUp, то есть «Отпущена ли клавиша». Присвоим ей значение TRUE. Если фокус ввода будет передан окну VIN, и если будет нажата клавиша, инвертируем значение IsKeyUp, а выполнение условий (рисунок 20) сделаем зависимым от IsKeyUp.
Почти всё. Осталось предостеречь нашу программу от ложного срабатывания при нажатии на клавиши Ctrl, Enter, Alt:
Переходим к завершающей стадии. В обработчике команд WndProc создадим case для сообщения LBN_SETFOCUS, то есть кодовое слово, которое окно lbVINотправит родительскому окну. Напоминаю. Нам нужно не просто перейти в окно lbVIN, но и выделить первую его строчку.
Чтобы выделить строку по индексу, окну типа listbox нужно отправить сообщение с кодовым словом LB_SETCURSEL (будем использовать это сообщение, вместо LB_SELITEMRANGE). В wParam указывается индекс строки (нумерация начинается с нуля), в lParam указывается NULL.
Компилируем, запускаем приложение. Переходим в строку VIN и вводим символ «А». Затем, когда появится окно-подсказка, нажмем клавишу Down:
Для навигации в окошке-подсказке, создадим еще один хук. Да, перемещаться в listbox с помощью клавиш Up и Down, без него будет гораздо проще. Но если не ввести перехватчик, мы не сможем выбрать понравившуюся нам строку из списка. Считать состояние строки (выделена она или нет), мы можем, даже сможем узнать текст, который она содержится, но это нам не даст ничего.
Мы не будем использовать старый хук по одной простой причине: в его процедуре прописаны операции для окна VIN, в том числе, нажатие на клавишу Down.
Новый хук назовем HookLBVIN, а процедуру HookLBVINProc.
Создаем хук из обработчика команд WndProc:
Теперь мы можем перехватывать сообщения от клавиатуры в окно lbVIN, не боясь навредить функциям из окна VIN.
В обработчик команд HookLBVINProcнам нужно добавить условия для клавиш Up, Down и Enter. Первые две нужны нам для перемещения в окне lbVIN, третий – для подтверждения выбора строки в окне-подсказке. Также клавиша Up нужна нам для возврата из окна lbVIN в окно VIN.
Любое другое воздействие будет игнорироваться. При нажатии на клавишу Down, нам нужно сместить выделение в lbVIN на одну строку вниз. Чтобы отслеживать номер выделенной строки введем в код глобальную переменную типа int. Назовем ее NulSelStr. Присвоим ей значение по умолчанию равное нулю. После активации хука HookLBVIN, при каждом нажатии Down, значение переменной NulSelStr будет увеличиваться на одну единицу.
Если мы оставим код в таком виде, то рискуем вновь столкнуться с «двойным прокликом». Добавим в resources.h новую переменную типа bool. Назовем ее IsKeyUpLbи присвоим ей значение TRUE. В обработчике HookLBVINProcдобавляем условие с инверсией булевой переменной, аналогичное тому, что мы применяли в HookProc.
Проверим, будет ли работать наша функция. Компилируем, запускаем приложение.
Всё работает. Теперь займемся перехватом и обработкой нажатия клавиши Up. Ничего сложного в этой операции нет. Она будет полностью аналогична Down, за исключением строки 629 (рисунок 29). Здесь мы будем не инкрементировать, а декрементировать значение переменной NumSelStr. То есть код для Up будет выглядеть вот так:
А теперь давайте поразмышляем: что будет с переменной NumSelStr, если мы дойдем до конца списка, пролистывая его вниз, и продолжим нажимать Down? Очевидно, что ее значение будет продолжать инкрементироваться. То же самое будет с NumSelStr, если мы пролистаем список в его начало, то есть до строки с индексом 0 и продолжим нажимать Up. Здесь даже будет еще более опасная ситуация: значения станут отрицательными и мы рискуем выделить весь список в lbVIN. Чтобы избежать подобного, нужно добавить условия к строкам 629 и 637. Сделаем так, чтобы в случае нахождения каретки в конце списка, значение NumSelStr прекращали инкрементироваться. Реализуем это так: перед входом в ветвления, сразу после инверсии значения переменной IsKeyUpLb, спросим у окна lbVIN, сколько в нем заполненных строк, а затем, перед инкрементом переменной NumSelStr в строке 629, проверим, не достигла ли она этого значения, и, если это так, инкрементировать ее не будем.
Обратите внимание: в строке 630, в условии, справа от знака сравнения, мы вычитаем единицу из переменной CountStringLB. Делаем мы это потому, что нумерация строк в окне lbVIN, как и в любом другом окне типа listbox, ведется с нуля. То есть, если в окне будет пять строк, то у первой индекс будет ноль, а у последней – четыре. С инкрементом разобрались. Теперь решим аналогичную проблему для декремента, только теперь нам нужно не остановить движение строки, когда значение CountStringLBстанет равным нулю, а «перебросить» фокус ввода в окно VIN. То есть, сделать операцию, обратную той, которая выполняется при переходе из окошка VIN-номер в окно подсказку. Мы уже умеем передавать фокус от одного окна другому. Для этого используем функцию SetFocus(). Вызовем ее по условию «если переменная CountStringLBравна нулю» и передадим в нее в качестве параметра дескриптор окна VIN-номер:
Проверяем. Компилируем, запускаем приложение:
При передаче фокуса окну VINмы покидаем окошко-подсказку и перемещаемся в окно VIN-номер. Когда окно VIN получает фокус ввода, оно действует по алгоритму, прописанному в обработчике команд WndProc, то есть устанавливает курсор на нулевую позицию. Нас это не устраивает, так как курсор при этом устанавливается слева от введенного ранее текста. Нужно, чтобы он возвращался на то же место, с которого мы уходили в окно-подсказку, нажимая на клавишу Down. Чтобы реализовать подобное нам нужно сделать следующее: при переходе из окна VIN в окно lbVIN считать позицию курсора в глобальную переменную, а затем, при возвращении из lbVIN в VIN, просто отправить окошку VIN сообщение EM_SETSEL с данным значением. Назовем эту переменную CursInd и присвоим ей значение по умолчанию ноль. Затем добавляем строку с кодом в обработчик HookProc:
При возврате из lbVIN в VIN, воспользуемся этим значением. Так как переменная CursInd глобальная, то просто передадим ее в качестве параметра в функцию PostMessage() в обработчике команд WndProc для case EN_SETFOCUS с wParam равным дескриптору окна VIN:
Если Вы были достаточно внимательны, то заметили, что раньше в строке 116 в качестве параметров lParam и wParam функции PostMessage() стояли нули. То есть раньше, при получении фокуса ввода окном VIN, курсор в нем ВСЕГДА позиционировался в ноль. Теперь же, при получении окном VIN фокуса ввода, если мы до этого переходили в окно-подсказку, курсор вернется на то место, где стоял прежде, то есть встанет справа от введенного ранее текста. По умолчанию же, позиция курсора, так и останется нулевой. Проверим, сработает ли наша придумка:
Как видим, всё работает правильно.
Не расслабляемся. У нас осталась еще одна клавиша, нажатие на которую нам нужно обработать - Enter. При нажатии на клавишу ввода, текст, из выделенной в этот момент строки в окне IbVIN, должен быть скопирован в окно VIN. Простая операция, реализуемая несколькими строчками кода. Делаем заготовку для алгоритма. Как всегда идем по шагам:
1. Остановить перехват сообщений;
2. Считываем текст из выделенной строки окна lbVIN;
3. Вставляем скопированный текст в окно VIN;
4. Удаляем окно lbVIN.
Сначала сделаем это, потом дополним список действий. Итак, сначала остановим наш хук:
Прежде чем выполнять операцию считывания текста из строки, нам нужно создать массив, в который будем загружать считанный текст. Создадим массив char-ов и назовем его VINBuf. Выделим под него 24 байта памяти. Затем отправим окну lbVINсообщение LB_GETTEXT, указав в wParam номер строки, из которой будем извлекать текст, а в lParam – массив, в который будем его записывать.
Теперь, с помощью функции SetWindowText() загрузим текст из массива VINBufв окно VIN:
И, наконец, удаляем окно lbVIN. Делаем мы это в самую последнюю очередь потому, что данное окошко перестраивается при изменении количества символов в окне VIN, и, если мы удалим его до того, как вставим в VINтекст из массива, оно появится вновь.
Также не забываем перенести курсор в конец строки VIN.
Теперь, если мы кликнем мышкой по окну VINпосле того, как в него вставится ВИН-номер из lbVIN, курсор в нем установится в конце строки.
Проверяем. Компилируем, запускаем:
Текст считался из выделенной строки и вставился в окошко VIN. Теперь мы можем нажать на кнопку «Считать» и программа считает из файла, хранящегося в папке «RZ9883458670GA090», ФИО клиента и выведет их в соответствующие окна. Но так не интересно! Пусть уж ФИО считывается автоматически, при полном заполнении окна VIN.
Создаем функцию, одноименную ReadFromFile() и переносим в нее весь закомментированный код из старой функции. Старую функцию переименовываем в, скажем, FindInDirectory(). В итоге получим две функции: одна будет перебирать папки в директории Databank и выводить их имена в окно lbVIN, другая – считывать информацию из указанного файла и заполнять окна Фамилия, Имя, Отчество:
Теперь вспомним бородатый анекдот про то, как американцы советский истребитель собирали и про инженера Васю с напильником: берем нашу функцию ReadFromFile() и «допиливаем» ее до требуемых на данный момент параметров. Составляем план действий:
1. Функция должна считать текст из строки VIN;
2. Функция должна построить путь к открываемому файлу в специально созданном для этого массиве;
3. Далее работать по умолчанию (то есть как и раньше).
Реализуем первый пункт:
Переходим ко второму пункту:
Внимание на строки 547, 550 – 553. Здесь мы создаем массив и поэтапно загружаем в него данные, чтобы в конечном итоге его содержимое содержало путь к файлу:
Путь к папке, в которой хранится директория Databank \\ Папка Databank\\ Папка, имя которой было считано из строки VIN-номер \\ Файл file.txt
В строке 554, в функции CreateFile() мы подменяем этим массивом указанный там ранее явно в качестве первого параметра путь к файлу file.txt. Очистим массив miniBuf. Делаем это обязательно, иначе в нем останутся ненужные нам в будущем символы.
Удаляем лишние параметры hWndParrent и hInstance из сигнатуры функции, оставляем только HWND hEdit.
Теперь мы можем вызвать функцию ReadFromFile() при нажатии клавиши Enter в окне lbVIN:
Проверяем: компилируем, запускаем приложение, в строке VIN начинаем вводить ВИН-код. Из выпадающего списка выбираем любой ВИН-номер и жмем Enter:
Превосходно! Всё работает. Переходим к более интересной части статьи. Зашифруем текстовые файлы. Мы не будем использовать «встроенные» способы шифрования, а создадим собственную функцию, которая будет этим заниматься. Назовем ее EnDeCoder():
Сразу же добавляем первый параметр в функцию – ED, сокращенно от «encrypt - decrypt». Введем два ключевых слова: T_ENCR и T_DECR. Первому присвоим идентификатор 350, второму – 352:
Если нужно будет закодировать текст, мы укажем в качестве параметра ED значение T_ENCR, если же нужно будет расшифровать закодированный ранее текст, мы укажем T_DECR. Шифровать будем самым простым способом: используя ключевое слово и сложение по модулю два. При повторном использовании XOR с тем же ключом будет выполнена дешифровка текста. Сначала явно зададим ключ к шифрованию. Пусть это будет строка «DefaultKey». Позже мы сделаем ключевое слово динамичным, однако, договоримся сразу, что длина его не будет превышать 32 байт. Итак, создаем массив char-ов, в котором будет храниться наш ключ:
Теперь добавим в функцию массив, в который будем загружать результат шифрования. Назовем его EnDeCrResultи выделим ему 2048 байт памяти (2 килоБайта!). Зачем так много?! Затем, что мы делаем универсальную функцию, которую будем использовать при чтении-записи не только маленьких файлов вроде file.txt, но и больших, например карточек, в которых будут храниться сведения об автомобилях.
Ага! У нас есть массив, в который будем грузить ответ, строка, с которой будем складывать по модулю два шифруемый текст, осталось добавить этот самый текст в функцию! Добавляем:
Мы уже много раз встречали тип данных LPCSTR при написании кода. Он означает не что иное, как строку, состоящую из символов типа char. А если быть точнее, то указатель на строку. А что такое строка из char-ов? Верно! Это массив, аналогичный тем, что мы создали в строках 592 и 593.
Предположим, что в массив Textбудет записана строка по умолчанию, то есть «Иванов\r\nИван\r\nИванович». В ней 22 байта. Что произойдет, если мы сложим эту строку с ключом? А произойдет нечто очень плохое. Почему? Потому, что в ключе по умолчанию всего 10 байт. Если мы начнем (причем побитово!) складывать нулевой байт массива Text с нулевым байтом массива Key, то есть с символом «D» второй байт с символом «е» и т.д., то обязательно доберемся до байта с индексом 10 в массиве Textи попробуем сложить его с одиннадцатым байтом массива Key. А там у нас ничего нет, то есть ноль. В результате мы, конечно же получим тот же текст, что и пытались зашифровать. Для тех, кто не понял, поясняю: «0» и 0 – совершенно разные значения с точки зрения компьютера. Первое – это символ, второе – это число. То есть, ноль, как число (здесь НЕ имеется ввиду int или другой числовой тип данных!), будет вписан в память в виде Nan, а вот как символ, ноль будет иметь вид 00110000. Почему так? Потому что символ «ноль», согласно таблице ASCII соответствует десятичному значению 48. Аналогично, десятичному нулю соответствует «пустое место». Давайте с Вами вспомним правила сложения чисел по модулю два. Складываются они побитово. В результате сложения двух нулей получаем ноль, в результате сложения нуля и единицы – единицу, в результате сложения двух единиц – ноль. И что у нас выйдет? Предположим, что мы шифруем букву «А». В таблице ASCII ей соответствует значение 192. Переведем его в двоичную систему счисления и получим 11000000. Сложим это число с нулем по модулю два:
Ничего не изменилось. От такого шифрования явно не будет смысла. Однако, подобную проблему решить достаточно просто: сделаем так, чтобы при шифровании ключ повторялся, то есть чтобы когда символы в строке ключа заканчивались, перебор ячеек в его массиве начинался заново. Поясняю понятнее. Смотрите. У нас есть кодовое слово «DefaultKey». Оно лежит в массиве из 32 байт. По факту, этот массив выглядит вот так:
Мы посмотрим в какой ячейке лежит последний символ кодового слова. В данном случае это ячейка 9. То есть символов 10. Мы поделим шифруемый массив на группы символов по 10 и каждую группу сложим по модулю два с кодовым словом. Для строки по умолчанию получится что-то вроде:
(«Иванов\r\nИван\r\nИванович») XOR («DefaultKeyDefaultKeyDe»)
Таким образом, весь текст будет зашифрован. Реализуем операцию шифрования в несколько этапов:
1. Считаем сколько символов в массиве Key (пусть это будет N символов);
2. Считаем сколько символов в массиве шифруемого текста (пусть это будет К символов);
3. Делим К на N. Узнаем частное и остаток (Пусть частное – М, остаток - R);
4. Создаем цикл for, совершающий Mитераций. В процессе каждой итерации будет выполняться сложение по модулю два части шифруемого текста с ключевым словом;
5. Если остаток не равен нулю, то оставшиеся R символов шифруемого текста складываются по модулю два с оставшимися R символами ключевого слова.
Приступаем к первой указанной операции. Считать символы в строках будем с помощью функции lstrlen(). Мы уже использовали ее ранее, поэтому я не буду добавлять сюда справочные данные по этой функции. Заодно выполним и вторую операцию – они аналогичны.
Как Вы уже, наверное, поняли, Nмы заменили на Nsim, а К заменили на Ksim. Переходим к операции номер три. Разделим Ksim на Nsim.
Если вдруг кому-то будет непонятно, объясняю. Оператор «/» используется при делении одного числа на другое. Если при этом есть остаток от деления, он отбрасывается, число НЕ округляется. Оператор «%» покажет только остаток от деления.
Теперь нужно реализовать цикл for. Количество итераций зададим Miter, то есть переменная в цикле будет бегать от нуля до Miter. Внутри у него будет вложенный цикл, в котором будут перебираться символы от нуля до Nsim.
Объясняю что написано в строчке 603. Для примера возьмем строку по умолчанию и ключ «DefaultKey». В шифруемом тексте у нас 22 байта, в ключе 10. Делим 22 на 10. Получаем М=2, R=2 (делим 22 на 10, получаем целым 2 и еще 2 в остатке, то есть 22=2*10+2). Вложенный цикл выполняет Nsim=10 итераций. Как только они завершатся, в дело вступит переменная m. С помощью нее мы будем сдвигать обрабатываемый участок в массивах EnDeCresult и Text. То есть если Nsim=10, переменная nпробежит 10 раз по коду во вложенном цикле, инкрементируя свое значение от нуля до 9. Эх! Думаю, всё равно непонятно. Придется показать. Смотрите.
Рассмотрим итерации во вложенном цикле (начинается со строчки 601 на рисунке 56 и заканчивается строчкой 604).
Итерация 1: n=0; 0 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[0] = Text[0] xor Key[0];
n=0+1=1;
Итерация 2: n=1; 1 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[1] = Text[1] xor Key[1];
n=1+1=2;
Итерация 3: n=2; 2 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[2] = Text[2] xor Key[2];
n=2+1=3;
Итерация 4: n=3; 3 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[3] = Text[3] xor Key[3];
n=3+1=4;
Итерация 5: n=4; 4 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[4] = Text[4] xor Key[4];
n=4+1=5;
Итерация 6: n=5; 5 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[5] = Text[5] xor Key[5];
n=5+1=6;
Итерация 7: n=6; 6 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[6] = Text[6] xor Key[6];
n=6+1=7;
Итерация 8: n=7; 7 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[7] = Text[7] xor Key[7];
n=7+1=8;
Итерация 9: n=8; 8 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[8] = Text[8] xor Key[8];
n=8+1=9;
Итерация 10: n=9; 9 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[9] = Text[9] xor Key[9];
n=9+1=10; 10 = 10, то есть n = Nsim, выходим из цикла.
Все эти операции были произведены, когда mв основном цикле была равна нулю. Как только мы выйдем из внутреннего цикла, значение переменной m в основном цикле (строка 599, рисунок 56) увеличится на единицу, то есть m станет равна единице. Как только это произойдет, вложенный цикл вновь запустится. Однако на этот раз, индексы в массивах EnDeCrResultи Textбудут сдвинуты на Nsim-1 ячеек вперед. То есть, если в прошлый проход по вложенному циклу мы остановились на девятых ячейках этих массивов, то теперь продолжим обработку с десятых:
Итерация 1: n=0; 0 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[0 + 1*10] = Text[0 + 1*10] xor Key[0];
n=0+1=1;
Итерация 2: n=1; 1 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[1 + 1*10] = Text[1 + 1*10] xor Key[1];
n=1+1=2;
Итерация 3: n=2; 2 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[2 + 1*10] = Text[2 + 1*10] xor Key[2];
n=2+1=3;
Итерация 4: n=3; 3 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[3 + 1*10] = Text[3 + 1*10] xor Key[3];
n=3+1=4;
Итерация 5: n=4; 4 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[4 + 1*10] = Text[4 + 1*10] xor Key[4];
n=4+1=5;
Итерация 6: n=5; 5 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[5 + 1*10] = Text[5 + 1*10] xor Key[5];
n=5+1=6;
Итерация 7: n=6; 6 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[6 + 1*10] = Text[6 + 1*10] xor Key[6];
n=6+1=7;
Итерация 8: n=7; 7 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[7 + 1*10] = Text[7 + 1*10] xor Key[7];
n=7+1=8;
Итерация 9: n=8; 8 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[8 + 1*10] = Text[8 + 1*10] xor Key[8];
n=8+1=9;
Итерация 10: n=9; 9 < 10, то есть n< Nsim, выполняем операцию.
EnDeCrResult[9 + 1*10] = Text[9 + 1*10] xor Key[9];
n=9+1=10; 10 = 10, то есть n = Nsim, выходим из цикла.
Мы выходим из вложенного цикла и увеличиваем значение m на единицу. Теперь m=2. Проверяем условие для основного цикла: меньше ли m, чем Miter. Нет. Не меньше, mстала равна Miter. Это значит, что мы должны завершить цикл. Теперь, надеюсь, Вам всё стало понятно.
Осталось прописать условие для пятой операции. Если есть остаток, мы должны выполнить еще несколько итераций, чтобы до конца зашифровать текст.
Обратите внимание на строки 606 – 612. Здесь мы используем тот же цикл, что и во вложенном цикле ранее. Однако здесь есть и одно отличие. Вместо m мы используем переменную Miter (строка 610). Посмотрите на пример выше и поймете почему (в последнем действии мы как раз остановили цикл на индексах 9+1*10, то есть на (Nsim-1)+(Miter-1)*Nsim). Это значит, что теперь мы должны начать цикл со значения на единицу больше предыдущего, то есть со значения (Nsim)+(Miter-1)*Nsim = 0+Nsim*Miter.
В общем, можно считать, что наша функция шифратор-дешифратор готова. Но как мы будем передавать куда бы то ни было шифрованный (ну или дешифрованный) текст? Ведь массив, который мы для него создали – локальный. Я предлагаю простое и действенное решение: изменим тип нашей функции с void на LPCSTR. Если мы это сделаем, то после проведенных над текстом операций по шифрованию, сможем вернуть строку. Создавать глобальную переменную не придется, ведь, по сути, функция сама является глобальной переменной.
Создаем описание функции в файле resources.h. Теперь мы можем шифровать текст. Проверим, как работает наша функция. Вызовем ее из функции TextInFile(), которая в свою очередь, напомню, вызывается из обработчика команд при нажатии на кнопку «Сохранить».
Вызываем мы ее прямо из MessageBox() – ничего страшного в этом нет. Проверяем. Компилируем, запускаем, в Поле VIN вводим произвольный ВИН-номер и жмем «Сохранить».
Абракадабра имеется, значит функция шифрует как надо! Дешифруем? Добавим в функцию TextInFile() массив, в который будем загружать шифрованный текст и передадим его в функцию EnDeCoder():
Компилируем, запускаем, проверяем.
Функция EnDeCoder() работает. И работает правильно. О том, зачем мы добавили в функцию EnDeCoder() параметр ED, которым в данный момент не пользуемся, поговорим позже (возможно в следующей статье), а пока что добавим шифрование текста в функцию записи в файл и, соответственно, функцию дешифровки в функцию чтения текста из файла. Для начала уберем лишние операции из функции TextInFile():
Обратите внимание: мы не стали удалять массив Text_2, а также оставили strcpy(), которая загружает в него данные из функции-шифратора. Сделано это намеренно, иначе, при записи текста в файл в строке 452, нам придется вызывать функцию EnDeCoder() дважды. При текущих же условиях, нам достаточно просто заменить «MegaBuf» в функции WriteFile() в строке 452 на Text_2.
Проверим, сохранится ли в файл шифрованный текст. Компилируем код, запускаем приложение, вводим в поле VINпроизвольный ВИН-номер и жмем «Сохранить».
После того, как нажали «Сохранить», закрываем приложение и переходим в папку, которую только что создали. Открываем файл file.txt, который там хранится:
Шифрованный текст успешно сохранился. Считаем его, не внося изменений в функцию ReadFromFile(). Открываем наше приложение, вводим ВИН-код, как на рисунке 65, выбираем его в окне-подсказке и жмем Enter:
Конечно же ничего хорошего не произойдет. Наша программа попросту не может понять, что за белиберда лежит в файле file.txt (при загрузке строк в окна Фамилия, Имя и Отчество, программа в цикле while ищет операторы «\r\n», если же их не будет там, то цикл станет бесконечным и приложение зависнет, что собственно мы и наблюдаем на рисунке 67). Поможем ей разобраться: модифицируем функцию ReadFromFile():
Что мы добавили: строка 248 – создали массив Text_2, строка 559 – считываем данные не в RBudкак раньше, а в Text_2, строка 561 – применяем функцию дешифратор к тексту, хранящемуся в массиве Text_2 и загружаем дешифрованный текст в массив RBuf. Проверим, будет ли работать наш код. Компилируем, запускаем приложение. В соответствующие графы вводим произвольные ФИО и вымышленный ВИН-код, жмем сохранить:
Закрываем приложение. Перейдем в папку с именем, указанным на рисунке 69 в графе VIN, которая лежит в директории Databank. Откроем файл file.txt. Здесь мы видим следующее:
Информативно. Мы убедились, что текст зашифрован. Теперь откроем наше приложение и начнем вводить ВИН-номер, перейдем в окно-подсказку и нажмем Enter:
Никаких зависаний, текст считан из файла и загружен в соответствующие рабочие области программы. Но что, если в папку с ВИН-номером попадет изначально «кривой» файл? Программа при его открытии постоянно будет виснуть, а пользователь повесит всех собак на нас? Так не пойдет. Добавим в функцию ReadFromFile() условие, согласно которому, программа либо будет пытаться записать дешифрованный текст в окна Фамилия, Имя, Отчество, либо откажется это делать ввиду того, что в файле с текстом есть ошибки. Условие построим на базе простого посимвольного сравнения дешифрованного текста с операторами С И если эти операторы обнаружатся, то продолжим выполнение функции, иначе – выведем сообщение с ошибкой.
Мы ввели две переменные типа int IsOperandn и IsOperandr: при работе функции, будем ориентироваться на их состояние. Если количество операторов «\r» и «\n» соответствует тому, которое должно быть в файле, то текст обработается функцией, если нет, то выведем сообщение:
Проверим. Компилируем код, запускаем приложение. Вводим тот же ВИН, что и на рисунках 69 и 71, переходим в окно-подсказку и жмем Enter:
Теперь намеренно испортим файл file.txt в этой папке. Мы знаем где в нем располагаются операторы «\r» и «\n». Заменим их на символы F:
Запускаем приложение и вновь пробуем загрузить текст из этого файла:
На этом, пока что остановимся. Всеми «недоделками» займемся в следующей статье, а пока что напомню, что мы с Вами еще планировали сделать в этой:
1. задать файлам собственное расширение,
2. задать файлам иконки в зависимости от их назначения.
Для второго нужно будет поработать с реестром. Ух! Как звучит страшно! Этим мы займемся потом (не в этой статье и даже не в следующей), а вот с первым, пожалуй, разберемся незамедлительно. Определимся, какие типы файлов у нас будут в программе.
- .crt– карточка с данными авто и клиента, телефон, почта, комментарии;
- .rem– записи механиков, электриков, диагностов;
- .cfg– файл конфигурации программы;
- .knt– договора.
Первое расширение мы зададим тем файлам, которые в настоящее время имеют расширение .txt. Реорганизовывать базу данных клиентов мы будем уже в следующей статье. Перед тем как завершать сегодняшнюю скажу еще пару слов о хуах. В чем заключается их главное преимущество и в то же время главная опасность. Хуки бывают локальные и глобальные. Локальные перехватывают сообщения в пределах программы, из которой их вызывают. Глобальные же способны анализировать сообщения во всей системе. То есть мы можем запустить любое приложение (или открыть любой файл, совершить любое действие со сторонними программами), перехватив то или иное сообщение от системы! Именно поэтому, глобальные хуки мы с Вами изучать не будем. Если не поняли почему, поясняю: первая причина – по неопытности можно много чего наворотить, вторая причина – получив опыт с глобальными хуками также можно много чего наворотить, но уже намеренно.
Что ж, сегодняшняя статья была интересной (по крайней мере, писать ее мне было интересно). Обозначим план на следующую статью:
- Удалим кнопку «Считать»;
- Поиграемся с кнопкой «Сохранить»;
- Доработаем функцию считывания текста из файла;
- Доработаем функцию шифрования;
- Центрируем главное окно программы;
- Создадим файл с конфигурациями программы.
Вот так вот. Спасибо, что читаете! Удачи в учебе и труде!