python/app/calendar.py · 18383 字节 · 下载原文
# calendar.py —— 常用小程序①:**日历**(第 4 层:`ui_*` 图形界面)
# calendar.py -- Everyday utility #1: a **calendar** (layer 4: the `ui_*` graphical interface)
#
# 一扇窗口里的月历:**今天高亮**,`←`/`→` 翻月、`↑`/`↓` 翻年,**点某一天**也能选中它,`Esc` 退出。
# A month calendar in a window: **today is highlighted**; Left/Right change the month, Up/Down the
# year, and **tapping a day** selects it; Esc quits.
# 这是 `Examples/c/app/calendar.c` 的 Python 移植:版式、交互、配色、"空闲 8 秒自动退出"一字不改。
# This is the Python port of `Examples/c/app/calendar.c`: the layout, the interaction, the colours
# and the "quit after 8 idle seconds" rule are unchanged.
#
# ## 第 4 层的三条规矩(不守就会"手机上行、桌面上歪")
# ## Three rules of layer 4 (break them and it works on the phone but drifts on the desktop)
#
# · **尺寸从宿主问**:`ui_scr_get_w()/_h()`,别写死。
# · **Ask the host for the size**: `ui_scr_get_w()/_h()`, never hard-code it.
# · **颜色是真彩 `0xAARRGGBB`**(不是索引色)。
# · **Colours are true colour `0xAARRGGBB`** (not palette indexes).
# · **画完必须 `ui_2d_present()`**,漏了就是黑屏(宿主不猜你画完了没)。
# · **Always finish with `ui_2d_present()`**; skip it and you get a black screen.
#
# ## 为什么"空闲 8 秒就自己退出"
# ## Why it exits by itself after 8 seconds of idling
#
# 例程要能被**自动验收**(`scripts/vmlcli` 加 `--frame` 出一张图就够了),所以循环必须有界。
# Samples have to be **automatically verifiable** (one `scripts/vmlcli` run with `--frame` is
# enough), so the loop must be bounded.
# 写真正的程序时把 `IDLE_EXIT_MS` 去掉,其余(画格子 / 读键 / 命中判定)就是完整写法。
# Drop `IDLE_EXIT_MS` when you write a real program; the rest of the file is the complete pattern.
#
# ## 移植时绕开的四条 Python 前端限制(都**实测**过,不是照着别处抄的)
# ## Four Python front-end limits worked around here (all **measured**, not copied from elsewhere)
#
# · **函数里给模块级标量赋值 = 新建一个局部量**:写完顶层读回来还是旧值。
# · **Assigning a module-level scalar inside a function only creates a new local**: the top level
# still reads the old value afterwards.
# ⇒ 布局那几个量一律由**顶层**算好,函数**只读**模块级状态。
# => the layout values are computed **at top level** and the functions only **read** state.
# · **`s[i]` 与 `"a" + "b"` 都是坏的**:前者回一个野地址(实测 `"hello"[0]` = 812843119)、
# · **`s[i]` and `"a" + "b"` are both broken**: the first returns a wild address (measured:
# `"hello"[0]` = 812843119) and the second an empty string (`strlen("abc" + "def")` = 0).
# 后者回空串(实测 `strlen("abc" + "def")` = 0)⇒ 取字符走 `str_charat`,拼串走
# => take characters with `str_charat` and assemble text with `strcpy`/`strcat` into buffers
# `strcpy`/`strcat` 写进 `vml_alloc` 出来的缓冲。
# obtained from `vml_alloc`.
# · **`str()` 是恒等、`chr()` 返回的是栈上地址** ⇒ `ui_2d_text` 收字符串,
# · **`str()` is the identity and `chr()` returns a stack address** => `ui_2d_text` takes a string,
# 所以整数转文字只能走共享库的 `itoa`(写进我们自己的缓冲,不碰它的静态缓冲)。
# so integers become text only through the shared library's `itoa`, into our own buffer.
# · **没有 `&` 也没有 `tm_t`**:C 版的 `localtime()->tm_year` 在本语言里拿不到。
# · **There is no `&` and no `tm_t`**: the C version's `localtime()->tm_year` is out of reach here.
# ⇒ 今天的年月日从宿主给的 `vml_get_date()`(实测回 `2026-10-07`)切三段再 `atoi`。
# => today's date is cut out of the host's `vml_get_date()` (measured: `2026-10-07`) with
# `str_substr`, then run through `atoi`.
# ── 显式引用:`ui_*` 来自共享调用库 `vmlui.vml`;`str_*`/`itoa`/`atoi`/`vml_alloc` 来自共享库 ──
# ── explicit references: `ui_*` from the shared call library `vmlui.vml`; `str_*`/`itoa`/`atoi`/
# `vml_alloc` from the shared library ──
import vml_ui
import shared
IDLE_EXIT_MS = 8000
# 配色(真彩 AA RRGGBB)
# Colours (true colour AA RRGGBB)
C_BG = 0xFF101018
C_CARD = 0xFF1A1A24
C_TITLE = 0xFFE8E8F0
C_DIM = 0xFF9AA0B0
C_ACCENT = 0xFF4A90D9
C_TODAY = 0xFFFFC64A
C_WEEKEND = 0xFFE06C50
C_DAY = 0xFFD8DCE8
C_ON_FILL = 0xFF101018
# ── 暂存字符串:地址由 `vml_alloc` 给,`str_*`/`itoa` 往里写,交给 `ui_2d_text` 的就是地址 ──
# ── scratch strings: the address comes from `vml_alloc`, `str_*`/`itoa` write into it, and the
# address is what `ui_2d_text` receives ──
S_TITLE = vml_alloc(48)
S_NUM = vml_alloc(16)
S_DAY = vml_alloc(8)
# 星期表头:C 版是一串三元表达式,本语言有列表 ⇒ 直接摆两张表,函数里按下标取。
# Weekday header: the C version is a chain of ternaries; this language has lists, so the two rows
# are plain tables and the functions index them.
WK_CN = ["日", "一", "二", "三", "四", "五", "六"]
WK_EN = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
# 界面语言(0 = 中文,1 = English);开局问一次,之后只读。
# UI language (0 = Chinese, 1 = English); asked once at startup, read-only afterwards.
LANG = ui_scr_get_language()
# 当前显示的月份 / 选中(高亮)的日号,0 = 没有 / 今天。
# The month on screen / the selected (highlighted) day, 0 = none / today's date.
cur_y = 0
cur_m = 0
sel_d = 0
today_y = 0
today_m = 0
today_d = 0
# 棋盘几何(画与命中判定**必须用同一份**,见 `hit_day`)—— 由顶层算好,函数只读。
# Grid geometry (drawing and hit testing **must share this one copy**, see `hit_day`) -- computed
# at top level, only read inside functions.
SW = 0
SH = 0
G_PAD = 0
G_CELL = 0
G_TOP = 0
# ── 小工具 ────────────────────────────────────────────
# ── small helpers ─────────────────────────────────────
# 是不是闰年
# Is it a leap year
def is_leap(y):
if y % 400 == 0:
return 1
if y % 100 == 0:
return 0
if y % 4 == 0:
return 1
return 0
# 这个月有几天
# How many days this month has
def days_in_month(y, m):
if m == 2:
if is_leap(y) == 1:
return 29
return 28
if m == 4 or m == 6 or m == 9 or m == 11:
return 30
return 31
# 某月 1 号是星期几(0=周日)。蔡勒公式,编号与 C 的 `tm_wday` 一致。
# Weekday of the 1st of a month (0 = Sunday). Zeller's congruence, numbered like C's `tm_wday`.
def weekday_of_first(y, m):
yy = y
mm = m
if mm < 3:
mm = mm + 12
yy = yy - 1
k = yy % 100
j = yy // 100
h = (1 + (13 * (mm + 1)) // 5 + k + k // 4 + j // 4 + 5 * j) % 7
return (h + 6) % 7
MONTHS = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"]
# 月份的英文名(列表按下标取,等价于 C 版那一串 `if`)
# The English month name (a list indexed by month, equivalent to the C version's chain of `if`s)
def month_en(m):
return MONTHS[m - 1]
# 两位日号(补零,7×7 的格子里对齐才好看)。`itoa` 不补零,所以再 `str_padstart` 一次。
# A two-digit day number (zero padded, which keeps the 7x7 grid aligned); `itoa` does not pad, so
# `str_padstart` finishes the job.
def daystr(d):
itoa(d, S_NUM)
str_padstart(S_DAY, S_NUM, 2, 48)
return S_DAY
# 第 idx 格那一格的左上角
# Top-left corner of cell `idx`
def cell_x(idx):
return G_PAD + (idx % 7) * G_CELL
def cell_y(idx):
return G_TOP + (idx // 7) * G_CELL
# ── 版面:四条"从宿主换算"的规则各收一处,顶层按 `scr_w/scr_h → pad → cell → top` 调一遍 ──
# ── layout: each of the four "derive from the host" rules lives in one place; the top level calls
# them in the order `scr_w/scr_h -> pad -> cell -> top` ──
def scr_w():
w = ui_scr_get_w()
if w <= 0:
w = 360
return w
def scr_h():
h = ui_scr_get_h()
if h <= 0:
h = 620
return h
def pad_for(sw):
p = sw // 18
if p < 6:
p = 6
return p
def cell_for(sw, pad):
c = (sw - pad * 2) // 7 # 7 列等宽;余数留在右边不动
# 7 equal columns; any remainder stays on the right
if c < 18:
c = 18
return c
def top_for(sh, cell):
# ⚠ **行数固定 6**:月历最长 6 行(31 天且 1 号在周六时)。按"这个月实际几行"算的话,
# ⚠ **Always six rows**: a month spans at most six rows. Sizing by the actual row count makes
# 翻月时整块会上下跳(实测过:5 行与 6 行来回切,日历像在抖)。
# the whole block jump when you change month (measured: five rows and six rows alternating
# makes the calendar visibly shake).
grid_h = 6 * cell
t = (sh - (64 + grid_h)) // 2 + 64 # 整块垂直居中
# centre the whole block vertically
if t < 80:
t = 80 # 矮屏兜底:别把标题顶出画布
# short screens
return t
# ── 绘制 ──────────────────────────────────────────────
# ── drawing ───────────────────────────────────────────
def draw():
first = weekday_of_first(cur_y, cur_m)
n = days_in_month(cur_y, cur_m)
cx = SW // 2
i = 0
d = 0
idx = 0
x = 0
y = 0
col = 0
is_today = 0
is_sel = 0
by = 0
ui_2d_clear(C_BG)
# 标题:**中文一律靠字符串字面量拼**,别逐字节写 —— 一个汉字在 UTF-8 里是 3 个字节。
# Title: **append Chinese text as a string literal**, never byte by byte -- one CJK glyph is
# three bytes in UTF-8, so a byte-wise write truncates it (measured on the C side).
if LANG == 0:
itoa(cur_y, S_NUM)
strcpy(S_TITLE, S_NUM)
strcat(S_TITLE, " 年 ")
itoa(cur_m, S_NUM)
strcat(S_TITLE, S_NUM)
strcat(S_TITLE, " 月")
else:
strcpy(S_TITLE, month_en(cur_m))
strcat(S_TITLE, " ")
itoa(cur_y, S_NUM)
strcat(S_TITLE, S_NUM)
ui_2d_text_styled(cx, G_TOP - 64, S_TITLE, C_TITLE, 18, VML_ANCHOR_CENTER, VML_FONT_BOLD)
# 星期表头:一格一个词,位置与下面的格子**同一套算式**
# Weekday header: one word per cell, positioned by the **same formula** the cells below use
i = 0
while i < 7:
col = C_DIM
if i == 0 or i == 6:
col = C_WEEKEND
if LANG == 0:
ui_2d_text(G_PAD + i * G_CELL + G_CELL // 2, G_TOP - 28, WK_CN[i], col, 13,
VML_ANCHOR_CENTER)
else:
ui_2d_text(G_PAD + i * G_CELL + G_CELL // 2, G_TOP - 28, WK_EN[i], col, 13,
VML_ANCHOR_CENTER)
i = i + 1
# 日期格:先铺底,再写日号
# Day cells: the background first, then the number
d = 1
while d <= n:
idx = first + d - 1
x = cell_x(idx)
y = cell_y(idx)
is_today = 0
if cur_y == today_y:
if cur_m == today_m:
if d == today_d:
is_today = 1
is_sel = 0
if d == sel_d:
is_sel = 1
if is_sel == 1:
ui_2d_rect(x + 3, y + 3, G_CELL - 6, G_CELL - 6, C_ACCENT, 1, 0, G_CELL // 4)
elif is_today == 1:
ui_2d_rect(x + 3, y + 3, G_CELL - 6, G_CELL - 6, C_TODAY, 1, 0, G_CELL // 4)
else:
ui_2d_rect(x + 3, y + 3, G_CELL - 6, G_CELL - 6, C_CARD, 1, 0, G_CELL // 4)
col = C_DAY
if idx % 7 == 0 or idx % 7 == 6:
col = C_WEEKEND
if is_sel == 1 or is_today == 1:
col = C_ON_FILL
ds = daystr(d)
ui_2d_text(x + G_CELL // 2, y + G_CELL // 2 - 7, ds, col, 15, VML_ANCHOR_CENTER)
d = d + 1
# 底部:高亮说明 + 操作提示
# Bottom: what is highlighted, and the controls
by = G_TOP + ((first + n - 1) // 7 + 1) * G_CELL + 12
if by > SH - 40:
by = SH - 40
if sel_d > 0:
ui_2d_text(cx, by, "已选中(点一天 / 用方向键翻月)" if LANG == 0 else "selected (tap a day / arrows change month)",
C_ACCENT, 12, VML_ANCHOR_CENTER)
else:
ui_2d_text(cx, by, "黄底 = 今天" if LANG == 0 else "yellow = today",
C_TODAY, 12, VML_ANCHOR_CENTER)
ui_2d_text(cx, by + 22, "← → 翻月 ↑ ↓ 翻年 点一天选中 Esc 退出" if LANG == 0
else "Left/Right month Up/Down year tap a day Esc quits",
C_DIM, 11, VML_ANCHOR_CENTER)
ui_2d_present() # ⚠ 不调这句 = 黑屏
# ⚠ without this call you get a black screen
# 点在某一天的格子里?返回日号,没点中返回 0。
# Did the tap land in a day cell? Returns the day number, or 0 when it missed.
# ⚠ **必须与 `draw` 用同一套几何**(`cell_x`/`cell_y`):各算一套就会出现"看着点在 7 号、
# ⚠ **It must use the same geometry as `draw`** (`cell_x`/`cell_y`): two sets of arithmetic produce
# 实际选中 8 号"这种"意图在变、位置不动"的老毛病。
# the classic "it looks like you tapped the 7th but the 8th got selected" bug.
def hit_day(px, py):
first = weekday_of_first(cur_y, cur_m)
n = days_in_month(cur_y, cur_m)
d = 1
idx = 0
x = 0
y = 0
while d <= n:
idx = first + d - 1
x = cell_x(idx)
y = cell_y(idx)
if px >= x and px < x + G_CELL and py >= y and py < y + G_CELL:
return d
d = d + 1
return 0
# ── 主程序 ────────────────────────────────────────────
# ── main program ──────────────────────────────────────
# 今天:C 版走 `time`/`localtime`,本语言没有取地址 ⇒ 切宿主的 `YYYY-MM-DD` 串。
# Today: the C version goes through `time`/`localtime`; this language has no address-of, so the
# host's `YYYY-MM-DD` string is cut into pieces instead.
DS = vml_get_date()
str_substr(S_NUM, DS, 0, 4)
today_y = atoi(S_NUM)
str_substr(S_NUM, DS, 5, 2)
today_m = atoi(S_NUM)
str_substr(S_NUM, DS, 8, 2)
today_d = atoi(S_NUM)
cur_y = today_y
cur_m = today_m
sel_d = 0
# 开窗时传 0:尺寸交给宿主(用真实可用区);开窗后再问一次真实画布(见下)。
# Open with 0 so the host decides the size (the real usable area), then ask the real canvas again
# below. Portrait plus no gamepad area (a calendar is all tapping).
ui_win_open_ex("calendar.py", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD)
# 开窗后再问一次 —— 画布尺寸与开窗前的估算**可能不同**(桌面 480×640、手机竖屏 ~395×744)。
# Ask again after opening: the canvas may differ from the pre-open estimate (desktop 480x640,
# phone portrait about 395x744).
# ⚠ 这一段是**赋值给模块级标量**,只能写在顶层(函数里写就是新建局部量,实测)。
# ⚠ These are assignments to module-level scalars, so they can only live at the top level (inside a
# function they would just create locals, measured).
SW = scr_w()
SH = scr_h()
G_PAD = pad_for(SW)
G_CELL = cell_for(SW, G_PAD)
G_TOP = top_for(SH, G_CELL)
draw()
T0 = ui_time_get_ms()
DONE = 0
while DONE == 0 and ui_win_is_closed() == 0:
T = ui_msg_fetch_wait(200)
if T == VML_MSG_NONE:
if ui_time_get_ms() - T0 > IDLE_EXIT_MS:
DONE = 1 # 空闲超时(验收用)
# idle timeout (for automated checks)
elif T == VML_MSG_KEYDOWN:
K = ui_msg_get_a()
T0 = ui_time_get_ms()
if K == VML_KEY_ESCAPE or K == 113 or K == 81:
DONE = 1
elif K == VML_KEY_LEFT:
cur_m = cur_m - 1
if cur_m < 1:
cur_m = 12
cur_y = cur_y - 1
sel_d = 0
draw()
elif K == VML_KEY_RIGHT:
cur_m = cur_m + 1
if cur_m > 12:
cur_m = 1
cur_y = cur_y + 1
sel_d = 0
draw()
elif K == VML_KEY_UP:
cur_y = cur_y - 1
sel_d = 0
draw()
elif K == VML_KEY_DOWN:
cur_y = cur_y + 1
sel_d = 0
draw()
elif T == VML_MSG_TOUCHDOWN or T == VML_MSG_MOUSEDOWN:
TX = ui_msg_get_a()
TY = ui_msg_get_b()
T0 = ui_time_get_ms()
HD = hit_day(TX, TY)
if HD > 0:
sel_d = HD
draw()
elif T == VML_MSG_WINDOWORIENT or T == VML_MSG_WINDOWRESIZE:
# 画布变了几何就全变 —— 与开窗后那段**同一串赋值**(规则本身在 scr_w/pad_for/... 里)。
# The canvas changed, so every derived value changes -- the same sequence of assignments as
# after the window opened (the rules themselves live in scr_w/pad_for/...).
SW = scr_w()
SH = scr_h()
G_PAD = pad_for(SW)
G_CELL = cell_for(SW, G_PAD)
G_TOP = top_for(SH, G_CELL)
draw()
elif T == VML_MSG_WINDOWCLOSE:
DONE = 1