Сколько раз я рисовал интерфейс прямо в коде: setCursor с числами от балды, перекомпиляция, взгляд, снова двигаю. На третий круг цифра уезжала под шапку, потому что сетку я держал в голове, а не на бумаге. Раскладка кадра — работа до первого display(): лист в клетку, 21 клетка в ширину, восемь в высоту, и все зоны прибора, обведённые карандашом, прежде чем им дадут координаты. Сетка у SSD1306 простая: шрифт размера 1 занимает знакоместо 6×8, значит по ширине 128 пикселей умещается 21 символ, по высоте 64 — ровно восемь строк. Координаты идут от нуля: x от 0 до 127, y от 0 до 63; за краем буфера текст молча не рисуется, без предупреждений на компиляции. Сетку и цену кадра я считал в уроке про графику и примитивы, сегодня — дисциплина раскладки: зоны, отступы и перенос бумажного макета в скетч.
Железо то же, что во всём разделе: SSD1306 128×64 на I2C и ручка на A0 для демо. Новых библиотек нет: drawRect, fillRect и drawLine из Adafruit GFX — весь инструмент. План такой: три типовых шаблона раскладки с числами, полный скетч каркаса с константами зон и симулятор, где шаблоны переключаются ручкой.
Правило, к которому я пришёл за десяток приборов: макет сначала на бумаге, координаты — потом. Пять минут карандашом экономят вечер перестановки чисел в коде.
Миллиметровка: зоны кадра
Любой приборный кадр у меня делится на три полосы. Шапка занимает строки 0-1, пиксели y = 0-15: имя прибора и статус, мелкий шрифт. Тело — строки 2-6, y = 16-55: данные, ради которых экран и стоит в корпусе. Футер — строка 7, y = 56-63: подсказка по кнопкам или единицы измерения. Полосы кратны восьми не из эстетики: строка шрифта размера 1 занимает ровно восемь пикселей по высоте, и границы зон, поставленные на 0, 16 и 56, никогда не режут буквы пополам. Куда поставить границу между 15 и 16 — вопрос вкуса, но она обязана лечь на кратность восьми, иначе верхний ряд букв тела утонет в шапке.
Отступы — вторая цифра на миллиметровке. Я ставлю три пикселя слева и по два сверху и снизу: текст, прилипший к нулевой колонке, выглядит прижатым к стеклу, а рамка по периметру забирает ещё по пикселю с каждой стороны. Каждую зону на макете подписываю четырьмя числами: левая граница, верх, ширина, высота. Эти же четыре числа потом уходят в аргументы fillRect и drawRect без пересчётов — механику самих примитивов, порядок аргументов w и h и работу со слоями разобрали в уроке про рамки и гистограммы, здесь они работают как перенос разметки.
Чертить удобнее в масштабе два к одному: клетка бумаги — полпикселя экрана, и весь кадр занимает прямоугольник 42 на 16 клеток — влезает на пол-листа А5, лист остаётся для заметок. Карандаш, не ручка: раскладка переживает три-четыре версии, и стирать дешевле, чем перечерчивать. На каждый кадр я подписываю сверху имя прибора и дату — через полгода тетрадь становится архивом раскладок, и новый интерфейс часто начинается с перевода старого. Клетчатая сетка экрана — те же 21 и 8 ячеек, что у текстового режима: в симуляторе ниже они видны один в один.
| Шаблон | Шапка (y) | Тело (y) | Футер (y) | Где живёт |
|---|---|---|---|---|
| Прибор | 0-15, плашка с именем | 16-47, крупная цифра размера 2 | 56-63, подсказка | термостаты, зарядки |
| График | 0-7, подпись максимума | 8-55, лента точек | 56-63, подпись минимума | самописцы, мониторы |
| Меню | 0-7, заголовок | 8-47, строки по 8 px | 56-63, статус | настройки, интерфейсы |
Шаблон — стартовая раскладка, а не догма: каждый прибор правит её под себя. Считать место надо по высоте букв: цифра размера 2 занимает 16 пикселей, размера 3 — все 24, и если в теле задуманы две крупные строки, тело обязано быть не уже 32 пикселей. Это видно на бумаге до кода, а в коде всплывает уже нарисованным. Про размеры шрифтов и сколько символов остаётся в строке на каждом размере — отдельный урок про setTextSize.
Отдельная история — компактные экраны 128×32: у них восемь знакомест по ширине остаются, а по высоте сжимаются до четырёх строк, и трёхполосная схема уже не помещается. Там выручает двухполосная раскладка без шапки: тело и футер, а имя прибора переезжает в шапку тела первой ячейкой. Приём перепланировки описан в уроке про перенос на 128×32 — полезно прогнать через него тот же макет, когда экран в проекте меняется на меньший.
Дальше — константы. Четыре цифры каждой зоны переезжают в начало скетча: OTSTUP, H_SHAPKA, Y_TELO, Y_FUTER. Магические числа внутри setCursor — главный источник съехавших кадров: через месяц уже не вспомнить, что значит 17, и правка одного числа ломает три места сразу. С константами смена раскладки — правка пяти строк в одном месте, а не поиск по всему скетчу.
Перенос макета в скетч
Adafruit_SSD1306 display(128, 64, &Wire, -1); // Zony kadra - vse chisla pereneseny s bumazhnogo maketaconst int OTSTUP = 3; // pole slevaconst int H_SHAPKA = 14; // vysota plashki shapkiconst int Y_TELO = 20; // verh krupnoj tsifryconst int Y_FUTER = 57; // stroka 7 void setup() { display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE);} void risujKadr(int znacheniye) { display.clearDisplay(); // 1. Ramka i zalivki - strogo do teksta display.drawRect(0, 0, 128, 64, SSD1306_WHITE); display.fillRect(1, 1, 126, H_SHAPKA, SSD1306_WHITE); display.drawLine(0, 55, 127, 55, SSD1306_WHITE); // 2. Shapka: chyornyj tekst po beloj plashke display.setTextSize(1); display.setTextColor(SSD1306_BLACK); display.setCursor(OTSTUP + 1, 3); display.print("REOSTAT A0"); // 3. Telo: krupnoye znacheniye display.setTextColor(SSD1306_WHITE); display.setTextSize(2); display.setCursor(OTSTUP, Y_TELO); display.print(znacheniye); // 4. Futer: podskazka display.setTextSize(1); display.setCursor(OTSTUP, Y_FUTER); display.print("Ruchka A0 - znacheniye"); display.display();} void loop() { int v = map(analogRead(A0), 0, 1023, 0, 99); risujKadr(v); delay(100);}Порядок в risujKadr — это порядок слоёв с миллиметровки: рамка и заливки раньше текста, текст поверх. Поменяйте местами плашку и печать шапки — получите белое поле с невидимым текстом под ним. Разделитель футера я кладу на y = 55, на два пикселя выше строки футера: линия стоит в зазоре и не наезжает на буквы. Рамку drawRect рисую первой, чтобы никакая заливка после неё не срезала контур — сигнатуры примитивов и ластик чёрным считаются в уроке про линии и фигуры.
Живое место из моей практики: в приборе для зарядки тело стояло на y = 14, потому что шапка была в одну строку, — а потом в шапке появился второй символ статуса, и цифра легла прямо на него. На бумаге это случилось бы тоже, но карандаш дешевле перекомпиляции, и заметил бы я наложение за минуту, а не на приборе. Теперь каждый новый кадр сначала занимает клетчатый лист в тетради: шапка, тело, футер, подписанные числа. Список того, что печатается в каждой зоне, помогает держать в голове порядок вызовов setCursor — та же механика курсора из урока про print и setCursor, только спланированная заранее.
Правки после сдачи прибора веду по той же схеме: сначала бумага, потом код. Хочется добавить колонку тока справа — карандашом прикидываю ширину, делю 128 на две колонки, смотрю, что остаётся телу, и только потом трогаю константы. Когда кадр обновляется по событиям, а не целиком, новые зоны честно переносятся в план частичного обновления — как режут кадр без полной перерисовки, разобрано в уроке про частичное обновление RAM. Раскладка на бумаге и там остаётся картой: зоны не меняются оттого, что рисуется их часть.
В симуляторе три шаблона живут в одной сетке 21×8, и при переключении видно главное: зоны не двигаются, двигается только их содержимое. Шапка остаётся шапкой у графика и у меню, футер — футером. Именно этого родства я добиваюсь на бумаге: когда все приборы проекта держат одинаковую сетку зон, глаз читает новый экран без привыкания, а половина функций отрисовки переезжает из старого скетча без правок.
На плате тот же скетч покажет и графику: рамку по периметру, плашку шапки, линию-разделитель. Симулятор честно говорит, что примитивы не рисует, — но числа в Serial это координаты ваших зон, и они совпадают с аргументами fillRect на плате один в один.
- 1. Сколько знакомест 6×8 помещается в строке из 128 пикселей?
- 2. Почему границы зон раскладки кратны восьми пикселям?
- 3. В каких пределах идут координаты пикселей экрана 128×64?
- 4. Что станет с 22-м символом строки размера 1?
- 5. Зачем координаты зон выносить в константы?
- Сетка 128×64: 21 знак в строке и 8 строк; координаты 0-127 и 0-63, за краем текст молча не рисуется
- Зоны кратны восьми пикселям: шапка 0-15, тело 16-47, футер 56-63 — границы не режут буквы
- Отступы 2-3 px от краёв и рамки ставятся на бумаге, до кода; каждая зона несёт четыре числа: x, y, w, h
- Порядок слоёв жёсткий: заливки и рамки до текста, текст поверх, контур рамки — первым вызовом
- Все координаты — константы в начале скетча: смена раскладки без поиска магических чисел
Частые вопросы по теме урока
- Сколько знакомест 6×8 помещается в строке из 128 пикселей?
- 128 делится на 6 с остатком: 21 целое знакоместо и два лишних пикселя. Отсюда потолок строки размера 1 — 21 символ. 21
- Почему границы зон раскладки кратны восьми пикселям?
- Знакоместо 6×8: границы на кратных восьми значениях не режут буквенную строку пополам при смене содержимого зоны. Строка шрифта размера 1 занимает ровно 8 пикселей по высоте
- В каких пределах идут координаты пикселей экрана 128×64?
- Счёт от нуля: 128 колонок — это x от 0 до 127, 64 строки — y от 0 до 63. Значение 128 уже за краем буфера. 0-127 и 0-63
- Что станет с 22-м символом строки размера 1?
- За пределами 128 пикселей буфера ничего не рисуется: ни предупреждения, ни переноса. Длину строк держат на бумаге. Не нарисуется — текст за краем молча теряется
- Зачем координаты зон выносить в константы?
- Константы зон держат всю разметку в начале скетча: смена раскладки — пять правок, а не охота за магическими числами. Чтобы править раскладку в одном месте без поиска чисел по скетчу