Предыстория


Мой сын, как, наверное, все дети программистов, получил свою первую клавиатуру ещё когда не умел сидеть. Сейчас ему чуть меньше года, но он уже понимает разницу между «игрушечной» и «настоящей» (папиной) клавиатурой — если колотить по кнопкам настоящей, то на экране меняется картинка, а компьютер иногда издаёт какие-то звуки.

КДПВ

Поскольку лишиться всех своих данных мне пока не хочется, ребёнку иногда разрешается нажимать на кнопки заблокированного компьютера. К сожалению, для ребёнка это не очень весело, поскольку компьютер имеет всего два режима (две картинки) — экран ввода пароля и собственно экран блокировки.

Чтобы процесс освоения компьютера стал для детёныша более увлекательным, я решил написать ему простенькую игру. Будучи программистом со стажем, весь процесс решено было построить «правильно».

Требования


Заказчик (мой сын, возраст <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)


  1. yar3333
    03.08.2015 13:06
    +4

    Вы — молодец! Тоже подумывал сделать нечто подобное — ребёнок (1 год от роду) тоже очень любит клавиатуру :)


    1. yar3333
      03.08.2015 13:14
      +1

      yadi.sk/d/SEW0QDaKiEo3j — ссылка на бинарники, автор, думаю, не против? :)


  1. el777
    03.08.2015 13:06
    +1

    [offtop]
    Точно — статья «из песочницы». )))
    P.S. Поздравления молодому отцу )
    [/offtop]


  1. Loxmatiymamont
    03.08.2015 13:19
    +6

    Я, конечно, параноик, но очень уж активно все офтальмологии говорят не смотреть детям в экран хотя бы до 2х лет.


    1. denser
      03.08.2015 13:50

      Детей привлекает все движущееся и новое, на экране этого добра может быть валом. Вредно смотреть долго на объект на одном расстоянии, в мониторе же нет глубины? Поэтому лучше дозировать или ограничить до возраста, когда детки могут тебя слушать и не смотреть любимый мультик, когда попросишь.


      1. Loxmatiymamont
        03.08.2015 13:55
        +26

        т.е. лет до тридцати? ))


        1. denser
          04.08.2015 09:29
          +1

          А то и больше )


    1. igolets
      04.08.2015 10:24

      К сожалению, если почти все взрослые, которые окружают нашего малыша смотрят в экран и тыкают по клавиатуре, очень сложно будет объяснить, что ему так делать не стоит. Понятно, что родительское внимание и воспитание нельзя заменить компьютером, но:

      • Больше, чем на 2-3 минуты его всё равно не хватает
      • Можно «вести себя как папа» и никто ему ничего не скажет ;)
      • Развивается мелкая моторика
      • Ребёнку стало интереснее нажимать на кнопки, а не выковыривать их (я замучался, если честно, вставлять на место ноутбучные кнопки)
      • Старшие дети заинтересовались программированием, будем учиться геймдеву :)


      1. Loxmatiymamont
        04.08.2015 10:39

        Правильно говорите, но сейчас «экран» больше ассоциируется со смартфонами и планшетиками, кои позволяют быстро и дешево успокоить и/или отвлечь ребёнка.

        Вот как тут поступать уже зависит исключительно от родителей. Я своему решительно запрещаю брать телефоны и планшет, а мелкую моторику гораздо лучше в песочнице развивать. А правильно профессии ещё успею научить =)


      1. MasMaX
        04.08.2015 15:46

        Лучше игрушки дайте, чем будет в экран пялится.
        У меня сыну 2.5, мультики по телеку смотрим минут по 15 в день, а то и меньше. Иногда на компе показываем. Нажимать клавиши разрешаем на выключенном компе, на включенном ребенок хорошо понимает что может что то там сломать.

        А для моторики купите конструкторы.


  1. Lipa
    03.08.2015 14:57
    +4

    Со стороны UI есть предложения:
    — увеличить размер символов (до 70% экранной высоты)
    — менять цвет символов на темном фоне


    1. WeslomPo
      04.08.2015 08:49

      — добавить плавную смену цветов, чтобы не вызвать эпилептический припадок


  1. rustler2000
    03.08.2015 14:58
    +2

    В свое время приделал русский для своего мелкого вот к этому — github.com/shanselman/babysmash


  1. samodum
    03.08.2015 16:27
    +2

    Иногда даже полезно дать клавиатуру ребёнку.
    Дочка в 2 года случайно нашла комбинацию в Skype — изображение кота при нажатии одновременно c+a+t
    А в айпаде нашла комбинацию как делать скриншоты. Пришлось потом гуглить, чтобы понять как она это сделала :)


    1. igolets
      03.08.2015 22:28
      +2

      Мой сын на заблокированном экране включил Narrator и ноут озвучивал нажатые клавиши. Несколько раз я отчетливо услышал «Unknown key».

      А в одной из промежуточных версий описываемой программы выводился сам символ, который введен на клавиатуре, так он смог ввести символ «многоточие».

      После этого анекдот про бесконечно много обезьян и «Войну и мир» приобретает новые смыслы ;)


    1. Igor_Sib
      04.08.2015 08:13

      Любые 3 кнопки рядом вроде раньше было чтобы показать котика


    1. WeslomPo
      04.08.2015 08:46

      Кот в skype появляется при беспорядочном нажатии на клавиши, и означает что по клавиатуре прошлась кошка, или пользователь не в себе.


  1. 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-smell


    1. SirEdvin
      03.08.2015 20:02

      Допустим, у вас есть некий класс, который заполнен функциональностью.
      Почему бы не использовать регионы для смыслового разделения кода всего класса на некие фрагменты? Или проще листать девять страниц кода?


      1. alkozko
        03.08.2015 22:15
        +2

        если класс занимает 9 страниц, то он скорее всего нарушает SRP и нуждается в рефакторинге, а не в регионах


        1. SirEdvin
          03.08.2015 23:11

          SRP очень круто звучит в теории, но на практике с ним происходят некоторые проблемы.
          Например, у меня есть класс матриц с значительным количеством методов 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, то тут он повел себя как антипаттерн, на мой взгляд.


          1. igolets
            04.08.2015 01:07

            Fluent-запись выглядит более модной, но, если честно, оба варианта вызывают ассоциацию с Haskell.
            Что именно не так с этим кодом по такому фрагменту сказать сложно и, наверное, лучше не в комментах, но результат вполне может быть началом путёвой статьи для Хабра.
            Если что, контакты мои в есть профиле. :)


        1. KvanTTT
          04.08.2015 01:54

          Есть исключения: перегрузка визиторов при разборе большого формального языка.


        1. return_true
          04.08.2015 11:43

          К сожалению регионы пихают не только в классы на 9 страниц. Вот у автора есть класс на 42 строки (без учёта пустых строк) из которых на регионы приходится 10. Практически четверть кода!


      1. KvanTTT
        04.08.2015 01:55

        Можно использовать partial классы.


        1. return_true
          04.08.2015 11:35

          Верно, регионы в C# и добавили для «сворачивания» автогенеренного кода WinForms. Потом уже придумали partial классы.


    1. igolets
      03.08.2015 22:42

      Следует понимать эту строку следующим образом:

      1. Правильный кодинг стайл включает в себя всё, не только регионы :)
      2. Использование комментариев и регионов — часть кодинг стайла

      Т.е. если кому-то не нравятся регионы, можно их и не использовать, но те же комментарии и прочее (правила именования классов, общее оформление) нужно применять и по возможности правильно.

      Лично мне регионы удобны, т.к. я могу быстро выделить только публичные методы/свойства класса и пробежаться по его «фасаду». Можно было бы обойтись просто группировкой, но с регионами нагляднее.


  1. ErhoSen
    03.08.2015 18:31

    Здорово!
    Недавно делал свой «What's color is it?», и для того чтобы картинка всегда оставалась контрастной применял к цифрам инвертированный цвет фона) Можно например сделать так, чтобы глаза меньше уставали)


    1. stansult
      03.08.2015 21:00
      +4

      “What’s” = “What is”

      Я бы написал “What color is it?” или “What’s the color?”


      1. OlegTar
        03.08.2015 21:53
        +3

        What's и is — чтоб наверняка


  1. dj_raphael
    03.08.2015 22:06
    +1

    Я разрешал молотить так по клавиатуре и елозить мышкой прямо на рабочем столе. И когда примерно чуть больше года ему было он славно потролил жену.
    Приходит она с кухни — все иконки выставлены горизонтально в ряд по центру рабочего стола. Ну может глюк после выхода из игры, после зумы бывает иконки сдвигаются. Расставила быстро всё по углам и опять ушла на кухню. Приходит через некоторое время — опять та же картина. «Что за хрень?» — расставила всё обратно и опять на кухню. Ребенок всё это время игрался в машинку на столе катал её туда сюда, иногда использовал мышку в качестве второй машинки для компании.
    Выглядывает она из-за угла и наблюдает картину, как ребенок мышкой расставляет иконки горизонтально в ряд. 1 год.
    Специально не учили. просто разрешали молотосить по клаве и елозить мышкой, везде. залазил на колени, когда играли в зуму, потом он отбирал мышку и весело расстреливал все шары по сторонам.


    1. igolets
      04.08.2015 10:10

      Если есть чужой ноутбук, то всё проще — отдал ребёнку ноут жены и не жалко. Свой нут на растерзание отдавать не хочется. ;)


  1. Igor_Sib
    04.08.2015 08:15

    Дети вообще странным образом понимают ценность предметов. То же самое в полтора-два года — нужен был именно мамин телефон, когда подсовывают старый не работающий — выбрасывал. :)


    1. igolets
      04.08.2015 10:13

      Мне думается, что это какой-то «попугайский» инстинкт. Мама тыкает пальцем по телефону и там меняется картинка. Папа тыкает пальцами по клавиатуре и тоже что-то происходит. Если запрещать, то возникнет либо нездоровый интерес (навязчивая идея), либо комплекс. В данном случае я решил разрешить пользоваться компьютером, но под контролем.
      Дети чуть постарше ведут себя похожим образом — наши старшие (начальные классы школы) очень заинтересовались написанием игр для брата, будем с ними вместе изучать программирование :)


  1. Impuls
    04.08.2015 08:44

    Мельтешат цвета. Запилите плавный переход к цвету, а то тут и до эпилепсии недалеко. Как пример — плавный переход цветов при установке windows 8.1 и выше.
    Вот пример. Смотреть с 7:20


    Правда там видео немного ускорено. В оригинале немного медленнее.


    1. igolets
      04.08.2015 10:17
      +1

      Идея понятна. Про эпилепсию, конечно, вопрос спорный, припадки возникают если есть предрасположенность, Просто мельтешения цветов мало. В любом случае, её лучше продиагностировать в раннем возрасте (шутка).
      Будем «наблюдать за сценариями использования программы», «собирать обратную связь от пользователя», а интересные идеи попробуем воплотить со старшими детьми. Возможно, это выльется в статью про обучение программированию школьников начальных классов :)
      Есть несколько идей как развивать игру по мере взросления малыша — учить слова, буквы.


      1. Impuls
        04.08.2015 10:22

        У меня ребенок на клавиши жмет очень быстро. Не так как на видео. А при таком режиме цвета действительно мельтешат.

        Кстати. Из того что я заметил — самая любимая кнопка — это кнопка выключения. Почему-то все тянутся именно к ней.


        1. igolets
          04.08.2015 10:46

          Полагаю, что причина в основном в том, что кнопка выключения светится :) Я настроил специальный профиль питания, где эта кнопка ничего не делает.

          На счет быстрого нажатия — будем экспериментировать. Переходы при быстром нажатии не помогут — они либо начнут тормозить и программа будет жить своей жизнью какое-то время после прекращения нажатий (как сейчас звук), либо переходы будут такими быстрыми, что снова всё будет мельтешить.

          Кстати, данная игра, иллюстрирует и правило Парето — 20% мелких функциональных наворотов (мельтешение, задержка звуков при быстром нажатии) займут 80% времени разработки. Вспомнилась статья по теме: russian.joelonsoftware.com/Articles/Craftsmanship.html


          1. Impuls
            04.08.2015 10:56

            Думаю что решением данной проблемы будет сопоставление ближайших цветов — рядом стоящим клавишам. Поясню на примере: Клавише «П» поставить в соответствие красный, «Р» — розовый, «О» — фиолетовый, «И», «Т», «Н», «Г» — далее по списку рядом стоящих цветов.
            Идея в том, чтобы смягчить переходы при нажатии на ближайшие кнопки, т.к. именно такие переходы и совершаются чаще всего (жми то, что находится под ладошкой :-))
            Думаю это не сильно усложнит сам код. Тяжелее будет подбирать такие сочетания цветов. Неплохой идеей будет поговорить со знакомым дизайнером или художником. Уж они то точно должны хорошо разбираться в цветах


            1. igolets
              04.08.2015 11:11

              Можете поучаствовать — ресурсный файл поправить в редакторе :)


  1. Kvarkas
    04.08.2015 13:45

    Супер! Моему правда 1,5 уже поздно, он пристрастился к Игре Клаш оф Кланс, уже умеет собирать элексир и монетки с шахт и строить войска, только пока еще передвигает башни по екрану в случайном порядке и обожает составлять стенки в кучку! :)