Предыстория
Мой сын, как, наверное, все дети программистов, получил свою первую клавиатуру ещё когда не умел сидеть. Сейчас ему чуть меньше года, но он уже понимает разницу между «игрушечной» и «настоящей» (папиной) клавиатурой — если колотить по кнопкам настоящей, то на экране меняется картинка, а компьютер иногда издаёт какие-то звуки.
Поскольку лишиться всех своих данных мне пока не хочется, ребёнку иногда разрешается нажимать на кнопки заблокированного компьютера. К сожалению, для ребёнка это не очень весело, поскольку компьютер имеет всего два режима (две картинки) — экран ввода пароля и собственно экран блокировки.
Чтобы процесс освоения компьютера стал для детёныша более увлекательным, я решил написать ему простенькую игру. Будучи программистом со стажем, весь процесс решено было построить «правильно».
Требования
Заказчик (мой сын, возраст <1 года), как и все нормальные заказчики затруднился письменно изложить непротиворечивые и полные требования к продукту, поэтому пришлось
Функциональные:
- Приложение работает в режиме полного экрана.
- Можно нажимать на всё подряд, но самые доступные методы выхода или переключения программ должны быть заблокированы.
- Визуальная обратная связь — цвет фона меняется при нажатии, в центре экрана отображается нажатый символ.
- Звуковая обратная связь — приложение издаёт звук при нажатии на клавишу.
- Предсказуемое поведение — цвет фона, символ и звук должны быть всегда одинаковыми для одной и той же клавиши.
Не функциональные:
- Мне должно быть не стыдно за написанный код.
- Код должен быть ценен сам по себе.
- Архитектура и все решения должны быть «правильными» — как в заказном проекте.
Кроме того было принято решение использовать гибкий итеративный подход к разработке с малыми циклами разработки, заканчивающимися получением обратной связи (SCRUM).
В качестве языка программирования и среды разработки были выбраны C# и Visual Studio, так как они обеспечивали исполнителю наибольшую скорость работы.
Реализация
Из одного из старых проектов был извлечен код для создания приложения, развернутого на весь экран:
FormBorderStyle = FormBorderStyle.None;
WindowState = FormWindowState.Maximized;
var screen = Screen.PrimaryScreen;
Bounds = screen.Bounds;
Далее в дебрях интернета была найдена библиотека MouseKeyHook, с примерами, как заблокировать кнопку Windows. Аналогично примерам были заблокированы Alt-Tab и Ctrl-Esc. Теперь выйти из приложения можно только по Alt-F4.
Далее был написан код, который инициализирует рандомный цвет фона для нажатой клавиши:
- Использовался new Random(seed), чтобы при каждом запуске рандом выдавал одни и те же значения.
- Чтобы цвета были более-менее осмысленными, рандом выбирал значение из перечисления KnownColor, которое затем преобразовывалось в Color и присваивалось Form.BackColor.
- Поддерживались буквенные символы и цифры.
- Символ выводился «как есть» — клавиша Q могла вывести «Q», «q», «Й», «й», в зависимости от активного языка ввода и состояния CapsLock.
Первые альфа-тесты на себе выявили следующие недостатки реализации:
- Form.BackColor категорически не согласен принимать цвет Transparent.
- Чёрный цвет принимается, но символа на нём не видно.
- Есть ряд клавиш, которые могут быть нажаты, у них есть символ, но они не обрабатываются программой или не отображают символ — Enter, Tab, Space, блок цифр над буквами и блок цифровых клавиш справа на клавиатуре.
- Очень не нравился код обработки KeyDown/KeyPress — нужно было выделять диапазоны символов 'A-Z' и '0-9', пробел, Enter. Много не очень внятных блоков условий и сложный код расчёта размера массива рандомных цветов и выборки цвета из него.
Во второй итерации были внесены следующие изменения:
- Написана простенькая WinForm утилита, которая точно так же «слушает» нажатия, сохраняет их в словарь Клавиша-Символ. Это позволило разрешить проблему вывода русских/английских букв.
- У утилиты есть кнопка сохранения словаря в файл.
- Поскольку клавиши Space и Enter в этом случае вызывали срабатывание обработчика кнопки, а Tab вызывал переход на кнопку, даже если она не выбрана, пришлось эти случаи отдельно обработать — установить TabStop=false для кнопок и вставить ActiveControl = null везде, где только можно.
- Утилита помогла выявить все значимые клавиши — она запоминала клавишу при KeyDown, но добавляла её в словарь только по KeyPress, соответственно, всё, что не имеет символьного представления (Alt. Shift, Ctrl, Windows, функциональные клавиши) игнорировалось.
- Обработку клавиши в самой игре можно будет значительно упростить до поиска по словарю.
- Формат файла был самый простой — готовые наборы разделяются переводом строки, а поля (Клавиша-Символ-Цвет) в наборе разделяются символом \0 (пробел, табуляцию, и символы вроде запятой использовать не получилось, так как они могли быть элементом набора)
- После сохранения невидимые символы вручную были заменены на Unicode-символы, отсутствующие на клавиатуре.
- Цвет подбирался не случайным образом, а брался последовательно из enum KnownColor, начиная со следующего после KnownColor.Black (KnownColor.Transparent идёт немного раньше).
Альфа-тестирование на себе прошло вполне успешно и была проведена демонстрация заказчику.
Заказчик проявил интерес к продукту, выделил целых 2 минуты на тестирование, оценил работу в целом положительно и указал на следующие недостатки:
- Недостаточная звуковая обратная связь (звук издает только клавиша PrintScreen).
- Некорректно обрабатывается маленькая светящаяся кнопочка в правом дальнем углу ноутбука (экран гаснет).
Воодушевившись поддержкой заказчика,
- Нужно использовать внешнюю клавиатуру без кнопок управления питанием или маскировать аппаратную кнопку рукой.
- Пора переходить к звуковой обратной связи.
Для звуковой обратной связи принято решение издавать звуки, соответствующие нотам (клавишам пианино). Быстрый поиск в интернете позволил найти формулу расчета частоты звука для каждой клавиши и данная формула была оперативно реализована в C# коде. Для непосредственного вывода звука на колонки использован Console.Beep (а что, работает же!).
Первый же прогон продемонстрировал недостатки:
- Автор невнимательно прочитал MSDN, а именно строку «ranging from 37 to 32767 hertz».
- Низкие звуки примерно до 110 Гц звучат отвратительно и их нельзя показывать заказчику.
- Длительность звука 300 мс — слишком долго.
- Звук выводится синхронно и вызывает задержку прорисовки фона.
По результатам были внесены следующие изменения:
- Формировать частоты от 110Гц (25-я клавиша пианино, A2).
- Длительность звука сделать 100мс.
- Выводить звук в отдельном потоке.
- Команда выразила подозрение, что нужно делать Lock во втором потоке на время выполнения Console.Beep. В дальнейшем подозрение не подтвердилось, но
удалять было леньблокировка осталась для дидактических целей. - Использовать двойной буфер при смене цвета, чтобы не было полос на экране при быстром нажатии на клавиши.
Данная версия получила высокую оценку самой команды, а поскольку до демо для заказчика оставалось время, команда решила провести рефакторинг:
- Реализовать паттерн MVC, выделить логику игры в контроллер, во View оставить только код специфичный для работы с формой (переход в полный экран, обработчики событий).
- Покрыть контроллер юнит-тестами
- Вынести файл-словарь с тройками «Клавиша-Символ-Цвет» в ресурсы и реализовать русскую и английскую версии.
- Поскольку на рабочем ноуте (а на нём мы планировали провести демо) у меня стоит локаль английская, было реализована настройка локали через конфиг. При этом в конфиге добавлена своя секция и реализован простенький файл для доступа к этой секции, возвращающий типизированные значения переменных конфига.
Итог
В результате мы получили забавную игрушку для ребенка, которая развивает мелкую моторику и память (на любимые цвета и ноты), а также код, который можно использовать для демонстрации реализации «правильных» подходов. Например, для студентов или Junior-девелоперов.
Вот список того, что можно изучить по коду игрушки:
- Работа с WinForms (полный экран, двойной буффер, обработка событий клавиатуры)
- Работа с локализованными ресурсами.
- Применение паттерна MVC для WinForms (да, да вовсе не обязательно для этого переходить на WPF).
- Применение паттерна Singletone (многопоточного).
- Работа с Moq при разработке юнит-тестов.
- Работа с Shouldly при разработке юнит-тестов.
- Парсинг строк/файлов.
- Многопоточность и блокировка потоков.
- Работа с конфиг-файлом и создание своих секций.
- Правильный кодинг-стайл и использование комментариев и регионов.
- Работа с отладочной консолью (логгирование событий).
- Перечисление значений enum при помощи Enum.GetValues.
- Работа со статическими методами Array (Copy, IndexOf).
- Работа с unmanaged-объектами (using).
- «Отзывчивая» работа формы — подтверждение выхода, использование диалога сохранения файла.
- Работа с NuGet и выкачивание пакетов при сборке.
Готовый код выложен в виде открытого репозитория на GitHub и доступен с лицензией MIT.
p.s. КДПВ © kobyakov