ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Qt6 实现 会动的界面像物理世界一样

Qt6 实现 会动的界面像物理世界一样 Qt6 实现 会动的界面像物理世界一样flyfish下载地址在文末1. 总体架构FluidDemo (主窗口) ├─ 控制条 bar : 阻尼/响应/打乱/复位/底部表单/减弱动效 ├─ 画布 m_canvas : 容纳若干 Tile子控件绝对定位 ├─ 底部表单 m_sheet : 一个 QWidget纵向位置由弹簧驱动 └─ 中央节拍器 m_timer: ~60fps 的 QTimer每帧调用 tickAll()核心数据模型每个会被动画的属性都是一条独立的Spring。一条弹簧 一个一维状态(value, velocity)朝target积分。磁贴位置m_x/m_y两条弹簧2D 运动拆成独立轴。磁贴按压缩放m_scale一条弹簧。底部表单纵向m_sheetY一条弹簧。为什么用中央节拍器而不是每条弹簧自带定时器所有物体共用同一个单调时钟QElapsedTimer逐帧步进静止即停表。这样位置、缩放、表单的动画天然同步也避免 N 个定时器互相抢。2. 名词解释名词含义Spring弹簧一个一维物理状态机value(当前值) velocity(速度) → 朝target积分。新输入只是改target运动连续不中断。damping阻尼比1.0 临界阻尼无回弹、平滑落定1 欠阻尼过冲并振荡更“弹”1 过阻尼慢吞吞收敛。response响应到达目标的快慢单位秒。注意这不是“时长”——弹簧没有固定时长落定时间由物理涌现。settled静止可中断性动画任意时刻可改target因为step()从当前value起跳不跳变、速度续接。直接操控拖拽时元素 1:1 粘住指针并保留“从哪抓的”偏移量。速度交接松手瞬间把手指速度px/s交给弹簧velocity动画无缝衔接拖拽。动量投射用释放速度把“静止位置”投出去再吸附到投射点最近的锚点而不是从释放点就近吸附。橡皮筋越界时渐进抵抗越超越远跟得越少不硬停。减弱动效无障碍模式落定更短、不续接速度、无过冲。3. 弹簧积分器Spring::step状态定义Spring.hstructSpring{doublevalue0,target0,velocity0;doubledamping1.0;// 阻尼比doubleresponse0.4;// 响应秒qint64 last0;// 上次步进时钟nsvoidstep(qint64 nowNs);boolsettled()const;voidresetClock(){last0;}};积分Spring.cpp——临界/欠/过阻尼用统一解析解直接算exp/cos/sin避免逐帧数值漂移voidSpring::step(qint64 nowNs){if(last0)lastnowNs;doubledt(nowNs-last)/1e9;lastnowNs;dtqMin(dt,1.0/30.0);// 夹住大跳变切回前台/久置后不爆冲constdoubleomega2.0/response;// 自然频率 ω 2/responseconstdoubleddamping;if(qAbs(d-1.0)1e-4){// 临界阻尼无回弹constdoubleestd::exp(-omega*dt);constdoublec1value-target;constdoublec2velocityomega*c1;value(c1c2*dt)*etarget;velocity(c2-omega*(c1c2*dt))*e;}elseif(d1.0){// 欠阻尼过冲并振荡constdoublewdomega*std::sqrt(1-d*d);// 阻尼自然频率constdoubleestd::exp(-d*omega*dt);constdoublec1value-target;constdoublec2(velocityd*omega*c1)/wd;valuee*(c1*std::cos(wd*dt)c2*std::sin(wd*dt))target;velocitye*(-c1*wd*std::sin(wd*dt)c2*wd*std::cos(wd*dt))-d*omega*(value-target);}else{// 过阻尼两根实数指数叠加constdoublerstd::sqrt(d*d-1);constdoubles1-omega*(d-r),s2-omega*(dr);constdoublee1std::exp(s1*dt),e2std::exp(s2*dt);constdoublec2(velocity-s1*(value-target))/(s2-s1);constdoublec1(value-target)-c2;valuec1*e1c2*e2target;velocityc1*s1*e1c2*s2*e2;}if(qAbs(value-target)1e-3qAbs(velocity)1e-3){valuetarget;velocity0;// 收尾精确吸附}}要点dt夹到1/30s防止一帧跨度过大比如窗口切回前台导致数值爆炸。last0表示“下一帧重新对表”让恢复/重定目标后的第一帧dt0不跳变。收敛判定里把value/velocity钉死到target/0消除亚像素抖动。4. 磁贴Tile的拖拽与释放磁贴是QWidget子类位置由m_x/m_y弹簧驱动按下缩放由m_scale驱动。4.1 按下即时反馈voidTile::mousePressEvent(QMouseEvent*e){m_draggingtrue;m_grabe-globalPosition()-mapToGlobal(QPoint(0,0));// 保留抓取点偏移m_scale.target0.92;// 按下即“咔哒”一下不等 releasegrabMouse();update();}mapToGlobal(QPoint(0,0))是磁贴左上角的屏幕坐标m_grab记下“指针相对左上角的偏移”松手/拖动时还原元素才不会跳到指针中心。4.2 移动1:1 跟随 橡皮筋voidTile::mouseMoveEvent(QMouseEvent*e){if(!m_dragging)return;constQPointF ge-globalPosition()-m_grab;QPoint topLeftparentWidget()-mapFromGlobal(g.toPoint());// 回到画布坐标topLeftclampToCanvas(topLeft);// 拖拽中直接钉死 valuetarget弹簧不参与避免和手指“打架”m_x.valuem_x.targettopLeft.x();m_y.valuem_y.targettopLeft.y();setGeometry(topLeft.x(),topLeft.y(),Size,Size);// 速度历史最近一帧的 (位置, 时刻)供松手时算手指速度constqint64 tm_clock.nsecsElapsed();constdoubledt(t-m_lastT)/1e9;if(dt1e-4){m_vx(topLeft.x()-m_lastPos.x())/dt;m_vy(topLeft.y()-m_lastPos.y())/dt;m_lastPosQPointF(topLeft);m_lastTt;}}越界时用橡皮筋软化越超越远跟得越少autorubber[](doubleover,doubledim){constdoublek0.55;return(over*dim*k)/(dimk*std::fabs(over));};4.3 释放把速度交出去voidTile::mouseReleaseEvent(QMouseEvent*e){m_draggingfalse;m_scale.target1.0;// 松开弹回 1.0releaseMouse();constdoublecap6000.0;// 夹掉甩太快/卡顿造成的脏值emitreleased(this,std::clamp(m_vx,-cap,cap),std::clamp(m_vy,-cap,cap));}5. 主窗口FluidDemo的节拍器与归位5.1 中央节拍器voidFluidDemo::tickAll(){constqint64 nowm_clock.nsecsElapsed();boolmovingfalse;for(auto*t:m_tiles)if(t-tick(now))movingtrue;// x/y/scale 各一条弹簧m_sheetY.step(now);m_sheet-setGeometry(0,qRound(m_sheetY.value),width(),kSheetH);if(!m_sheetY.settled())movingtrue;if(!moving)m_timer-stop();// 全部静止 → 停表}boolTile::tick(qint64 nowNs){m_x.step(nowNs);m_y.step(nowNs);m_scale.step(nowNs);setGeometry(qRound(m_x.value),qRound(m_y.value),Size,Size);update();return!(m_x.settled()m_y.settled()m_scale.settled());}5.2 释放后动量投射 → 最近空闲吸附点voidFluidDemo::onTileReleased(Tile*self,doublevx,doublevy){constboolreducedm_reducedChk-isChecked();// 动量投射用释放速度把“静止位置”投出去指数衰减非物理 v²/2aautoproject[](doublev){return(v/1000.0)*0.998/(1.0-0.998);};constQPoint centerself-geometry().topLeft()QPoint(Tile::Size/2,Tile::Size/2);constQPoint projcenterQPoint(int(project(vx)),int(project(vy)));constintidxpickSlot(self,proj);// 投射点最近的、且没被别的磁贴占住的吸附点self-setSlotIndex(idx);self-setTarget(m_slots.value(idx),reduced?0:vx,reduced?0:vy);// 速度交接ensureTimer();}pickSlot对全部吸附点按到proj的距离排序返回第一个“未被其它磁贴占住”的避免两块叠在一起。Tile::setTargetvoidTile::setTarget(QPoint topLeft,doublevx,doublevy){if(m_reduced){m_x.responsem_y.response0.18;m_x.velocitym_y.velocity0;}m_x.targettopLeft.x();m_y.targettopLeft.y();m_x.velocityvx;m_y.velocityvy;// 手里速度直接续接m_x.resetClock();m_y.resetClock();}5.3 为什么可中断连点打乱、或拖动途中再点别的——这些只是改写m_x.target/m_y.target。下一帧step()从当前value屏幕上真实位置继续积分速度自动续接不重建动画、不跳变。这正是弹簧替掉QPropertyAnimation的根本原因。6. 底部表单Sheet一个QWidget纵向位置由单条弹簧驱动用 抽屉默认值欠阻尼、带轻回弹m_sheetY.damping0.8;// 抽屉/表单欠阻尼轻微回弹m_sheetY.response0.3;// 开target height()-kSheetH 关target height()藏到窗口下沿外每帧tickAll里m_sheetY.step(now)后写setGeometry。下载地址 源码下载地址 可执行7. 减弱动效勾选后Tile::setReduced(true)把磁贴response降到0.18、清零velocity。效果是“快速落定、不甩、不回弹”的等价反馈而非无反馈。生产环境应读系统开关constboolsysReducedQGuiApplication::styleHints()-animationDuration()0;
返回列表