Предыстория
Мой сын, как, наверное, все дети программистов, получил свою первую клавиатуру ещё когда не умел сидеть. Сейчас ему чуть меньше года, но он уже понимает разницу между «игрушечной» и «настоящей» (папиной) клавиатурой — если колотить по кнопкам настоящей, то на экране меняется картинка, а компьютер иногда издаёт какие-то звуки.
Поскольку лишиться всех своих данных мне пока не хочется, ребёнку иногда разрешается нажимать на кнопки заблокированного компьютера. К сожалению, для ребёнка это не очень весело, поскольку компьютер имеет всего два режима (две картинки) — экран ввода пароля и собственно экран блокировки.
Чтобы процесс освоения компьютера стал для детёныша более увлекательным, я решил написать ему простенькую игру. Будучи программистом со стажем, весь процесс решено было построить «правильно».
Требования
Заказчик (мой сын, возраст <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
Комментарии (41)
el777
03.08.2015 13:06+1[offtop]
Точно — статья «из песочницы». )))
P.S. Поздравления молодому отцу )
[/offtop]
Loxmatiymamont
03.08.2015 13:19+6Я, конечно, параноик, но очень уж активно все офтальмологии говорят не смотреть детям в экран хотя бы до 2х лет.
denser
03.08.2015 13:50Детей привлекает все движущееся и новое, на экране этого добра может быть валом. Вредно смотреть долго на объект на одном расстоянии, в мониторе же нет глубины? Поэтому лучше дозировать или ограничить до возраста, когда детки могут тебя слушать и не смотреть любимый мультик, когда попросишь.
igolets
04.08.2015 10:24К сожалению, если почти все взрослые, которые окружают нашего малыша смотрят в экран и тыкают по клавиатуре, очень сложно будет объяснить, что ему так делать не стоит. Понятно, что родительское внимание и воспитание нельзя заменить компьютером, но:
- Больше, чем на 2-3 минуты его всё равно не хватает
- Можно «вести себя как папа» и никто ему ничего не скажет ;)
- Развивается мелкая моторика
- Ребёнку стало интереснее нажимать на кнопки, а не выковыривать их (я замучался, если честно, вставлять на место ноутбучные кнопки)
- Старшие дети заинтересовались программированием, будем учиться геймдеву :)
Loxmatiymamont
04.08.2015 10:39Правильно говорите, но сейчас «экран» больше ассоциируется со смартфонами и планшетиками, кои позволяют быстро и дешево успокоить и/или отвлечь ребёнка.
Вот как тут поступать уже зависит исключительно от родителей. Я своему решительно запрещаю брать телефоны и планшет, а мелкую моторику гораздо лучше в песочнице развивать. А правильно профессии ещё успею научить =)
MasMaX
04.08.2015 15:46Лучше игрушки дайте, чем будет в экран пялится.
У меня сыну 2.5, мультики по телеку смотрим минут по 15 в день, а то и меньше. Иногда на компе показываем. Нажимать клавиши разрешаем на выключенном компе, на включенном ребенок хорошо понимает что может что то там сломать.
А для моторики купите конструкторы.
rustler2000
03.08.2015 14:58+2В свое время приделал русский для своего мелкого вот к этому — github.com/shanselman/babysmash
samodum
03.08.2015 16:27+2Иногда даже полезно дать клавиатуру ребёнку.
Дочка в 2 года случайно нашла комбинацию в Skype — изображение кота при нажатии одновременно c+a+t
А в айпаде нашла комбинацию как делать скриншоты. Пришлось потом гуглить, чтобы понять как она это сделала :)igolets
03.08.2015 22:28+2Мой сын на заблокированном экране включил Narrator и ноут озвучивал нажатые клавиши. Несколько раз я отчетливо услышал «Unknown key».
А в одной из промежуточных версий описываемой программы выводился сам символ, который введен на клавиатуре, так он смог ввести символ «многоточие».
После этого анекдот про бесконечно много обезьян и «Войну и мир» приобретает новые смыслы ;)
WeslomPo
04.08.2015 08:46Кот в skype появляется при беспорядочном нажатии на клавиши, и означает что по клавиатуре прошлась кошка, или пользователь не в себе.
return_true
03.08.2015 17:10Интересное утверждение про регионы и правильный кодинг-стайл :)
Если честно, я не знаю ни одного нормального аргумента за регионы.
blog.codinghorror.com/the-problem-with-code-folding
programmers.stackexchange.com/questions/53086/are-regions-an-antipattern-or-code-smellSirEdvin
03.08.2015 20:02Допустим, у вас есть некий класс, который заполнен функциональностью.
Почему бы не использовать регионы для смыслового разделения кода всего класса на некие фрагменты? Или проще листать девять страниц кода?alkozko
03.08.2015 22:15+2если класс занимает 9 страниц, то он скорее всего нарушает SRP и нуждается в рефакторинге, а не в регионах
SirEdvin
03.08.2015 23:11SRP очень круто звучит в теории, но на практике с ним происходят некоторые проблемы.
Например, у меня есть класс матриц с значительным количеством методов fill. Я, конечно, могу сказать, что так нельзя и нужно вынести филлеры матрицы в отдельный класс (хотя момент достаточно спорный, учитывая, что филлеры используют особенности хранения матрицы).
Какой код удобнее читать и писать?
Такой:
DoubleMatrix a = b.fillRow(1,(i,j)->i+j).fillRow(2,(i,j)->Math.cos(i+j));
Или такой:
DoubleMatrix a = fillRow(1, (i,j)->i+j, fillRow(2, (i,j)->Math.cos(i+j), b));
Учитывая, что вызовов функций заполнения может быть в разы больше.
Так же, то как эту проблему в Java 8 со свойстами, при помощи нескольких абстрактных классов и кучи интерфейсов с методами по умолчанию мне жутко не нравится. Если это из-за SRP, то тут он повел себя как антипаттерн, на мой взгляд.igolets
04.08.2015 01:07Fluent-запись выглядит более модной, но, если честно, оба варианта вызывают ассоциацию с Haskell.
Что именно не так с этим кодом по такому фрагменту сказать сложно и, наверное, лучше не в комментах, но результат вполне может быть началом путёвой статьи для Хабра.
Если что, контакты мои в есть профиле. :)
KvanTTT
04.08.2015 01:54Есть исключения: перегрузка визиторов при разборе большого формального языка.
return_true
04.08.2015 11:43К сожалению регионы пихают не только в классы на 9 страниц. Вот у автора есть класс на 42 строки (без учёта пустых строк) из которых на регионы приходится 10. Практически четверть кода!
KvanTTT
04.08.2015 01:55Можно использовать partial классы.
return_true
04.08.2015 11:35Верно, регионы в C# и добавили для «сворачивания» автогенеренного кода WinForms. Потом уже придумали partial классы.
igolets
03.08.2015 22:42Следует понимать эту строку следующим образом:
- Правильный кодинг стайл включает в себя всё, не только регионы :)
- Использование комментариев и регионов — часть кодинг стайла
Т.е. если кому-то не нравятся регионы, можно их и не использовать, но те же комментарии и прочее (правила именования классов, общее оформление) нужно применять и по возможности правильно.
Лично мне регионы удобны, т.к. я могу быстро выделить только публичные методы/свойства класса и пробежаться по его «фасаду». Можно было бы обойтись просто группировкой, но с регионами нагляднее.
ErhoSen
03.08.2015 18:31Здорово!
Недавно делал свой «What's color is it?», и для того чтобы картинка всегда оставалась контрастной применял к цифрам инвертированный цвет фона) Можно например сделать так, чтобы глаза меньше уставали)
dj_raphael
03.08.2015 22:06+1Я разрешал молотить так по клавиатуре и елозить мышкой прямо на рабочем столе. И когда примерно чуть больше года ему было он славно потролил жену.
Приходит она с кухни — все иконки выставлены горизонтально в ряд по центру рабочего стола. Ну может глюк после выхода из игры, после зумы бывает иконки сдвигаются. Расставила быстро всё по углам и опять ушла на кухню. Приходит через некоторое время — опять та же картина. «Что за хрень?» — расставила всё обратно и опять на кухню. Ребенок всё это время игрался в машинку на столе катал её туда сюда, иногда использовал мышку в качестве второй машинки для компании.
Выглядывает она из-за угла и наблюдает картину, как ребенок мышкой расставляет иконки горизонтально в ряд. 1 год.
Специально не учили. просто разрешали молотосить по клаве и елозить мышкой, везде. залазил на колени, когда играли в зуму, потом он отбирал мышку и весело расстреливал все шары по сторонам.igolets
04.08.2015 10:10Если есть чужой ноутбук, то всё проще — отдал ребёнку ноут жены и не жалко. Свой нут на растерзание отдавать не хочется. ;)
Igor_Sib
04.08.2015 08:15Дети вообще странным образом понимают ценность предметов. То же самое в полтора-два года — нужен был именно мамин телефон, когда подсовывают старый не работающий — выбрасывал. :)
igolets
04.08.2015 10:13Мне думается, что это какой-то «попугайский» инстинкт. Мама тыкает пальцем по телефону и там меняется картинка. Папа тыкает пальцами по клавиатуре и тоже что-то происходит. Если запрещать, то возникнет либо нездоровый интерес (навязчивая идея), либо комплекс. В данном случае я решил разрешить пользоваться компьютером, но под контролем.
Дети чуть постарше ведут себя похожим образом — наши старшие (начальные классы школы) очень заинтересовались написанием игр для брата, будем с ними вместе изучать программирование :)
Impuls
04.08.2015 08:44Мельтешат цвета. Запилите плавный переход к цвету, а то тут и до эпилепсии недалеко. Как пример — плавный переход цветов при установке windows 8.1 и выше.
Вот пример. Смотреть с 7:20
Правда там видео немного ускорено. В оригинале немного медленнее.igolets
04.08.2015 10:17+1Идея понятна. Про эпилепсию, конечно, вопрос спорный, припадки возникают если есть предрасположенность, Просто мельтешения цветов мало. В любом случае, её лучше продиагностировать в раннем возрасте (шутка).
Будем «наблюдать за сценариями использования программы», «собирать обратную связь от пользователя», а интересные идеи попробуем воплотить со старшими детьми. Возможно, это выльется в статью про обучение программированию школьников начальных классов :)
Есть несколько идей как развивать игру по мере взросления малыша — учить слова, буквы.Impuls
04.08.2015 10:22У меня ребенок на клавиши жмет очень быстро. Не так как на видео. А при таком режиме цвета действительно мельтешат.
Кстати. Из того что я заметил — самая любимая кнопка — это кнопка выключения. Почему-то все тянутся именно к ней.igolets
04.08.2015 10:46Полагаю, что причина в основном в том, что кнопка выключения светится :) Я настроил специальный профиль питания, где эта кнопка ничего не делает.
На счет быстрого нажатия — будем экспериментировать. Переходы при быстром нажатии не помогут — они либо начнут тормозить и программа будет жить своей жизнью какое-то время после прекращения нажатий (как сейчас звук), либо переходы будут такими быстрыми, что снова всё будет мельтешить.
Кстати, данная игра, иллюстрирует и правило Парето — 20% мелких функциональных наворотов (мельтешение, задержка звуков при быстром нажатии) займут 80% времени разработки. Вспомнилась статья по теме: russian.joelonsoftware.com/Articles/Craftsmanship.htmlImpuls
04.08.2015 10:56Думаю что решением данной проблемы будет сопоставление ближайших цветов — рядом стоящим клавишам. Поясню на примере: Клавише «П» поставить в соответствие красный, «Р» — розовый, «О» — фиолетовый, «И», «Т», «Н», «Г» — далее по списку рядом стоящих цветов.
Идея в том, чтобы смягчить переходы при нажатии на ближайшие кнопки, т.к. именно такие переходы и совершаются чаще всего (жми то, что находится под ладошкой :-))
Думаю это не сильно усложнит сам код. Тяжелее будет подбирать такие сочетания цветов. Неплохой идеей будет поговорить со знакомым дизайнером или художником. Уж они то точно должны хорошо разбираться в цветах
Kvarkas
04.08.2015 13:45Супер! Моему правда 1,5 уже поздно, он пристрастился к Игре Клаш оф Кланс, уже умеет собирать элексир и монетки с шахт и строить войска, только пока еще передвигает башни по екрану в случайном порядке и обожает составлять стенки в кучку! :)
yar3333
Вы — молодец! Тоже подумывал сделать нечто подобное — ребёнок (1 год от роду) тоже очень любит клавиатуру :)
yar3333
yadi.sk/d/SEW0QDaKiEo3j — ссылка на бинарники, автор, думаю, не против? :)