Qt纯代码模仿WPS登录窗口:QSS与QPainter从布局到交互

发布时间:2026/10/17 3:32:26
Qt纯代码模仿WPS登录窗口:QSS与QPainter从布局到交互 前阵子折腾了一个 Qt 纯代码模仿 WPS 登录界面的小项目没有切图、没有背景素材窗口上的 logo、圆角卡片、输入框、按钮和阴影全部用 QSS 加 QPainter 手写出来。这篇文章就从布局拆解、窗口底层、QSS 细节、交互实现到踩坑记录完整复盘一下这个登录窗口是怎么从空白工程一步步长出来的。如果你最近也在写自定义窗体、想摆脱对设计师切图的依赖或者单纯想看看一个“看起来还算精致”的登录界面需要哪些基本功这篇应该能给你不少可以直接抄作业的细节。1. 纯代码方案的出发点与界面拆解1.1 为什么要用纯代码做而不是直接找素材很多开发者一听“模仿某某界面”第一反应是去网上扒图把 logo、背景、图标全部切成图片然后往 QLabel 里一贴。这个思路最省事但后患不小。图片资源一旦需要换肤或者适配高分屏要么模糊要么拉伸变形想在 hover 时让图标换个颜色还得准备两套甚至三套图到了深色模式整套素材又得推翻重做。纯代码路线的核心优势是“参数化”。所有颜色、圆角、间距、阴影强度都是代码里的常量改一个变量整个界面跟着变。我这次刻意把 logo、协议勾选框、视觉装饰全部用 QPainter 和 QSS 子控件来实现全程零图片依赖目的就是让这套代码可以任意移植到别的项目里。实测下来纯代码界面的帧率表现也很稳因为不需要解码位图内存占用少启动速度快。不过要泼一盆冷水纯代码不代表“不用设计”。恰恰相反它要求你把设计语言拆得更细色值、字重、间距、层级都要心里有数。简单说图片方案是把设计打包成黑盒纯代码方案是让设计变成了可编辑的配置文件。我建议新手练这种项目时先观察再动手不要急着敲代码。1.2 WPS 登录界面到底长什么样拆开看我参照的是日常使用中常见的 WPS 登录弹窗形态没有官方设计稿全靠肉眼观察和坐标测量。整个窗口可以拆成三层结构最外层是一个透明背景的无边框窗口悬浮着一张白色圆角卡片。卡片内容是垂直布局顶部 logo 区中间账号与密码输入区底部主按钮再往下是第三方登录方式和协议勾选。视觉上最有辨识度的不是某个具体控件而是整体留白节奏和主按钮的颜色主按钮在灰色输入框中间尤其抢眼。从代码角度拆大概分成这几个区Logo 区卡片顶部居中或左对齐高度约 80px 左右下方留 20px 间隙。表单区账号输入框、密码输入框每个输入框高度约 42px间距 16px。主操作区登录按钮高度约 44px宽度撑满卡片内容区圆角略大于输入框。辅助信息区协议勾选框、忘记密码链接、第三方登录图标通常弱化颜色。这个拆解特别重要因为模仿不是“像素级复制”而是抓住结构和层次。我最终做出来的窗口尺寸是 420x560这个比例是在参考常见弹窗基础上调整出来的卡片左右留了 24px 的透明边距正好用来显示阴影。2. 窗口骨架与无边框阴影实现2.1 工程目录与类划分动工之前先定工程结构。项目虽然不大但为了后面好维护我把“窗口逻辑”和“自绘图标”拆成了两个部分LoginDemo/ ├── main.cpp ├── LoginWindow.h ├── LoginWindow.cpp ├── LoginWindow.qss ├── IconPainter.h └── IconPainter.cppmain.cpp 只负责创建 QApplication 和显示窗口LoginWindow 负责整体布局、事件处理、登录状态切换IconPainter 是一个静态绘图工具类专门提供画 logo、画协议勾、画 loading 弧线的函数。之所以拆出 IconPainter是因为 QSS 能覆盖大部分静态样式但 loading 转圈、动态 logo 这类需要旋转角度的东西QSS 做不了必须用 QPainter 绘制而绘制代码混进窗口类里会让文件越变越长。qss 文件单独拎出来是为了方便整体替换主题。我习惯在 main 函数里用 QFile 读取 qss 然后 setStyleSheet而不是把样式字符串硬编码在窗口构造函数里。这样以后想换一套夜间主题只加一个 qss 文件就行。2.2 无边框窗口与圆角卡片的底层设置登录窗口第一步是去掉系统标题栏。Qt 里最常用的做法是设置窗口标志setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinimizeButtonHint); setAttribute(Qt::WA_TranslucentBackground); setFixedSize(420, 560);这里有两个关键点。Qt::FramelessWindowHint 会去掉标题栏和边框但窗口本身依然是矩形真正让窗口变成“圆角卡片”的是 WA_TranslucentBackground 这个属性——它让整个窗口背景透明于是你只能在窗口上画出一个圆角矩形的“卡片”其余区域全部透明。对应到代码上我在中央容器上设置了 objectNameauto *card new QWidget(this); card-setObjectName(loginCard);然后在 QSS 里#loginCard { background-color: #FFFFFF; border-radius: 12px; }这样卡片就有了白色圆角背景透明区域透出桌面。这里我踩过一个小坑如果直接在顶层窗口上设置 background-color 和 border-radius很多平台不会生效因为顶层窗口有系统边框逻辑介入。常规做法是“透明窗口 内部圆角容器”这个套路做登录弹窗基本是标准答案。2.3 给卡片增加阴影而不是画一条灰边登录窗口最外层的悬浮感主要靠阴影。Qt 里最省事的方案是 QGraphicsDropShadowEffectauto *shadow new QGraphicsDropShadowEffect(card); shadow-setBlurRadius(24); shadow-setOffset(0, 4); shadow-setColor(QColor(0, 0, 0, 35)); card-setGraphicsEffect(shadow);参数选择上blurRadius 24 是为了让阴影边缘过渡自然offset 的 Y 方向给 4px 是为了制造“卡片略微浮在桌面上方”的感觉颜色用半透明黑而不是纯黑否则会显得很脏。这个方案唯一的坑是阴影会被窗口边界裁剪。因为窗口本身只有 420x560而阴影要画在卡片外所以必须在布局时给卡片四周留出余量。我的做法是窗口最外层放一个 QVBoxLayoutlayout 的 margins 设为 24px然后把卡片放进去。这样阴影有地方画不会被切掉。还有一点值得注意QGraphicsDropShadowEffect 本质是让控件在离屏缓冲区里重绘一次再做高斯模糊。如果 shadow 作用的控件特别大或者模糊半径特别大移动窗口时会感觉到掉帧。登录窗口尺寸不大所以没问题。如果你的项目里有复杂图表建议只对静态容器加阴影不要大面积滥用。3. 核心界面与 QSS 实现3.1 先定色板和字体再写样式写 QSS 之前我先做了一张“调色盘”把使用到的颜色全部列出来。这一步很多人会忽略但实际效果非常明显——没有色板调着调着颜色就飘了。用途色值说明卡片背景#FFFFFF登录卡片底色页面底色#F5F6FA透明窗口透出的底层登录页常用浅灰主按钮#2878FF偏蓝的办公风主色主按钮 hover#1D66E5鼠标悬停时加深主按钮 pressed#1754C7按下时进一步加深输入框背景#F7F8FA未聚焦时的浅灰底输入框边框#E5E6EB未聚焦时的细边框主文本#1D2129账号、标题等主要文字次要文本#86909C协议、辅助说明错误提示#F53F3F密码错误时的提示色字体方面我设置了“Microsoft YaHei, PingFang SC, Segoe UI, sans-serif”这个顺序中文优先用微软雅黑苹果系统回退到苹方英文数字回退到 Segoe UI。字体栈要写在窗口整体样式上而不是每个控件单独写这样所有子控件自动继承。3.2 输入框和按钮的 QSS 写法输入框是登录界面的门面我实现了三种状态默认、聚焦、禁用。默认状态是浅灰底加浅边框聚焦状态切换成白底加蓝色边框。用 QSS 写起来很清晰#accountEdit, #passwordEdit { background-color: #F7F8FA; border: 1px solid #E5E6EB; border-radius: 6px; height: 42px; padding: 0px 12px; font-size: 14px; color: #1D2129; } #accountEdit:focus, #passwordEdit:focus { background-color: #FFFFFF; border: 1px solid #2878FF; }这里有个细节给 QLineEdit 设置 height 时实际上设置的是控件整体高度而不是内部文本区域。所以文字会垂直居中不需要额外调 padding。当然了如果你在某些平台上发现文字偏上可以适当调大 padding 的上下值。主按钮的 QSS 也比较常规#loginButton { background-color: #2878FF; border: none; border-radius: 8px; height: 44px; color: #FFFFFF; font-size: 15px; font-weight: 500; } #loginButton:hover { background-color: #1D66E5; } #loginButton:pressed { background-color: #1754C7; } #loginButton:disabled { background-color: #A0C4FF; color: #FFFFFF; }按钮禁用状态要单独考虑因为登录请求发出后我会暂时禁用按钮防止用户重复点击。如果不写 :disabled 伪类禁用时就会保留 hover 的高亮样式视觉上很怪。3.3 纯代码画 Logo、协议勾选框和动态圆弧WPS 登录框的 logo 我不打算复刻它的商标图形而是用 QPainter 画一个表达“文档叠加”概念的抽象图标既贴合办公场景又避免原样照搬的麻烦。画法是在 IconPainter 里定义一个函数void IconPainter::drawLogo(QPainter p, const QRectF r, const QColor c) { p.save(); p.setRenderHint(QPainter::Antialiasing); // 底部圆角矩形代表基础文档 QRectF bottomRect(r.left() r.width() * 0.2, r.top() r.height() * 0.3, r.width() * 0.6, r.height() * 0.62); p.setBrush(c); p.setPen(Qt::NoPen); p.drawRoundedRect(bottomRect, 8, 8); // 顶部倾斜矩形表示叠加的文档页 p.save(); p.translate(r.center().x(), r.top() r.height() * 0.36); p.rotate(15); QRectF topRect(-r.width() * 0.22, -r.height() * 0.25, r.width() * 0.44, r.height() * 0.5); p.drawRoundedRect(topRect, 6, 6); p.restore(); p.restore(); }这个函数只用了两个圆角矩形通过旋转和叠加模拟出“多文档堆叠”的视觉效果。因为是纯矢量绘制logo 可以任意缩放也可以随时通过参数换颜色比位图灵活得多。协议勾选框我用 QCheckBox QSS 子控件实现。其实就是把 indicator 的默认勾选样式替换成自绘的圆角方块#agreeCheckBox { color: #86909C; font-size: 12px; spacing: 6px; } #agreeCheckBox::indicator { width: 16px; height: 16px; border-radius: 4px; border: 1px solid #C9CDD4; background-color: #FFFFFF; } #agreeCheckBox::indicator:checked { border: 1px solid #2878FF; background-color: #2878FF; image: none; }checked 状态下的勾号如果不用图片最简单的办法是在 QCheckBox 里放一个子类重写 paintEvent 画一个白色对勾。否则就只能在 QSS 的 image 属性里嵌入一张 base64 图形这又变成“纯代码”之外的资源依赖了。我选择了重写 QCheckBox几行代码就能画出平滑的对勾线条。loading 转圈同样用 QPainter 实现。我在窗口里放了一个自定义的 LoadingCircle 控件paintEvent 里根据当前角度画一段圆弧然后用 QVariantAnimation 持续改变角度void LoadingCircle::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); QPen pen(QColor(#FFFFFF), 3); pen.setCapStyle(Qt::RoundCap); p.setPen(pen); // 只画 270 度的弧留出缺口产生旋转感 p.drawArc(rect().adjusted(4, 4, -4, -4), m_angle * 16, 270 * 16); }动画循环里每次把 m_angle 减 1等于 360 度转一圈。圆弧的缺口让视觉上有“加载中”的节奏比单纯转一整圈更像真实 Loading。4. 交互细节拖动、回车、焦点与状态切换4.1 无边框窗口拖动去掉系统标题栏后窗口默认无法拖动必须自己处理鼠标事件。我是在卡片顶部留了一个 50px 高的标题区域作为“可拖拽热区”void LoginWindow::mousePressEvent(QMouseEvent *event) { if (event-button() Qt::LeftButton) { m_dragOffset event-globalPosition().toPoint() - frameGeometry().topLeft(); event-accept(); } } void LoginWindow::mouseMoveEvent(QMouseEvent *event) { if (event-buttons() Qt::LeftButton) { move(event-globalPosition().toPoint() - m_dragOffset); } }这段代码核心逻辑是按下鼠标时记录“鼠标相对窗口左上角”的偏移量移动时用当前鼠标全局坐标减去这个偏移量得到窗口应该移动到的位置。这里有两个容易踩的坑。第一个坑是坐标系。Qt5 里用 event-globalPos()Qt6 里改成了 event-globalPosition().toPoint()。如果你在升级版本时空了指针编译能过但运行时位置会偏一个像素。第二个坑是 frameGeometry 和 geometry 的区别。对无边框窗口来说两者基本一样但如果你以后加了阴影窗口或者保留系统边框计算偏移量时就要改成 frameGeometry否则拖动时会突然“跳一下”。4.2 回车登录与按钮状态模拟登录交互不能只靠鼠标点击。用户习惯在密码框里敲完回车就登录所以我在两个输入框上都监听了 returnPressed 信号统一指向 doLogin 槽函数connect(m_passwordEdit, QLineEdit::returnPressed, this, LoginWindow::doLogin); connect(m_loginButton, QPushButton::clicked, this, LoginWindow::doLogin);doLogin 里做的第一件事不是发起网络请求而是做输入校验账号为空时提示“请输入账号”密码长度不足时提示“请输入密码”。这些提示都显示在一个红色 QLabel 上避免用 QMessageBox 弹窗打断流程。状态切换用 QTimer 模拟了常见登录场景下的耗时操作void LoginWindow::doLogin() { if (m_accountEdit-text().trimmed().isEmpty()) { m_tipLabel-setText(请输入账号); return; } m_loginButton-setEnabled(false); m_loginButton-setText(登录中...); showLoading(true); QTimer::singleShot(1200, this, [this]() { // 模拟登录成功后的处理 m_loginButton-setEnabled(true); m_loginButton-setText(登录); showLoading(false); m_tipLabel-setText(登录成功纯模拟); }); }这里有个小细节按钮禁用的时候如果直接改文字QSS 的 :disabled 样式会接管按钮颜色但文字还是可以用 setText 控制。我把按钮文本改成“登录中...”后配合 LoadingCircle 控件显示用户在等待期间看到的是一条转圈动画加变化文字比单纯禁用按钮有反馈感。4.3 Tab 焦点顺序和键盘可达性很多人自定义界面后会忽略键盘操作。默认情况下Qt 会按照控件创建顺序决定 Tab 顺序但在复杂布局里经常不是我们想要的。所以我在构造函数的末尾显式设置了焦点顺序setTabOrder(m_accountEdit, m_passwordEdit); setTabOrder(m_passwordEdit, m_loginButton); setTabOrder(m_loginButton, m_agreeCheckBox);同时QSS 里给输入框的 :focus 样式增加一个淡蓝色外圈提示。由于我前面已经写了 focus 边框变色键盘用户按下 Tab 切到输入框时能清楚看到焦点位置而不是全靠猜。这一点听起来简单但实际很多客户端界面都没做好。Esc 键的处理也要考虑。无边框窗口默认 Esc 不会关闭我在 keyPressEvent 里做了监听如果当前不在登录请求中Esc 就关闭窗口如果正在登录中则忽略 Esc避免用户误关窗口导致提示状态还没结束。5. 常见问题与排查技巧实录5.1 阴影发黑、边缘发虚怎么办使用 QGraphicsDropShadowEffect 后最常出现的问题是阴影颜色太深或者边缘生硬。我调过几轮总结出两个直接原因。第一个原因是 color 的 alpha 值过高。很多人从网上下载代码直接抄看到 setColor(QColor(0, 0, 0, 255)) 就拿过来用阴影自然黑得像描边。我的建议是 alpha 先给 30~50然后整体看效果不够再逐步加不要一上来就 100 以上。第二个原因是窗口边缘裁切。如果卡片布局没有给阴影预留空间阴影被窗口边界切掉一刀视觉上就会出现“一半阴影一半没有”的断层。检查方法很简单把窗口透明背景色临时改成纯红色看看卡片周围是否还有一圈红边如果有说明 layout margin 留得不够加大到 24px 以上。5.2 QSS 不生效的高频原因我整理了一个速查表基本覆盖了写 QSS 时最容易翻车的几类问题现象原因解决办法样式一点不生效没加载 qss 文件或选择器写错在 main 里 QFile 读取后 setStyleSheet用 #objectName 时确认 setObjectName 已执行子控件样式不生效父级 QSS 把子控件覆盖了给子控件单独设 objectName用更具体的选择器hover 样式不生效控件没有使能鼠标跟踪QPushButton 等默认支持QLabel 需要重写 enterEvent 或安装事件过滤器背景图不显示资源路径写错优先用 qrc 资源路径避免相对路径border-radius 部分失效同时设置了 background-image背景图片会把圆角遮住改用 QPainter 或去掉图片还有一个小坑值得单独说QSS 里的 height 对 QLineEdit 起作用但对某些自定义控件不一定起作用比如 QComboBox 的 height 设置后下拉按钮的位置可能还会偏。遇到这种问题别死磕 QSS直接继承控件重写 sizeHint 才是正路。5.3 高 DPI 缩放和绘制模糊现在高分屏已经很普及了如果把 DPI 处理忽略界面会出现两种问题一是控件整体变小字发虚二是 QPainter 画的图形边缘有锯齿。Qt5 环境里main 函数开头要加QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);Qt6 默认开启高分屏缩放不需要手动设置但要注意 setFont 的参数。如果你用 pointSize 设置字号Qt 会按照屏幕 DPI 自动换算如果你用 pixelSize那在高分屏上字体可能比预期小。我全局采用 pointSize这样在不同缩放下表现一致。QPainter 绘制方面DrawArc、drawRoundedRect 这类函数只要设置了 Antialiasing在高分屏上一般没问题。真正容易出问题的是绘制到 QPixmap 上再贴到界面里的场景必须用 devicePixelRatio 来设置 pixmap 的分辨率否则图形会糊。我的 LoadingCircle 和 Logo 都是实时绘制到控件上的绕开了 PxielMap 那套逻辑反而最省心。最后补一个实操心得这个项目做完之后我最大的体会是模仿界面的难点从来不是某个控件怎么写而是整个界面的“呼吸感”怎么调。输入框之间的间距、logo 和表单的距离、按钮和协议说明之间的距离这些数值每改一次视觉效果都会明显变化。建议所有间距都抽成常量集中放在一个命名空间里方便批量微调。另外如果你想让这个登录页看起来更生动可以给 logo 加一个非常轻量的透明度循环动画周期 3 秒透明度在 0.75 到 1.0 之间缓慢变化。因为幅度很小不会影响可读性但会让人觉得界面不是“死”的。用 QVariantAnimation 实现几十行代码就能搞定。登录页模仿到这里剩下的就是根据自己的项目配色去微调了希望你也能做出自己满意的登录界面。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询