
С 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.destroy() можно несколькими способами.- Использовать запаздывание time. Можно запустить отдельный «поток», который будет периодически «заглядывать» в основной и удалять root-окно. А отсчет времени реализовать, например, на базе time.sleep().
- Использовать встроенный метод root.after(). С помощью него я удаляю старые кадры.
... # после запуска Tkinter удалит кадр через 1 секунду при speed = 2 root.after(1200 - speed*100, root.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.

