查看: 263|回复: 0

Python+Slint实现蜂巢菜单:鱼眼缩放与Model回调

[复制链接]
发表于 3 小时前 | 显示全部楼层 |阅读模式
本文围绕 Python + Slint 的蜂巢菜单实现,重点放在可落地的代码结构、布局计算、鱼眼缩放、模型推送、鼠标回调与定时渲染循环。整体架构是 Python 负责逻辑计算,Slint 负责界面渲染:Python 端完成图片加载、蜂巢坐标计算、鱼眼缩放算法、事件回调与模型更新;Slint 端负责绘制图标、处理鼠标交互、属性动画与底部状态栏。两者通过 Slint 的 ListModel 与 callback 通信,适合桌面启动器、工具面板、图标选择器等交互场景。

一、工程结构与运行入口
核心文件为 main.py、main.slint 和 image 图片目录。main.py 通过 Path(__file__).parent.resolve() 定位基准目录,IMAGE_DIR 指向 image,SLINT_PATH 指向 main.slint。图片扩展名集合为 .svg、.png、.jpg、.jpeg、.bmp、.webp。load_images() 先判断图片目录是否存在,不存在则打印错误并 sys.exit(1);再对目录内文件排序,并用 suffix.lower() 过滤扩展名;如果过滤后没有图片,同样打印错误并退出。build_items() 负责把图片映射到蜂巢格子,使用 idx % n 循环复用图片,避免图片数量少于格子数量时越界。

二、蜂巢布局参数与算法
关键常量包括 CELL_SIZE = 110、ICON_D = CELL_SIZE、H_SPACE = int(ICON_D * 0.98)、V_SPACE = int(ICON_D * 0.85)、CANVAS_W = 960、CANVAS_H = 580。HEX_LAYOUT 采用 3-4-5-4-3 的 5 行对称结构,共 19 格:
  1. HEX_LAYOUT = [
  2.     [0, 1, 2],
  3.     [3, 4, 5, 6],
  4.     [7, 8, 9, 10, 11],
  5.     [12, 13, 14, 15],
  6.     [16, 17, 18],
  7. ]
复制代码
在 build_items() 中,center_x = CANVAS_W / 2;每一行通过 first_cx = center_x - (cnt - 1) * H_SPACE / 2 计算起始 X,使整行水平居中;cy = 100 + ri * V_SPACE 按行递增;cx = first_cx + ci * H_SPACE 计算每个格子中心。水平间距略小于直径、垂直间距略小于直径,配合行间错位,形成蜂巢式紧密排列。每个 item 保存 cx、cy、name 和 img_idx,name 取图片文件的 stem。

三、鱼眼缩放算法
鱼眼参数为 SCALE_MIN = 0.55、SCALE_MAX = 1.50、FISHEYE_R = 200.0。compute_scales() 遍历所有格子,计算格子中心与鼠标位置的欧几里得距离。如果 dist >= FISHEYE_R,缩放系数直接取 SCALE_MIN;否则令 t = dist / FISHEYE_R,并按 SCALE_MIN + (SCALE_MAX - SCALE_MIN) * math.exp(-2.5 * t * t) 计算,最后 round(scale, 3)。距离越近,指数项越接近 1,图标越接近 1.50 倍;距离越远,缩放平滑衰减到 0.55 倍。该连续衰减避免了生硬跳变。
  1. def compute_scales(items, mx, my):
  2.     result = []
  3.     for it in items:
  4.         dx = it['cx'] - mx
  5.         dy = it['cy'] - my
  6.         dist = math.sqrt(dx * dx + dy * dy)
  7.         if dist >= FISHEYE_R:
  8.             scale = SCALE_MIN
  9.         else:
  10.             t = dist / FISHEYE_R
  11.             scale = SCALE_MIN + (SCALE_MAX - SCALE_MIN) * math.exp(-2.5 * t * t)
  12.         result.append(round(scale, 3))
  13.     return result
复制代码

四、Python 与 Slint 的数据交互
Python 到 Slint 方向使用 ListModel。初始化时把每个格子的 cx、cy、scale、name、img-idx 放入 init_data,创建 slint.ListModel(init_data),再赋值给 ui.hex_items。鼠标移动并完成鱼眼计算后,update_model() 使用 _model_ref.set_row_data(i, {...}) 逐行更新模型,Slint 监听到模型变化后自动重绘。若 _model_ref 或 _items_cache 尚未初始化,函数直接 return。
  1. def update_model(scales):
  2.     if _model_ref is None or _items_cache is None:
  3.         return
  4.     for i, it in enumerate(_items_cache):
  5.         _model_ref.set_row_data(i, {
  6.             'cx': it['cx'],
  7.             'cy': it['cy'],
  8.             'scale': scales[i],
  9.             'name': it['name'],
  10.             'img-idx': it['img_idx'],
  11.         })
复制代码
Slint 到 Python 方向使用 callback。Slint 侧 TouchArea 通过 pointer-event 捕获移动事件,判断 event.kind == PointerEventKind.move 后,把 self.mouse-x / 1px 和 self.mouse-y / 1px 传给 root.mouse-moved。Python 侧注册 ui.mouse_moved = on_mouse_moved,回调中在 lock 保护下更新 mouse_state 的 x、y,并把 dirty 置为 True。点击时,Slint 的 IconCell 触发 cell-clicked,MainWindow 更新 selected-idx、selected-name,并调用 item-clicked;Python 注册 ui.item_clicked = on_click 后即可拿到名称和索引。
  1. def on_mouse_moved(mx, my):
  2.     with lock:
  3.         mouse_state['x'] = float(mx)
  4.         mouse_state['y'] = float(my)
  5.         mouse_state['dirty'] = True
  6. ui.mouse_moved = on_mouse_moved
复制代码

五、定时器驱动的渲染循环与性能
鼠标高频移动时,如果每次 move 都立刻重算并重绘,会带来不必要的 CPU 开销。这里的策略是事件只更新坐标和 dirty 标记,由定时器采样。slint.Timer 以 slint.TimerMode.Repeated 模式启动,间隔为 datetime.timedelta(milliseconds=UPDATE_MS),其中 UPDATE_MS = 25,也就是约 40 FPS。tick() 每次加锁检查 dirty:为 True 时取出最新坐标,执行 compute_scales() 并 update_model();为 False 时直接 return。
  1. def tick():
  2.     with lock:
  3.         if mouse_state['dirty']:
  4.             mouse_state['dirty'] = False
  5.             mx, my = mouse_state['x'], mouse_state['y']
  6.         else:
  7.             return
  8.     scales = compute_scales(items, mx, my)
  9.     update_model(scales)
复制代码
这种事件驱动加定时采样的组合,把渲染频率稳定在 25ms 一帧,兼顾流畅性和计算开销。需要更低 CPU 占用时可增大 UPDATE_MS;需要更灵敏的鱼眼响应时可减小 UPDATE_MS,但要同步观察性能。

六、Slint 端动画与视觉细节
Slint 侧用 HexItem 结构体承接数据,字段为 cx、cy、scale、name、img-idx。IconCell 组件封装单个图标,核心是 anim-scale 与外部 cell-scale 的双向绑定,并用 animate anim-scale { duration: 180ms; easing: ease-out; } 做平滑过渡。图标尺寸 sz = cell-size * anim-scale,偏移 off = (cell-size - sz) / 2,保证缩放时圆心位置不变。圆形底盘使用 @radial-gradient(circle, #1e293b, #0f172a),边框环使用 1.5px 的 #334155 描边。图片只在 img-idx >= 0 且 img-idx < fruit-imgs.length 时显示,source 取 fruit-imgs[img-idx],image-fit 为 contain。名称标签仅在 anim-scale > 1.15 时显示,避免缩小时文字拥挤。
  1. export struct HexItem {
  2.     cx: length,
  3.     cy: length,
  4.     scale: float,
  5.     name: string,
  6.     img-idx: int,
  7. }
  8. component IconCell inherits Rectangle {
  9.     in property <float> cell-scale: 1.0;
  10.     property <float> anim-scale <=> cell-scale;
  11.     animate anim-scale { duration: 180ms; easing: ease-out; }
  12.     property <length> sz: cell-size * anim-scale;
  13.     property <length> off: (cell-size - sz) / 2;
  14. }
复制代码
MainWindow 中维护 fruit-imgs 图片数组,并用 for itm[idx] in hex-items 渲染 IconCell,传入 cell-scale、item-name、img-idx 和 fruit-imgs。底部状态栏根据 selected-idx 是否大于等于 0 显示“已选择:”加 selected-name,否则显示操作提示,并显示“共 hex-items.length 项”。

七、常见问题排查
图片目录不存在或目录中没有匹配图片时,load_images() 会直接打印错误并退出,优先检查 IMAGE_DIR 路径和扩展名大小写。main.slint 编译失败时,slint.load_file 会抛出异常,代码会打印 e.args[1] 中的诊断信息,按行修复 Slint 语法、属性名或图片路径。模型不更新时,确认 _model_ref、_items_cache 已初始化,并且 ui.hex_items 已赋值。鼠标回调不触发时,检查 TouchArea 是否覆盖窗口、pointer-event 是否过滤 move 事件、坐标是否除以 1px。图标越界时,确认 Image 条件包含 img-idx < fruit-imgs.length。性能不理想时,优先调整 UPDATE_MS、FISHEYE_R、SCALE_MAX 和图标数量。另需注意,原文 main.slint 的底部信息栏部分出现了重复的 Rectangle 片段,实际落地时应合并为一份,避免 Slint 解析异常。

八、适用场景与调整建议
该实现适合 19 个左右图标的桌面菜单、启动器或工具选择面板。要增减图标,可改 HEX_LAYOUT 的行数与每行索引,同时配套调整 CELL_SIZE、H_SPACE、V_SPACE、CANVAS_W、CANVAS_H。要改变鱼眼强度,调整 SCALE_MIN、SCALE_MAX、FISHEYE_R;要改变动画手感,调整 Slint 侧 180ms 动画时长与 easing;要改变刷新频率,调整 UPDATE_MS。整体思路是:Python 负责坐标与缩放计算,Slint 负责声明式渲染与属性动画,Model 负责数据推送,Callback 负责事件回传,Timer 负责采样循环。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-10 15:21 , Processed in 0.035146 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部