ARTICLE DETAIL

资讯详情

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

纯Qt Widgets实现360风格界面:无边框窗口、QSS与QPainter

纯Qt Widgets实现360风格界面:无边框窗口、QSS与QPainter 简介这是一份基于Qt 5.9框架仿360安全卫士界面的完整实例源码包面向正在学习Qt桌面开发、希望提升自定义界面与交互设计能力的中初级开发者。压缩包约2.98MB共39个文件主要包含C源文件、界面定义文件、样式表、资源集合文件及工程配置同时配有28张PNG图片素材便于查看界面元素和资源组织方式。已有1727人学习下载。示例代码覆盖QMainWindow主窗口搭建、QWidget与布局管理器应用、按钮与状态栏排布等核心内容并通过信号与槽机制实现点击响应和进度动画帮助体会事件驱动编程。此外qss样式表和Qt Designer生成的ui文件展示了快速定制360风格主题的方法让读者在一套完整源码中掌握从控件布局到视觉美化的整体流程。资源包整体结构清晰模块划分和命名直观导入Qt Creator后即可浏览运行。 不知道你有没有过这样的念头明明用Qt做工具类软件很方便但出来的界面总是带着一股“默认控件味”按钮是灰色的边框是系统的怎么看都像调试工具。而360那种杀毒界面圆角卡片、渐变头图、彩色仪表盘、自定义标题栏第一眼就让人觉得“这软件做得很精致”。我以前也觉得这种界面得靠第三方UI引擎才能搞直到自己用纯Qt Widgets把一套类似的界面从零搭出来才发现核心其实就三件事无边框窗口、QSS细节、QPainter自绘。这篇分享就是围绕一个可以在Qt5.9环境里完整编译的“360UI界面Demo”展开的。我会把这个Demo里最关键的实现挨个拆开窗口怎么去掉系统边框还能正常拖动、标题栏上的最小化最大化关闭按钮怎么处理事件、圆角卡片和阴影怎么做不卡、主界面那个圆形安全指数仪表盘是怎么画出来的。适合已经会建Qt工程、写过信号槽但还没系统做过自定义界面的开发者如果你刚接触Qt也能照着代码跑起来再慢慢理解里面的思路。1. 为什么偏偏用Qt做这种厚重视觉风格1.1 360UI风格到底指的是什么说到“360UI”不能简单理解成“绿色盾牌图标”。它是一种典型的桌面安全软件视觉风格特征非常明显窗口不是直角矩形四周带圆角标题栏是自绘的系统那套最大化最小化按钮被替换成更夸张的样式主界面被切成好几块圆角卡片卡片之间靠阴影拉开层次还会有一个非常抢眼的圆形仪表盘或者环形进度条用来展示“安全指数”。这套风格的难点不在单个控件而在整体框架。普通的QWidget窗口默认就是直角矩形加系统标题栏你往里面塞再多QSS也做不出那种“一体感”。所以第一步一定是先破除系统窗口的边界把整块画布交给自己。1.2 Qt5.9这种老版本为什么也够用标题里写了Qt5.9其实这是个很务实的版本选择。Qt5.9是LTS版本稳定性好Widgets模块非常成熟QSS的支持程度和新版本差别不大。你不需要引入QML、不需要额外装第三方界面库纯Widgets加QPainter就能实现这套界面。更重要的一点是用老版本反而能逼你把原理搞清楚。因为5.9里某些接口还很“原始”比如鼠标事件里直接取event-globalPos()就能拿到全局坐标写起来特别直观。新手照着老版本代码学不会遇到新版接口变更的干扰。等你在5.9上跑通换到Qt6无非改几个函数名而已。2. 无边框窗口整套界面的地基2.1 先去掉系统标题栏想让界面变成360那种圆角平面风格第一步就是把系统边框干掉。设置方式很简单setWindowFlags(Qt::FramelessWindowHint | Qt::Window); setAttribute(Qt::WA_TranslucentBackground);FramelessWindowHint负责去掉标题栏和边框WA_TranslucentBackground允许窗口背景透明。设置之后你的窗口就会变成一个没有任何装饰的矩形画布这时候窗口背景如果直接填充白色四周依然是直角看起来还是不像360风格。所以实际操作里我会在窗口中放一个统一的背景容器比如一个继承自QWidget的BgWidget在它的paintEvent里画一个带圆角的圆角矩形颜色接近白色背景色透明。如果你不想让整个窗口透明还有一个折中方案窗口本身不透明只设置很小的一段透明边距让阴影能露出来。我个人更推荐这种后面讲阴影的时候细说。2.2 窗口拖动最简单的三行思路去掉标题栏之后原来由系统负责的窗口拖动也要自己实现。原理不复杂鼠标按下时记录按下点与窗口左上角的偏移量鼠标移动时用当前全局坐标减去这个偏移量得到窗口新位置。void FramelessWindow::mousePressEvent(QMouseEvent *event) { if (event-button() Qt::LeftButton) { m_dragPosition event-globalPos() - frameGeometry().topLeft(); event-accept(); } } void FramelessWindow::mouseMoveEvent(QMouseEvent *event) { if (event-buttons() Qt::LeftButton) { move(event-globalPos() - m_dragPosition); event-accept(); } }这段代码几乎是所有无边框窗口的标配。需要注意一个细节m_dragPosition必须用frameGeometry()而不是geometry()因为无边框状态下两个值差别不大但带阴影或者透明边距的时候frameGeometry()更准确。2.3 边缘缩放很多人在这里卡住拖动解决了缩放又是一个坎。系统标题栏被去掉后窗口边缘的缩放热区也没了。最简单的做法是固定窗口大小360主窗口其实很少让你随意拉伸很多仿版直接setFixedSize既省事又不会破坏布局。但如果你的界面需要支持缩放通常有两个方向一是用eventFilter在鼠标靠近边缘时改变光标形状按下后计算新的窗口大小和位置。这个方式代码量不大但容易在快速拖动时抖动。二是通过nativeEvent拦截Windows的WM_NCHITTEST消息把窗口边缘告诉系统让系统自动处理缩放。这个方案体验最接近原生但代码依赖Windows头文件跨平台性差一些。对于纯学习Demo我建议先固定尺寸把精力放在界面上。2.4 最大化按钮的连带坑如果你决定保留最大化功能一定会遇到一个新问题无边框窗口最大化时会遮住任务栏。这是因为最大化状态没有走系统的那套逻辑。处理方案是监听窗口状态变化在最大化时把窗口geometry设为可用屏幕区域或者在nativeEvent里处理WM_GETMINMAXINFO消息。这部分代码比较绕新手容易在这里卡很久。所以我的建议很直接仿360UI的Demo阶段setFixedSize是最理性的选择。360本身的主窗口也是固定尺寸加局部卡片拉伸很少把整个窗口随意扯大扯小。别让窗口管理器的问题抢了界面制作的风头。3. 自绘标题栏按钮、事件与交互细节3.1 为什么标题栏必须自己画无边框之后屏幕上沿那一块区域就空出来了。你要在这块区域自己画出程序图标、标题文字以及三个控制按钮。360的标题栏视觉上很轻按钮通常只在高亮时出现背景色块整体颜色和顶部区域融为一体。工程上我一般用一个QWidget作为标题栏容器水平布局左侧放一个QLabel显示图标和标题右侧放三个QPushButton。这三个按钮不直接调用系统API而是通过信号连接到主窗口的槽函数。connect(ui-btnMin, QPushButton::clicked, this, QWidget::showMinimized); connect(ui-btnClose, QPushButton::clicked, this, QWidget::close);3.2 三个按钮的QSS写法按钮视觉要做出360那种“轻底色高亮”的效果QSS非常合适。这里有个小技巧自定义窗口按钮时记得把默认的边框和背景先清掉否则Windows经典样式的灰色底会冒出来。QPushButton#btnMin, QPushButton#btnClose { border: none; background: transparent; border-radius: 4px; } QPushButton#btnMin:hover { background: rgba(80, 80, 80, 20); } QPushButton#btnClose:hover { background: rgba(220, 60, 60, 180); color: white; }关闭按钮的hover用红色背景这是一个交互习惯用户不用看完文字瞄一眼颜色就知道哪个能关窗口。360实际也是这么做的。3.3 双击最大化与右键菜单除了三个按钮标题栏还应该处理两件事双击标题栏最大化或还原、右键弹出系统菜单。双击处理可以在标题栏容器上重写mouseDoubleClickEvent判断当前是否为最大化状态再切换。右键菜单我通常不做因为仿360界面主窗口本身固定大小右键菜单的收益不大。但如果你的程序需要可以用QMenu::exec在鼠标位置弹一个简单菜单里面放“置顶”“打开文件位置”“退出”等条目。这部分属于加分项不影响主体框架。3.4 子控件的鼠标事件过滤这里有一个非常容易踩的坑你给标题栏写了拖动逻辑但标题栏上面还放了按钮和标签。如果直接重写标题栏的mousePressEvent点击按钮的时候会把拖动逻辑也触发看起来就像按钮“黏”在鼠标上。解决办法是给按钮和标签单独安装事件过滤器或者在拖动逻辑里判断一下childAt(event-pos())只有点击空白区域时才触发拖动。实际开发中我更推荐childAt判断代码干净不用绕事件过滤器那套流程。4. 卡片、阴影和导航条的工程化做法4.1 圆角卡片用QSS还是自绘360主界面里的功能模块全是圆角卡片。在Qt里做圆角卡片最省事的是QSSQFrame#cardHealth { background: #ffffff; border-radius: 10px; }只要父窗口背景颜色和卡片有明显差异一个圆角白色卡片就出来了。但如果你需要的不是纯色卡片而是带渐变或者带内阴影QSS就有点力不从心。这时候我会写一个叫CardWidget的通用容器继承QFrame重写paintEventvoid CardWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); QPainterPath path; path.addRoundedRect(rect(), 10, 10); painter.fillPath(path, QColor(#ffffff)); // 这里可以继续画渐变、边框、底部阴影等 }封装一个通用卡片的好处是你可以在构造时传背景色、圆角半径、边框色一个类管所有卡片QSS只需要处理布局和文字颜色。4.2 阴影效果的性能取舍很多仿360的初学者喜欢给卡片加QGraphicsDropShadowEffect效果确实好看但有一个严重问题这个类在元素移动或父窗口重绘时会触发离屏渲染卡片一多或者窗口频繁重绘整个界面会明显掉帧CPU占用也会上去。我的个人实践是大面积的卡片阴影不用QGraphicsDropShadowEffect而是用“伪阴影”。做法很简单在卡片下层叠一层颜色略深的圆角矩形偏移几个像素就模拟出阴影层次。如果层次不够再叠加一层偏移更大、颜色更浅的矩形视觉上完全够用性能开销几乎为零。这个技巧尤其在多个卡片同时加载时体感差别非常明显。4.3 顶部导航切换的实现思路360主界面顶部通常有一排导航项点击后下方内容整体切换。这个功能在Qt里有很多做法最简单的不是用QStackedWidget直接切而是维护一个按钮列表点击时用QSS动态切换选中状态void MainWindow::onNavClicked() { QPushButton *btn qobject_castQPushButton *(sender()); if (!btn) return; for (QPushButton *b : m_navButtons) { b-setProperty(selected, b btn); b-style()-unpolish(b); b-style()-polish(b); } m_stack-setCurrentIndex(btn-property(pageIndex).toInt()); }这里setProperty加unpolish/polish是QSS动态切换样式的关键。很多新手改了setProperty后样式没变化就是因为没有重新触发样式计算。注意每个导航按钮都要预留一个pageIndex属性这样增删页面时不需要在槽函数里写一堆if-else。5. 安全指数仪表盘用QPainter画主视觉5.1 从画一个扇形开始360界面最标志性的视觉元素就是圆形安全指数。用QPainter画这个其实很直观核心是drawArc函数。先画一个灰色底环再画一个绿色或橙色的扫描弧线最后在中间放百分比文字。void SafetyDial::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); QRectF arcRect(20, 20, width() - 40, height() - 40); QPen pen; pen.setColor(QColor(#e8ecf2)); pen.setWidth(16); painter.setPen(pen); painter.drawArc(arcRect, 0, 360 * 16); pen.setColor(QColor(#12b88a)); pen.setWidth(16); pen.setCapStyle(Qt::RoundCap); painter.setPen(pen); painter.drawArc(arcRect, -90 * 16, -static_castint(m_percent * 270 * 16)); }注意drawArc的角度单位是1/16度不是直接传度数。-90 * 16表示从12点钟方向开始画顺时针扫270度剩90度缺口制造出“没有闭合”的科技感。这里我踩过一次坑角度方向写反导致弧形从上方往左转而不是往右走。5.2 让弧线动起来静态弧形只是一个图片真正让界面有生命感的是扫描动画。实现方式不复杂维护一个m_sweepAngle成员变量定时增加并触发update()重绘void SafetyDial::startScan() { m_timer.start(30); } void SafetyDial::onTimeout() { m_sweepAngle 2; if (m_sweepAngle 270) m_sweepAngle 0; m_percent m_sweepAngle / 270.0; update(); }完整的360风格是弧线扫一圈后面跟一段渐变色尾迹。那个尾迹用普通画笔不好画我一般会用QConicalGradient做锥形渐变或者直接再画一条半透明的短弧线模拟拖尾。对于入门Demo纯色弧线加百分比数字已经足够好看。5.3 中心的百分比文字和数字仪表盘中心要显示一个百分比或“安全”两个字。这一块我推荐直接在paintEvent里用painter.drawText画不要在控件上加QLabel否则每次位置调整都要同步非常麻烦。painter.setPen(QColor(#262626)); QFont font painter.font(); font.setPixelSize(34); font.setBold(true); painter.setFont(font); painter.drawText(arcRect, Qt::AlignCenter, QString(%1%).arg(m_percent * 100, 0, f, 0));这里有个细节数字字体用setPixelSize而不是setPointSize。因为界面要适配高分屏点值在不同DPI下渲染大小会变像素值更可控。5.4 其他自绘控件可以举一反三学会了仪表盘主界面下方的“垃圾清理”“木马扫描”等进度条也都能自己画。环形进度条就是把drawArc从固定270度改成按百分比扫描横向进度条则是画一个圆角底条再画一个带渐变的前景条。整套界面最烧脑的自绘部分其实就是这几个控件其他都能靠QSS堆出来。6. 编译运行避坑记录6.1 QSS加载后没反应这是新手最容易卡住的地方。QSS文件写好了但程序一跑界面还是默认样式。原因多半是没有读取QSS文件或者读取了没应用到全局。检查两步QFile qssFile(:/style/default.qss); qssFile.open(QFile::ReadOnly); QString styleSheet QLatin1String(qssFile.readAll()); qApp-setStyleSheet(styleSheet);注意qApp的全局样式一旦设置子窗口会继承。如果你的某个控件不想被全局QSS影响可以用setProperty(class, custom)配合选择器单独控制。6.2 无边框窗口中文输入法光标错位无边框窗口不使用系统标题栏时在Windows上中文输入法的候选框位置经常跑偏跑到屏幕左上角。这个问题的根因是Qt和输入法之间的窗口坐标同步。解决办法是给顶层窗口设置Qt::WA_InputMethodEnabled部分情况下还要在showEvent里调用activateWindow()让窗口获得焦点。这块属于系统级兼容问题不同输入法表现不一样没有一劳永逸的解法只能按场景微调。6.3 图标和资源的组织方式360界面里图标占了很大比重按钮上要放图、卡片上要放图、标题栏上也要放图。我建议把所有图标放到Qt资源文件里用前缀区分模块比如:/icons/action/scan.png、:/icons/nav/clean.png。QSS里引用资源路径时也要写成url(:/icons/nav/clean.png)少一个冒号都会加载失败这个错误很隐蔽我遇到过不下三次。6.4 最后说点个人习惯代码组织方面我会把自定义控件全部放到独立的类文件里一个控件一个文件主窗口只负责布局和业务逻辑。这样后续换皮肤、增删功能会非常舒服。另外建议所有颜色值抽成常量或者QSS变量Qt5.9虽然不支持QSS变量但可以维护一份theme.h头文件统一管理颜色改起来快很多。这套界面做完以后我最大的感受是别被“360UI”这个名字吓到它本质上就是无边框窗口加QSS加QPainter三件套的组合。先把无边框窗口的地基打稳再把几个自定义控件逐个画出来最后用QSS把导航、按钮、卡片串起来一个看起来很有“商业感”的界面就出来了。过程中不用追求所有效果一步到位固定窗口尺寸、用伪阴影替代实时阴影、用QPainter包办特殊造型这些取舍踩过的坑我都写在前面了照着走能省不少时间。本文还有配套的精品资源点击获取
返回列表