С 2016 года у некоторых моделей MacBook Pro есть сенсорная OLED-панель. По сути, она просто заменяет функциональные клавиши. Но с ней чуть интересней: на тачбар можно вывести закладки и даже медиаэлементы.

На примере игры с динозавриком показываю, как написать свою программу для тачбара с помощью открытой библиотеки PyTouchBar.

В конце статьи — конкурс на плюшевого тирекса.

Меню игры: основные элементы библиотеки


Кнопка «играть». PyTouchBar работает в связке с Tkinter. Для начала нужно установить и первый, и второй модули. А после — подготовить GUI-окно, которое будет отображаться на тачбаре. И добавить, например, кнопку «играть».

from tkinter import *
import PyTouchBar

# создание окна Tkinter
root = Tk() 
# параметризация кнопки
starter = PyTouchBar.TouchBarItems.Button(title='играть', color=(PyTouchBar.Color.green)) 

# добавление элемента
PyTouchBar.set_touchbar([starter])
# подготовка окна 
PyTouchBar.prepare_tk_windows(root) 

root.mainloop()

Подготовка, добавление кнопки.

После запуска программы кнопка отобразится на панели.


Для окрашивания кнопки я использую встроенную константу PuTouchBar.Color, хотя то же самое можно сделать через rgba. Для этого в функцию нужно передать кортеж формата (r, g, b, a), где r, g, b, a — значения от 0 до 1.

Если нажать на кнопку, ничего не произойдет. Для запуска подпрограммы нужно добавить аргумент action и сослаться на имя функции.

...

def start(button):
   # здесь будет запуск игровой сцены game
   print('Hello world')

starter = PyTouchBar.TouchBarItems.Button(title='играть', color=(PyTouchBar.Color.green), action=start)

...

Добавление функции для action кнопки.

Подпрограмма start нужна для запуска игровой сцены game.

Настройка скорости динозаврика. Библиотека поддерживает не только простые кнопки, но и так называемые «степперы» — с помощью них можно вводить числовые значения. В нашем случае — скорость динозаврика.

...

# минимальная скорость
speed = 2 

def set_speed(stepper): 
  global speed
  speed = int(stepper.value)

# параметризация степпера
speed_p = PyTouchBar.TouchBarItems.Stepper(min = 2, max = 8, action = set_speed)

# добавление кнопки и степпера
PyTouchBar.set_touchbar([speed_p, starter]) 

...

Добавление степпера.

После запуска программы элементы отобразятся на панели.


На самом деле PyTouchBar поддерживает больше элементов. Среди них — палитры, текстовые лейблы, слайдеры и другие. Некоторые из них мы добавим в игру ниже. С полным списком можно ознакомиться в официальной документации.


Статическая сцена: загрузка карты и ассетов


Представление 2D-сцены. После нажатия кнопки «играть» должна сработать специальная функция game, которая нужна для отрисовки сцены — динозаврика и кактусов. Их я тоже добавил с помощью кнопок, которые условно называю чанками.

Каждый чанк — целочисленное значение:
  • -2 — динозавр врезался в кактус,
  • -1 — кактус,
  • 0 — плоскость,
  • 1 — динозаврик,
  • 2 — динозавр перепрыгнул через кактус.

Карту можно представить так:

map = [1, 0, 0, -1, 0, -1, 0]

Визуализация элементов. Кнопки поддерживают параметр image: на фон можно поставить любое статическое изображение.

Так, например, можно «пройтись» циклом по map и наполнить список buttons кнопками с изображениями, выбранными по значениям чанков.

for chunk in range(len(map)):
   if map[chunk] == 1:
       buttons[chunk] = PyTouchBar.TouchBarItems.Button(image='assets/trex.png', color=(PyTouchBar.Color.black), action=jumping)
   elif map[chunk] == 0:
       buttons[chunk] = PyTouchBar.TouchBarItems.Button(image='assets/platform.png', color=(PyTouchBar.Color.black))
   else:
       buttons[chunk] = PyTouchBar.TouchBarItems.Button(image='assets/cactus.png', color=(PyTouchBar.Color.black))

Отрисовка карты map с помощью кнопок.

В результате схема карты преобразится в игровую сцену.

Представление игровой сцены.

Что нужно учитывать. Для запуска сцены нужно закрыть root-окно и запустить новое. PyTouchBar не поддерживает обновление Tkinter-программ.

def game(map):
    # создание нового root-окна
    root = Tk()
    # отрисовка карты map
    ...
    # запуск нового root-окна
    root.mainloop()

def prepare():
    root = Tk()
    
    def start(button): # запускается по клику кнопки «играть»
        root.destroy()
        ...
        # генерируем карту и передаем в функцию игровой сцены
        game(map, speed)
           
    ...

    PyTouchBar.set_touchbar([starter])
    PyTouchBar.prepare_tk_windows(root)
    root.mainloop()
           
prepare()

Переключение между окнами.

Но как тогда заставить динозаврика бежать?

Динамическая сцена: работа с кадрами


Чтобы динозаврик побежал, нужно как-то обновлять сцену без root.update(). То есть уничтожать настоящую сцену с помощью root.destroy() и запускать новую с обновленными параметрами map. Получается некое обновление кадров.

Удаление старых кадров. После переключения из меню игры на сцену удаление root было заложено в функцию, которая активировалась при нажатии кнопки. Автоматизировать root.destroy() можно несколькими способами.

  1. Использовать запаздывание time. Можно запустить отдельный «поток», который будет периодически «заглядывать» в основной и удалять root-окно. А отсчет времени реализовать, например, на базе time.sleep().
  2. Использовать встроенный метод root.after(). С помощью него я удаляю старые кадры.

...

# после запуска Tkinter удалит кадр через 1 секунду при speed = 2
root.after(1200 - speed*100, start.destroy) 
root.mainloop()

...

root.after(), пример вызова метода destroy.

Первый аргумент — время, спустя которое удалится root-окно. С помощью него можно установить скорость игры: чем быстрее обновляются кадры, тем быстрее бежит динозаврик.

Создание новых кадров. По сути, генерировать новые кадры можно разными способами. Я выбрал один из самых простых: делаю срез списка map на один элемент и добавляю рандомный чанк в конец. А после — запускаю на новой карте игровую сцену game.

# бесконечная генерация новых чанков и кадров
while True:
     new_map = map[1:]
     # первый чанк — всегда динозавр (1)
     new_map[0] = 1

     # условие, чтобы не генерировать два кактуса подряд  
     if map[-1] == 0:
               new_map.append(random.choice([0, -1]))
     else:
               new_map.append(0)

     map = new_map
     game(new_map, speed)

Генерация новых кадров. Одна итерация — один новый кадр и чанк.

На тачбаре это выглядит вот так:


Для большей плавности нужно создать ассеты для промежуточных кадров. И добавить в игровую сцену дополнительный цикл при отрисовке чанков.


Добавление событий. Предпоследний этап — то, ради чего играют в Google-динозаврика, — «паркур по кактусам».

Чтобы добавить прыжок, нужно завести переменную jump — к ней мы будем прибавлять единицу при нажатии на кнопку с динозавром. И модифицировать список map таким образом, чтобы в первом чанке кактус и динозавр могли встретиться несколькими способами.
  • Динозаврик врезался в кактус — чанк -2. Комбинация [1, -1], jump = 0.


  • Динозаврик перепрыгнул кактус — чанк 2. Комбинация [1, -1], jump = 1.


Вот как «сценарии» записаны в программе:

...

jump = 0
# первый элемент — комбинация значений, где второе значение — следующий чанк
map = [[1,0], 0, 0, -1, 0, -1, 0]

...

while True:
     # итоговое значение для комбинации: 
     # [1,0] → 1 кактуса нет, динозаврик бежит
     # [1,-1] → кактус есть: если jump = 1, все хорошо
     zero_chunk = 1
     # записываем комбинацию 
     map[0] = [1,map[1]]
           
     # динозаврик не прыгнул на прошлом чанке и врезался
     if jump == 0 and map[0] == [1, -1]:
               zero_chunk = -2
     # на чанке без кактуса jump обнуляется
     elif jump >= 1 and map[0] == [1, 0]:
               jump = 0
               zero_chunk = 1
     # динозаврик прыгнул на прошлом чанке, все хорошо
     elif jump == 1 and map[0] == [1,-1]:
               zero_chunk = 2
               points += 1
  
     # настраиваем формат карты для передачи в функцию
     new_map = map[1:]
     new_map[0] = zero_chunk
     if map[-1] == 0:
          new_map.append(random.choice([0, -1]))
     else:
          new_map.append(0)

     map = new_map 
     game(new_map, speed)

Ситуативная генерация новых кадров.

В коде отрисовки сцены также нужно добавить новые события:

def game(buttons)
...

     for b in range(len(map)):
          # если динозаврик не перепрыгнул, загружаем ассет со столкновением
          if map[b] == -2:
               buttons[b] = PyTouchBar.TouchBarItems.Button(image='assets/loss.png', color=(PyTouchBar.Color.black))
          # если динозаврик перепрыгнул, загружаем ассет с прыжком
          elif map[b] == 2:
               buttons[b] = PyTouchBar.TouchBarItems.Button(image='assets/lucky.png', color=(PyTouchBar.Color.black))

...

Отрисовка игровой сцены.

На тачбаре это выглядит вот так:


Динозаврик научился бегать и прыгать!

Возможно, эти тексты тоже вас заинтересуют:

Docker на роутере MikroTik: как развернуть и не утонуть в багах
Паттерны взаимодействия с ботами в Telegram: неочевидные практики на Python и баг в мессенджере
Делаем тетрис в QR-коде, который работает

Сообщение об окончании игры: элемент Label


Если динозаврик все же врезался, игру необходимо завершить: показать игроку количество набранных очков и сообщение.

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

Вывод сообщения. Чтобы вывести сообщение, нужно создать новое окно Tkinter, подготовить его и добавить текстовое поле.

def finish(points):
    ...

    # объявление элемента label
    label = PyTouchBar.TouchBarItems.Label(text = f'Упс:( Вы набрали: {points}')

    ...

Добавление элемента Label.

Элемент Label поддерживает разные шрифты, цвета и масштабы. Полный список параметров есть в официальной документации.

Притом вызвать функцию finish нужно после отрисовки сцены.

while True:
    points = 0 
    ...

    # динозаврик не прыгнул на прошлом чанке и врезался
    if jump == 0 and map[0] == [1, -1]:
        zero_chunk = -2
    elif jump == 1 and map[0] == [1, -1]:
        zero_chunk = 2
        points += 1
    ...
    game(new_map, speed)
  
    # если динозаврик врезался — вызвать finish()
    if zero_chunk == -2:
        finish(points)

Обработка события «конец игры».

Арендуйте выделенный сервер с запуском от 2 минут и бесплатной заменой комплектующих. И используйте его ресурсы для гейминга.



Как закрыть сообщение и программу? У PyTouchBar есть особенность: с помощью библиотеки можно модифицировать встроенную кнопку escape. Например, сделать ее кнопкой для закрытия программы.

...

def exit_f(button):
    exit()

esc = PyTouchBar.TouchBarItems.Button(title = "exit", action = exit_f)
PyTouchBar.set_touchbar(... , esc_key = esc)

...

Полная версия кода доступна на GitHub. Подключайтесь и предлагайте свои улучшения.

Особенности в работе с PyTouchBar


Отсутствие настройки расстояний. Минимальное расстояние между кнопками фиксированное. Это плохо, если нужно сделать «непрерывное» изображение на панели.

Расстояния между элементами нельзя сократить, но можно увеличить. Для этого в библиотеке есть элемент Space — пустое пространство.

«Сырые» контроллеры. В библиотеке есть контроллеры (Control). По сути, это те же кнопки, но сопряженные между собой. Хотя у них нет некоторых параметров. Например, нельзя задать цвет фона.


А другие настройки и вовсе работают хуже, чем в элементе Button. Например, чтобы добавить изображение, его нужно «подогнать» под размеры контроллера. И, возможно, дополнительно использовать параметр image_scale, перебирая различные константы в поисках лучшего разрешения картинки.

Элементы не поддерживают gif. Это плохо, если нужно написать программу с большим количеством анимаций. Конечно, можно сделать раскадровку самостоятельно и обновлять окно приложения с заданной периодичностью. Но это сильно усложняет алгоритм.

«Непрямое» обновление окон. Единственный способ обновить окно — закрыть его и создать заново. Метод root.update() не работает.

В сети нет подробной документации. О назначении некоторых функций остается только догадываться. Документация ограничивается репозиторием на GitHub. А некоторая ее часть не актуальна.

Например, есть раздел про интеграцию библиотеки с Pygame. Буквально несколько строчек кода — и все, больше ничего нет. К тому же загрузка Pygame через PyTouchBar на данный момент не работает. Будем ждать апдейтов от разработчиков.

На что способна библиотека?


Даже с ограничениями библиотека кажется интересной. С помощью нее можно написать софт для решения повседневных задач. Например, сделать дополнительный ряд кнопок для всяких спецсимволов, если надоела раскладка Бирмана.

Но Doom с помощью PyTouchBar запустить будет сложно: не понятно, как «вытягивать» отдельные кадры и управлять игрой через панель. Для более сложных проектов лучше «прыгнуть в нору за кроликом» и программировать тачбар с помощью Objective-C. А если нужно просто подключить какой-то виджет, лучше установить утилиту вроде BetterTouchTool.

Придумайте, какую еще программу или игру можно написать для тачбара. Самому креативному комментатору отправим нашего маскота — плюшевого тирекса Selectel.

Комментарии (11)


  1. s_suhanov
    09.11.2022 15:31
    -10

    Очень полезная статья, учитьівая, что тач-бара нет в макбуках уже года так два. ????

    Похоже, что вам там на руси все тяжелее следить за новинками в мире. Та и не нужно оно вам, наверное. Дедьі и без макбуков воевали. ????


    1. Doctor_IT Автор
      09.11.2022 15:51
      +4

      В новых моделях — нет. Но прошки прошлых лет перевыпускают с тачбаром. Например, в новом MacBook Pro 13 его оставили.


    1. Eugeeny
      09.11.2022 23:28
      +1

      Зачем "деды" маскировать? Я было подумал, у меня пыль на экране.


      1. PanDubls
        10.11.2022 02:40
        +1

        У человека нет Ы на клавиатуре.


    1. naff
      10.11.2022 10:34
      +1

      Он прям со всех исчез? У меня вот валяется два - ни на одном не исчез, странно это.


  1. i273
    09.11.2022 20:05
    +1

    Читая статью, уже хотел про doom написать, а автор уже предвидел такие вопросы.

    Предложение к написанию: счётчик введённых символов и километраж пройденный мышкой, втрое - максимальные значения, которые удалось достигнуть


  1. KonstantinGreat
    09.11.2022 20:42
    +2

    вывел кнопку, счастью нет предела)

    я добавил к старту:
    pip install tk
    pip install PyTouchBar
    pip install -U pyobjc


  1. sheeva
    10.11.2022 11:25
    +1

    Спасибо, интересная статья.
    Идеи такие:
    1) клавиатурный тренажер: на тачбар выводить набираемую фразу и статистику,
    2) полный или частичный интерфейс для оффлайн или онлайн (Ютьюб, Нетфликс) медиа плеера: название песни/фильма, тайминг, кнопки пауза/старт, таймлайн с тайм-кодами и возможностью перехода, эквалайзер, или даже караоке,
    3) муз. игры и тренажеры: показывать биты/ноты,
    4) квизы, тесты: показывается вопрос с переходом по тапу на варианты ответа + таймер,
    5) полноценная игра типа Wordle/Nerdle/Mastermind (надо поработать над интерфейсом представления пред. попыток),
    6) или доп. интерфейс для соревновательных и ММО игр, напр. Дота, Лол, ВоВ.


  1. andy-takker
    10.11.2022 12:30
    +1

    Идея: Таймер помодоро - чтобы всегда был перед глазами и можно было быстро настроить по кнопкам


    1. sheeva
      10.11.2022 16:03
      +1

      Да, отличный вариант! А если еще добавить визуализацию истекающего времени в виде уменьшающегося бара - приближающегося пи... дед-лайна %()
      Уже хочу


  1. el_glumo
    11.11.2022 14:04
    +1

    Можно сделать игру, где кнопки тачбара будут выполнять роль клавиш синтезатора. А на экране тем временем перпендикулярно клавишам будут падать ноты в ритм каких-нибудь мелодий. Аналог Synthesia, если кратко.

    Так же можно сэмулировать драм-машину и по нажатию клавиш воспроизводить записанные микрофоном звуки. Сами клавиши раскрасить в цвета диско)