拿到 M5Paper 这块 4.7 寸电子墨水屏开发板后,除了用它做信息展示类的应用,我一直在想——能不能在上面做一些有实际交互价值的工具?
正好平时写代码时需要偶尔算个表达式,打开手机计算器总觉得太慢。于是就有了这个念头:在 M5Paper 上做一个计算器。墨水屏的特性决定了它不能像普通 LCD 那样高刷,但作为计算器场景,每次按键后刷新一次,完美契合。
界面设计
计算器的界面设计参考了经典 Casio 计算器的布局。M5Paper 的屏幕是 540x960 竖屏,我把整个屏幕分成两个区域:
顶部显示区:显示输入表达式和计算结果
底部按键区:数字、运算符和功能按键
按键布局采用 4 列网格:
+-----+-----+-----+-----+ | C | ← | % | ÷ | +-----+-----+-----+-----+ | 7 | 8 | 9 | × | +-----+-----+-----+-----+ | 4 | 5 | 6 | - | +-----+-----+-----+-----+ | 1 | 2 | 3 | + | +-----+-----+-----+-----+ | ± | 0 | . | = | +-----+-----+-----+-----+
每个按键采用圆角矩形设计,墨水屏的 16 级灰度虽然没有色彩冲击力,但浅灰底色 + 深灰按键的搭配,阅读体验反而比彩色 LCD 更舒服——有种打印出来的纸质感。
E-Ink 刷新策略
墨水屏开发最重要的一点:不能频繁刷新。
计算器这种高频交互场景,我设计了一套"按需刷新"策略:
按键触发 → 更新表达式 → 局部刷新显示区 → 全屏刷新(每5次)
具体来说:
按数字键时,只刷新顶部的表达式显示区域
按 "=" 键计算结果时,全屏刷新确保结果清晰
每 5 次操作后全屏刷新一次,防止残影积累
# 局部刷新表达式区域 def refresh_expression(expr): # 只清除和重绘表达式区域 M5.Lcd.fillRect(10, 10, 520, 120, COLOR_BG) M5.Lcd.setCursor(20, 40) M5.Lcd.setTextColor(COLOR_BLACK, COLOR_BG) M5.Lcd.setFont(M5.Lcd.FONTS.DejaVu40) M5.Lcd.print(expr)
日本字体风格
为了让计算器的数字显示更有质感,我专门找了一款开源的日本字体——这算是开发过程中比较有意思的点。
墨水屏因为低分辨率(相对 LCD),字体选择直接影响观感。我测试了几种字体后发现:
DejaVu 系列:数字显示最清晰,适合主数值显示
日文字体 VLW 格式:中文字符显示效果好,适合按键标签
字体文件需要转换为 VLW 格式才能被 M5Paper 的 UIFlow2 固件识别。我用了一个在线字体转换工具,把 TTF 字体转成了 VLW 格式,存储在 SD 卡上。
触摸交互实现
M5Paper 的 GT911 触摸芯片支持两点触控,但计算器场景用单指就够了。
触摸检测的逻辑比较简单:
def get_touch_position(): if M5.Touch.getCount() > 0: x = M5.Touch.getX() y = M5.Touch.getY() if x > 0 and y > 0: return (x, y) return None def find_button(x, y): # 遍历按键布局,检测触摸区域 for i, btn in enumerate(buttons): bx, by, bw, bh = btn['rect'] if bx <= x <= bx + bw and by <= y <= by + bh: return btn return None
核心计算逻辑
计算器的核心是一个简单的表达式解析器。我不打算用 eval(安全性问题),而是自己实现了一个基于操作符优先级的中缀表达式计算器:
def calculate(expression):
"""计算表达式值,支持加减乘除和括号"""
try:
# 将表达式转为 RPN(逆波兰表示法)
output = []
ops = []
precedence = {'+': 1, '-': 1, '×': 2, '÷': 2, '%': 2}
for token in tokenize(expression):
if token.is_number:
output.append(token)
elif token in precedence:
while ops and ops[-1] != '(' and precedence[ops[-1]] >= precedence[token]:
output.append(ops.pop())
ops.append(token)
elif token == '(':
ops.append(token)
elif token == ')':
while ops and ops[-1] != '(':
output.append(ops.pop())
ops.pop()
while ops:
output.append(ops.pop())
# 计算 RPN 表达式
stack = []
for token in output:
if token.is_number:
stack.append(token.value)
else:
b = stack.pop()
a = stack.pop()
if token == '+': stack.append(a + b)
elif token == '-': stack.append(a - b)
elif token == '×': stack.append(a * b)
elif token == '÷': stack.append(a / b)
elif token == '%': stack.append(a % b)
return stack[0]
except Exception as e:
return "Error"这个实现支持:
四则运算(+ - × ÷)
取余(%)
正负号切换(±)
括号优先级
错误处理(除零、语法错误等)
踩坑记录
1. 触摸坐标偏移
最开始发现触摸按键不太准,总是点偏几毫米。排查后发现是屏幕旋转导致触摸坐标没跟着旋转。
解决方法:手动将触摸坐标映射到正确的屏幕坐标系。
2. 残影问题
连续按数字键后,墨水屏上的数字会出现残影,旧的数字隐约可见。
解决方法:
每 5 次操作做一次全屏刷新
按键时先清除旧内容再写入新内容
使用 EPD_FAST 模式减少刷新时间
3. 字体加载失败
刚加载日文字体时,程序直接崩溃。排查发现是微雪提供的 VLW 字体文件格式和 M5Paper 的 UIFlow2 固件不完全兼容。
解决方法:使用 UIFlow2 自带的字体转换工具重新生成 VLW 文件。
完整代码框架
import M5
from M5 import *
import time
# 颜色定义
COLOR_BG = 0xeeeeee
COLOR_BLACK = 0x000000
COLOR_GRAY = 0x999999
COLOR_DARK = 0x555555
# 屏幕尺寸
SCREEN_W = 540
SCREEN_H = 960
# 按键布局
BUTTONS = { ... } # 4x5 行列布局
class Calculator:
"""M5Paper 计算器主类"""
def __init__(self):
self.expression = ""
self.result = ""
self.last_full_refresh = 0
self.op_count = 0
def draw_keypad(self):
"""绘制按键面板"""
for row in BUTTONS:
for btn in row:
self.draw_button(btn)
def draw_button(self, btn):
"""绘制单个按键"""
x, y, w, h, label = btn
M5.Lcd.fillRoundRect(x, y, w, h, 8, COLOR_DARK)
text_x = x + (w - len(label) * 20) // 2
M5.Lcd.setCursor(text_x, y + 20)
M5.Lcd.setTextColor(COLOR_BG, COLOR_DARK)
M5.Lcd.setFont(M5.Lcd.FONTS.EFontCN24)
M5.Lcd.print(label)
def handle_touch(self, x, y):
"""处理触摸事件"""
for row in BUTTONS:
for bx, by, bw, bh, label in row:
if bx <= x <= bx + bw and by <= y <= by + bh:
self.on_key_press(label)
return
def on_key_press(self, key):
"""按键逻辑处理"""
if key == "=":
self.result = str(calculate(self.expression))
self.full_refresh()
elif key == "C":
self.expression = ""
self.result = ""
self.full_refresh()
elif key == "←":
self.expression = self.expression[:-1]
else:
self.expression += key
self.op_count += 1
if self.op_count % 5 == 0:
self.full_refresh()
else:
self.partial_refresh()
def setup():
"""初始化"""
M5.begin()
calc = Calculator()
calc.draw_keypad()
def loop():
"""主循环"""
M5.update()
touch = M5.Touch
if touch.getCount() > 0:
x, y = touch.getX(), touch.getY()
calc.handle_touch(x, y)
time.sleep(0.05)总结
在 M5Paper 上做一个计算器,技术难度不大,但墨水屏适配是个好课题。通过这个项目,我深入理解了以下几点:
墨水屏的刷新策略:按需刷新 + 定期全刷,是墨水屏应用的核心设计模式
触摸交互优化:触摸坐标校正和防抖处理是用户体验的关键
字体选择:中英文混排场景,日本字体是个不错的中间选择
Canvas 渲染:E-Ink 屏幕必须使用 Canvas 缓冲,直接绘制会出问题
这个计算器已经能满足日常使用,后续还会继续完善。如果你对 M5Paper 开发感兴趣,欢迎留言交流!

我要赚赏金
