← 都来码 / Dolaima← 例程索引python/app/calendar.py原文

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