1. 从零拆一个登入界面:TextField 与 Label 到底该怎么组合
QML 自定义控件这件事,很多人第一次写登入界面时都会卡在同一个地方:Label 和 TextField 单独用都会,但一旦要封装成可复用组件、还要把账号框和密码框的差异(比如密码要掩码、提示文字不同、字体大小要统一)通过属性暴露出去,就开始乱了。这篇就聚焦这个场景,把「登入界面显示」这件事拆开讲清楚:Label 负责静态提示,TextField 负责输入与占位提示,外层用 Item 包一层做自定义控件,再通过 property alias 把内部属性暴露给调用方。
先说清楚这套东西是什么、能做什么、适合谁。QML 是 Qt 的声明式 UI 语言,TextField 是 QtQuick.Controls 2 里的文本输入控件,Label 是只读文本控件。把两者组合进一个自定义 Item,你就能得到一个「登入输入行」组件——左边一个 Label 写「登入账号」,右边一个 TextField 接收输入。适合谁?适合正在用 Qt Quick 做桌面端或嵌入式端登入页、又不想每个输入框都复制一遍样式的开发者。核心检索词就是 QML 自定义控件、登入界面、TextField、Label 这几个,下面全部围绕它们展开。
我试过最直接的做法:不封装,直接在 main.qml 里摆两个 Label 加两个 TextField。能跑,但账号框和密码框的字体、圆角背景、选中颜色要写两遍,改一次样式改两处,第三个输入框出现时就开始失控。所以正确的路径是先做一个 Fjf_Login.qml 自定义控件,把 TextField 包进去,再用 property alias 把 fontSize、selectColor、placeholderText、echoMode 这些属性暴露出来。这样 main.qml 里只需要实例化两次,账号框传普通模式,密码框传 Password 模式,其余样式自动统一。
这里有个关键点容易被忽略:property alias 暴露的是「引用」,不是「拷贝」。也就是说property alias fontShow: textField.placeholderText之后,外部给 fontShow 赋值,等于直接改内部 TextField 的 placeholderText。这正是自定义控件好用的地方——你不用为每个想暴露的属性写 getter/setter,一行 alias 搞定。但也要注意 alias 只能指向已存在的 id,且不能指向另一个 alias 链太深的地方,否则运行时会报Cannot assign to non-existent property。
再讲 Label 的角色。Label 在登入界面里就是「说明文字」,它不接收输入,只负责告诉用户这一行是账号还是密码。它的 text、font.family、font.pixelSize 是三个最常调的属性。字体建议统一用「微软雅黑」这类系统常见字体,pixelSize 用 20 左右在 640x480 的窗口里视觉刚好。Label 的定位通常用 anchors 挂在 TextField 左侧,rightMargin 留 10 像素做间距,这样文字和输入框不会贴在一起。
把组件拆分、属性暴露、信号回传这三件事串起来,登入界面的骨架就成立了。组件拆分是「一个输入行 = 一个自定义控件」;属性暴露是「用 alias 把内部 TextField 的可变项开放出去」;信号回传是「TextField 自带的 accepted、editingFinished 以及外部 Button 的 onClicked 把输入值取出来」。下一篇会展开信号,这篇先把显示和交互反馈跑通。下面进入前置准备,把 TaoToken 的 Key 和 API 通道配好,方便你用 AI 工具辅助生成和校验 QML 配置。
2. 前置准备:用 TaoToken 统一 Key 与 API 通道辅助生成 QML 配置
写 QML 自定义控件时,最烦的不是语法,而是属性名记不全、alias 指向写错、echoMode 枚举值拼错。这类问题完全可以让 AI 工具帮你生成骨架和校验配置。但前提是你得有一个稳定的 API 通道,不然工具调不通,生成到一半断掉更浪费时间。TaoToken 在这里的作用就是统一 Key 和 API 入口:你只需要一个 Key,就能在模型对话、Coding Plan、API Keys 管理之间切换,不用为每个工具单独配一套凭证。
先说清楚 TaoToken 是什么、能做什么、适合谁。它是一个统一的 AI 模型接入平台,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。适合谁?适合需要把 AI 能力接进日常开发流程、又不想管理一堆 Key 的开发者。对这篇的场景来说,你可以用它来生成 Fjf_Login.qml 的初始骨架,或者把写好的 QML 贴进去让它检查 alias 有没有指向不存在的属性。
前置准备分三步。第一步,拿到 Key。打开 https://taotoken.net/api-keys ,登录后创建一个 API Key,复制保存。这个 Key 就是你后面所有工具共用的凭证。第二步,确认你要用哪种接入方式。如果你只是偶尔让模型帮你看看 QML 代码,用模型对话就行,地址是 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。如果你打算长期用 AI 辅助写 QML 和调试,建议开 Coding Plan,地址是 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,额度更划算。第三步,把 Base URL 和 Key 记下来,后面配置工具时要用。
这里要强调一个原则:Base URL 统一用 https://taotoken.net/api ,不要自己拼路径。很多接入失败都是因为把 Base URL 写成了带具体端点的地址,导致工具在拼接时重复。Key 放在请求头里,格式通常是Authorization: Bearer <你的Key>。Model ID 按你实际要用的模型填,比如 claude 系列或 gpt 系列的标识,具体以控制台里列出的为准。这三件套——Base URL、Key、Model ID——是后面所有配置的基础,缺一个都调不通。
如果你用的是 Claude Code 这类命令行工具,配置入口在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite ,里面会告诉你环境变量怎么设。如果你用的是 Cline 或带 MCP 的编辑器插件,配置入口在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=cline_mcp&utm_campaign=rewrite 。不管哪种,核心都是那三件套。配好之后,你就可以让 AI 帮你生成 QML 骨架了。下面进入可复制配置环节,给出完整的 Fjf_Login.qml 和 main.qml。
3. 可复制配置:Fjf_Login.qml 与 main.qml 完整骨架
这一节直接给可复制的代码。先建一个 Qt Quick Application 工程,Qt 版本建议 5.12 以上,因为 QtQuick.Controls 2.2 在这个版本里比较稳定。工程建好后,在项目目录下新建 Fjf_Login.qml,内容如下。注意文件名的首字母大写,QML 组件文件名即组件类型名,main.qml 里用Fjf_Login {}实例化时大小写必须一致,否则会报Fjf_Login is not a type。
// Fjf_Login.qml import QtQuick 2.0 import QtQuick.Controls 2.2 Item { // 暴露给外部的属性,全部通过 alias 指向内部 TextField property alias fontSize: textField.font.pixelSize property alias selectColor: textField.selectionColor property alias fontShow: textField.placeholderText property alias echoModeTmp: textField.echoMode property alias inputText: textField.text TextField { id: textField verticalAlignment: Text.AlignVCenter font.pixelSize: 12 font.family: "微软雅黑" color: "white" cursorVisible: true selectByMouse: true // 鼠标可选中,输错能全选删除 selectionColor: "red" // 选中时高亮颜色 placeholderText: qsTr("请输入登入密码") echoMode: TextInput.Normal // 默认明文,密码框由外部覆盖 width: 280 height: 40 background: Rectangle { border.width: 0 radius: 4 color: "blue" opacity: 0.05 implicitHeight: 40 implicitWidth: 280 } } }这段骨架的关键在开头那五行 property alias。fontSize 指向 font.pixelSize,selectColor 指向 selectionColor,fontShow 指向 placeholderText,echoModeTmp 指向 echoMode,inputText 指向 text。前四个是样式和显示控制,最后一个 inputText 是为了让外部能读到用户输入的值——登入按钮点击时要用。注意 alias 的名字不能和内部 id 或已有属性冲突,否则会报Duplicate property name。
然后是 main.qml,负责把两个输入行和两个按钮摆出来。
// main.qml import QtQuick 2.9 import QtQuick.Window 2.2 import QtQuick.Controls 2.2 Window { visible: true width: 640 height: 480 title: "登入界面" color: "silver" Label { id: label_1 anchors.top: parent.top anchors.topMargin: 108 anchors.right: fjf_1.left anchors.rightMargin: 10 text: "登入账号" font.family: "微软雅黑" font.pixelSize: 20 } Fjf_Login { id: fjf_1 anchors.top: parent.top anchors.topMargin: 100 anchors.left: parent.left anchors.leftMargin: 200 fontShow: "请输入登入账号" } Label { id: label_2 anchors.top: parent.top anchors.topMargin: 188 anchors.right: fjf_2.left anchors.rightMargin: 10 text: "登入密码" font.family: "微软雅黑" font.pixelSize: 20 } Fjf_Login { id: fjf_2 anchors.top: parent.top anchors.topMargin: 180 anchors.left: parent.left anchors.leftMargin: 200 echoModeTmp: TextInput.Password fontShow: "请输入登录密码" } Button { id: button_1 anchors.top: parent.top anchors.topMargin: 300 anchors.left: parent.left anchors.leftMargin: 100 width: 100 height: 30 background: Rectangle { height: 30 width: 100 color: button_1.down ? "silver" : "gray" Text { anchors.centerIn: parent text: "确定" font.pixelSize: 16 } } onClicked: { console.log("账号:", fjf_1.inputText) console.log("密码:", fjf_2.inputText) } } Button { id: button_2 anchors.top: parent.top anchors.topMargin: 300 anchors.left: parent.left anchors.leftMargin: 400 width: 100 height: 30 background: Rectangle { height: 30 width: 100 color: button_2.down ? "silver" : "gray" Text { anchors.centerIn: parent text: "取消" font.pixelSize: 16 } } onClicked: { fjf_1.inputText = "" fjf_2.inputText = "" } } }对照一下参数。账号框 fjf_1 只传了 fontShow,echoMode 用默认的 Normal,所以是明文。密码框 fjf_2 传了 echoModeTmp: TextInput.Password,显示掩码。两个 Label 的 font.pixelSize 都是 20,而 TextField 内部默认是 12,这里没有通过 fontSize 覆盖,所以输入框里的字比 Label 小。如果你想让它们一致,在实例化时加fontSize: 20即可。selectionColor 默认是红色,来自组件内部,外部没覆盖。inputText 是新增的 alias,用来在按钮点击时取值。
如果你用 AI 工具辅助生成,可以把上面两段贴进模型对话,让它检查 alias 是否有指向不存在的属性、echoMode 枚举是否拼写正确。TaoToken 的模型对话入口在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,把 QML 贴进去问「这段 alias 有没有问题」就行。配置工具时记得三件套:Base URL 用 https://taotoken.net/api ,Key 用你在 api-keys 页面创建的,Model ID 按控制台列出的填。下面进入验证环节。
4. 验证请求:运行工程看登入界面显示与交互反馈
代码写完,接下来是跑起来验证。用 Qt Creator 打开工程,选择 Desktop 套件,点绿色运行按钮。如果一切正常,你会看到一个 640x480 的银色窗口,标题「登入界面」,左上偏中位置有两行:第一行 Label「登入账号」加一个输入框,占位提示「请输入登入账号」;第二行 Label「登入密码」加一个输入框,占位提示「请输入登录密码」,且输入时显示掩码。底部两个按钮「确定」和「取消」。
验证分几个点。第一,占位提示是否显示。如果输入框里没有灰字提示,检查 fontShow 是否传对,以及组件内部 placeholderText 的 alias 是否拼写一致。第二,密码框是否掩码。在密码框里敲几个字符,如果显示的是圆点或星号,说明 echoModeTmp: TextInput.Password 生效;如果显示明文,检查是不是把 echoModeTmp 写成了 echoMode 或其他名字。第三,选中颜色。用鼠标在输入框里拖选一段文字,应该显示红色高亮,这是 selectionColor 的作用。第四,按钮反馈。点「确定」,Qt Creator 的应用程序输出里会打印账号和密码;点「取消」,两个输入框清空。
这里有个容易踩的坑:TextField 的 text 属性和 inputText alias 的关系。你在输入框里打字,text 实时变化,inputText 因为指向 text,所以也实时同步。点「确定」时读 fjf_1.inputText 拿到的是当前输入值。但如果你在组件内部又写了一个property string inputText,那就和 alias 冲突了,运行时会报Cannot override FINAL property或类似错误。所以 alias 和普通 property 不能同名。
再验证一下窗口缩放。把窗口拉大,Label 和输入框的位置会跟着 anchors 走,但 topMargin 是固定值,所以它们不会自动居中。这是当前骨架的局限——用固定 margin 定位,适合固定尺寸窗口。如果你要自适应,得换成 ColumnLayout 或 RowLayout。这个改进留到后面讲布局的篇章。当前这篇的目标是「一次跑通登入界面显示与交互反馈」,固定定位已经够用。
如果你在运行前想先用 AI 校验一遍配置,可以把 main.qml 贴进模型对话,问「anchors 有没有循环依赖」。常见的循环是 A 的 left 锚到 B 的 right,B 的 right 又锚到 A 的 left,运行时会报QML Item: Binding loop detected。上面代码里 Label 锚到 TextField 的 left,TextField 锚到 parent 的 left,没有互相锚,所以不会循环。验证通过后,登入界面的显示和交互就成立了。下面进入排错环节。
5. 常见报错排查:从 401 到 alias 指向失败逐条对照
跑 QML 工程时,报错分两类:一类是 QML 运行时报错,一类是 AI 工具接入时报错。先讲 QML 侧的。最常见的第一个是Fjf_Login is not a type。原因通常是文件名和组件名不一致,比如文件叫 fjf_login.qml(小写),main.qml 里写Fjf_Login {}。QML 组件类型名必须和文件名完全一致,包括大小写。改成 Fjf_Login.qml 即可。
第二个是Cannot assign to non-existent property "fontShow"。这说明 alias 没写对,或者 alias 指向的属性在内部不存在。检查 Fjf_Login.qml 里是否有property alias fontShow: textField.placeholderText,以及 textField 这个 id 是否存在。如果 alias 指向的 id 拼错,比如写成textfield,就会报这个错。另外 alias 不能指向另一个 alias,也不能指向不存在的属性。
第三个是Binding loop detected for property "width"。这通常出现在 anchors 互相依赖时。比如你把 TextField 的 width 锚到 parent 的 width,parent 的 width 又依赖 TextField 的 implicitWidth,就成环了。解决办法是给其中一个设固定值,或者用 Layout。上面代码里 TextField 的 width 是固定 280,不会循环。
第四个是密码框不掩码。检查 echoModeTmp 传的值是不是TextInput.Password。注意是 TextInput 不是 TextField,枚举值定义在 TextInput 里。如果写成TextField.Password会报Cannot assign to non-existent property。另外 echoMode 的四个值:Normal 明文、Password 掩码、NoEcho 不显示、PasswordEchoOnEdit 编辑时显示否则掩码,按需选。
再讲 AI 工具接入侧的报错。第一个是 401 Unauthorized。这通常是 Key 没传对或过期。检查请求头里Authorization: Bearer <Key>的 Key 是否和 api-keys 页面创建的一致,有没有多余空格。第二个是local proxy failed。这通常出现在工具配置了本地代理但代理没启动时。检查工具的代理设置,如果不需要代理就关掉,Base URL 直接用 https://taotoken.net/api 。第三个是reading choices相关报错,通常是响应格式和工具预期不符,检查 Model ID 是否填对,以及 Base URL 有没有多拼路径。第四个是 OAuth 相关报错,出现在 Claude Code 这类工具首次登录时,按文档走 OAuth 流程即可,入口在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 。
如果你用 Cline 或带 MCP 的插件,配置时三件套要写全:Base URL 用 https://taotoken.net/api ,Key 用创建的,Model ID 按控制台填。少一个都会报错。Cline MCP 的配置入口在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=cline_mcp&utm_campaign=rewrite 。Codex 的 auth.json 配置也是同样三件套,Base URL、Key、Model ID 缺一不可。排错时先确认这三件套,再看 QML 侧的问题,顺序别反。
6. 把登入界面接进你的工程:下一步与实用建议
登入界面显示跑通之后,下一步是接信号和校验。当前「确定」按钮只是打印输入值,实际工程里你要把账号密码发给后端做鉴权。这时候 inputText 这个 alias 就派上用场了——在 onClicked 里读 fjf_1.inputText 和 fjf_2.inputText,做非空校验,再发请求。如果要做实时校验,可以监听 TextField 的 textChanged 信号,但 alias 不能直接暴露信号,需要在组件内部加一个signal textChanged(string value),然后在 TextField 的 onTextChanged 里 emit。这是自定义控件信号回传的标准做法。
另一个实用建议是样式统一。当前账号框和密码框的字体大小不一致(Label 20,TextField 12),视觉上有点跳。你可以在实例化时给两个 Fjf_Login 都加fontSize: 20,让输入框文字和 Label 对齐。选中颜色 selectionColor 默认红色,如果和你的主题不搭,实例化时传selectColor: "#4A90D9"覆盖即可。背景色在组件内部是color: "blue"加opacity: 0.05,看起来是淡蓝,想换的话改组件内部或再加一个 alias 暴露出去。
如果你打算长期用 AI 辅助写 QML,建议开 Coding Plan,入口在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。把常用的 QML 骨架、报错信息、配置片段整理成提示词模板,每次让模型基于模板生成,比每次从零描述快得多。Key 和 API 通道统一用 TaoToken,Base URL 固定 https://taotoken.net/api ,这样换工具时不用重新配。模型对话入口在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,API Keys 管理在 https://taotoken.net/api-keys ,接入文档在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
最后说一个我踩过的坑:alias 暴露的属性越多,组件越灵活,但也越容易在外部传错值。比如 echoModeTmp 传了一个不存在的枚举,运行时才报错。建议在组件内部给关键属性设默认值,外部不传就用默认,传了才覆盖。这样即使调用方漏传,界面也能正常显示。登入界面这种场景,默认值尤其重要——密码框忘了传 echoModeTmp,就会明文显示密码,这是安全问题。所以密码框的实例化一定要显式传echoModeTmp: TextInput.Password,别依赖默认。