资讯动态

PyQt+QML 实现蜂巢菜单源码分享

发布时间:2026/10/27 10:04:38 来源:尧图企业网站定制
PyQtQML 实现蜂巢菜单源码分享一、效果展示二、源码分享1、工程结构2、库安装3、main.py4、honeycomb.qml三、实现原理1、整体架构2、蜂巢网格布局3、圆形裁剪4、鱼眼凸起效果5、点击检测6、交互细节一、效果展示二、源码分享1、工程结构2、库安装3、main.pyimportsysimportosfromPyQt6.QtCoreimportQUrlfromPyQt6.QtGuiimportQGuiApplicationfromPyQt6.QtQmlimportQQmlApplicationEnginedefmain():appQGuiApplication(sys.argv)engineQQmlApplicationEngine()qml_pathos.path.join(os.path.dirname(os.path.abspath(__file__)),honeycomb.qml)engine.load(QUrl.fromLocalFile(qml_path))ifnotengine.rootObjects():print(错误无法加载 QML 文件,filesys.stderr)sys.exit(-1)sys.exit(app.exec())if__name____main__:main()4、honeycomb.qmlimportQtQuickimportQtQuick.Controls Window{id:root width:700height:480visible:true title:蜂巢菜单color:#0a0a0a//─── 凸起效果中心跟随鼠标───propertyreal bulgeCX:350propertyreal bulgeCY:245//─── 选中图标索引-1未选中───propertyintselectedIndex:-1//─── 布局常量 ─── readonlypropertyreal clockCX:350readonlypropertyreal clockCY:220readonlypropertyreal gridOffsetY:22//图标整体下移量 readonlypropertyreal visualCY:clockCYgridOffsetY//实际视觉中心 Y readonlypropertyreal clipR:155readonlypropertyreal baseSize:54//═══════════════════════════════════════════════//图标数据//═══════════════════════════════════════════════ readonlypropertyvar iconData:[//第1行(y100,3个){baseX:290,baseY:100,name:apple,image:image/apple.svg},{baseX:348,baseY:100,name:avocado,image:image/avocado.svg},{baseX:406,baseY:100,name:banana,image:image/banana.svg},//第2行(y150,5个){baseX:205,baseY:150,name:bayberry,image:image/bayberry.svg},{baseX:263,baseY:150,name:blueberry,image:image/blueberry.svg},{baseX:321,baseY:150,name:cherry,image:image/cherry.svg},{baseX:379,baseY:150,name:dragonfruit,image:image/dragonfruit.svg},{baseX:437,baseY:150,name:grape,image:image/grape.svg},//第3行(y200,5个,中间行){baseX:234,baseY:200,name:mango,image:image/mango.svg},{baseX:292,baseY:200,name:mangosteen,image:image/mangosteen.svg},{baseX:350,baseY:200,name:orange,image:image/orange.svg},{baseX:408,baseY:200,name:peach,image:image/peach.svg},{baseX:466,baseY:200,name:persimmon,image:image/persimmon.svg},//第4行(y250,5个){baseX:205,baseY:250,name:pineapple,image:image/pineapple.svg},{baseX:263,baseY:250,name:strawberry,image:image/strawberry.svg},{baseX:321,baseY:250,name:watermelon,image:image/watermelon.svg},{baseX:379,baseY:250,name:waxapple,image:image/waxapple.svg},{baseX:437,baseY:250,name:apple,image:image/apple.svg},//第5行(y300,3个){baseX:290,baseY:300,name:avocado,image:image/avocado.svg},{baseX:348,baseY:300,name:banana,image:image/banana.svg},{baseX:406,baseY:300,name:bayberry,image:image/bayberry.svg},]//═══════════════════════════════════════════════//点击检测//遍历所有图标找距点击位置最近且在圆形范围内的图标//═══════════════════════════════════════════════ function findIconAt(mx,my){var bestIdx-1var bestDistInfinityfor(var i0;iiconData.length;i){var iconiconData[i]var iyicon.baseYgridOffsetY//偏移后的实际 Y var dxicon.baseX-mx var dyiy-my var distMath.sqrt(dx*dxdy*dy)//圆形裁剪偏移后的图标到表盘中心的距离 var cdxicon.baseX-clockCX var cdyiy-visualCY var cdistMath.sqrt(cdx*cdxcdy*cdy)if(cdistclipR-14distbaseSize/2distbestDist){bestDistdist bestIdxi}}returnbestIdx}//═══════════════════════════════════════════════//全屏背景//═══════════════════════════════════════════════ Rectangle{anchors.fill:parent color:#0a0a0a}//═══════════════════════════════════════════════//表盘外框三层渐变金属边框//═══════════════════════════════════════════════//外层装饰环 Rectangle{x:root.clockCX-root.clipR-8y:root.clockCY-root.clipR-8width:root.clipR*216height:root.clipR*216radius:root.clipR8gradient:Gradient{orientation:Gradient.Horizontal GradientStop{position:0.0;color:#2e2e2e}GradientStop{position:0.5;color:#0f0f0f}GradientStop{position:1.0;color:#1e1e1e}}border.width:1.5border.color:#555}//中层倒角 Rectangle{x:root.clockCX-root.clipR-3y:root.clockCY-root.clipR-3width:root.clipR*26height:root.clipR*26radius:root.clipR3gradient:Gradient{orientation:Gradient.Horizontal GradientStop{position:0.0;color:#222}GradientStop{position:0.5;color:#0a0a0a}GradientStop{position:1.0;color:#181818}}border.width:1border.color:#333}//内层黑色表盘底色 Rectangle{x:root.clockCX-root.clipR y:root.clockCY-root.clipR width:root.clipR*2height:root.clipR*2radius:root.clipR color:#050505border.width:0.5border.color:#1a1a1a}//═══════════════════════════════════════════════//蜂巢图标网格鱼眼凸起效果//═══════════════════════════════════════════════ Repeater{model:root.iconData delegate:Rectangle{//─── 从 JS 对象提取数据 ─── readonlypropertyreal baseX:modelData.baseX readonlypropertyreal baseY:modelData.baseYroot.gridOffsetY//─── 到表盘中心的距离决定是否裁剪─── readonlypropertyreal cdx:baseX-root.clockCX readonlypropertyreal cdy:baseY-root.clockCY readonlypropertyreal cdist:Math.sqrt(cdx*cdxcdy*cdy)readonlypropertyboolinCircle:cdistroot.clipR-14//─── 到鼠标凸起中心的距离 → 放大系数 ─── readonlypropertyreal mdx:baseX-root.bulgeCX readonlypropertyreal mdy:baseY-root.bulgeCY readonlypropertyreal mdist2:mdx*mdxmdy*mdy readonlypropertyreal bulge:0.380.82*Math.exp(-mdist2/5500.0)//─── 缩放后的尺寸 ─── readonlypropertyreal scaledSz:root.baseSize*bulge readonlypropertyboolisSelected:indexroot.selectedIndex visible:inCircle x:baseX-scaledSz/2y:baseY-scaledSz/2width:scaledSz height:scaledSz radius:scaledSz*0.24gradient:Gradient{orientation:Gradient.Horizontal GradientStop{position:0.0;color:isSelected ?#2a3a50:#1e1e20}GradientStop{position:1.0;color:isSelected ?#152030:#0c0c0e}}border.width:isSelected ?1.5:0.5border.color:isSelected ?#4a8eff80:#3a3a3a50//─── 图标图片居中占瓦片60%─── Image{source:modelData.image width:parent.width*0.60height:parent.height*0.60anchors.centerIn:parent sourceSize.width:width*2//提高 SVG 渲染清晰度 sourceSize.height:height*2}}}//═══════════════════════════════════════════════//鼠标交互鱼眼凸起跟随点击检测//═══════════════════════════════════════════════ MouseArea{anchors.fill:parent hoverEnabled:false//关闭悬浮监听鼠标悬浮不再触发效果propertyboolisPressed:falsepropertyboolisClick:false onPressed:{isPressedtrue isClicktrue}onReleased:{isPressedfalseif(isClick){root.selectedIndexroot.findIconAt(mouse.x,mouse.y)}isClickfalse}//只有按住鼠标拖动时才更新凸起中心点 onPositionChanged:{if(isPressed){root.bulgeCXmouse.x root.bulgeCYmouse.y}isClickfalse}onClicked:{}/*onExited:{isPressedfalse//鼠标移出窗口时重置凸起中心可选不需要可以删掉 root.bulgeCXroot.clockCX root.bulgeCYroot.clockCY}*/}//═══════════════════════════════════════════════//底部信息面板//═══════════════════════════════════════════════ Rectangle{x:root.clockCX-150y:root.clockCYroot.clipR24width:300height:46radius:23color:root.selectedIndex0?#1a1a2e:#111118border.width:1border.color:root.selectedIndex0?#4a8eff40:#222Text{anchors.centerIn:parent width:parent.width-20horizontalAlignment:Text.AlignHCenter verticalAlignment:Text.AlignVCenter font.pixelSize:14font.weight:Font.Light color:root.selectedIndex0?#8ab4f8:#555text:{if(root.selectedIndex0root.iconData[root.selectedIndex]){return #root.selectedIndex root.iconData[root.selectedIndex].name}return点击图标查看详情}}}}三、实现原理1、整体架构本项目采用PyQt6 QML混合架构Python 负责程序入口与 QML 文件加载QML 负责全部界面渲染与交互逻辑。核心思路是用 QML 的声明式语法描述蜂巢网格用 JavaScript 函数实现点击检测与鱼眼凸起效果两者通过属性绑定与信号机制协同工作。2、蜂巢网格布局蜂巢网格并非使用传统网格布局组件而是通过Repeater 手工坐标实现在iconData数组中每个图标预先定义好基准坐标baseX/baseY按行错位排列形成蜂巢状交错布局。第 1 行 3 个、第 2 行 5 个、第 3 行 5 个……通过行内数量与 X 坐标的错位自然形成六边形蜂巢轮廓。每个瓦片由Repeater的 delegate 动态生成modelData提供图标名称、图片路径与坐标。3、圆形裁剪蜂巢整体被限制在一个圆形表盘内实现方式为距离判断 可见性控制以表盘中心(clockCX, clockCY)为圆心clipR为半径。每个瓦片计算自身到圆心的距离cdist当cdist clipR - 14时inCircle为真瓦片才可见。这样超出圆形范围的瓦片自动隐藏形成干净的圆形裁剪效果。4、鱼眼凸起效果鱼眼效果是本项目的视觉亮点核心公式为高斯衰减函数鼠标位置作为凸起中心(bulgeCX, bulgeCY)。每个瓦片计算到凸起中心的距离平方mdist2。放大系数bulge 0.38 0.82 * exp(-mdist2 / 5500.0)距离越近放大越明显距离越远越接近基础大小。瓦片的实际尺寸scaledSz baseSize * bulge同时 X / Y 坐标也随尺寸调整保证瓦片始终以自身中心为基准缩放。5、点击检测点击检测在findIconAt(mx, my)函数中完成遍历所有图标计算点击点到图标中心的距离dist。同时判断该图标是否在圆形裁剪范围内。当dist baseSize / 2且距离最近时判定为命中该图标。命中后更新selectedIndex底部信息面板随之显示对应图标名称。6、交互细节凸起跟随只有按住鼠标拖动时onPositionChanged才更新凸起中心点实现“按住拖动”的鱼眼跟随效果。点击判定按下时记录isClick拖动时清除isClick松开时若仍为点击状态则执行选中逻辑避免拖动误触。视觉反馈选中瓦片使用高亮渐变与边框颜色底部面板同步显示选中信息。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑